);
};
diff --git a/client/src/pages/quicklab/Homepage.jsx b/client/src/pages/quicklab/Homepage.jsx
index da7d7f6..c213abb 100644
--- a/client/src/pages/quicklab/Homepage.jsx
+++ b/client/src/pages/quicklab/Homepage.jsx
@@ -1,5 +1,8 @@
// QuickLabHomepage.jsx
-import React from 'react';
+import React, { useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../../context/authContext';
+import getDashboardPath from '../../utility/getDashboardPath';
import '../../quicklab.css';
import HeroSection from '../../components/quicklab/HomePage/HeroSection';
import FeaturesSection from '../../components/quicklab/HomePage/FeaturesSection';
@@ -9,6 +12,20 @@ import PromiseSection from '../../components/quicklab/HomePage/PromiseSection';
import DesktopFooter from '../../components/quicklab/DesktopFooter';
const QuickLabHomepage = () => {
+ const { isAuthenticated, user } = useAuth();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (isAuthenticated && user) {
+ // Redirect lab_admin and lab_staff to their dashboards
+ if (user.role === 'lab_admin' || user.role === 'lab_staff') {
+ const dashboardPath = getDashboardPath(user.role);
+ navigate(dashboardPath, { replace: true });
+ }
+ // For other roles (patient, doctor, admin), stay on homepage
+ }
+ }, [isAuthenticated, user, navigate]);
+
return (
diff --git a/client/src/pages/quicklab/LabAdminAddLab.jsx b/client/src/pages/quicklab/LabAdminAddLab.jsx
new file mode 100644
index 0000000..1c33ea2
--- /dev/null
+++ b/client/src/pages/quicklab/LabAdminAddLab.jsx
@@ -0,0 +1,341 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Building2, Phone, Mail, Globe, MapPin, Upload, Image } from 'lucide-react';
+import Loading from '../../components/ui/Loading';
+import { createLab } from '../../service/labAdminService';
+import '../../quicklab.css';
+
+const initialState = {
+ name: '',
+ description: '',
+ contact: {
+ phone: '',
+ email: '',
+ website: '',
+ },
+ address: {
+ formattedAddress: '',
+ city: '',
+ state: '',
+ zipCode: '',
+ country: '',
+ },
+ generalHomeCollectionFee: '',
+};
+
+export default function LabAdminAddLab() {
+ const navigate = useNavigate();
+ const [form, setForm] = useState(initialState);
+ const [logoFile, setLogoFile] = useState(null);
+ const [photoFiles, setPhotoFiles] = useState([]);
+ const [logoPreview, setLogoPreview] = useState(null);
+ const [photoPreviews, setPhotoPreviews] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const handleInputChange = (path, value) => {
+ setForm((prev) => {
+ // Safe deep clone for nested updates
+ const updated = JSON.parse(JSON.stringify(prev));
+ const keys = path.split('.');
+ let target = updated;
+ keys.slice(0, -1).forEach((k) => {
+ target[k] = target[k] || {};
+ target = target[k];
+ });
+ target[keys[keys.length - 1]] = value;
+ return updated;
+ });
+ };
+
+ const handleLogoChange = (e) => {
+ const file = e.target.files?.[0];
+ setLogoFile(file || null);
+ setLogoPreview(file ? URL.createObjectURL(file) : null);
+ };
+
+ const handlePhotosChange = (e) => {
+ const files = Array.from(e.target.files || []);
+ setPhotoFiles(files);
+ setPhotoPreviews(files.map((file) => URL.createObjectURL(file)));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+
+ try {
+ if (!form.name.trim() || !form.contact.phone.trim()) {
+ setError('Lab name and contact phone are required.');
+ setLoading(false);
+ return;
+ }
+
+ const payload = {
+ ...form,
+ generalHomeCollectionFee: form.generalHomeCollectionFee
+ ? Number(form.generalHomeCollectionFee)
+ : undefined,
+ };
+
+ await createLab(payload, logoFile, photoFiles);
+ navigate('/quick-lab/dashboard', { replace: true });
+ } catch (err) {
+ setError(err?.response?.data?.message || 'Failed to create lab');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
Add Your Lab
+
+ Provide your lab details so patients and doctors can find and trust your services.
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/quicklab/LabAdminAppointments.jsx b/client/src/pages/quicklab/LabAdminAppointments.jsx
new file mode 100644
index 0000000..e7ae76b
--- /dev/null
+++ b/client/src/pages/quicklab/LabAdminAppointments.jsx
@@ -0,0 +1,604 @@
+import { useEffect, useState } from 'react';
+import {
+ Calendar,
+ Clock,
+ User,
+ MapPin,
+ Phone,
+ FileText,
+ Filter,
+ X,
+ CheckCircle,
+ XCircle,
+ AlertCircle,
+ Home,
+ Building2,
+ ChevronDown,
+ Search,
+} from 'lucide-react';
+import { toast } from 'react-toastify';
+import {
+ getLabAppointments,
+ assignStaffForCollection,
+ updateLabAppointmentStatus,
+} from '../../service/labAppointmentService';
+import { getLabStaff } from '../../service/labAdminService';
+import '../../quicklab.css';
+
+export default function LabAdminAppointments() {
+ const [appointments, setAppointments] = useState([]);
+ const [filteredAppointments, setFilteredAppointments] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [staff, setStaff] = useState([]);
+ const [showFilters, setShowFilters] = useState(false);
+ const [selectedAppointment, setSelectedAppointment] = useState(null);
+ const [showAssignModal, setShowAssignModal] = useState(false);
+ const [showStatusModal, setShowStatusModal] = useState(false);
+ const [searchQuery, setSearchQuery] = useState('');
+
+ const [filters, setFilters] = useState({
+ status: '',
+ collectionType: '',
+ date: '',
+ });
+
+ const [summary, setSummary] = useState({
+ total: 0,
+ pending: 0,
+ confirmed: 0,
+ completed: 0,
+ cancelled: 0,
+ homeCollection: 0,
+ visitLab: 0,
+ });
+
+ const statusColors = {
+ pending:
+ 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800',
+ confirmed:
+ 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-400 border-blue-200 dark:border-blue-800',
+ sample_collected:
+ 'bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-400 border-purple-200 dark:border-purple-800',
+ processing:
+ 'bg-indigo-100 dark:bg-indigo-900/30 text-indigo-800 dark:text-indigo-400 border-indigo-200 dark:border-indigo-800',
+ completed:
+ 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400 border-green-200 dark:border-green-800',
+ cancelled:
+ 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-400 border-red-200 dark:border-red-800',
+ };
+
+ const statusOptions = [
+ { value: 'pending', label: 'Pending', icon: AlertCircle },
+ { value: 'confirmed', label: 'Confirmed', icon: CheckCircle },
+ { value: 'sample_collected', label: 'Sample Collected', icon: FileText },
+ { value: 'processing', label: 'Processing', icon: Clock },
+ { value: 'completed', label: 'Completed', icon: CheckCircle },
+ { value: 'cancelled', label: 'Cancelled', icon: XCircle },
+ ];
+
+ useEffect(() => {
+ fetchAppointments();
+ fetchStaff();
+ }, [filters.status, filters.collectionType, filters.date]);
+
+ useEffect(() => {
+ applyFilters();
+ }, [appointments, searchQuery]);
+
+ const fetchAppointments = async () => {
+ try {
+ setLoading(true);
+ const params = {};
+ if (filters.status) params.status = filters.status;
+ if (filters.collectionType) params.collectionType = filters.collectionType;
+ if (filters.date) params.date = filters.date;
+
+ const response = await getLabAppointments(params);
+ const data = response.data || response;
+ setAppointments(data.appointments || []);
+ calculateSummary(data.appointments || []);
+ } catch (error) {
+ console.error('Failed to fetch appointments:', error);
+ toast.error('Failed to load appointments');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchStaff = async () => {
+ try {
+ const response = await getLabStaff();
+ const data = response.data || response;
+ setStaff(data.staff || []);
+ } catch (error) {
+ console.error('Failed to fetch staff:', error);
+ toast.error('Failed to load staff list');
+ }
+ };
+
+ const calculateSummary = (appts) => {
+ const sum = {
+ total: appts.length,
+ pending: appts.filter((a) => a.status === 'pending').length,
+ confirmed: appts.filter((a) => a.status === 'confirmed').length,
+ completed: appts.filter((a) => a.status === 'completed').length,
+ cancelled: appts.filter((a) => a.status === 'cancelled').length,
+ homeCollection: appts.filter((a) => a.collectionType === 'home_collection').length,
+ visitLab: appts.filter((a) => a.collectionType === 'visit_lab').length,
+ };
+ setSummary(sum);
+ };
+
+ const applyFilters = () => {
+ let filtered = [...appointments];
+
+ // Search filter
+ if (searchQuery.trim()) {
+ const query = searchQuery.toLowerCase();
+ filtered = filtered.filter(
+ (apt) =>
+ apt.patientId?.firstName?.toLowerCase().includes(query) ||
+ apt.patientId?.lastName?.toLowerCase().includes(query) ||
+ apt.tests?.some((test) => test.testName?.toLowerCase().includes(query)) ||
+ apt._id?.toLowerCase().includes(query)
+ );
+ }
+
+ // Debug: Log first appointment with home collection
+ const homeCollectionApt = filtered.find((a) => a.collectionType === 'home_collection');
+ if (homeCollectionApt) {
+ console.log('Home Collection Appointment:', homeCollectionApt);
+ console.log('Collection Address:', homeCollectionApt.collectionAddress);
+ }
+
+ setFilteredAppointments(filtered);
+ };
+
+ const handleAssignStaff = async (staffId) => {
+ try {
+ await assignStaffForCollection(selectedAppointment._id, staffId);
+ toast.success('Staff assigned successfully');
+ setShowAssignModal(false);
+ fetchAppointments();
+ } catch (error) {
+ toast.error(error.response?.data?.message || 'Failed to assign staff');
+ }
+ };
+
+ const handleUpdateStatus = async (newStatus) => {
+ try {
+ await updateLabAppointmentStatus(selectedAppointment._id, newStatus);
+ toast.success('Status updated successfully');
+ setShowStatusModal(false);
+ fetchAppointments();
+ } catch (error) {
+ toast.error(error.response?.data?.message || 'Failed to update status');
+ }
+ };
+
+ const formatDate = (dateString) => {
+ return new Date(dateString).toLocaleDateString('en-IN', {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ });
+ };
+
+ const formatTime = (timeString) => {
+ return new Date(`2000-01-01T${timeString}`).toLocaleTimeString('en-IN', {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: true,
+ });
+ };
+
+ const formatAddress = (addr) => {
+ if (!addr) return '';
+ const parts = [addr.street, addr.city, addr.state, addr.zipCode, addr.country].filter(Boolean);
+ return parts.join(', ');
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+ Appointment Management
+
+
+ Manage and track all lab appointments
+
+
+
+ {/* Summary Cards */}
+
+
+
+ {summary.total}
+
+
Total
+
+
+
+ {summary.pending}
+
+
Pending
+
+
+
+ {summary.confirmed}
+
+
Confirmed
+
+
+
+ {summary.completed}
+
+
Completed
+
+
+
+ {summary.cancelled}
+
+
Cancelled
+
+
+
+ {summary.homeCollection}
+
+
Home
+
+
+
+ {summary.visitLab}
+
+
Lab Visit
+
+
+
+ {/* Search and Filters */}
+
+
+ {/* Appointments List */}
+ {loading ? (
+
+
+
+ Loading appointments...
+
+
+ ) : filteredAppointments.length === 0 ? (
+
+
+
+ No appointments found
+
+
+ {searchQuery || filters.status || filters.collectionType || filters.date
+ ? 'Try adjusting your filters'
+ : 'No appointments have been booked yet'}
+
+
+ ) : (
+
+ {filteredAppointments.map((appointment) => (
+
+
+ {/* Left: Patient & Appointment Info */}
+
+ {/* Status Badge */}
+
+
+ {appointment.status.replace(/_/g, ' ').toUpperCase()}
+
+
+ {appointment.collectionType === 'home_collection' ? (
+
+ Home Collection
+
+ ) : (
+
+ Lab Visit
+
+ )}
+
+
+
+ {/* Patient Info */}
+
+
+
+
+ {appointment.patientId?.firstName} {appointment.patientId?.lastName}
+
+
+
+ {appointment.patientId?.phoneNumber}
+
+
+
+
+ {/* Date & Time */}
+
+
+
+ {formatDate(appointment.appointmentDate)}
+
+
+
+ {formatTime(appointment.appointmentTime)}
+
+
+
+ {/* Collection Address (for home collection) */}
+ {appointment.collectionType === 'home_collection' &&
+ appointment.collectionAddress && (
+
+
+
+
+ Home Collection Address
+
+
+ {formatAddress(appointment.collectionAddress)}
+
+
+
+ )}
+
+ {/* Debug: Show if home collection but no address */}
+ {appointment.collectionType === 'home_collection' &&
+ !appointment.collectionAddress && (
+
+
+
+
No collection address provided
+
Patient needs to provide their address
+
+
+ )}
+
+ {/* Assigned Staff */}
+ {appointment.assignedStaffId && (
+
+
+
+ Assigned to: {appointment.assignedStaffId.firstName}{' '}
+ {appointment.assignedStaffId.lastName}
+
+
+ )}
+
+ {/* Tests */}
+
+
+ Tests Ordered:
+
+
+ {appointment.tests?.map((test, idx) => (
+
+ {test.testName}
+ ₹{test.price}
+
+ ))}
+
+
+ Total: ₹{appointment.totalAmount}
+
+
+
+
+ {/* Right: Action Buttons */}
+
+
+
+ {appointment.collectionType === 'home_collection' &&
+ !appointment.assignedStaffId && (
+
+ )}
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Assign Staff Modal */}
+ {showAssignModal && selectedAppointment && (
+
setShowAssignModal(false)}
+ >
+
e.stopPropagation()}
+ >
+
+
+ Assign Staff Member
+
+
+
+
+
+ {staff.filter((s) => s.isAvailableForHomeCollection).length === 0 ? (
+
+ No staff available for home collection
+
+ ) : (
+ staff
+ .filter((s) => s.isAvailableForHomeCollection)
+ .map((staffMember) => (
+
+ ))
+ )}
+
+
+
+ )}
+
+ {/* Update Status Modal */}
+ {showStatusModal && selectedAppointment && (
+
setShowStatusModal(false)}
+ >
+
e.stopPropagation()}
+ >
+
+
+ Update Appointment Status
+
+
+
+
+
+ Current status:{' '}
+
+ {selectedAppointment.status.replace(/_/g, ' ').toUpperCase()}
+
+
+
+
+ {statusOptions
+ .filter((opt) => opt.value !== selectedAppointment.status)
+ .map((option) => {
+ const Icon = option.icon;
+ return (
+
+ );
+ })}
+
+
+
+ )}
+
+ );
+}
diff --git a/client/src/pages/quicklab/LabAdminDashboard.jsx b/client/src/pages/quicklab/LabAdminDashboard.jsx
new file mode 100644
index 0000000..4b7fe62
--- /dev/null
+++ b/client/src/pages/quicklab/LabAdminDashboard.jsx
@@ -0,0 +1,313 @@
+import { useEffect, useState } from 'react';
+import {
+ Users,
+ TestTube,
+ Calendar,
+ Settings,
+ RefreshCw,
+ TrendingUp,
+ Clock,
+ CheckCircle,
+} from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+import { getLabInfo, getLabStaff } from '../../service/labAdminService';
+import { getLabAppointments } from '../../service/labAppointmentService';
+import { toast } from 'react-toastify';
+import '../../quicklab.css';
+
+export default function LabAdminDashboard() {
+ const navigate = useNavigate();
+ const [lab, setLab] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [stats, setStats] = useState([
+ { label: 'Total Tests', value: '0', icon: TestTube, color: 'lab-yellow' },
+ { label: 'Staff Members', value: '0', icon: Users, color: 'lab-blue' },
+ { label: 'Appointments', value: '0', icon: Calendar, color: 'lab-yellow' },
+ ]);
+
+ useEffect(() => {
+ loadDashboardData();
+ }, []);
+
+ const loadDashboardData = async () => {
+ try {
+ setLoading(true);
+ const [labRes, staffRes, apptRes] = await Promise.all([
+ getLabInfo(),
+ getLabStaff(),
+ getLabAppointments({ limit: 100 }),
+ ]);
+
+ const labData = labRes.data?.lab || labRes.lab;
+ const staffData = staffRes.data?.staff || staffRes.staff || [];
+ const apptData = apptRes.data?.appointments || apptRes.appointments || [];
+
+ setLab(labData);
+ setStats([
+ {
+ label: 'Total Tests',
+ value: String(labData?.tests?.length || 0),
+ icon: TestTube,
+ color: 'lab-yellow',
+ },
+ {
+ label: 'Staff Members',
+ value: String(staffData?.length || 0),
+ icon: Users,
+ color: 'lab-blue',
+ },
+ {
+ label: 'Total Appointments',
+ value: String(apptData?.length || 0),
+ icon: Calendar,
+ color: 'lab-yellow',
+ },
+ ]);
+ } catch (error) {
+ console.error('Failed to load dashboard data:', error);
+ toast.error('Failed to load dashboard data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRefresh = () => {
+ loadDashboardData();
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Welcome back
+
+
+ {lab?.name || 'Lab Dashboard'}
+
+
+ Manage your lab, staff, and appointments
+
+
+
+
+
+ {/* Stats Grid */}
+
+ {stats.map((stat, idx) => {
+ const Icon = stat.icon;
+ return (
+
+
+
+
+ {stat.label}
+
+
+ {stat.value}
+
+
+
+
+
+
+
+
+ View details
+
+
+ );
+ })}
+
+
+ {/* Quick Actions */}
+
+ {/* Manage Tests */}
+
+
+
+
+ Manage Tests
+
+
+ Add, update, or remove test services
+
+
+
+
+
+
+
+ {/* Manage Staff */}
+
+
+
+
+ Manage Staff
+
+
+ Add or manage lab staff members
+
+
+
+
+
+
+
+ {/* Lab Settings */}
+
+
+
+
+ Lab Settings
+
+
+ Update profile, charges, and media
+
+
+
+
+
+
+
+ {/* Appointments */}
+
+
+
+
+ Appointments
+
+
+ Track and manage lab appointments
+
+
+
+
+
+
+
+
+ {/* Lab Overview & Quick Stats */}
+ {lab && (
+
+ {/* Lab Summary */}
+
+
+
+
+
+
+
+ Lab Overview
+
+
{lab.name}
+
+
+
+
+
+ Location
+
+
+ {lab.address?.city || '—'}, {lab.address?.state || '—'}
+
+
+
+
Contact
+
+ {lab.contact?.phone || '—'}
+
+
+
+
+
+ {/* Quick Actions Card */}
+
+
+
+ Quick Access
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Recent Activity */}
+
+
+
+
+ System Status
+
+
+
+
+ ✓ Lab Profile
+ Active
+
+
+
+ ✓ Database Connection
+
+ Connected
+
+
+ All systems operational. Keep managing your lab efficiently!
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/quicklab/LabAdminLabSettings.jsx b/client/src/pages/quicklab/LabAdminLabSettings.jsx
new file mode 100644
index 0000000..3b8d2f2
--- /dev/null
+++ b/client/src/pages/quicklab/LabAdminLabSettings.jsx
@@ -0,0 +1,520 @@
+import { useEffect, useState } from 'react';
+import {
+ Building2,
+ Upload,
+ Save,
+ Image as ImageIcon,
+ Phone,
+ MapPin,
+ Globe,
+ Mail,
+ DollarSign,
+ Clock,
+ RefreshCw,
+} from 'lucide-react';
+import { toast } from 'react-toastify';
+import { getLabInfo, updateLabInfo } from '../../service/labAdminService';
+import '../../quicklab.css';
+
+export default function LabAdminLabSettings() {
+ const [lab, setLab] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [logoFile, setLogoFile] = useState(null);
+ const [newPhotos, setNewPhotos] = useState([]);
+ const [formData, setFormData] = useState({
+ name: '',
+ description: '',
+ phone: '',
+ email: '',
+ website: '',
+ formattedAddress: '',
+ street: '',
+ city: '',
+ state: '',
+ zipCode: '',
+ country: '',
+ generalHomeCollectionFee: '',
+ existingPhotos: [],
+ });
+
+ useEffect(() => {
+ loadLab();
+ }, []);
+
+ const loadLab = async () => {
+ try {
+ setLoading(true);
+ const response = await getLabInfo();
+ const data = response.data || response;
+ const labInfo = data.lab;
+ setLab(labInfo);
+ setFormData({
+ name: labInfo?.name || '',
+ description: labInfo?.description || '',
+ phone: labInfo?.contact?.phone || '',
+ email: labInfo?.contact?.email || '',
+ website: labInfo?.contact?.website || '',
+ formattedAddress: labInfo?.address?.formattedAddress || '',
+ street: labInfo?.address?.street || '',
+ city: labInfo?.address?.city || '',
+ state: labInfo?.address?.state || '',
+ zipCode: labInfo?.address?.zipCode || '',
+ country: labInfo?.address?.country || '',
+ generalHomeCollectionFee:
+ labInfo?.generalHomeCollectionFee !== undefined ? labInfo.generalHomeCollectionFee : '',
+ existingPhotos: labInfo?.photos || [],
+ });
+ } catch (error) {
+ console.error('Failed to load lab info:', error);
+ toast.error('Failed to load lab info');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleInput = (e) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({ ...prev, [name]: value }));
+ };
+
+ const handleRemovePhoto = (url) => {
+ setFormData((prev) => ({
+ ...prev,
+ existingPhotos: prev.existingPhotos.filter((p) => p !== url),
+ }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!formData.name) {
+ toast.error('Lab name is required');
+ return;
+ }
+ if (!formData.phone) {
+ toast.error('Contact phone is required');
+ return;
+ }
+
+ try {
+ setSaving(true);
+ const payload = {
+ name: formData.name,
+ description: formData.description,
+ contact: {
+ phone: formData.phone,
+ email: formData.email,
+ website: formData.website,
+ },
+ address: {
+ formattedAddress: formData.formattedAddress,
+ street: formData.street,
+ city: formData.city,
+ state: formData.state,
+ zipCode: formData.zipCode,
+ country: formData.country,
+ },
+ generalHomeCollectionFee: formData.generalHomeCollectionFee,
+ existingPhotos: formData.existingPhotos,
+ };
+
+ const response = await updateLabInfo(payload, logoFile, newPhotos);
+ const updated = response.data?.lab || response.lab;
+ setLab(updated);
+ setNewPhotos([]);
+ setLogoFile(null);
+ setFormData((prev) => ({
+ ...prev,
+ existingPhotos: updated?.photos || prev.existingPhotos,
+ }));
+ toast.success('Lab info updated');
+ } catch (error) {
+ console.error('Failed to update lab:', error);
+ toast.error(error?.response?.data?.message || 'Failed to update lab info');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const formatDate = (value) => {
+ if (!value) return '—';
+ return new Date(value).toLocaleString();
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading lab settings...
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Lab Admin
+
+
+ Lab Settings
+
+
+ View and edit your lab profile, charges, and media
+
+
+
+
+
+ {/* Snapshot */}
+ {lab && (
+
+
+
+
+
Lab name
+
+ {lab.name}
+
+
+
+
+
+
+
+ Home collection fee
+
+
+ {lab.generalHomeCollectionFee ? `₹${lab.generalHomeCollectionFee}` : 'Not set'}
+
+
+
+
+
+
+
Last updated
+
+ {formatDate(lab.updatedAt)}
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ Change log
+
+
+
+
+ Created
+ {formatDate(lab?.createdAt)}
+
+
+ Last updated
+ {formatDate(lab?.updatedAt)}
+
+
+ Future updates will include detailed field-level change history.
+
+
+
+
+
+
+
+
+ At a glance
+
+
+
+
+ Tests offered
+ {lab?.tests?.length || 0}
+
+
+ Staff on roster
+ {lab?.staff?.length || 0}
+
+
+ Home collection enabled
+
+ {lab?.tests?.some((t) => t.homeCollectionAvailable) ? 'Yes' : 'No'}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/quicklab/LabAdminManageStaff.jsx b/client/src/pages/quicklab/LabAdminManageStaff.jsx
new file mode 100644
index 0000000..eaf0d91
--- /dev/null
+++ b/client/src/pages/quicklab/LabAdminManageStaff.jsx
@@ -0,0 +1,304 @@
+import { useEffect, useMemo, useState } from 'react';
+import '../../quicklab.css';
+import { toast } from 'react-toastify';
+import { Search, Mail, IdCard, UserPlus, Users, Trash2, X } from 'lucide-react';
+import {
+ searchLabStaff,
+ addStaffToLab,
+ removeStaffFromLab,
+ getLabStaff,
+} from '../../service/labAdminService';
+
+export default function LabAdminManageStaff() {
+ const [query, setQuery] = useState({ email: '', staffId: '' });
+ const [searchLoading, setSearchLoading] = useState(false);
+ const [result, setResult] = useState(null);
+ const [listLoading, setListLoading] = useState(true);
+ const [staffList, setStaffList] = useState([]);
+ const [adding, setAdding] = useState(false);
+ const [removingIds, setRemovingIds] = useState({});
+ const [showAddPanel, setShowAddPanel] = useState(false);
+ const [labSearch, setLabSearch] = useState('');
+
+ const canSearch = useMemo(() => {
+ return (
+ (query.email && query.email.includes('@')) || (!!query.staffId && query.staffId.length >= 8)
+ );
+ }, [query]);
+
+ const filtered = useMemo(() => {
+ const q = labSearch.trim().toLowerCase();
+ if (!q) return staffList;
+ return staffList.filter((s) => {
+ const name = `${s.firstName || ''} ${s.lastName || ''}`.trim().toLowerCase();
+ const email = (s.userId?.email || '').toLowerCase();
+ const id = (s._id || '').toLowerCase();
+ return name.includes(q) || email.includes(q) || id.includes(q);
+ });
+ }, [labSearch, staffList]);
+
+ const loadStaffList = async () => {
+ try {
+ setListLoading(true);
+ const res = await getLabStaff();
+ setStaffList(res?.staff || []);
+ } catch (err) {
+ console.error(err);
+ toast.error(err?.message || 'Failed to load staff list');
+ } finally {
+ setListLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadStaffList();
+ }, []);
+
+ const handleSearch = async (e) => {
+ e?.preventDefault?.();
+ if (!canSearch) return;
+ try {
+ setSearchLoading(true);
+ setResult(null);
+ const params = {};
+ if (query.email) params.email = query.email.trim();
+ if (query.staffId) params.staffId = query.staffId.trim();
+ const res = await searchLabStaff(params);
+ setResult(res?.staff);
+ toast.success(res?.message || 'Staff found');
+ } catch (err) {
+ console.error(err);
+ setResult(null);
+ toast.error(err?.message || 'No staff found');
+ } finally {
+ setSearchLoading(false);
+ }
+ };
+
+ const handleAddStaff = async (staffId) => {
+ try {
+ setAdding(true);
+ await addStaffToLab(staffId);
+ toast.success('Staff added to your lab');
+ setResult(null);
+ setQuery({ email: '', staffId: '' });
+ setShowAddPanel(false);
+ await loadStaffList();
+ } catch (err) {
+ console.error(err);
+ toast.error(err?.response?.data?.message || 'Failed to add staff');
+ } finally {
+ setAdding(false);
+ }
+ };
+
+ const handleRemoveStaff = async (staffId) => {
+ try {
+ setRemovingIds((s) => ({ ...s, [staffId]: true }));
+ await removeStaffFromLab(staffId);
+ toast.success('Staff removed');
+ await loadStaffList();
+ } catch (err) {
+ console.error(err);
+ toast.error(err?.response?.data?.message || 'Failed to remove staff');
+ } finally {
+ setRemovingIds((s) => ({ ...s, [staffId]: false }));
+ }
+ };
+
+ return (
+
+
+ {/* Page Header */}
+
+
+
Manage Staff
+
Search, add and manage your lab staff
+
+
+
+
+
+
+
+ {/* Add Staff Panel (search unassigned) */}
+ {showAddPanel && (
+
+
+
Add Staff
+
+
+
+ Search unassigned staff by email or Staff ID and add to your lab.
+
+
+
+ {/* Search Result */}
+ {result && (
+
+
Result
+
+
+
+ {result.firstName} {result.lastName}
+
+
{result.email}
+
+ Role: {result.role?.replace(/_/g, ' ')}
+
+
+
+
+
+ )}
+
+ )}
+
+ {/* Current Staff List */}
+
+
+
Your Staff
+
+
+
+ setLabSearch(e.target.value)}
+ placeholder="Search in your staff (name, email, ID)"
+ className="w-80 pl-10 pr-3 py-2 border border-lab-black-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-lab-yellow-500 text-lab-black-900"
+ />
+
+
{staffList.length} total
+
+
+
+
+
+
+
+ |
+ Name
+ |
+
+ Email
+ |
+
+ Role
+ |
+
+ Phone
+ |
+ |
+
+
+
+ {listLoading ? (
+
+ |
+ Loading staff...
+ |
+
+ ) : filtered.length === 0 ? (
+
+ |
+ No matching staff.
+ |
+
+ ) : (
+ filtered.map((s) => (
+
+ |
+ {s.firstName} {s.lastName}
+ |
+ {s.userId?.email || '-'} |
+
+ {s.role?.replace(/_/g, ' ')}
+ |
+ {s.phoneNumber || '-'} |
+
+
+ |
+
+ ))
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/quicklab/LabAdminManageTests.jsx b/client/src/pages/quicklab/LabAdminManageTests.jsx
new file mode 100644
index 0000000..81795b2
--- /dev/null
+++ b/client/src/pages/quicklab/LabAdminManageTests.jsx
@@ -0,0 +1,608 @@
+import { useEffect, useState } from 'react';
+import {
+ Plus,
+ Edit,
+ Trash2,
+ Search,
+ X,
+ Save,
+ FlaskConical,
+ DollarSign,
+ Clock,
+ Home,
+ AlertCircle,
+ CheckCircle,
+} from 'lucide-react';
+import { toast } from 'react-toastify';
+import { getLabInfo, addLabTest, updateLabTest } from '../../service/labAdminService';
+import '../../quicklab.css';
+
+export default function LabAdminManageTests() {
+ const [tests, setTests] = useState([]);
+ const [filteredTests, setFilteredTests] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [showModal, setShowModal] = useState(false);
+ const [editingTest, setEditingTest] = useState(null);
+ const [formData, setFormData] = useState({
+ testName: '',
+ testLabel: '',
+ testCode: '',
+ description: '',
+ category: '',
+ price: '',
+ discountedPrice: '',
+ preparationInstructions: '',
+ sampleType: '',
+ reportDeliveryTime: '',
+ homeCollectionAvailable: false,
+ requiresEquipment: false,
+ equipmentDetails: '',
+ isActive: true,
+ });
+
+ const testCategories = [
+ { value: 'blood_test', label: 'Blood Test' },
+ { value: 'urine_test', label: 'Urine Test' },
+ { value: 'stool_test', label: 'Stool Test' },
+ { value: 'imaging', label: 'Imaging (X-Ray, CT, MRI)' },
+ { value: 'ecg', label: 'ECG' },
+ { value: 'other', label: 'Other' },
+ ];
+
+ const sampleTypes = ['Blood', 'Urine', 'Stool', 'Saliva', 'Swab', 'Tissue', 'Other'];
+
+ useEffect(() => {
+ fetchTests();
+ }, []);
+
+ useEffect(() => {
+ applyFilters();
+ }, [tests, searchQuery]);
+
+ const fetchTests = async () => {
+ try {
+ setLoading(true);
+ const response = await getLabInfo();
+ const data = response.data || response;
+ setTests(data.lab?.tests || []);
+ } catch (error) {
+ console.error('Failed to fetch tests:', error);
+ toast.error('Failed to load tests');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const applyFilters = () => {
+ let filtered = [...tests];
+
+ if (searchQuery.trim()) {
+ const query = searchQuery.toLowerCase();
+ filtered = filtered.filter(
+ (test) =>
+ test.testName?.toLowerCase().includes(query) ||
+ test.testCode?.toLowerCase().includes(query) ||
+ test.category?.toLowerCase().includes(query)
+ );
+ }
+
+ setFilteredTests(filtered);
+ };
+
+ const handleOpenModal = (test = null) => {
+ if (test) {
+ setEditingTest(test);
+ setFormData({
+ testName: test.testName || '',
+ testCode: test.testCode || '',
+ description: test.description || '',
+ category: test.category || '',
+ price: test.price || '',
+ discountedPrice: test.discountedPrice || '',
+ preparationInstructions: test.preparationInstructions || '',
+ sampleType: test.sampleType || '',
+ reportDeliveryTime: test.reportDeliveryTime || '',
+ homeCollectionAvailable: test.homeCollectionAvailable || false,
+ requiresEquipment: test.requiresEquipment || false,
+ equipmentDetails: test.equipmentDetails || '',
+ isActive: test.isActive !== false,
+ });
+ } else {
+ setEditingTest(null);
+ setFormData({
+ testName: '',
+ testCode: '',
+ description: '',
+ category: '',
+ price: '',
+ discountedPrice: '',
+ preparationInstructions: '',
+ sampleType: '',
+ reportDeliveryTime: '',
+ homeCollectionAvailable: false,
+ requiresEquipment: false,
+ equipmentDetails: '',
+ isActive: true,
+ });
+ }
+ setShowModal(true);
+ };
+
+ const handleCloseModal = () => {
+ setShowModal(false);
+ setEditingTest(null);
+ };
+
+ const handleInputChange = (e) => {
+ const { name, value, type, checked } = e.target;
+ setFormData((prev) => ({
+ ...prev,
+ [name]: type === 'checkbox' ? checked : value,
+ }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ // Validation
+ if (!formData.testName || !formData.price || !formData.category) {
+ toast.error('Please fill in all required fields');
+ return;
+ }
+
+ try {
+ if (editingTest) {
+ await updateLabTest(editingTest._id, formData);
+ toast.success('Test updated successfully');
+ } else {
+ await addLabTest(formData);
+ toast.success('Test added successfully');
+ }
+ handleCloseModal();
+ fetchTests();
+ } catch (error) {
+ toast.error(error.response?.data?.message || 'Failed to save test');
+ }
+ };
+
+ const handleToggleActive = async (test) => {
+ try {
+ await updateLabTest(test._id, { isActive: !test.isActive });
+ toast.success(`Test ${!test.isActive ? 'activated' : 'deactivated'} successfully`);
+ fetchTests();
+ } catch (error) {
+ toast.error('Failed to update test status');
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Manage Tests
+
+
+ Add, edit, and manage lab tests offered by your lab
+
+
+
+
+
+ {/* Search Bar */}
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Search by test name, code, or category..."
+ className="w-full pl-10 pr-4 py-2 border border-lab-black-300 dark:border-lab-black-600 rounded-lg bg-white dark:bg-lab-black-900 text-lab-black-900 dark:text-lab-black-50 focus:ring-2 focus:ring-lab-yellow-500 focus:border-transparent transition-colors"
+ />
+
+
+
+ {/* Tests Grid */}
+ {loading ? (
+
+ ) : filteredTests.length === 0 ? (
+
+
+
+ No tests found
+
+
+ {searchQuery ? 'Try adjusting your search' : 'Start by adding your first test'}
+
+ {!searchQuery && (
+
+ )}
+
+ ) : (
+
+ {filteredTests.map((test) => (
+
+ {/* Test Header */}
+
+
+
+ {test.testName}
+
+ {test.testCode && (
+
+ Code: {test.testCode}
+
+ )}
+
+
+
+
+
+
+
+ {/* Status Badge */}
+ {test.isActive === false && (
+
+
+ Inactive
+
+
+ )}
+
+ {/* Category */}
+
+
+ {test.category?.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())}
+
+
+
+ {/* Description */}
+ {test.description && (
+
+ {test.description}
+
+ )}
+
+ {/* Price */}
+
+
+
+ ₹{test.price}
+
+ {test.discountedPrice && test.discountedPrice < test.price && (
+
+ ₹{test.discountedPrice}
+
+ )}
+
+
+ {/* Details */}
+
+ {test.sampleType && (
+
+
+ Sample: {test.sampleType}
+
+ )}
+ {test.reportDeliveryTime && (
+
+
+ Report: {test.reportDeliveryTime}
+
+ )}
+ {test.homeCollectionAvailable && (
+
+
+ Home Collection Available
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ {/* Add/Edit Modal */}
+ {showModal && (
+
+
e.stopPropagation()}
+ >
+
+
+ {editingTest ? 'Edit Test' : 'Add New Test'}
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/client/src/pages/quicklab/LabAdminProfileComplete.jsx b/client/src/pages/quicklab/LabAdminProfileComplete.jsx
new file mode 100644
index 0000000..5e965aa
--- /dev/null
+++ b/client/src/pages/quicklab/LabAdminProfileComplete.jsx
@@ -0,0 +1,239 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Upload, User, Phone } from 'lucide-react';
+import Loading from '../../components/ui/Loading';
+import { createLabAdminProfile } from '../../service/labAdminService';
+import '../../quicklab.css';
+
+export default function LabAdminProfileComplete() {
+ const navigate = useNavigate();
+
+ const [fields, setFields] = useState({
+ firstName: '',
+ lastName: '',
+ phoneNumber: '',
+ profilePicture: null,
+ });
+ const [profilePreview, setProfilePreview] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const handleChange = (e) => {
+ const { name, value, files } = e.target;
+ if (name === 'profilePicture' && files && files[0]) {
+ setFields((f) => ({
+ ...f,
+ profilePicture: files[0],
+ }));
+ setProfilePreview(URL.createObjectURL(files[0]));
+ } else {
+ setFields((f) => ({
+ ...f,
+ [name]: value,
+ }));
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+ try {
+ const { firstName, lastName, phoneNumber } = fields;
+ if (!firstName || !lastName || !phoneNumber) {
+ setError('Please fill out all required fields');
+ setLoading(false);
+ return;
+ }
+
+ const profileData = { firstName, lastName, phoneNumber };
+ await createLabAdminProfile(profileData);
+ navigate('/quick-lab/add-lab', { replace: true });
+ } catch (err) {
+ setError(err?.response?.data?.message || 'Failed to complete profile');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+ Complete Your Profile
+
+
+ Help us get to know you better. Your profile information will help us serve you best.
+
+
+
+ {/* Main Card */}
+
+ {error && (
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/quicklab/LabDetails.jsx b/client/src/pages/quicklab/LabDetails.jsx
new file mode 100644
index 0000000..0bdeaee
--- /dev/null
+++ b/client/src/pages/quicklab/LabDetails.jsx
@@ -0,0 +1,692 @@
+import { useEffect, useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import {
+ MapPin,
+ Phone,
+ Mail,
+ Globe,
+ Star,
+ Clock,
+ Home,
+ ArrowLeft,
+ Calendar,
+ FileText,
+ CheckCircle,
+ AlertCircle,
+} from 'lucide-react';
+import { getLabById, bookLabAppointment } from '../../service/labService';
+import { getPatientPrescriptions } from '../../service/prescriptionApiSevice';
+import { useAuth } from '../../context/authContext';
+import { toast } from 'react-toastify';
+import '../../quicklab.css';
+
+export default function LabDetails() {
+ const { labId } = useParams();
+ const navigate = useNavigate();
+ const { user, isAuthenticated } = useAuth();
+
+ const [lab, setLab] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [recommendedTests, setRecommendedTests] = useState([]);
+ const [showBookingModal, setShowBookingModal] = useState(false);
+ const [selectedTests, setSelectedTests] = useState([]);
+ const [bookingData, setBookingData] = useState({
+ appointmentDate: '',
+ appointmentTime: '',
+ collectionType: 'lab_visit',
+ collectionAddress: {
+ street: '',
+ city: '',
+ state: '',
+ zipCode: '',
+ country: 'India',
+ },
+ notes: '',
+ });
+ const [booking, setBooking] = useState(false);
+
+ useEffect(() => {
+ fetchLabDetails();
+ if (isAuthenticated && user?.role === 'patient') {
+ fetchRecommendedTests();
+ }
+ }, [labId, isAuthenticated, user]);
+
+ const fetchLabDetails = async () => {
+ try {
+ setLoading(true);
+ const response = await getLabById(labId);
+ setLab(response.data);
+ } catch (err) {
+ console.error('Failed to fetch lab details:', err);
+ toast.error('Failed to load lab details');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchRecommendedTests = async () => {
+ try {
+ const response = await getPatientPrescriptions();
+ if (response.prescriptions && response.prescriptions.length > 0) {
+ // Get tests from most recent prescriptions (last 30 days)
+ const thirtyDaysAgo = new Date();
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
+
+ const recentPrescriptions = response.prescriptions.filter(
+ (p) => new Date(p.date) >= thirtyDaysAgo
+ );
+
+ const testsFromPrescriptions = recentPrescriptions.flatMap((p) =>
+ (p.tests || []).map((test) => ({
+ name: test.name,
+ instructions: test.instructions,
+ prescriptionDate: p.date,
+ doctorName: p.doctorId ? `${p.doctorId.firstName} ${p.doctorId.lastName}` : 'Unknown',
+ }))
+ );
+
+ setRecommendedTests(testsFromPrescriptions);
+ }
+ } catch (err) {
+ console.error('Failed to fetch prescriptions:', err);
+ }
+ };
+
+ const handleTestSelection = (test) => {
+ setSelectedTests((prev) => {
+ const exists = prev.find((t) => t._id === test._id);
+ if (exists) {
+ return prev.filter((t) => t._id !== test._id);
+ } else {
+ return [...prev, test];
+ }
+ });
+ };
+
+ const handleBookAppointment = () => {
+ if (!isAuthenticated) {
+ toast.error('Please login to book an appointment');
+ navigate('/login');
+ return;
+ }
+
+ if (user?.role !== 'patient') {
+ toast.error('Only patients can book lab appointments');
+ return;
+ }
+
+ if (selectedTests.length === 0) {
+ toast.error('Please select at least one test');
+ return;
+ }
+
+ setShowBookingModal(true);
+ };
+
+ const submitBooking = async (e) => {
+ e.preventDefault();
+
+ if (!bookingData.appointmentDate || !bookingData.appointmentTime) {
+ toast.error('Please select date and time');
+ return;
+ }
+
+ try {
+ setBooking(true);
+ await bookLabAppointment({
+ labId: labId,
+ tests: selectedTests.map((t) => t._id),
+ collectionType: bookingData.collectionType,
+ appointmentDate: bookingData.appointmentDate,
+ appointmentTime: bookingData.appointmentTime,
+ collectionAddress:
+ bookingData.collectionType === 'home_collection'
+ ? bookingData.collectionAddress
+ : undefined,
+ notes: bookingData.notes,
+ });
+
+ toast.success('Appointment booked successfully!');
+ setShowBookingModal(false);
+ setSelectedTests([]);
+ setBookingData({
+ appointmentDate: '',
+ appointmentTime: '',
+ collectionType: 'lab_visit',
+ collectionAddress: {
+ street: '',
+ city: '',
+ state: '',
+ zipCode: '',
+ country: 'India',
+ },
+ notes: '',
+ });
+ } catch (err) {
+ console.error('Booking failed:', err);
+ toast.error(err.message || 'Failed to book appointment');
+ } finally {
+ setBooking(false);
+ }
+ };
+
+ const calculateTotal = () => {
+ const testsTotal = selectedTests.reduce((sum, test) => sum + (test.price || 0), 0);
+ const homeCollectionFee =
+ bookingData.collectionType === 'home_collection' && lab?.generalHomeCollectionFee
+ ? lab.generalHomeCollectionFee
+ : 0;
+ return testsTotal + homeCollectionFee;
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading lab details...
+
+
+ );
+ }
+
+ if (!lab) {
+ return (
+
+
+
Lab not found
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Back Button */}
+
+
+
+ {/* Lab Details - Left Side */}
+
+ {/* Header Card */}
+
+ {lab.logo && (
+

+ )}
+
+
+
{lab.name}
+
+ {/* Rating */}
+ {lab.ratings?.average > 0 && (
+
+
+
+
+ {lab.ratings.average.toFixed(1)}
+
+
+
+ ({lab.ratings.count} reviews)
+
+
+ )}
+
+ {lab.description &&
{lab.description}
}
+
+ {/* Contact Info */}
+
+ {lab.address && (
+
+
+
+
Address
+
+ {lab.address.formattedAddress ||
+ `${lab.address.city}, ${lab.address.state} ${lab.address.zipCode}`}
+
+
+
+ )}
+
+ {lab.contact?.phone && (
+
+
+
+
Phone
+
{lab.contact.phone}
+
+
+ )}
+
+ {lab.contact?.email && (
+
+
+
+
Email
+
{lab.contact.email}
+
+
+ )}
+
+ {lab.contact?.website && (
+
+ )}
+
+
+ {/* Home Collection */}
+ {lab.generalHomeCollectionFee !== undefined && (
+
+
+
+
+
+ Home Collection Available
+
+
+ Additional fee: ₹{lab.generalHomeCollectionFee}
+
+
+
+
+ )}
+
+
+
+ {/* Recommended Tests from Prescriptions */}
+ {recommendedTests.length > 0 && (
+
+
+
+
+
+ Recommended Tests from Your Prescriptions
+
+
+
+
+ {recommendedTests.map((test, index) => (
+
+
+
+
{test.name}
+ {test.instructions && (
+
{test.instructions}
+ )}
+
+ Prescribed by Dr. {test.doctorName} on{' '}
+ {new Date(test.prescriptionDate).toLocaleDateString()}
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+ {/* Available Tests */}
+
+
+
Available Tests
+
+ {lab.tests && lab.tests.length > 0 ? (
+
+ {lab.tests
+ .filter((test) => test.isActive !== false)
+ .map((test) => (
+
t._id === test._id)
+ ? 'border-lab-yellow-500 bg-lab-yellow-50'
+ : 'border-lab-black-200 hover:border-lab-yellow-300'
+ }`}
+ onClick={() => handleTestSelection(test)}
+ >
+
+
+
+
+ {test.testName}
+
+ {selectedTests.find((t) => t._id === test._id) && (
+
+ )}
+
+
+ {test.testCode && (
+
Code: {test.testCode}
+ )}
+
+ {test.description && (
+
+ {test.description}
+
+ )}
+
+
+ {test.category && (
+
+ {test.category.replace(/_/g, ' ')}
+
+ )}
+
+ {test.homeCollectionAvailable && (
+
+
+ Home Collection
+
+ )}
+
+ {test.reportDeliveryTime && (
+
+
+ {test.reportDeliveryTime}
+
+ )}
+
+
+ {test.preparationInstructions && (
+
+ Preparation: {test.preparationInstructions}
+
+ )}
+
+
+
+
+
+ ))}
+
+ ) : (
+
No tests available
+ )}
+
+
+
+
+ {/* Booking Summary - Right Side */}
+
+
+
+
Booking Summary
+
+ {selectedTests.length > 0 ? (
+ <>
+
+ {selectedTests.map((test) => (
+
+ {test.testName}
+ ₹{test.price}
+
+ ))}
+
+
+
+
+ Total
+ ₹{calculateTotal()}
+
+
+
+
+
+ {!isAuthenticated && (
+
+ Please login as a patient to book
+
+ )}
+ >
+ ) : (
+
+ Select tests to book an appointment
+
+ )}
+
+
+
+
+
+
+ {/* Booking Modal */}
+ {showBookingModal && (
+
+
+
+
Book Lab Appointment
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/client/src/pages/quicklab/LabSearchResults.jsx b/client/src/pages/quicklab/LabSearchResults.jsx
new file mode 100644
index 0000000..2c2ce83
--- /dev/null
+++ b/client/src/pages/quicklab/LabSearchResults.jsx
@@ -0,0 +1,505 @@
+import { useEffect, useState } from 'react';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+import { Search, MapPin, Filter, X, Star, Phone, Mail, Clock, Home } from 'lucide-react';
+import { searchLabs } from '../../service/labService';
+import { detectUserCity } from '../../service/geolocationService';
+import '../../quicklab.css';
+
+export default function LabSearchResults() {
+ const navigate = useNavigate();
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const [labs, setLabs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [detectedCity, setDetectedCity] = useState('');
+ const [showFilters, setShowFilters] = useState(false);
+ const [showCityModal, setShowCityModal] = useState(false);
+ const [newCity, setNewCity] = useState('');
+
+ // Search and filter states
+ const [searchQuery, setSearchQuery] = useState(searchParams.get('query') || '');
+ const [filters, setFilters] = useState({
+ city: searchParams.get('city') || '',
+ testCategory: searchParams.get('testCategory') || '',
+ minPrice: searchParams.get('minPrice') || '',
+ maxPrice: searchParams.get('maxPrice') || '',
+ homeCollection: searchParams.get('homeCollection') || '',
+ sort: searchParams.get('sort') || 'rating_desc',
+ });
+
+ const [pagination, setPagination] = useState({
+ page: 1,
+ limit: 20,
+ total: 0,
+ pages: 0,
+ });
+
+ // Extract unique test categories from current lab results
+ const availableTestCategories = () => {
+ const categoriesSet = new Set();
+ labs.forEach((lab) => {
+ if (lab.tests && Array.isArray(lab.tests)) {
+ lab.tests.forEach((test) => {
+ if (test.category) {
+ categoriesSet.add(test.category);
+ }
+ });
+ }
+ });
+ return Array.from(categoriesSet).sort();
+ };
+
+ useEffect(() => {
+ initializeSearch();
+ }, []);
+
+ useEffect(() => {
+ if (filters.city) {
+ performSearch();
+ }
+ }, [filters, searchQuery, pagination.page]);
+
+ const initializeSearch = async () => {
+ try {
+ // Get user's city if not already set
+ let city = filters.city;
+ if (!city) {
+ setLoading(true);
+ city = await detectUserCity();
+ if (city) {
+ setDetectedCity(city);
+ setFilters((prev) => ({ ...prev, city }));
+ } else {
+ setLoading(false);
+ }
+ } else {
+ setDetectedCity(city);
+ }
+ } catch (err) {
+ console.error('Failed to detect city:', err);
+ setLoading(false);
+ }
+ };
+
+ const performSearch = async () => {
+ try {
+ setLoading(true);
+ const params = {
+ query: searchQuery,
+ ...filters,
+ page: pagination.page,
+ limit: pagination.limit,
+ };
+
+ // Remove empty params
+ Object.keys(params).forEach((key) => {
+ if (!params[key]) delete params[key];
+ });
+
+ const response = await searchLabs(params);
+ setLabs(response.data || []);
+ setPagination((prev) => ({ ...prev, ...response.pagination }));
+ } catch (err) {
+ console.error('Search failed:', err);
+ setLabs([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSearch = (e) => {
+ e.preventDefault();
+ setPagination((prev) => ({ ...prev, page: 1 }));
+ updateURLParams();
+ };
+
+ const updateURLParams = () => {
+ const params = new URLSearchParams();
+ if (searchQuery) params.set('query', searchQuery);
+ if (filters.city) params.set('city', filters.city);
+ if (filters.testCategory) params.set('testCategory', filters.testCategory);
+ if (filters.minPrice) params.set('minPrice', filters.minPrice);
+ if (filters.maxPrice) params.set('maxPrice', filters.maxPrice);
+ if (filters.homeCollection) params.set('homeCollection', filters.homeCollection);
+ if (filters.sort) params.set('sort', filters.sort);
+ setSearchParams(params);
+ };
+
+ const handleFilterChange = (key, value) => {
+ setFilters((prev) => ({ ...prev, [key]: value }));
+ setPagination((prev) => ({ ...prev, page: 1 }));
+ };
+
+ const clearFilters = () => {
+ setFilters({
+ city: detectedCity,
+ testCategory: '',
+ minPrice: '',
+ maxPrice: '',
+ homeCollection: '',
+ sort: 'rating_desc',
+ });
+ setPagination((prev) => ({ ...prev, page: 1 }));
+ };
+
+ const handleLabClick = (labId) => {
+ navigate(`/quick-lab/lab/${labId}`);
+ };
+ const handleChangeLocation = () => {
+ setNewCity(filters.city || '');
+ setShowCityModal(true);
+ };
+
+ const handleSaveCity = () => {
+ if (newCity.trim()) {
+ setDetectedCity(newCity.trim());
+ setFilters((prev) => ({ ...prev, city: newCity.trim() }));
+ setPagination((prev) => ({ ...prev, page: 1 }));
+ setShowCityModal(false);
+ }
+ };
+ return (
+
+ {/* Search Header */}
+
+
+
+
+ {/* Location Display */}
+ {detectedCity && (
+
+
+
+ Showing results in {detectedCity}
+
+
+
+ )}
+
+
+
+ {/* City Change Modal */}
+ {showCityModal && (
+
setShowCityModal(false)}
+ >
+
e.stopPropagation()}
+ >
+
+
Change Location
+
+
+
+
+ setNewCity(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleSaveCity()}
+ placeholder="e.g., Mumbai, Delhi, Bangalore"
+ className="w-full px-4 py-2 border border-lab-black-300 rounded-lg focus:ring-2 focus:ring-lab-yellow-500 focus:border-transparent"
+ autoFocus
+ />
+
+
+
+
+
+
+
+ )}
+
+ {/* Filters Sidebar */}
+ {showFilters && (
+
setShowFilters(false)}>
+
e.stopPropagation()}
+ >
+
+
Filters
+
+
+
+
+ {/* City */}
+
+
+ handleFilterChange('city', e.target.value)}
+ placeholder="Enter city"
+ className="w-full p-2 border border-lab-black-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-lab-yellow-500"
+ />
+
+
+ {/* Test Category */}
+
+
+
+
+
+ {/* Price Range */}
+
+
+ {/* Home Collection */}
+
+
+
+
+ {/* Sort */}
+
+
+
+
+
+ {/* Clear Filters */}
+
+
+
+
+ )}
+
+ {/* Results */}
+
+ {loading ? (
+
+ ) : labs.length === 0 ? (
+
+
No labs found
+
Try adjusting your search criteria
+
+ ) : (
+ <>
+
+
+ Found {pagination.total} labs
+
+
+
+
+ {labs.map((lab) => (
+
handleLabClick(lab._id)}
+ className="card-quicklab bg-white border border-lab-black-100 hover:shadow-lg transition-shadow cursor-pointer"
+ >
+ {/* Lab Logo */}
+ {lab.logo && (
+

+ )}
+
+
+
{lab.name}
+
+ {/* Rating */}
+ {lab.ratings?.average > 0 && (
+
+
+
+
+ {lab.ratings.average.toFixed(1)}
+
+
+
+ ({lab.ratings.count} reviews)
+
+
+ )}
+
+ {/* Address */}
+ {lab.address && (
+
+
+
+ {lab.address.city && `${lab.address.city}, `}
+ {lab.address.state}
+
+
+ )}
+
+ {/* Contact */}
+ {lab.contact?.phone && (
+
+
+
{lab.contact.phone}
+
+ )}
+
+ {/* Tests Count */}
+ {lab.tests && lab.tests.length > 0 && (
+
+
+ {lab.tests.length} tests available
+
+
+ )}
+
+ {/* Home Collection Badge */}
+ {lab.generalHomeCollectionFee !== undefined && (
+
+
+
+ Home Collection
+
+
+ )}
+
+
+ ))}
+
+
+ {/* Pagination */}
+ {pagination.pages > 1 && (
+
+
+
+ Page {pagination.page} of {pagination.pages}
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+}
diff --git a/client/src/pages/quicklab/LabStaffDashboard.jsx b/client/src/pages/quicklab/LabStaffDashboard.jsx
new file mode 100644
index 0000000..cc3428c
--- /dev/null
+++ b/client/src/pages/quicklab/LabStaffDashboard.jsx
@@ -0,0 +1,170 @@
+import { useEffect, useState } from 'react';
+import { useAuth } from '../../context/authContext';
+import '../../quicklab.css';
+import { CheckSquare, Clock, AlertCircle, Zap, Home, Phone, MoreVertical } from 'lucide-react';
+
+export default function LabStaffDashboard() {
+ const { user } = useAuth();
+ const [staffInfo] = useState({
+ firstName: 'John',
+ lastName: 'Doe',
+ role: 'Phlebotomist',
+ });
+
+ // Demo stats
+ const stats = [
+ { label: "Today's Tasks", value: 8, icon: CheckSquare },
+ { label: 'Pending Tasks', value: 3, icon: Clock },
+ { label: 'Home Collections', value: 2, icon: Home },
+ ];
+
+ // Demo upcoming assignments
+ const upcomingAssignments = [
+ {
+ id: 1,
+ type: 'Blood Collection',
+ patient: 'Patient Name',
+ time: '10:30 AM',
+ location: 'Home',
+ status: 'pending',
+ },
+ {
+ id: 2,
+ type: 'Sample Processing',
+ patient: 'Lab Counter',
+ time: '2:00 PM',
+ location: 'Lab',
+ status: 'pending',
+ },
+ {
+ id: 3,
+ type: 'Report Preparation',
+ patient: 'Sample ID: #12345',
+ time: '4:00 PM',
+ location: 'Lab',
+ status: 'in-progress',
+ },
+ ];
+
+ // Quick action buttons
+ const quickActions = [
+ { label: 'View My Tasks', icon: Zap, action: () => alert('View tasks') },
+ { label: 'Home Collections', icon: Home, action: () => alert('View home collections') },
+ { label: 'My Profile', icon: Phone, action: () => alert('View profile') },
+ ];
+
+ return (
+
+
+ {/* Header */}
+
+
+ Welcome, {staffInfo.firstName}
+
+
{staffInfo.role} • Ready for today's tasks
+
+
+ {/* Stats Grid */}
+
+ {stats.map((stat, idx) => {
+ const Icon = stat.icon;
+ return (
+
+
+
+
{stat.label}
+
{stat.value}
+
+
+
+
+
+
+ );
+ })}
+
+
+ {/* Quick Actions */}
+
+
Quick Actions
+
+ {quickActions.map((action, idx) => {
+ const Icon = action.icon;
+ return (
+
+ );
+ })}
+
+
+
+ {/* Upcoming Assignments */}
+
+
+
Today's Assignments
+
+
+
+
+ {upcomingAssignments.map((assignment) => (
+
+
+
+ {assignment.type}
+
+
+
+
+
{assignment.patient}
+
+
+
+
+ {assignment.time}
+
+
+
+ {assignment.location}
+
+
+
+
+
+ {assignment.status === 'in-progress' ? 'In Progress' : 'Pending'}
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/client/src/pages/quicklab/LabStaffMyAppointments.jsx b/client/src/pages/quicklab/LabStaffMyAppointments.jsx
new file mode 100644
index 0000000..46457b8
--- /dev/null
+++ b/client/src/pages/quicklab/LabStaffMyAppointments.jsx
@@ -0,0 +1,624 @@
+import { useEffect, useState } from 'react';
+import {
+ Calendar,
+ Clock,
+ User,
+ MapPin,
+ Phone,
+ Home,
+ Building2,
+ AlertCircle,
+ CheckCircle,
+ XCircle,
+ X,
+ RefreshCw,
+ ChevronRight,
+ TestTube,
+ Upload,
+} from 'lucide-react';
+import { toast } from 'react-toastify';
+import {
+ getMyAssignments,
+ updateMyAssignmentStatus,
+ uploadReportAndComplete,
+} from '../../service/labStaffService';
+import '../../quicklab.css';
+
+export default function LabStaffMyAppointments() {
+ const [assignments, setAssignments] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedAppointment, setSelectedAppointment] = useState(null);
+ const [showDetailModal, setShowDetailModal] = useState(false);
+ const [updatingStatus, setUpdatingStatus] = useState(false);
+ const [reportFile, setReportFile] = useState(null);
+ const [uploadingReport, setUploadingReport] = useState(false);
+ const [showUploadModal, setShowUploadModal] = useState(false);
+ const [uploadNotes, setUploadNotes] = useState('');
+
+ const statusColors = {
+ pending:
+ 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800',
+ confirmed:
+ 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-400 border-blue-200 dark:border-blue-800',
+ sample_collected:
+ 'bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-400 border-purple-200 dark:border-purple-800',
+ processing:
+ 'bg-indigo-100 dark:bg-indigo-900/30 text-indigo-800 dark:text-indigo-400 border-indigo-200 dark:border-indigo-800',
+ completed:
+ 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400 border-green-200 dark:border-green-800',
+ cancelled:
+ 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-400 border-red-200 dark:border-red-800',
+ };
+
+ const statusLabels = {
+ pending: 'Pending',
+ confirmed: 'Confirmed',
+ sample_collected: 'Sample Collected',
+ processing: 'Processing',
+ completed: 'Completed',
+ cancelled: 'Cancelled',
+ };
+
+ useEffect(() => {
+ fetchAssignments();
+ }, []);
+
+ const fetchAssignments = async () => {
+ try {
+ setLoading(true);
+ const response = await getMyAssignments();
+ const data = response.data || response;
+ setAssignments(data.assignments || []);
+ } catch (error) {
+ console.error('Failed to fetch assignments:', error);
+ toast.error('Failed to load assignments');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getNextAllowedStatuses = (currentStatus) => {
+ const statusOrder = ['pending', 'confirmed', 'sample_collected', 'processing', 'completed'];
+ const currentIndex = statusOrder.indexOf(currentStatus);
+
+ if (currentStatus === 'cancelled' || currentStatus === 'completed') {
+ return [];
+ }
+
+ // Staff can move forward or stay in same status
+ return statusOrder.slice(currentIndex + 1, currentIndex + 3).filter((s) => s !== 'completed');
+ };
+
+ const handleStatusUpdate = async (appointmentId, newStatus) => {
+ try {
+ setUpdatingStatus(true);
+ await updateMyAssignmentStatus(appointmentId, newStatus);
+ toast.success('Status updated successfully');
+ fetchAssignments();
+ setShowDetailModal(false);
+ } catch (error) {
+ console.error('Failed to update status:', error);
+ toast.error(error?.response?.data?.message || 'Failed to update status');
+ } finally {
+ setUpdatingStatus(false);
+ }
+ };
+
+ const handleViewDetails = (appointment) => {
+ setSelectedAppointment(appointment);
+ setShowDetailModal(true);
+ };
+
+ const handleUploadReport = async (appointmentId) => {
+ if (!reportFile) {
+ toast.error('Please select a PDF file');
+ return;
+ }
+
+ if (!reportFile.name.toLowerCase().endsWith('.pdf')) {
+ toast.error('Only PDF files are allowed');
+ return;
+ }
+
+ try {
+ setUploadingReport(true);
+ await uploadReportAndComplete(appointmentId, reportFile, uploadNotes);
+ toast.success('Report uploaded and appointment marked as completed');
+ fetchAssignments();
+ setShowUploadModal(false);
+ setShowDetailModal(false);
+ setReportFile(null);
+ setUploadNotes('');
+ } catch (error) {
+ console.error('Failed to upload report:', error);
+ toast.error(error?.response?.data?.message || 'Failed to upload report');
+ } finally {
+ setUploadingReport(false);
+ }
+ };
+
+ const formatDate = (dateString) => {
+ return new Date(dateString).toLocaleDateString('en-US', {
+ weekday: 'short',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading assignments...
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+ Lab Staff
+
+
+ My Appointments
+
+
+ View and manage your assigned appointments
+
+
+
+
+
+ {/* Summary Cards */}
+
+
+
+
+
+
+
+
Total
+
+ {assignments.length}
+
+
+
+
+
+
+
+
+
+
+
+
Pending
+
+ {assignments.filter((a) => a.status === 'pending').length}
+
+
+
+
+
+
+
+
+
+
+
+
Collected
+
+ {
+ assignments.filter(
+ (a) => a.status === 'sample_collected' || a.status === 'processing'
+ ).length
+ }
+
+
+
+
+
+
+
+
+
+
+
+
Completed
+
+ {assignments.filter((a) => a.status === 'completed').length}
+
+
+
+
+
+
+ {/* Appointments List */}
+
+
+ All Assignments
+
+
+ {assignments.length === 0 ? (
+
+
+
+ No assignments found. Check back later.
+
+
+ ) : (
+
+ {assignments.map((appointment) => (
+
+
+
+ {/* Header */}
+
+
+
+
+
+
+
+ {appointment.patientId?.firstName} {appointment.patientId?.lastName}
+
+
+ {appointment.patientId?.phoneNumber}
+
+
+
+
+ {statusLabels[appointment.status]}
+
+
+
+ {/* Details */}
+
+
+
+ {formatDate(appointment.appointmentDate)}
+
+
+
+ {appointment.appointmentTime}
+
+
+ {appointment.collectionType === 'home_collection' ? (
+
+ ) : (
+
+ )}
+
+ {appointment.collectionType === 'home_collection'
+ ? 'Home Collection'
+ : 'Visit Lab'}
+
+
+
+
+ {appointment.tests?.length || 0} test(s)
+
+
+
+ {/* Address for home collection */}
+ {appointment.collectionType === 'home_collection' &&
+ appointment.collectionAddress && (
+
+
+
+ {appointment.collectionAddress.street},{' '}
+ {appointment.collectionAddress.city},{' '}
+ {appointment.collectionAddress.state}{' '}
+ {appointment.collectionAddress.zipCode}
+
+
+ )}
+
+
+ {/* Actions */}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+ {/* Detail Modal */}
+ {showDetailModal && selectedAppointment && (
+
+
+
+
+ Appointment Details
+
+
+
+
+
+ {/* Patient Info */}
+
+
+ Patient Information
+
+
+
+ Name: {selectedAppointment.patientId?.firstName}{' '}
+ {selectedAppointment.patientId?.lastName}
+
+
+ Phone: {selectedAppointment.patientId?.phoneNumber}
+
+ {selectedAppointment.patientId?.address && (
+
+ Address: {selectedAppointment.patientId.address.street},{' '}
+ {selectedAppointment.patientId.address.city}
+
+ )}
+
+
+
+ {/* Tests */}
+
+
+ Tests
+
+
+ {selectedAppointment.tests?.map((test, idx) => (
+
+
+
+ {test.testName}
+
+ {test.testCode && (
+
+ Code: {test.testCode}
+
+ )}
+
+
+ ₹{test.price}
+
+
+ ))}
+
+
+
+ {/* Appointment Details */}
+
+
+ Appointment Details
+
+
+
+ Date: {formatDate(selectedAppointment.appointmentDate)}
+
+
+ Time: {selectedAppointment.appointmentTime}
+
+
+ Type:{' '}
+ {selectedAppointment.collectionType === 'home_collection'
+ ? 'Home Collection'
+ : 'Visit Lab'}
+
+
+ Status:{' '}
+
+ {statusLabels[selectedAppointment.status]}
+
+
+
+
+
+ {/* Collection Address */}
+ {selectedAppointment.collectionType === 'home_collection' &&
+ selectedAppointment.collectionAddress && (
+
+
+ Collection Address
+
+
+
{selectedAppointment.collectionAddress.street}
+
+ {selectedAppointment.collectionAddress.city},{' '}
+ {selectedAppointment.collectionAddress.state}{' '}
+ {selectedAppointment.collectionAddress.zipCode}
+
+
{selectedAppointment.collectionAddress.country}
+
+
+ )}
+
+ {/* Status Update Actions */}
+ {selectedAppointment.status !== 'completed' &&
+ selectedAppointment.status !== 'cancelled' && (
+
+
+ Update Status
+
+
+ {getNextAllowedStatuses(selectedAppointment.status).map((status) => (
+
+ ))}
+
+
+ Note: You cannot move back to a previous status once updated.
+
+
+ )}
+
+ {/* Upload Report and Complete */}
+ {selectedAppointment.status === 'processing' && (
+
+
+ Complete Appointment
+
+
+ Upload the lab report PDF to mark this appointment as completed.
+
+
+
+ )}
+
+ {selectedAppointment.notes && (
+
+
Notes
+
+ {selectedAppointment.notes}
+
+
+ )}
+
+
+
+ )}
+
+ {/* Upload Report Modal */}
+ {showUploadModal && (
+
+
+
+
+ Upload Lab Report
+
+
+
+
+
+
+
+
setReportFile(e.target.files[0])}
+ className="block w-full text-sm text-lab-black-900 dark:text-lab-black-100
+ file:mr-4 file:py-2 file:px-4
+ file:rounded-lg file:border-0
+ file:text-sm file:font-semibold
+ file:bg-lab-yellow-500 file:text-white
+ hover:file:bg-lab-yellow-600
+ file:cursor-pointer cursor-pointer
+ border border-lab-black-300 dark:border-lab-black-600 rounded-lg p-2
+ bg-lab-black-50 dark:bg-lab-black-700"
+ />
+ {reportFile && (
+
+ Selected: {reportFile.name}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/client/src/pages/quicklab/LabStaffProfileComplete.jsx b/client/src/pages/quicklab/LabStaffProfileComplete.jsx
new file mode 100644
index 0000000..a895530
--- /dev/null
+++ b/client/src/pages/quicklab/LabStaffProfileComplete.jsx
@@ -0,0 +1,298 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { toast } from 'react-toastify';
+import { createStaffProfile } from '../../service/labStaffService';
+import '../../quicklab.css';
+import { Upload } from 'lucide-react';
+
+export default function LabStaffProfileComplete() {
+ const navigate = useNavigate();
+ const [loading, setLoading] = useState(false);
+ const [profilePicture, setProfilePicture] = useState(null);
+ const [previewUrl, setPreviewUrl] = useState(null);
+ const [formData, setFormData] = useState({
+ firstName: '',
+ lastName: '',
+ phoneNumber: '',
+ role: 'assistant',
+ qualifications: [],
+ experience: 0,
+ });
+ const [qualificationInput, setQualificationInput] = useState('');
+
+ const roles = ['technician', 'phlebotomist', 'assistant', 'sample_collector'];
+
+ const handleChange = (e) => {
+ const { name, value, type } = e.target;
+ if (type === 'file') {
+ const file = e.target.files[0];
+ if (file) {
+ setProfilePicture(file);
+ setPreviewUrl(URL.createObjectURL(file));
+ }
+ } else if (name === 'experience') {
+ setFormData({ ...formData, [name]: Number(value) });
+ } else {
+ setFormData({ ...formData, [name]: value });
+ }
+ };
+
+ const handleAddQualification = () => {
+ if (qualificationInput.trim()) {
+ setFormData({
+ ...formData,
+ qualifications: [...formData.qualifications, qualificationInput.trim()],
+ });
+ setQualificationInput('');
+ }
+ };
+
+ const handleRemoveQualification = (index) => {
+ setFormData({
+ ...formData,
+ qualifications: formData.qualifications.filter((_, i) => i !== index),
+ });
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!formData.firstName || !formData.lastName || !formData.phoneNumber) {
+ toast.error('Please fill in all required fields');
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ // The createStaffProfile expects individual parameters: formData and profilePicture
+ await createStaffProfile(formData, profilePicture);
+ toast.success('Profile created successfully!');
+ navigate('/quick-lab/staff-waiting', { replace: true });
+ } catch (error) {
+ console.error('Error creating staff profile:', error);
+ toast.error(error.message || 'Failed to create profile');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
Complete Your Profile
+
Help us get to know you better
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/quicklab/LabStaffWaitingForAssignment.jsx b/client/src/pages/quicklab/LabStaffWaitingForAssignment.jsx
new file mode 100644
index 0000000..b6faaef
--- /dev/null
+++ b/client/src/pages/quicklab/LabStaffWaitingForAssignment.jsx
@@ -0,0 +1,142 @@
+import { useEffect, useState } from 'react';
+import { useAuth } from '../../context/authContext';
+import { checkStaffProfileExists } from '../../service/labStaffService';
+import '../../quicklab.css';
+import { Copy, CheckCircle, Clock } from 'lucide-react';
+
+export default function LabStaffWaitingForAssignment() {
+ const { user } = useAuth();
+ const [staffId, setStaffId] = useState(null);
+ const [copied, setCopied] = useState(false);
+ const [isAssigned, setIsAssigned] = useState(false);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const checkStatus = async () => {
+ try {
+ const res = await checkStaffProfileExists();
+ setStaffId(res.staffId);
+ setIsAssigned(res.isAssignedToLab);
+ setLoading(false);
+
+ // Poll every 5 seconds to check if assigned
+ if (!res.isAssignedToLab) {
+ const interval = setInterval(async () => {
+ const updated = await checkStaffProfileExists();
+ if (updated.isAssignedToLab) {
+ setIsAssigned(true);
+ window.location.href = '/quick-lab/staff-dashboard';
+ }
+ }, 5000);
+
+ return () => clearInterval(interval);
+ }
+ } catch (error) {
+ console.error('Error checking staff status:', error);
+ setLoading(false);
+ }
+ };
+
+ checkStatus();
+ }, []);
+
+ const handleCopyId = () => {
+ if (staffId) {
+ navigator.clipboard.writeText(staffId);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (isAssigned) {
+ return (
+
+
+
+
+
+
Assignment Confirmed!
+
Redirecting to your dashboard...
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
Waiting for Assignment
+
Share your Staff ID with your Lab Admin
+
+
+
+
+ Your Staff ID
+
+
+
+ {staffId}
+
+
+
+ {copied && (
+
+ Copied to clipboard!
+
+ )}
+
+
+
+
What's Next?
+
+ -
+ 1.
+ Copy your Staff ID using the button above
+
+ -
+ 2.
+ Share this ID with your Lab Admin
+
+ -
+ 3.
+ The Lab Admin will add you to their lab
+
+ -
+ 4.
+ Once added, you'll be redirected to your dashboard
+
+
+
+
+
+
+
Waiting for assignment...
+
+
+
+
+ );
+}
diff --git a/client/src/pages/quicklab/PatientLabAppointments.jsx b/client/src/pages/quicklab/PatientLabAppointments.jsx
new file mode 100644
index 0000000..2f60314
--- /dev/null
+++ b/client/src/pages/quicklab/PatientLabAppointments.jsx
@@ -0,0 +1,494 @@
+import { useEffect, useMemo, useState } from 'react';
+import {
+ Calendar,
+ Clock,
+ MapPin,
+ Phone,
+ Home,
+ Building2,
+ CheckCircle,
+ AlertCircle,
+ XCircle,
+ TestTube,
+ FileText,
+ User,
+ Download,
+} from 'lucide-react';
+import { toast } from 'react-toastify';
+import { getPatientLabAppointments } from '../../service/labAppointmentService';
+import '../../quicklab.css';
+
+export default function PatientLabAppointments() {
+ const [appointments, setAppointments] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [filters, setFilters] = useState({ status: 'all', collectionType: 'all' });
+ const [selectedAppointment, setSelectedAppointment] = useState(null);
+ const [showDetailModal, setShowDetailModal] = useState(false);
+
+ const statusColors = {
+ pending:
+ 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800',
+ confirmed:
+ 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-400 border-blue-200 dark:border-blue-800',
+ sample_collected:
+ 'bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-400 border-purple-200 dark:border-purple-800',
+ processing:
+ 'bg-indigo-100 dark:bg-indigo-900/30 text-indigo-800 dark:text-indigo-400 border-indigo-200 dark:border-indigo-800',
+ completed:
+ 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400 border-green-200 dark:border-green-800',
+ cancelled:
+ 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-400 border-red-200 dark:border-red-800',
+ };
+
+ const statusLabels = {
+ pending: 'Pending',
+ confirmed: 'Confirmed',
+ sample_collected: 'Sample Collected',
+ processing: 'Processing',
+ completed: 'Completed',
+ cancelled: 'Cancelled',
+ };
+
+ useEffect(() => {
+ fetchAppointments();
+ }, []);
+
+ const fetchAppointments = async () => {
+ try {
+ setLoading(true);
+ const response = await getPatientLabAppointments();
+ const data = response.data || response;
+ setAppointments(data.appointments || []);
+ setError(null);
+ } catch (err) {
+ console.error('Failed to fetch lab appointments:', err);
+ setError('Failed to load lab appointments');
+ toast.error('Failed to load lab appointments');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const filteredAppointments = useMemo(() => {
+ const term = searchTerm.trim().toLowerCase();
+ return appointments.filter((appt) => {
+ if (filters.status !== 'all' && appt.status !== filters.status) return false;
+ if (filters.collectionType !== 'all' && appt.collectionType !== filters.collectionType)
+ return false;
+
+ if (term) {
+ const matchesLab = appt.labId?.name?.toLowerCase().includes(term);
+ const matchesTest = appt.tests?.some((t) => t.testName?.toLowerCase().includes(term));
+ return matchesLab || matchesTest;
+ }
+ return true;
+ });
+ }, [appointments, filters, searchTerm]);
+
+ const formatDate = (dateString) => {
+ return new Date(dateString).toLocaleDateString('en-US', {
+ weekday: 'short',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ };
+
+ const upcomingCount = appointments.filter(
+ (a) => a.status === 'pending' || a.status === 'confirmed' || a.status === 'sample_collected'
+ ).length;
+
+ if (loading) {
+ return (
+
+
+
+
+ Loading your lab appointments...
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
{error}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ My Lab Appointments
+
+
+ Track tests & reports
+
+
+ View collection details, assigned staff, and download your reports.
+
+
+
+
+
+
Total
+
+ {appointments.length}
+
+
+
+
Upcoming
+
+ {upcomingCount}
+
+
+
+
Completed
+
+ {appointments.filter((a) => a.status === 'completed').length}
+
+
+
+
+
+ {/* Filters */}
+
+
+ setSearchTerm(e.target.value)}
+ className="w-full px-4 py-2 rounded-lg border border-lab-black-200 dark:border-lab-black-700 bg-white dark:bg-lab-black-800 text-lab-black-900 dark:text-lab-black-50 focus:outline-none focus:ring-2 focus:ring-lab-yellow-500"
+ />
+
+
+
+
+
+
+
+
+ {/* Appointments list */}
+ {filteredAppointments.length === 0 ? (
+
+
+
No lab appointments found
+
+ Try clearing filters or book a new test.
+
+
+ ) : (
+
+ {filteredAppointments.map((appointment) => (
+
+
+
+
+
+ {appointment.labId?.name || 'Lab'}
+
+
+ {statusLabels[appointment.status]}
+
+
+ {appointment.collectionType === 'home_collection'
+ ? 'Home collection'
+ : 'Visit lab'}
+
+
+
+
+
+
+ {formatDate(appointment.appointmentDate)}
+
+
+
+ {appointment.appointmentTime}
+
+
+
+ {appointment.tests?.length || 0} test(s)
+
+
+
+ {appointment.collectionType === 'home_collection' &&
+ appointment.collectionAddress && (
+
+
+
+ {appointment.collectionAddress.street},{' '}
+ {appointment.collectionAddress.city},
+ {appointment.collectionAddress.state}{' '}
+ {appointment.collectionAddress.zipCode}
+
+
+ )}
+
+
+
+
+ Total: ₹{appointment.totalAmount}
+
+
+ Payment: {appointment.paymentStatus || 'pending'}
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Detail Modal */}
+ {showDetailModal && selectedAppointment && (
+
+
+
+
+ {selectedAppointment.labId?.name || 'Lab Appointment'}
+
+
+
+
+
+
+
+ {statusLabels[selectedAppointment.status]}
+
+
+ {selectedAppointment.collectionType === 'home_collection'
+ ? 'Home collection'
+ : 'Visit lab'}
+
+
+ Payment: {selectedAppointment.paymentStatus || 'pending'}
+
+
+
+
+
+
+ Schedule
+
+
+ {' '}
+ {formatDate(selectedAppointment.appointmentDate)}
+
+
+ {' '}
+ {selectedAppointment.appointmentTime}
+
+
+ {' '}
+ {selectedAppointment.tests?.length || 0} tests
+
+
+
+
+
+ Lab details
+
+
+ {selectedAppointment.labId?.name}
+
+ {selectedAppointment.labId?.address && (
+
+
+
+ {selectedAppointment.labId.address.street},{' '}
+ {selectedAppointment.labId.address.city},
+ {selectedAppointment.labId.address.state}{' '}
+ {selectedAppointment.labId.address.zipCode}
+
+
+ )}
+ {selectedAppointment.labId?.contact?.phone && (
+
+ {' '}
+ {selectedAppointment.labId.contact.phone}
+
+ )}
+
+
+
+ {/* Collection Address / Staff */}
+
+
+
+ {selectedAppointment.collectionType === 'home_collection' ? (
+
+ ) : (
+
+ )}
+ {selectedAppointment.collectionType === 'home_collection'
+ ? 'Home collection address'
+ : 'Visit lab'}
+
+ {selectedAppointment.collectionType === 'home_collection' &&
+ selectedAppointment.collectionAddress ? (
+
+
+
+ {selectedAppointment.collectionAddress.street},{' '}
+ {selectedAppointment.collectionAddress.city},
+ {selectedAppointment.collectionAddress.state}{' '}
+ {selectedAppointment.collectionAddress.zipCode}
+
+
+ ) : (
+
+ Please arrive 10 minutes before the scheduled time.
+
+ )}
+
+
+
+
+ Assigned staff
+
+ {selectedAppointment.assignedStaffId ? (
+ <>
+
+ {selectedAppointment.assignedStaffId.firstName}{' '}
+ {selectedAppointment.assignedStaffId.lastName}
+
+ {selectedAppointment.assignedStaffId.phoneNumber && (
+
+
+ {selectedAppointment.assignedStaffId.phoneNumber}
+
+ )}
+ >
+ ) : (
+
+ Staff assignment will appear once confirmed.
+
+ )}
+
+
+
+ {/* Tests list */}
+
+
+ Tests
+
+
+ {selectedAppointment.tests?.map((test, idx) => (
+
+
+
+ {test.testName}
+
+ {test.testCode && (
+
+ Code: {test.testCode}
+
+ )}
+
+
+ ₹{test.price}
+
+
+ ))}
+
+
+
+ {/* Notes & Report */}
+
+
+
+ Notes
+
+
+ {selectedAppointment.notes || 'No additional notes'}
+
+
+
+
+
+ Report
+
+ {selectedAppointment.reportId?.reportFile?.url ? (
+
+ Download report
+
+ ) : (
+
+ Report will be available after completion.
+
+ )}
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/client/src/quicklab.css b/client/src/quicklab.css
index 447f199..0fa18c7 100644
--- a/client/src/quicklab.css
+++ b/client/src/quicklab.css
@@ -45,6 +45,16 @@
--color-lab-blue-500: #60a5fa;
--color-lab-blue-600: #3b82f6;
}
+
+ body {
+ background-color: var(--color-lab-black-50);
+ color: var(--color-lab-black-900);
+ }
+
+ .dark body {
+ background-color: var(--color-lab-black-900);
+ color: var(--color-lab-black-50);
+ }
}
/* Custom Component Classes */
diff --git a/client/src/routes/QuickLabRoutes.jsx b/client/src/routes/QuickLabRoutes.jsx
index b01c1dc..7c5a423 100644
--- a/client/src/routes/QuickLabRoutes.jsx
+++ b/client/src/routes/QuickLabRoutes.jsx
@@ -1,13 +1,180 @@
import { Routes, Route } from 'react-router-dom';
-import NotFoundPage from '../components/ui/NotFoundPage';
import Navbar from '../components/quicklab/Navbar';
import QuickLabHomepage from '../pages/quicklab/Homepage';
+import LabSearchResults from '../pages/quicklab/LabSearchResults';
+import LabDetails from '../pages/quicklab/LabDetails';
+import LabAdminProfileComplete from '../pages/quicklab/LabAdminProfileComplete';
+import LabAdminAddLab from '../pages/quicklab/LabAdminAddLab';
+import LabAdminDashboard from '../pages/quicklab/LabAdminDashboard';
+import LabAdminManageStaff from '../pages/quicklab/LabAdminManageStaff';
+import LabAdminAppointments from '../pages/quicklab/LabAdminAppointments';
+import LabAdminManageTests from '../pages/quicklab/LabAdminManageTests';
+import LabAdminLabSettings from '../pages/quicklab/LabAdminLabSettings';
+import LabStaffProfileComplete from '../pages/quicklab/LabStaffProfileComplete';
+import LabStaffWaitingForAssignment from '../pages/quicklab/LabStaffWaitingForAssignment';
+import LabStaffDashboard from '../pages/quicklab/LabStaffDashboard';
+import LabStaffMyAppointments from '../pages/quicklab/LabStaffMyAppointments';
+import LabAdminPreventGuard from './guards/LabAdminPreventGuard';
+import LabAdminSetupGuard from './guards/LabAdminSetupGuard';
+import LabStaffPreventGuard from './guards/LabStaffPreventGuard';
+import LabStaffSetupGuard from './guards/LabStaffSetupGuard';
+import ProtectedRoute from './guards/protectedRoutes';
+
export default function QuickLabRoutes() {
return (
<>
- } />
+ {/* ============ LAB ADMIN ROUTES ============ */}
+
+ {/* Lab Admin Profile Completion */}
+
+
+
+
+
+ }
+ />
+
+ {/* Add Lab - Requires profile, prevent if lab already exists */}
+
+
+
+
+
+
+
+ }
+ />
+
+ {/* Lab Admin Dashboard - Requires profile AND lab */}
+
+
+
+
+
+ }
+ />
+
+ {/* Lab Admin - Manage Staff */}
+
+
+
+
+
+ }
+ />
+
+ {/* Lab Admin - Appointments Management */}
+
+
+
+
+
+ }
+ />
+
+ {/* Lab Admin - Manage Tests */}
+
+
+
+
+
+ }
+ />
+
+ {/* Lab Admin - Lab settings */}
+
+
+
+
+
+ }
+ />
+
+ {/* ============ LAB STAFF ROUTES ============ */}
+
+ {/* Lab Staff Profile Completion */}
+
+
+
+
+
+ }
+ />
+
+ {/* Lab Staff Waiting for Assignment */}
+
+
+
+
+
+
+
+ }
+ />
+
+ {/* Lab Staff Dashboard - Requires profile AND lab assignment */}
+
+
+
+
+
+ }
+ />
+
+ {/* Lab Staff - My Appointments */}
+
+
+
+
+
+ }
+ />
+
+ {/* ============ PUBLIC ROUTES ============ */}
+
+ {/* Public homepage */}
+ } />
+
+ {/* Lab Search Results */}
+ } />
+
+ {/* Lab Details */}
+ } />
>
);
diff --git a/client/src/routes/guards/LabAdminPreventGuard.jsx b/client/src/routes/guards/LabAdminPreventGuard.jsx
new file mode 100644
index 0000000..3cb359f
--- /dev/null
+++ b/client/src/routes/guards/LabAdminPreventGuard.jsx
@@ -0,0 +1,67 @@
+// src/routes/guards/LabAdminPreventGuard.jsx
+import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { toast } from 'react-toastify';
+import Loading from '../../components/ui/Loading';
+import { useAuth } from '../../context/authContext';
+import { checkLabAdminProfileExists, checkLabExists } from '../../service/labAdminService';
+
+const LabAdminPreventGuard = ({ preventProfile = false, preventLab = false, children }) => {
+ const { isAuthenticated, user } = useAuth();
+ const navigate = useNavigate();
+ const [loading, setLoading] = useState(true);
+ const [checked, setChecked] = useState(false);
+
+ useEffect(() => {
+ let mounted = true;
+
+ if (checked) return; // Prevent re-running if already checked
+
+ const runChecks = async () => {
+ if (!isAuthenticated || (user?.role !== 'lab_admin' && user?.role !== 'lab_staff')) {
+ if (mounted) navigate('/', { replace: true });
+ return;
+ }
+
+ try {
+ const [profileRes, labRes] = await Promise.all([
+ preventProfile ? checkLabAdminProfileExists() : Promise.resolve({}),
+ preventLab && user?.role === 'lab_admin' ? checkLabExists() : Promise.resolve({}),
+ ]);
+
+ if (!mounted) return;
+
+ if (preventProfile && profileRes.exists) {
+ toast.info('Profile already completed.');
+ navigate('/quick-lab/add-lab', { replace: true });
+ return;
+ }
+
+ if (preventLab && user?.role === 'lab_admin' && labRes.exists) {
+ toast.info('Lab already added.');
+ navigate('/quick-lab/dashboard', { replace: true });
+ return;
+ }
+
+ if (mounted) {
+ setLoading(false);
+ setChecked(true);
+ }
+ } catch (err) {
+ console.error('LabAdminPreventGuard check failed:', err);
+ if (mounted) navigate('/', { replace: true });
+ }
+ };
+
+ runChecks();
+ return () => {
+ mounted = false;
+ };
+ }, [isAuthenticated, user?.role, preventProfile, preventLab, navigate, checked]);
+
+ if (loading) return
;
+
+ return children;
+};
+
+export default LabAdminPreventGuard;
diff --git a/client/src/routes/guards/LabAdminSetupGuard.jsx b/client/src/routes/guards/LabAdminSetupGuard.jsx
new file mode 100644
index 0000000..02e3fbf
--- /dev/null
+++ b/client/src/routes/guards/LabAdminSetupGuard.jsx
@@ -0,0 +1,67 @@
+// src/routes/guards/LabAdminSetupGuard.jsx
+import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { toast } from 'react-toastify';
+import Loading from '../../components/ui/Loading';
+import { useAuth } from '../../context/authContext';
+import { checkLabAdminProfileExists, checkLabExists } from '../../service/labAdminService';
+
+const LabAdminSetupGuard = ({ requireProfile = true, requireLab = true, children }) => {
+ const { isAuthenticated, user } = useAuth();
+ const navigate = useNavigate();
+ const [loading, setLoading] = useState(true);
+ const [checked, setChecked] = useState(false);
+
+ useEffect(() => {
+ let mounted = true;
+
+ if (checked) return; // Prevent re-running if already checked
+
+ const runChecks = async () => {
+ if (!isAuthenticated || (user?.role !== 'lab_admin' && user?.role !== 'lab_staff')) {
+ if (mounted) navigate('/', { replace: true });
+ return;
+ }
+
+ try {
+ const [profileRes, labRes] = await Promise.all([
+ requireProfile ? checkLabAdminProfileExists() : Promise.resolve({}),
+ requireLab && user?.role === 'lab_admin' ? checkLabExists() : Promise.resolve({}),
+ ]);
+
+ if (!mounted) return;
+
+ if (requireProfile && !profileRes.exists) {
+ toast.warning('Please complete your profile first.');
+ navigate('/quick-lab/complete-profile', { replace: true });
+ return;
+ }
+
+ if (requireLab && user?.role === 'lab_admin' && !labRes.exists) {
+ toast.warning('Please add your lab details first.');
+ navigate('/quick-lab/add-lab', { replace: true });
+ return;
+ }
+
+ if (mounted) {
+ setLoading(false);
+ setChecked(true);
+ }
+ } catch (err) {
+ console.error('LabAdminSetupGuard check failed:', err);
+ if (mounted) navigate('/', { replace: true });
+ }
+ };
+
+ runChecks();
+ return () => {
+ mounted = false;
+ };
+ }, [isAuthenticated, user?.role, requireProfile, requireLab, navigate, checked]);
+
+ if (loading) return
;
+
+ return children;
+};
+
+export default LabAdminSetupGuard;
diff --git a/client/src/routes/guards/LabStaffPreventGuard.jsx b/client/src/routes/guards/LabStaffPreventGuard.jsx
new file mode 100644
index 0000000..41b2b34
--- /dev/null
+++ b/client/src/routes/guards/LabStaffPreventGuard.jsx
@@ -0,0 +1,71 @@
+// src/routes/guards/LabStaffPreventGuard.jsx
+import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../../context/authContext';
+import { checkStaffProfileExists } from '../../service/labStaffService';
+import Loading from '../../components/ui/Loading';
+
+const LabStaffPreventGuard = ({ preventProfile = false, preventWaiting = false, children }) => {
+ const { isAuthenticated, user } = useAuth();
+ const navigate = useNavigate();
+ const [loading, setLoading] = useState(true);
+ const [checked, setChecked] = useState(false);
+
+ useEffect(() => {
+ let mounted = true;
+
+ if (checked) return; // Prevent re-running if already checked
+
+ const runChecks = async () => {
+ if (!isAuthenticated || user?.role !== 'lab_staff') {
+ if (mounted) {
+ setLoading(false);
+ setChecked(true);
+ }
+ return;
+ }
+
+ try {
+ const staffStatus = await checkStaffProfileExists();
+
+ if (!mounted) return;
+
+ // Prevent re-doing profile if already complete
+ if (preventProfile && staffStatus.exists) {
+ navigate('/quick-lab/staff-waiting', { replace: true });
+ return;
+ }
+
+ // Prevent accessing waiting page if already assigned to lab
+ if (preventWaiting && staffStatus.isAssignedToLab) {
+ navigate('/quick-lab/staff-dashboard', { replace: true });
+ return;
+ }
+
+ if (mounted) {
+ setLoading(false);
+ setChecked(true);
+ }
+ } catch (err) {
+ console.error('LabStaffPreventGuard check failed:', err);
+ if (mounted) {
+ setLoading(false);
+ setChecked(true);
+ }
+ }
+ };
+
+ runChecks();
+ return () => {
+ mounted = false;
+ };
+ }, [isAuthenticated, user?.role, preventProfile, preventWaiting, navigate, checked]);
+
+ if (loading) {
+ return
;
+ }
+
+ return children;
+};
+
+export default LabStaffPreventGuard;
diff --git a/client/src/routes/guards/LabStaffSetupGuard.jsx b/client/src/routes/guards/LabStaffSetupGuard.jsx
new file mode 100644
index 0000000..6a86d8b
--- /dev/null
+++ b/client/src/routes/guards/LabStaffSetupGuard.jsx
@@ -0,0 +1,66 @@
+// src/routes/guards/LabStaffSetupGuard.jsx
+import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { toast } from 'react-toastify';
+import Loading from '../../components/ui/Loading';
+import { useAuth } from '../../context/authContext';
+import { checkStaffProfileExists } from '../../service/labStaffService';
+
+const LabStaffSetupGuard = ({ requireProfile = true, requireLabAssignment = true, children }) => {
+ const { isAuthenticated, user } = useAuth();
+ const navigate = useNavigate();
+ const [loading, setLoading] = useState(true);
+ const [checked, setChecked] = useState(false);
+
+ useEffect(() => {
+ let mounted = true;
+
+ if (checked) return; // Prevent re-running if already checked
+
+ const runChecks = async () => {
+ if (!isAuthenticated || user?.role !== 'lab_staff') {
+ if (mounted) navigate('/', { replace: true });
+ return;
+ }
+
+ try {
+ const staffStatus = await checkStaffProfileExists();
+
+ if (!mounted) return;
+
+ if (requireProfile && !staffStatus.exists) {
+ toast.warning('Please complete your profile first.');
+ navigate('/quick-lab/staff-profile', { replace: true });
+ return;
+ }
+
+ if (requireLabAssignment && staffStatus.exists && !staffStatus.isAssignedToLab) {
+ toast.info('Waiting for lab assignment from your admin.');
+ navigate('/quick-lab/staff-waiting', { replace: true });
+ return;
+ }
+
+ if (mounted) {
+ setLoading(false);
+ setChecked(true);
+ }
+ } catch (err) {
+ console.error('LabStaffSetupGuard check failed:', err);
+ if (mounted) navigate('/', { replace: true });
+ }
+ };
+
+ runChecks();
+ return () => {
+ mounted = false;
+ };
+ }, [isAuthenticated, user?.role, requireProfile, requireLabAssignment, navigate, checked]);
+
+ if (loading) {
+ return
;
+ }
+
+ return children;
+};
+
+export default LabStaffSetupGuard;
diff --git a/client/src/service/geolocationService.js b/client/src/service/geolocationService.js
new file mode 100644
index 0000000..4bbb127
--- /dev/null
+++ b/client/src/service/geolocationService.js
@@ -0,0 +1,112 @@
+// Geolocation service to detect user's city
+import apiService from './apiservice';
+
+/**
+ * Get user's city from browser geolocation API
+ */
+export const getCityFromGeolocation = () => {
+ return new Promise((resolve, reject) => {
+ if (!navigator.geolocation) {
+ reject(new Error('Geolocation not supported'));
+ return;
+ }
+
+ navigator.geolocation.getCurrentPosition(
+ async (position) => {
+ try {
+ const { latitude, longitude } = position.coords;
+ // Use reverse geocoding to get city from coordinates
+ const city = await reversGeocode(latitude, longitude);
+ resolve(city);
+ } catch (err) {
+ reject(err);
+ }
+ },
+ (error) => {
+ reject(error);
+ },
+ { timeout: 10000 }
+ );
+ });
+};
+
+/**
+ * Reverse geocode coordinates to city name using a free API
+ */
+const reversGeocode = async (lat, lon) => {
+ try {
+ const response = await fetch(
+ `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}&addressdetails=1`
+ );
+ const data = await response.json();
+ return data.address?.city || data.address?.town || data.address?.village || null;
+ } catch (err) {
+ console.error('Reverse geocoding failed:', err);
+ return null;
+ }
+};
+
+/**
+ * Get user's city from IP-based geolocation (fallback)
+ */
+export const getCityFromIP = async () => {
+ try {
+ const response = await fetch('https://ipapi.co/json/');
+ const data = await response.json();
+ return data.city || null;
+ } catch (err) {
+ console.error('IP-based geolocation failed:', err);
+ return null;
+ }
+};
+
+/**
+ * Get user's city with fallback mechanism
+ * First tries browser geolocation, then falls back to IP
+ */
+export const detectUserCity = async () => {
+ try {
+ // Try localStorage first for cached city
+ const cachedCity = localStorage.getItem('userCity');
+ if (cachedCity) {
+ return cachedCity;
+ }
+
+ // Try browser geolocation
+ try {
+ const city = await getCityFromGeolocation();
+ if (city) {
+ localStorage.setItem('userCity', city);
+ return city;
+ }
+ } catch (err) {
+ console.log('Browser geolocation failed, falling back to IP');
+ }
+
+ // Fallback to IP-based geolocation
+ const city = await getCityFromIP();
+ if (city) {
+ localStorage.setItem('userCity', city);
+ return city;
+ }
+
+ return null;
+ } catch (err) {
+ console.error('City detection failed:', err);
+ return null;
+ }
+};
+
+/**
+ * Clear cached city
+ */
+export const clearCachedCity = () => {
+ localStorage.removeItem('userCity');
+};
+
+export default {
+ detectUserCity,
+ getCityFromGeolocation,
+ getCityFromIP,
+ clearCachedCity,
+};
diff --git a/client/src/service/labAdminService.js b/client/src/service/labAdminService.js
new file mode 100644
index 0000000..98b4114
--- /dev/null
+++ b/client/src/service/labAdminService.js
@@ -0,0 +1,57 @@
+import apiService from './apiservice';
+import { createFormDataFromObject } from '../utility/formDataHelper';
+
+export const createLabAdminProfile = (data) => apiService.post('/lab-admin/profile', data);
+
+export const createLab = (labData, logoFile, photoFiles = []) => {
+ const formData = createFormDataFromObject(labData, { logo: logoFile, photos: photoFiles });
+ return apiService.post('/lab-admin/lab', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+};
+
+export const searchLabStaff = (query) =>
+ apiService.get('/lab-admin/staff/search', { params: query });
+export const addStaffToLab = (staffId) => apiService.post('/lab-admin/staff/add', { staffId });
+export const removeStaffFromLab = (staffId) => apiService.delete(`/lab-admin/staff/${staffId}`);
+export const getLabStaff = () => apiService.get('/lab-admin/staff');
+
+export const addLabTest = (testData) => apiService.post('/lab-admin/tests', testData);
+export const updateLabTest = (testId, updates) =>
+ apiService.put(`/lab-admin/tests/${testId}`, updates);
+export const getLabInfo = () => apiService.get('/lab-admin/lab/info');
+export const updateLabInfo = (labData, logoFile, photoFiles = []) => {
+ const formData = createFormDataFromObject(labData, { logo: logoFile, photos: photoFiles });
+ return apiService.put('/lab-admin/lab/info', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+};
+
+/**
+ * Check if Lab Exists for current lab admin
+ */
+export const checkLabExists = async () => {
+ return await apiService.get('/lab-admin/lab/status');
+};
+
+/**
+ * Check if Lab Admin Profile Exists
+ */
+export const checkLabAdminProfileExists = async () => {
+ return await apiService.get('/lab-admin/profile/status');
+};
+
+export default {
+ createLabAdminProfile,
+ createLab,
+ searchLabStaff,
+ addStaffToLab,
+ removeStaffFromLab,
+ getLabStaff,
+ addLabTest,
+ updateLabTest,
+ getLabInfo,
+ updateLabInfo,
+ checkLabExists,
+ checkLabAdminProfileExists,
+};
diff --git a/client/src/service/labAppointmentService.js b/client/src/service/labAppointmentService.js
new file mode 100644
index 0000000..4e66749
--- /dev/null
+++ b/client/src/service/labAppointmentService.js
@@ -0,0 +1,19 @@
+import apiService from './apiservice';
+
+export const bookLabAppointment = (payload) => apiService.post('/lab-appointment/book', payload);
+export const getPatientLabAppointments = (params = {}) =>
+ apiService.get('/lab-appointment/patient', { params });
+export const getLabAppointments = (params = {}) =>
+ apiService.get('/lab-appointment/lab', { params });
+export const assignStaffForCollection = (appointmentId, staffId) =>
+ apiService.put(`/lab-appointment/${appointmentId}/assign-staff`, { staffId });
+export const updateLabAppointmentStatus = (appointmentId, status) =>
+ apiService.put(`/lab-appointment/${appointmentId}/status`, { status });
+
+export default {
+ bookLabAppointment,
+ getPatientLabAppointments,
+ getLabAppointments,
+ assignStaffForCollection,
+ updateLabAppointmentStatus,
+};
diff --git a/client/src/service/labPublicService.js b/client/src/service/labPublicService.js
new file mode 100644
index 0000000..4ccf0a6
--- /dev/null
+++ b/client/src/service/labPublicService.js
@@ -0,0 +1,11 @@
+import apiService from './apiservice';
+
+export const searchLabs = (params = {}) => apiService.get('/lab/search', { params });
+export const getLabDetails = (labId) => apiService.get(`/lab/${labId}`);
+export const getLabTests = (labId) => apiService.get(`/lab/${labId}/tests`);
+
+export default {
+ searchLabs,
+ getLabDetails,
+ getLabTests,
+};
diff --git a/client/src/service/labReportService.js b/client/src/service/labReportService.js
new file mode 100644
index 0000000..3163581
--- /dev/null
+++ b/client/src/service/labReportService.js
@@ -0,0 +1,28 @@
+import apiService from './apiservice';
+
+export const uploadLabReport = (appointmentId, file, testResults) => {
+ const formData = new FormData();
+ if (file) {
+ formData.append('reportFile', file);
+ }
+ if (testResults) {
+ formData.append('testResults', JSON.stringify(testResults));
+ }
+ return apiService.post(`/lab-report/upload/${appointmentId}`, formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+};
+
+export const getPatientLabReports = () => apiService.get('/lab-report/patient');
+export const getDoctorPatientReports = () => apiService.get('/lab-report/doctor');
+export const addDoctorRemarks = (reportId, remarks) =>
+ apiService.put(`/lab-report/${reportId}/remarks`, { remarks });
+export const getReportDetails = (reportId) => apiService.get(`/lab-report/${reportId}`);
+
+export default {
+ uploadLabReport,
+ getPatientLabReports,
+ getDoctorPatientReports,
+ addDoctorRemarks,
+ getReportDetails,
+};
diff --git a/client/src/service/labService.js b/client/src/service/labService.js
new file mode 100644
index 0000000..f417edd
--- /dev/null
+++ b/client/src/service/labService.js
@@ -0,0 +1,44 @@
+import apiService from './apiservice';
+
+/**
+ * Search labs with filters
+ */
+export const searchLabs = async (params) => {
+ return await apiService.get('/public/labs/search', { params });
+};
+
+/**
+ * Get lab details by ID
+ */
+export const getLabById = async (labId) => {
+ return await apiService.get(`/public/labs/${labId}`);
+};
+
+/**
+ * Get nearby labs by city
+ */
+export const getNearbyLabs = async (city) => {
+ return await apiService.get('/public/labs/nearby', { params: { city } });
+};
+
+/**
+ * Book lab appointment (requires authentication)
+ */
+export const bookLabAppointment = async (appointmentData) => {
+ return await apiService.post('/lab-appointment/book', appointmentData);
+};
+
+/**
+ * Get patient's lab appointments
+ */
+export const getPatientLabAppointments = async () => {
+ return await apiService.get('/lab-appointment/patient');
+};
+
+export default {
+ searchLabs,
+ getLabById,
+ getNearbyLabs,
+ bookLabAppointment,
+ getPatientLabAppointments,
+};
diff --git a/client/src/service/labStaffService.js b/client/src/service/labStaffService.js
new file mode 100644
index 0000000..c944379
--- /dev/null
+++ b/client/src/service/labStaffService.js
@@ -0,0 +1,55 @@
+import apiService from './apiservice';
+import { createFormDataFromObject } from '../utility/formDataHelper';
+
+export const createStaffProfile = (profileData, profilePicture) => {
+ const formData = createFormDataFromObject(profileData, { photos: [], logo: null });
+ if (profilePicture) {
+ formData.append('profilePicture', profilePicture);
+ }
+ return apiService.post('/lab-staff/profile', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+};
+
+export const updateStaffProfile = (updates, profilePicture) => {
+ const formData = createFormDataFromObject(updates, { photos: [], logo: null });
+ if (profilePicture) {
+ formData.append('profilePicture', profilePicture);
+ }
+ return apiService.put('/lab-staff/profile', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+};
+
+export const getStaffProfile = () => apiService.get('/lab-staff/profile');
+export const checkStaffProfileExists = () => apiService.get('/lab-staff/profile/check');
+export const getMyAssignments = (params = {}) =>
+ apiService.get('/lab-staff/assignments', { params });
+export const getAssignmentDetails = (appointmentId) =>
+ apiService.get(`/lab-staff/assignments/${appointmentId}`);
+export const updateMyAssignmentStatus = (appointmentId, status, notes) =>
+ apiService.put(`/lab-staff/assignments/${appointmentId}/status`, { status, notes });
+export const completeAssignment = (appointmentId, notes) =>
+ apiService.post(`/lab-staff/assignments/${appointmentId}/complete`, { notes });
+export const uploadReportAndComplete = (appointmentId, reportFile, notes) => {
+ const formData = new FormData();
+ formData.append('reportFile', reportFile);
+ if (notes) {
+ formData.append('notes', notes);
+ }
+ return apiService.post(`/lab-staff/assignments/${appointmentId}/upload-report`, formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+};
+
+export default {
+ createStaffProfile,
+ updateStaffProfile,
+ getStaffProfile,
+ checkStaffProfileExists,
+ getMyAssignments,
+ getAssignmentDetails,
+ updateMyAssignmentStatus,
+ completeAssignment,
+ uploadReportAndComplete,
+};
diff --git a/client/src/service/prescriptionApiSevice.js b/client/src/service/prescriptionApiSevice.js
index d3f90b3..3dc0050 100644
--- a/client/src/service/prescriptionApiSevice.js
+++ b/client/src/service/prescriptionApiSevice.js
@@ -50,6 +50,15 @@ export const updatePrescription = async (prescriptionId, updateData) => {
export const getPatientAppointmentPrescription = async (appointmentId) => {
return await apiService.get(`/prescriptions/patient/appointments/${appointmentId}`);
};
+
+/**
+ * Get nearby labs by city
+ */
+export const getNearbyLabs = async (city) => {
+ return await apiService.get('/public/labs/nearby', {
+ params: { city },
+ });
+};
export default {
createPrescription,
getPatientPrescriptions,
diff --git a/client/src/utility/getDashboardPath.js b/client/src/utility/getDashboardPath.js
index 06de3f5..0407306 100644
--- a/client/src/utility/getDashboardPath.js
+++ b/client/src/utility/getDashboardPath.js
@@ -6,6 +6,10 @@ function getDashboardPath(role) {
return '/doctor/dashboard';
case 'admin':
return '/admin/dashboard';
+ case 'lab_admin':
+ return '/quick-lab/dashboard';
+ case 'lab_staff':
+ return '/quick-lab/staff-dashboard';
default:
return '/';
}
diff --git a/server/Controllers/QuickLab/labAdminController.js b/server/Controllers/QuickLab/labAdminController.js
index cc9e962..beea2e8 100644
--- a/server/Controllers/QuickLab/labAdminController.js
+++ b/server/Controllers/QuickLab/labAdminController.js
@@ -55,6 +55,36 @@ export const createLab = async (req, res) => {
const { userId, profileId } = req.user;
const labData = req.body;
+ // Rebuild nested fields from multipart form-data (dot-notation fallbacks)
+ const contact = {
+ phone:
+ labData.contact?.phone || labData['contact.phone'] || labData.contactPhone || labData.phone,
+ email:
+ labData.contact?.email || labData['contact.email'] || labData.contactEmail || labData.email,
+ website:
+ labData.contact?.website ||
+ labData['contact.website'] ||
+ labData.contactWebsite ||
+ labData.website,
+ };
+
+ const address = {
+ formattedAddress:
+ labData.address?.formattedAddress ||
+ labData['address.formattedAddress'] ||
+ labData.formattedAddress,
+ city: labData.address?.city || labData['address.city'] || labData.city,
+ state: labData.address?.state || labData['address.state'] || labData.state,
+ zipCode: labData.address?.zipCode || labData['address.zipCode'] || labData.zipCode,
+ country: labData.address?.country || labData['address.country'] || labData.country,
+ };
+
+ // Normalize numeric fee
+ const generalHomeCollectionFee =
+ labData.generalHomeCollectionFee !== undefined && labData.generalHomeCollectionFee !== ''
+ ? Number(labData.generalHomeCollectionFee)
+ : undefined;
+
const labAdmin = await LabAdmin.findById(profileId);
if (!labAdmin) {
return res.status(404).json({ message: 'Lab admin profile not found' });
@@ -64,7 +94,7 @@ export const createLab = async (req, res) => {
return res.status(400).json({ message: 'Lab admin can only manage one lab' });
}
- if (!labData.name || !labData.contact?.phone) {
+ if (!labData.name || !contact.phone) {
return res.status(400).json({ message: 'Lab name and contact phone are required' });
}
@@ -86,6 +116,9 @@ export const createLab = async (req, res) => {
const lab = new Lab({
...labData,
+ contact,
+ address,
+ generalHomeCollectionFee,
labAdminId: profileId,
});
@@ -227,6 +260,11 @@ export const removeStaffFromLab = async (req, res) => {
return res.status(404).json({ message: 'Staff not found' });
}
+ // If staff is not assigned to any lab, respond gracefully
+ if (!staff.labId) {
+ return res.status(400).json({ message: 'Staff is not assigned to any lab' });
+ }
+
if (staff.labId.toString() !== labAdmin.labId.toString()) {
return res.status(403).json({ message: 'Staff does not belong to your lab' });
}
@@ -277,6 +315,11 @@ export const updateTest = async (req, res) => {
const { testId } = req.params;
const updates = req.body;
+ // Convert sampleType to lowercase if provided
+ if (updates.sampleType) {
+ updates.sampleType = updates.sampleType.toLowerCase();
+ }
+
const labAdmin = await LabAdmin.findById(profileId);
if (!labAdmin || !labAdmin.labId) {
return res.status(400).json({ message: 'Lab admin not associated with any lab' });
@@ -327,6 +370,114 @@ export const getLabInfo = async (req, res) => {
}
};
+// Update lab info (basic details, contact, address, charges, logo/photos)
+export const updateLabInfo = async (req, res) => {
+ try {
+ const { profileId } = req.user;
+
+ const labAdmin = await LabAdmin.findById(profileId);
+ if (!labAdmin || !labAdmin.labId) {
+ return res.status(400).json({ message: 'Lab admin not associated with any lab' });
+ }
+
+ const lab = await Lab.findById(labAdmin.labId);
+ if (!lab) {
+ return res.status(404).json({ message: 'Lab not found' });
+ }
+
+ const body = req.body;
+
+ // Rebuild nested contact/address fields (form-data friendly)
+ const contact = {
+ phone: body.contact?.phone || body['contact.phone'] || body.contactPhone || body.phone,
+ email: body.contact?.email || body['contact.email'] || body.contactEmail || body.email,
+ website:
+ body.contact?.website || body['contact.website'] || body.contactWebsite || body.website,
+ };
+
+ const address = {
+ formattedAddress:
+ body.address?.formattedAddress ||
+ body['address.formattedAddress'] ||
+ body.formattedAddress ||
+ lab.address?.formattedAddress,
+ street: body.address?.street || body['address.street'] || body.street || lab.address?.street,
+ city: body.address?.city || body['address.city'] || body.city || lab.address?.city,
+ state: body.address?.state || body['address.state'] || body.state || lab.address?.state,
+ zipCode:
+ body.address?.zipCode || body['address.zipCode'] || body.zipCode || lab.address?.zipCode,
+ country:
+ body.address?.country || body['address.country'] || body.country || lab.address?.country,
+ };
+
+ let generalHomeCollectionFee = lab.generalHomeCollectionFee;
+ if (body.generalHomeCollectionFee !== undefined && body.generalHomeCollectionFee !== '') {
+ generalHomeCollectionFee = Number(body.generalHomeCollectionFee);
+ }
+
+ // Opening hours can come as JSON string from form-data
+ let openingHours = lab.openingHours;
+ if (body.openingHours) {
+ try {
+ openingHours =
+ typeof body.openingHours === 'string' ? JSON.parse(body.openingHours) : body.openingHours;
+ } catch (err) {
+ return res.status(400).json({ message: 'Invalid openingHours format' });
+ }
+ }
+
+ const updates = {
+ name: body.name || lab.name,
+ description: body.description !== undefined ? body.description : lab.description,
+ contact,
+ address,
+ generalHomeCollectionFee,
+ openingHours,
+ };
+
+ // Optional logo upload
+ if (req.files?.logo) {
+ const result = await uploadToCloudinary(req.files.logo[0].path);
+ updates.logo = result.url;
+ }
+
+ // Photos: keep existing ones (sent from client) + newly uploaded
+ let existingPhotos = lab.photos || [];
+ if (body.existingPhotos) {
+ if (Array.isArray(body.existingPhotos)) {
+ existingPhotos = body.existingPhotos.filter(Boolean);
+ } else if (typeof body.existingPhotos === 'string') {
+ existingPhotos = body.existingPhotos
+ .split(',')
+ .map((p) => p.trim())
+ .filter(Boolean);
+ }
+ }
+
+ if (req.files?.photos) {
+ const newPhotoUrls = await Promise.all(
+ req.files.photos.map(async (file) => {
+ const result = await uploadToCloudinary(file.path);
+ return result.url;
+ })
+ );
+ updates.photos = [...existingPhotos, ...newPhotoUrls];
+ } else {
+ updates.photos = existingPhotos;
+ }
+
+ const updatedLab = await Lab.findByIdAndUpdate(labAdmin.labId, updates, {
+ new: true,
+ runValidators: true,
+ });
+
+ res.json({ message: 'Lab info updated successfully', lab: updatedLab });
+ } catch (error) {
+ console.error('Error updating lab info:', error);
+ res.status(500).json({ message: 'Failed to update lab info' });
+ }
+};
+
export const addTest = async (req, res) => {
try {
const { profileId } = req.user;
@@ -389,7 +540,7 @@ export const addTest = async (req, res) => {
description,
price,
preparationInstructions,
- sampleType,
+ sampleType: sampleType ? sampleType.toLowerCase() : undefined,
homeCollectionAvailable: canBeHomeCollected,
homeCollectionFee: homeCollectionFee || 0,
reportDeliveryTime,
@@ -398,7 +549,6 @@ export const addTest = async (req, res) => {
isActive: true,
};
- const Lab = (await import('../models/Lab/Lab.js')).default;
const lab = await Lab.findByIdAndUpdate(
labAdmin.labId,
{ $push: { tests: testData } },
@@ -417,3 +567,74 @@ export const addTest = async (req, res) => {
});
}
};
+
+// Check if Lab Admin Profile Exists
+export const checkLabAdminProfileExists = async (req, res) => {
+ try {
+ const { userId } = req.user;
+
+ if (!mongoose.Types.ObjectId.isValid(userId)) {
+ return res.status(400).json({ message: 'Invalid user ID format' });
+ }
+
+ const existingAdmin = await LabAdmin.findOne({ userId });
+
+ if (existingAdmin) {
+ return res.status(200).json({
+ exists: true,
+ message: 'Lab admin profile exists',
+ });
+ } else {
+ return res.status(200).json({
+ exists: false,
+ message: 'Lab admin profile does not exist',
+ });
+ }
+ } catch (error) {
+ console.error('Error checking lab admin profile:', error);
+ res.status(500).json({
+ message: 'Failed to check lab admin profile',
+ error: process.env.NODE_ENV === 'development' ? error.message : undefined,
+ });
+ }
+};
+
+// Check if Lab Exists for the Lab Admin
+export const checkLabExists = async (req, res) => {
+ try {
+ const { profileId } = req.user;
+
+ // If no profile exists yet, return exists: false
+ if (!profileId) {
+ return res.status(200).json({
+ exists: false,
+ message: 'No lab admin profile found yet',
+ });
+ }
+
+ if (!mongoose.Types.ObjectId.isValid(profileId)) {
+ return res.status(400).json({ message: 'Invalid profile ID format' });
+ }
+
+ const labAdmin = await LabAdmin.findById(profileId);
+
+ if (labAdmin?.labId) {
+ return res.status(200).json({
+ exists: true,
+ labId: labAdmin.labId,
+ message: 'Lab already exists for this admin',
+ });
+ }
+
+ return res.status(200).json({
+ exists: false,
+ message: 'No lab found for this admin',
+ });
+ } catch (error) {
+ console.error('Error checking lab status:', error);
+ res.status(500).json({
+ message: 'Failed to check lab status',
+ error: process.env.NODE_ENV === 'development' ? error.message : undefined,
+ });
+ }
+};
diff --git a/server/Controllers/QuickLab/labAppointmentController.js b/server/Controllers/QuickLab/labAppointmentController.js
index 64a3b2e..5f34031 100644
--- a/server/Controllers/QuickLab/labAppointmentController.js
+++ b/server/Controllers/QuickLab/labAppointmentController.js
@@ -265,14 +265,14 @@ export const getLabAppointments = async (req, res) => {
let labId;
if (role === 'lab_admin') {
- const LabAdmin = (await import('../models/Users/LabAdmin.js')).default;
+ const LabAdmin = (await import('../../models/Lab/LabAdmin.js')).default;
const labAdmin = await LabAdmin.findById(profileId);
if (!labAdmin || !labAdmin.labId) {
return res.status(400).json({ message: 'Lab admin not associated with any lab' });
}
labId = labAdmin.labId;
} else if (role === 'lab_staff') {
- const LabStaff = (await import('../models/Users/LabStaff.js')).default;
+ const LabStaff = (await import('../../models/Lab/LabStaff.js')).default;
const staff = await LabStaff.findById(profileId);
if (!staff) {
return res.status(404).json({ message: 'Staff not found' });
@@ -344,7 +344,7 @@ export const assignStaffForCollection = async (req, res) => {
});
}
- const LabStaff = (await import('../models/Users/LabStaff.js')).default;
+ const LabStaff = (await import('../../models/Lab/LabStaff.js')).default;
const staff = await LabStaff.findById(staffId);
if (!staff) {
return res.status(404).json({ message: 'Staff not found' });
diff --git a/server/Controllers/QuickLab/labStaffController.js b/server/Controllers/QuickLab/labStaffController.js
index 61648b3..0d605c7 100644
--- a/server/Controllers/QuickLab/labStaffController.js
+++ b/server/Controllers/QuickLab/labStaffController.js
@@ -1,7 +1,7 @@
-// controllers/labStaffController.js
import mongoose from 'mongoose';
import LabStaff from '../../models/Lab/LabStaff.js';
import LabAppointment from '../../models/Lab/LabAppointment.js';
+import LabReport from '../../models/Lab/LabReport.js';
import Lab from '../../models/Lab/Lab.js';
import { uploadToCloudinary } from '../../services/uploadService.js';
@@ -246,6 +246,34 @@ export const updateMyAssignmentStatus = async (req, res) => {
return res.status(403).json({ message: 'Unauthorized to update this assignment' });
}
+ // Status progression rules - prevent going backwards
+ const statusOrder = ['pending', 'confirmed', 'sample_collected', 'processing', 'completed'];
+ const currentStatusIndex = statusOrder.indexOf(assignment.status);
+ const newStatusIndex = statusOrder.indexOf(status);
+
+ if (newStatusIndex < currentStatusIndex) {
+ return res.status(400).json({
+ message: 'Cannot move appointment to a previous status',
+ });
+ }
+
+ // Cancelled appointments cannot be changed
+ if (assignment.status === 'cancelled') {
+ return res.status(400).json({
+ message: 'Cannot update cancelled appointments',
+ });
+ }
+
+ // Once sample is collected or processing, cannot cancel
+ if (
+ status === 'cancelled' &&
+ ['sample_collected', 'processing', 'completed'].includes(assignment.status)
+ ) {
+ return res.status(400).json({
+ message: 'Cannot cancel appointments after sample collection',
+ });
+ }
+
assignment.status = status;
if (notes) {
assignment.notes = notes;
@@ -304,3 +332,75 @@ export const completeAssignment = async (req, res) => {
res.status(500).json({ message: 'Failed to complete assignment' });
}
};
+
+// Upload report and mark appointment as completed
+export const uploadReportAndComplete = async (req, res) => {
+ try {
+ const { profileId } = req.user;
+ const { appointmentId } = req.params;
+ const { notes } = req.body;
+
+ if (!req.file) {
+ return res.status(400).json({ message: 'Report PDF file is required' });
+ }
+
+ const staff = await LabStaff.findById(profileId);
+ if (!staff) {
+ return res.status(404).json({ message: 'Staff profile not found' });
+ }
+
+ const assignment = await LabAppointment.findById(appointmentId).populate('patientId labId');
+ if (!assignment) {
+ return res.status(404).json({ message: 'Assignment not found' });
+ }
+
+ if (assignment.assignedStaffId?.toString() !== profileId.toString()) {
+ return res.status(403).json({ message: 'Unauthorized to upload report for this assignment' });
+ }
+
+ // Upload PDF to Cloudinary
+ const uploadResult = await uploadToCloudinary(req.file.path, {
+ resource_type: 'raw',
+ folder: 'lab-reports',
+ });
+
+ // Create lab report
+ const labReport = new LabReport({
+ appointmentId: assignment._id,
+ patientId: assignment.patientId._id,
+ labId: assignment.labId._id,
+ reportFile: {
+ url: uploadResult.url,
+ fileName: req.file.originalname,
+ uploadedAt: new Date(),
+ },
+ reportDate: new Date(),
+ });
+
+ await labReport.save();
+
+ // Update appointment with report and mark as completed
+ assignment.reportId = labReport._id;
+ assignment.status = 'completed';
+ if (notes) {
+ assignment.notes = notes;
+ }
+ await assignment.save();
+
+ res.json({
+ message: 'Report uploaded and appointment marked as completed',
+ assignment: await assignment.populate([
+ { path: 'patientId', select: 'firstName lastName' },
+ { path: 'labId', select: 'name' },
+ { path: 'reportId' },
+ ]),
+ report: labReport,
+ });
+ } catch (error) {
+ console.error('Error uploading report:', error);
+ res.status(500).json({
+ message: 'Failed to upload report',
+ error: process.env.NODE_ENV === 'development' ? error.message : undefined,
+ });
+ }
+};
diff --git a/server/Controllers/publicController.js b/server/Controllers/publicController.js
index 0016d73..55f5074 100644
--- a/server/Controllers/publicController.js
+++ b/server/Controllers/publicController.js
@@ -3,6 +3,7 @@
import Clinic from '../models/Clinic/Clinic.js';
import Schedule from '../models/Clinic/Schedule.js';
import Doctor from '../models/Users/Doctor.js';
+import Lab from '../models/Lab/Lab.js';
// Helper functions
const getDayName = (date) =>
@@ -494,3 +495,162 @@ export const getSearchSuggestions = async (req, res) => {
res.status(500).json({ success: false, error: error.message });
}
};
+
+// Get nearby labs by city
+export const getNearbyLabs = async (req, res) => {
+ try {
+ const { city } = req.query;
+
+ if (!city) {
+ return res.status(400).json({ success: false, message: 'City parameter is required' });
+ }
+
+ // Find labs in the same city
+ const labs = await Lab.find({
+ 'address.city': { $regex: city, $options: 'i' },
+ isActive: { $ne: false }, // Exclude inactive labs if the field exists
+ })
+ .select('name address contact tests generalHomeCollectionFee')
+ .lean();
+
+ res.json({
+ success: true,
+ count: labs.length,
+ data: labs,
+ });
+ } catch (error) {
+ console.error('Error fetching nearby labs:', error);
+ res.status(500).json({ success: false, error: error.message });
+ }
+};
+
+// Search labs with comprehensive filters
+export const searchLabs = async (req, res) => {
+ try {
+ const {
+ query, // search term for lab name or test name
+ city,
+ testCategory,
+ minPrice,
+ maxPrice,
+ homeCollection,
+ sort = 'rating_desc',
+ page = 1,
+ limit = 20,
+ } = req.query;
+
+ const skip = (page - 1) * limit;
+ const searchQuery = {};
+
+ // City filter (required for location-based search)
+ if (city) {
+ searchQuery['address.city'] = { $regex: city, $options: 'i' };
+ }
+
+ // Active labs only
+ searchQuery.isActive = { $ne: false };
+
+ // Search by lab name or test name
+ if (query && query.trim()) {
+ searchQuery.$or = [
+ { name: { $regex: query, $options: 'i' } },
+ { description: { $regex: query, $options: 'i' } },
+ { 'tests.testName': { $regex: query, $options: 'i' } },
+ ];
+ }
+
+ // Test category filter
+ if (testCategory) {
+ searchQuery['tests.category'] = testCategory;
+ }
+
+ // Home collection filter
+ if (homeCollection === 'true') {
+ searchQuery['tests.homeCollectionAvailable'] = true;
+ }
+
+ // Price range filter (on tests)
+ if (minPrice || maxPrice) {
+ const priceQuery = {};
+ if (minPrice) priceQuery.$gte = parseFloat(minPrice);
+ if (maxPrice) priceQuery.$lte = parseFloat(maxPrice);
+ searchQuery['tests.price'] = priceQuery;
+ }
+
+ // Sort options
+ let sortOption = {};
+ switch (sort) {
+ case 'rating_desc':
+ sortOption = { 'ratings.average': -1 };
+ break;
+ case 'rating_asc':
+ sortOption = { 'ratings.average': 1 };
+ break;
+ case 'name_asc':
+ sortOption = { name: 1 };
+ break;
+ case 'name_desc':
+ sortOption = { name: -1 };
+ break;
+ default:
+ sortOption = { 'ratings.average': -1 };
+ }
+
+ const [labs, total] = await Promise.all([
+ Lab.find(searchQuery)
+ .sort(sortOption)
+ .skip(skip)
+ .limit(parseInt(limit))
+ .select(
+ 'name description address contact logo photos tests ratings openingHours generalHomeCollectionFee'
+ )
+ .lean(),
+ Lab.countDocuments(searchQuery),
+ ]);
+
+ res.json({
+ success: true,
+ data: labs,
+ pagination: {
+ page: parseInt(page),
+ limit: parseInt(limit),
+ total,
+ pages: Math.ceil(total / limit),
+ },
+ filters: {
+ city,
+ query,
+ testCategory,
+ minPrice,
+ maxPrice,
+ homeCollection,
+ },
+ });
+ } catch (error) {
+ console.error('Error searching labs:', error);
+ res.status(500).json({ success: false, error: error.message });
+ }
+};
+
+// Get single lab details by ID
+export const getLabById = async (req, res) => {
+ try {
+ const { labId } = req.params;
+
+ const lab = await Lab.findById(labId)
+ .populate('staff', 'firstName lastName role phoneNumber')
+ .lean();
+
+ if (!lab) {
+ return res.status(404).json({ success: false, message: 'Lab not found' });
+ }
+
+ res.json({
+ success: true,
+ data: lab,
+ });
+ } catch (error) {
+ console.error('Error fetching lab details:', error);
+ res.status(500).json({ success: false, error: error.message });
+ }
+};
diff --git a/server/Routes/QuickLab/labAdminRoutes.js b/server/Routes/QuickLab/labAdminRoutes.js
index 1c99cb0..77db630 100644
--- a/server/Routes/QuickLab/labAdminRoutes.js
+++ b/server/Routes/QuickLab/labAdminRoutes.js
@@ -11,12 +11,22 @@ import {
addTest,
updateTest,
getLabInfo,
+ updateLabInfo,
+ checkLabExists,
+ checkLabAdminProfileExists,
} from '../../Controllers/QuickLab/labAdminController.js';
import upload from '../../Middleware/upload.js';
const router = express.Router();
router.post('/profile', authenticate, authorize('lab_admin'), createLabAdminProfile);
+router.get(
+ '/profile/status',
+ authenticate,
+ authorize('lab_admin', 'lab_staff'),
+ checkLabAdminProfileExists
+);
+router.get('/lab/status', authenticate, authorize('lab_admin'), checkLabExists);
router.post(
'/lab',
authenticate,
@@ -40,5 +50,15 @@ router.put('/tests/:testId', authenticate, authorize('lab_admin'), updateTest);
// Lab info
router.get('/lab/info', authenticate, authorize('lab_admin'), getLabInfo);
+router.put(
+ '/lab/info',
+ authenticate,
+ authorize('lab_admin'),
+ upload.fields([
+ { name: 'logo', maxCount: 1 },
+ { name: 'photos', maxCount: 10 },
+ ]),
+ updateLabInfo
+);
export default router;
diff --git a/server/Routes/QuickLab/labStaffRoutes.js b/server/Routes/QuickLab/labStaffRoutes.js
index d830771..3c57b4a 100644
--- a/server/Routes/QuickLab/labStaffRoutes.js
+++ b/server/Routes/QuickLab/labStaffRoutes.js
@@ -10,6 +10,7 @@ import {
getAssignmentDetails,
updateMyAssignmentStatus,
completeAssignment,
+ uploadReportAndComplete,
} from '../../Controllers/QuickLab/labStaffController.js';
import upload from '../../Middleware/upload.js';
@@ -53,5 +54,12 @@ router.post(
authorize('lab_staff'),
completeAssignment
);
+router.post(
+ '/assignments/:appointmentId/upload-report',
+ authenticate,
+ authorize('lab_staff'),
+ upload.single('reportFile'),
+ uploadReportAndComplete
+);
export default router;
diff --git a/server/Routes/publicRoutes.js b/server/Routes/publicRoutes.js
index 1a1b427..2e6d6b2 100644
--- a/server/Routes/publicRoutes.js
+++ b/server/Routes/publicRoutes.js
@@ -10,6 +10,9 @@ import {
getSearchSuggestions,
searchClinics,
searchDoctors,
+ getNearbyLabs,
+ searchLabs,
+ getLabById,
} from '../Controllers/publicController.js';
const router = express.Router();
@@ -27,4 +30,9 @@ router.get('/clinics/search', searchClinics);
router.get('/clinics/:id', getClinicById);
router.get('/clinics/:clinicId/doctors', getClinicDoctors);
+// Lab routes
+router.get('/labs/search', searchLabs);
+router.get('/labs/nearby', getNearbyLabs);
+router.get('/labs/:labId', getLabById);
+
export default router;
diff --git a/server/models/Appointment/Prescription.js b/server/models/Appointment/Prescription.js
index 1df57b3..4755a84 100644
--- a/server/models/Appointment/Prescription.js
+++ b/server/models/Appointment/Prescription.js
@@ -32,6 +32,10 @@ const prescriptionSchema = new mongoose.Schema({
{
name: { type: String },
instructions: { type: String },
+ labId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Lab',
+ },
},
],
notes: { type: String },