diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cdaadd1..d6fc66c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,6 +2,7 @@ - [ ] QuickClinic - [ ] QuickMed +- [ ] QuickLab ## Change Type diff --git a/client/src/App.jsx b/client/src/App.jsx index c96cd82..65ae8c2 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -60,6 +60,9 @@ function AppInner() { return ; case 'doctor': return ; + case 'lab_admin': + case 'lab_staff': + return ; default: // Fallback for unknown roles navigate('/', { replace: true }); diff --git a/client/src/components/Patient/PatientDesktopNavbar.jsx b/client/src/components/Patient/PatientDesktopNavbar.jsx index 811e962..594da2e 100644 --- a/client/src/components/Patient/PatientDesktopNavbar.jsx +++ b/client/src/components/Patient/PatientDesktopNavbar.jsx @@ -5,10 +5,23 @@ import { useNavigate } from 'react-router-dom'; import { useAuth } from '../../context/authContext'; import SearchBar from './SearchBar'; +import { useState, useRef, useEffect } from 'react'; const PatientDesktopNavbar = () => { const navigate = useNavigate(); const { logout } = useAuth(); + const [isExploreOpen, setIsExploreOpen] = useState(false); + const exploreRef = useRef(null); + + useEffect(() => { + const onDocClick = (e) => { + if (exploreRef.current && !exploreRef.current.contains(e.target)) { + setIsExploreOpen(false); + } + }; + document.addEventListener('mousedown', onDocClick); + return () => document.removeEventListener('mousedown', onDocClick); + }, []); const handleLogout = async () => { try { @@ -72,6 +85,38 @@ const PatientDesktopNavbar = () => { Profile + {/* Explore Dropdown */} +
+ + {isExploreOpen && ( +
+ + +
+ )} +
+ {/* Notification Bell Icon */} ); })} + {/* Explore toggle */} + + {isExploreOpen && ( +
+ + +
+ )} {/* Rest of the hamburger menu and other components */} diff --git a/client/src/components/auth/AuthButton.jsx b/client/src/components/auth/AuthButton.jsx index 5846e7f..e039b0b 100644 --- a/client/src/components/auth/AuthButton.jsx +++ b/client/src/components/auth/AuthButton.jsx @@ -1,7 +1,5 @@ import { Loader2 } from 'lucide-react'; -import Loading from '../ui/Loading'; - export const AuthButton = ({ children, isLoading = false, @@ -12,12 +10,11 @@ export const AuthButton = ({ variant = 'primary', }) => { const baseClasses = - 'w-full flex justify-center items-center gap-2 px-8 py-4 border border-transparent rounded-xl text-lg font-semibold transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transform hover:scale-[1.02] active:scale-[0.98]'; + 'w-full flex justify-center items-center gap-2 px-4 py-3 rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed'; const variantClasses = { - primary: - 'text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:ring-blue-500 shadow-lg hover:shadow-xl', - secondary: 'text-blue-700 bg-blue-50 border-blue-200 hover:bg-blue-100 focus:ring-blue-500', + primary: 'text-white bg-blue-600 hover:bg-blue-700 focus:ring-blue-500', + secondary: 'text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 focus:ring-gray-500', }; const isDisabled = disabled || isLoading; @@ -31,7 +28,8 @@ export const AuthButton = ({ > {isLoading ? ( <> - + + Please wait... ) : ( children diff --git a/client/src/components/auth/ErrorMessage.jsx b/client/src/components/auth/ErrorMessage.jsx index 7218ff6..edb012d 100644 --- a/client/src/components/auth/ErrorMessage.jsx +++ b/client/src/components/auth/ErrorMessage.jsx @@ -1,13 +1,31 @@ -import { AlertCircle } from 'lucide-react'; +import { AlertCircle, X } from 'lucide-react'; +import { useState } from 'react'; export const ErrorMessage = ({ error, className = '' }) => { - if (!error) return null; + const [isDismissed, setIsDismissed] = useState(false); + + if (!error || isDismissed) return null; + return ( -
- -

{error}

+
+
+
+
+
+ +
+
+
+

{error}

+
+ +
+
); }; diff --git a/client/src/components/auth/PasswordInput.jsx b/client/src/components/auth/PasswordInput.jsx index 539fedf..12dc115 100644 --- a/client/src/components/auth/PasswordInput.jsx +++ b/client/src/components/auth/PasswordInput.jsx @@ -1,4 +1,4 @@ -import { Eye, EyeOff, Lock } from 'lucide-react'; +import { Eye, EyeOff } from 'lucide-react'; export const PasswordInput = ({ id, @@ -13,38 +13,38 @@ export const PasswordInput = ({ error = null, }) => { return ( -
- {label && ( - - )} +
-
- -
- {error &&

{error}

} + {error &&

{error}

}
); }; diff --git a/client/src/components/public/DesktopNavbar.jsx b/client/src/components/public/DesktopNavbar.jsx index 6b53556..05f650e 100644 --- a/client/src/components/public/DesktopNavbar.jsx +++ b/client/src/components/public/DesktopNavbar.jsx @@ -1,9 +1,22 @@ import { FiLogIn, FiMapPin, FiUser } from 'react-icons/fi'; import { useNavigate } from 'react-router-dom'; +import { useState, useRef, useEffect } from 'react'; import SearchBar from './SearchBar'; const Navbar = () => { const navigate = useNavigate(); + const [isExploreOpen, setIsExploreOpen] = useState(false); + const exploreRef = useRef(null); + + useEffect(() => { + const onDocClick = (e) => { + if (exploreRef.current && !exploreRef.current.contains(e.target)) { + setIsExploreOpen(false); + } + }; + document.addEventListener('mousedown', onDocClick); + return () => document.removeEventListener('mousedown', onDocClick); + }, []); return (
+ )} + + {/* Tests Section */} + {suggestions.tests?.length > 0 && ( +
+

+ Tests +

+ {suggestions.tests.map((test, index) => ( + + ))} +
+ )} + + {/* Loading State */} + {loading && ( +
+ Searching... +
+ )} +
+ )} - + {/* Right Side Actions */}
{/* Dark Mode Toggle */} - {/* Login/Register Buttons */} - - + {/* Explore Dropdown (Quick Clinic / Quick Med) for patients & public */} + {(!isAuthenticated || user?.role === 'patient') && ( +
+ + {isExploreOpen && ( +
+ + +
+ )} +
+ )} + + {/* Lab Admin: Manage Appointments */} + {isAuthenticated && user?.role === 'lab_admin' && ( + + )} + + {/* Lab Admin: Manage Staff shortcut */} + {isAuthenticated && user?.role === 'lab_admin' && ( + + )} + + {/* Lab Admin: Manage Tests */} + {isAuthenticated && user?.role === 'lab_admin' && ( + + )} + + {/* Lab Admin: Lab Settings */} + {isAuthenticated && user?.role === 'lab_admin' && ( + + )} + + {/* Lab Staff: My Appointments */} + {isAuthenticated && user?.role === 'lab_staff' && ( + + )} + + {/* Patient: Appointments (doctor & lab) */} + {isAuthenticated && user?.role === 'patient' && ( + + )} + + {/* Conditional: Login/Register OR Logout */} + {isAuthenticated ? ( + + ) : ( + <> + + + + )}
diff --git a/client/src/components/quicklab/MobileNavbar.jsx b/client/src/components/quicklab/MobileNavbar.jsx index f5768d9..5279401 100644 --- a/client/src/components/quicklab/MobileNavbar.jsx +++ b/client/src/components/quicklab/MobileNavbar.jsx @@ -1,22 +1,143 @@ // MobileNavbar.jsx -import { Search, Home, BookOpen, User } from 'lucide-react'; +import { + Search, + Home, + BookOpen, + User, + LogOut, + Users, + Building2, + FlaskConical, + Calendar, + TestTube, + Settings, +} from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../../context/authContext'; import DarkModeToggle from '../ui/DarkModeToggle'; +import { useState, useEffect, useRef } from 'react'; +import { searchLabs } from '../../service/labService'; export default function MobileNavbar({ searchQuery, setSearchQuery }) { + const navigate = useNavigate(); + const { isAuthenticated, logout, user } = useAuth(); + const [suggestions, setSuggestions] = useState([]); + const [showSuggestions, setShowSuggestions] = useState(false); + const [loading, setLoading] = useState(false); + const [isExploreOpen, setIsExploreOpen] = useState(false); + const searchRef = useRef(null); + const debounceTimeout = useRef(null); + + const handleLogout = async () => { + await logout(); + navigate('/', { replace: true }); + }; + + // Close suggestions when clicking outside + useEffect(() => { + const handleClickOutside = (event) => { + if (searchRef.current && !searchRef.current.contains(event.target)) { + setShowSuggestions(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // Fetch suggestions as user types + useEffect(() => { + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + + if (searchQuery.trim().length < 2) { + setSuggestions([]); + setShowSuggestions(false); + return; + } + + debounceTimeout.current = setTimeout(async () => { + try { + setLoading(true); + const response = await searchLabs({ query: searchQuery.trim(), limit: 5 }); + const labs = response.data || []; + + // Extract unique tests from labs + const testsMap = new Map(); + labs.forEach((lab) => { + if (lab.tests) { + lab.tests.forEach((test) => { + if ( + test.testName && + test.testName.toLowerCase().includes(searchQuery.toLowerCase()) + ) { + testsMap.set(test.testName, { ...test, labId: lab._id, labName: lab.name }); + } + }); + } + }); + + const uniqueTests = Array.from(testsMap.values()).slice(0, 5); + + setSuggestions({ + labs: labs.slice(0, 3), + tests: uniqueTests, + }); + setShowSuggestions(true); + } catch (err) { + console.error('Failed to fetch suggestions:', err); + setSuggestions({ labs: [], tests: [] }); + } finally { + setLoading(false); + } + }, 300); + + return () => { + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + }; + }, [searchQuery]); + + const handleSearch = (e) => { + e.preventDefault(); + setShowSuggestions(false); + if (searchQuery.trim()) { + navigate(`/quick-lab/search?query=${encodeURIComponent(searchQuery.trim())}`); + } else { + navigate('/quick-lab/search'); + } + }; + + const handleLabClick = (labId) => { + setShowSuggestions(false); + setSearchQuery(''); + navigate(`/quick-lab/lab/${labId}`); + }; + + const handleTestClick = (test) => { + setShowSuggestions(false); + setSearchQuery(''); + navigate(`/quick-lab/lab/${test.labId}`); + }; + return ( <> {/* Top Bar - Search Only */}
{/* Logo */} -
+
navigate('/quick-lab')} + >
Quick Labs
-
+
@@ -24,16 +145,89 @@ export default function MobileNavbar({ searchQuery, setSearchQuery }) { type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} - placeholder="Search labs..." + onFocus={() => searchQuery.trim().length >= 2 && setShowSuggestions(true)} + placeholder="Search labs or tests..." className="block w-full pl-10 pr-3 py-2 border border-slate-300 dark:border-slate-700 rounded-lg bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-50 placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-yellow-500 dark:focus:ring-yellow-400 focus:border-transparent transition-all" /> -
+ + {/* Suggestions Dropdown */} + {showSuggestions && (suggestions.labs?.length > 0 || suggestions.tests?.length > 0) && ( +
+ {/* Labs Section */} + {suggestions.labs?.length > 0 && ( +
+

+ Labs +

+ {suggestions.labs.map((lab) => ( + + ))} +
+ )} + + {/* Tests Section */} + {suggestions.tests?.length > 0 && ( +
+

+ Tests +

+ {suggestions.tests.map((test, index) => ( + + ))} +
+ )} + + {/* Loading State */} + {loading && ( +
+ Searching... +
+ )} +
+ )} +
{/* Bottom Navigation Bar */}
-
+
{/* Home/Labs */}
- {/* Login/Account */} - + {/* Explore toggle for patients & public */} + {(!isAuthenticated || user?.role === 'patient') && ( + <> + + {isExploreOpen && ( +
+ + +
+ )} + + )} + + {/* Lab Admin: Manage Appointments */} + {isAuthenticated && user?.role === 'lab_admin' && ( + + )} + + {/* Lab Admin: Manage Tests */} + {isAuthenticated && user?.role === 'lab_admin' && ( + + )} + + {/* Lab Admin: Manage Staff */} + {isAuthenticated && user?.role === 'lab_admin' && ( + + )} + + {/* Lab Admin: Lab Settings */} + {isAuthenticated && user?.role === 'lab_admin' && ( + + )} + + {/* Lab Staff: My Appointments */} + {isAuthenticated && user?.role === 'lab_staff' && ( + + )} + + {/* Patient: Appointments */} + {isAuthenticated && user?.role === 'patient' && ( + + )} + + {/* Conditional: Login/Account OR Logout */} + {isAuthenticated ? ( + + ) : ( + + )}
diff --git a/client/src/components/quickmed/Navbar.jsx b/client/src/components/quickmed/Navbar.jsx index f0c060f..7f08a54 100644 --- a/client/src/components/quickmed/Navbar.jsx +++ b/client/src/components/quickmed/Navbar.jsx @@ -11,6 +11,8 @@ const Navbar = () => { const [isLoading, setIsLoading] = useState(false); const searchRef = useRef(null); const navigate = useNavigate(); + const [isExploreOpen, setIsExploreOpen] = useState(false); + const exploreRef = useRef(null); // Handle search input changes const handleSearchChange = async (e) => { const value = e.target.value; @@ -47,6 +49,9 @@ const Navbar = () => { if (searchRef.current && !searchRef.current.contains(event.target)) { setShowSuggestions(false); } + if (exploreRef.current && !exploreRef.current.contains(event.target)) { + setIsExploreOpen(false); + } }; document.addEventListener('mousedown', handleClickOutside); @@ -117,6 +122,39 @@ const Navbar = () => { {/* Desktop Navigation */}
+ {/* Explore Dropdown (patients & public) */} + {(!isAuthenticated || user?.role === 'patient') && ( +
+ + {isExploreOpen && ( +
+ + +
+ )} +
+ )}
@@ -185,6 +223,21 @@ const Navbar = () => {
)} + {/* Explore quick links */} +
+ + +
)} diff --git a/client/src/pages/doctor/AppointmentDetails.jsx b/client/src/pages/doctor/AppointmentDetails.jsx index 7420856..f6c41f8 100644 --- a/client/src/pages/doctor/AppointmentDetails.jsx +++ b/client/src/pages/doctor/AppointmentDetails.jsx @@ -24,6 +24,7 @@ import { createPrescription, getAppointmentPrescription, updatePrescription, + getNearbyLabs, } from '../../service/prescriptionApiSevice'; import { getMedicineSuggestions } from '../../service/medicineApiService'; @@ -42,11 +43,13 @@ const AppointmentDetails = () => { const [prescriptionData, setPrescriptionData] = useState({ diagnosis: '', medications: [{ name: '', dosage: '', frequency: '', duration: '', instructions: '' }], - tests: [{ name: '', instructions: '' }], + tests: [{ name: '', instructions: '', labId: '' }], notes: '', followUpDate: '', }); const [savingPrescription, setSavingPrescription] = useState(false); + const [nearbyLabs, setNearbyLabs] = useState([]); + const [loadingLabs, setLoadingLabs] = useState(false); const useDebounce = (callback, delay) => { const timeoutRef = useRef(null); @@ -105,6 +108,11 @@ const AppointmentDetails = () => { console.log(err); } + // Fetch nearby labs based on patient city + if (patientRes.patient?.address?.city) { + fetchNearbyLabs(patientRes.patient.address.city); + } + setError(null); } catch (err) { setError('Failed to fetch appointment details'); @@ -114,6 +122,19 @@ const AppointmentDetails = () => { } }; + const fetchNearbyLabs = async (city) => { + try { + setLoadingLabs(true); + const response = await getNearbyLabs(city); + setNearbyLabs(response.data || []); + } catch (err) { + console.error('Error fetching nearby labs:', err); + setNearbyLabs([]); + } finally { + setLoadingLabs(false); + } + }; + const handleStatusUpdate = async (newStatus) => { try { await updateAppointmentStatus(appointmentId, { status: newStatus }); @@ -203,7 +224,7 @@ const AppointmentDetails = () => { const addTest = () => { setPrescriptionData((prev) => ({ ...prev, - tests: [...prev.tests, { name: '', instructions: '' }], + tests: [...prev.tests, { name: '', instructions: '', labId: '' }], })); }; @@ -790,9 +811,12 @@ const AppointmentDetails = () => { {prescriptionData.tests.map((test, index) => (
-
+
+ { className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500" />
-
+
+ + + {loadingLabs && ( +

Loading labs...

+ )} + {!loadingLabs && nearbyLabs.length === 0 && ( +

+ No labs found in patient's city +

+ )} +
+
+ { />
{prescriptionData.tests.length > 1 && ( - +
+ +
)}
))} diff --git a/client/src/pages/patient/PatientAppointments.jsx b/client/src/pages/patient/PatientAppointments.jsx index e27529e..191b833 100644 --- a/client/src/pages/patient/PatientAppointments.jsx +++ b/client/src/pages/patient/PatientAppointments.jsx @@ -1,7 +1,23 @@ import { useEffect, useMemo, useState } from 'react'; +import { + Home, + Building2, + Calendar, + Clock, + MapPin, + Phone, + TestTube, + AlertCircle, + XCircle, + CheckCircle, + User, + Download, + FileText, +} from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { getPatientAppointments } from '../../service/appointmentApiService'; import { getPatientAppointmentPrescription } from '../../service/prescriptionApiSevice'; +import { getPatientLabAppointments } from '../../service/labAppointmentService'; import AppointmentTabs from '../../components/Patient/PatientAppointments/AppointmentTabs'; import AppointmentHeader from '../../components/Patient/PatientAppointments/AppointmentHeader'; import ErrorState from '../../components/Patient/PatientAppointments/ErrorState'; @@ -15,10 +31,20 @@ const PatientAppointments = () => { const [searchTerm, setSearchTerm] = useState(''); const [filterStatus, setFilterStatus] = useState('all'); const [activeTab, setActiveTab] = useState('upcoming'); + const [labAppointments, setLabAppointments] = useState([]); + const [labLoading, setLabLoading] = useState(true); + const [labError, setLabError] = useState(null); + const [activeSection, setActiveSection] = useState('doctor'); + const [labSearchTerm, setLabSearchTerm] = useState(''); + const [labStatusFilter, setLabStatusFilter] = useState('all'); + const [labCollectionFilter, setLabCollectionFilter] = useState('all'); + const [selectedLabAppointment, setSelectedLabAppointment] = useState(null); + const [showLabModal, setShowLabModal] = useState(false); const navigate = useNavigate(); useEffect(() => { fetchAppointments(); + fetchLabAppointments(); }, []); const fetchAppointments = async () => { @@ -74,6 +100,50 @@ const PatientAppointments = () => { return filtered; }, [appointments, searchTerm, filterStatus]); + const labStatusColors = { + pending: 'bg-amber-100 text-amber-800', + confirmed: 'bg-blue-100 text-blue-800', + sample_collected: 'bg-purple-100 text-purple-800', + processing: 'bg-indigo-100 text-indigo-800', + completed: 'bg-green-100 text-green-800', + cancelled: 'bg-red-100 text-red-800', + }; + + const labFilteredAppointments = useMemo(() => { + const term = labSearchTerm.trim().toLowerCase(); + return labAppointments.filter((appt) => { + if (labStatusFilter !== 'all' && appt.status !== labStatusFilter) return false; + if (labCollectionFilter !== 'all' && appt.collectionType !== labCollectionFilter) + return false; + + if (term) { + const matchLab = appt.labId?.name?.toLowerCase().includes(term); + const matchTest = appt.tests?.some((t) => t.testName?.toLowerCase().includes(term)); + return matchLab || matchTest; + } + return true; + }); + }, [labAppointments, labCollectionFilter, labSearchTerm, labStatusFilter]); + + const labUpcomingCount = labAppointments.filter( + (a) => a.status === 'pending' || a.status === 'confirmed' || a.status === 'sample_collected' + ).length; + + const fetchLabAppointments = async () => { + try { + setLabLoading(true); + const response = await getPatientLabAppointments(); + const data = response.data || response; + setLabAppointments(data.appointments || []); + setLabError(null); + } catch (err) { + setLabError('Failed to fetch lab appointments.'); + console.error('Error fetching lab appointments:', err); + } finally { + setLabLoading(false); + } + }; + if (loading) { return ; } @@ -84,22 +154,391 @@ const PatientAppointments = () => { return (
-
- - - navigate(`/patient/appointment/${id}`)} - /> +
+
+
+
+

My Appointments

+ + +
+
+

Upcoming

+
+ + Doctor: {filteredAppointments.filter((a) => a.status !== 'completed').length} + + Lab: {labUpcomingCount} +
+
+
+ +
+ {activeSection === 'doctor' && ( +
+ + + navigate(`/patient/appointment/${id}`)} + /> +
+ )} + + {activeSection === 'lab' && ( +
+
+
+

Lab Appointments

+

+ Search, filter, and open any lab appointment to view full details. +

+
+
+ +
+
+ setLabSearchTerm(e.target.value)} + className="w-full px-4 py-2 rounded-lg border border-gray-200 bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-yellow-500" + /> + + +
+
+ + {labLoading ? ( +
+ Loading lab appointments... +
+ ) : labError ? ( + + ) : labFilteredAppointments.length === 0 ? ( +
+ +

No lab appointments match your filters.

+

Try adjusting filters or search.

+
+ ) : ( +
+ {labFilteredAppointments.map((labAppt) => ( + + ))} +
+ )} +
+ )} +
+
+ + {showLabModal && selectedLabAppointment && ( +
+
+
+
+

+ {selectedLabAppointment.labId?.name || 'Lab Appointment'} +

+
+ + {selectedLabAppointment.status.replace('_', ' ')} + + + {selectedLabAppointment.collectionType === 'home_collection' + ? 'Home collection' + : 'Visit lab'} + + + Payment: {selectedLabAppointment.paymentStatus || 'pending'} + +
+
+ +
+ +
+
+
+

+ Schedule +

+

+ + {new Date(selectedLabAppointment.appointmentDate).toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + year: 'numeric', + })} +

+

+ {' '} + {selectedLabAppointment.appointmentTime} +

+

+ {' '} + {selectedLabAppointment.tests?.length || 0} tests +

+
+ +
+

+ Lab details +

+

{selectedLabAppointment.labId?.name}

+ {selectedLabAppointment.labId?.address && ( +

+ + + {selectedLabAppointment.labId.address.street},{' '} + {selectedLabAppointment.labId.address.city},{' '} + {selectedLabAppointment.labId.address.state}{' '} + {selectedLabAppointment.labId.address.zipCode} + +

+ )} + {selectedLabAppointment.labId?.contact?.phone && ( +

+ {' '} + {selectedLabAppointment.labId.contact.phone} +

+ )} +
+
+ +
+
+

+ {selectedLabAppointment.collectionType === 'home_collection' ? ( + + ) : ( + + )} + {selectedLabAppointment.collectionType === 'home_collection' + ? 'Home collection address' + : 'Visit lab'} +

+ {selectedLabAppointment.collectionType === 'home_collection' && + selectedLabAppointment.collectionAddress ? ( +

+ + + {selectedLabAppointment.collectionAddress.street},{' '} + {selectedLabAppointment.collectionAddress.city},{' '} + {selectedLabAppointment.collectionAddress.state}{' '} + {selectedLabAppointment.collectionAddress.zipCode} + +

+ ) : ( +

Please arrive 10 minutes early.

+ )} +
+ +
+

+ Assigned staff +

+ {selectedLabAppointment.assignedStaffId ? ( + <> +

+ {selectedLabAppointment.assignedStaffId.firstName}{' '} + {selectedLabAppointment.assignedStaffId.lastName} +

+ {selectedLabAppointment.assignedStaffId.phoneNumber && ( +

+ + {selectedLabAppointment.assignedStaffId.phoneNumber} +

+ )} + + ) : ( +

+ Staff assignment will show once confirmed. +

+ )} +
+
+ +
+

+ Tests +

+
+ {selectedLabAppointment.tests?.map((test, idx) => ( +
+
+

{test.testName}

+ {test.testCode && ( +

Code: {test.testCode}

+ )} +
+

₹{test.price}

+
+ ))} +
+
+ +
+
+

+ Notes +

+

+ {selectedLabAppointment.notes || 'No additional notes'} +

+
+ +
+

+ Report +

+ {selectedLabAppointment.reportId?.reportFile?.url ? ( + + Download report + + ) : ( +

+ Report will be available after completion. +

+ )} +
+
+
+
+
+ )}
); }; diff --git a/client/src/pages/public/LoginPage.jsx b/client/src/pages/public/LoginPage.jsx index e413536..9cfd739 100644 --- a/client/src/pages/public/LoginPage.jsx +++ b/client/src/pages/public/LoginPage.jsx @@ -1,19 +1,24 @@ -import { Mail } from 'lucide-react'; +import { Mail, ArrowRight, Shield } from 'lucide-react'; import { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { AuthButton } from '../../components/auth/AuthButton.jsx'; -import { AuthLayout } from '../../components/auth/AuthLayout.jsx'; import { ErrorMessage } from '../../components/auth/ErrorMessage.jsx'; import { PasswordInput } from '../../components/auth/PasswordInput.jsx'; import { useAuth } from '../../context/authContext.jsx'; const LoginPage = ({ error, setError }) => { - const [formData, setFormData] = useState({ email: '', password: '' }); + const navigate = useNavigate(); + const { user, login, isAuthenticated } = useAuth(); + + const [formData, setFormData] = useState({ + email: '', + password: '', + }); + const [showPassword, setShowPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [rememberMe, setRememberMe] = useState(false); const [fieldErrors, setFieldErrors] = useState({}); - const { login, isAuthenticated, user } = useAuth(); - const navigate = useNavigate(); useEffect(() => { if (isAuthenticated && user) { @@ -22,19 +27,27 @@ const LoginPage = ({ error, setError }) => { navigate('/doctor-dashboard'); break; case 'admin': - navigate('/admin/complete-profile'); + navigate('/admin-dashboard'); + break; + case 'lab_admin': + navigate('/lab-admin/dashboard'); + break; + case 'lab_staff': + navigate('/lab-staff/dashboard'); break; default: navigate('/patient-dashboard'); } } - }, [isAuthenticated, navigate]); + }, [isAuthenticated, navigate, user]); const validateForm = () => { const errors = {}; if (!formData.email) errors.email = 'Email is required'; - else if (!/\S+@\S+\.\S+/.test(formData.email)) errors.email = 'Email is invalid'; + else if (!/\S+@\S+\.\S+/.test(formData.email)) errors.email = 'Invalid email address'; + if (!formData.password) errors.password = 'Password is required'; + setFieldErrors(errors); return Object.keys(errors).length === 0; }; @@ -48,102 +61,171 @@ const LoginPage = ({ error, setError }) => { const handleSubmit = async (e) => { e.preventDefault(); - if (!validateForm()) return; setIsLoading(true); - setError(''); // Clear any existing errors + setError(''); - // Call the login function and handle the result const result = await login({ email: formData.email, password: formData.password, }); if (!result.success) { - setError(result.error || 'Login failed. Please try again.'); + setError(result.error || 'Invalid credentials. Please try again.'); } - setIsLoading(false); }; return ( - -
-
-

Welcome Back

-

Sign in to your account

-
- - - -
- {/* Email Input */} -
- -
- - +
+ {/* Left Side - Form */} +
+
+ {/* Header */} +
+
+
- {fieldErrors.email &&

{fieldErrors.email}

} +

+ Sign in to Quick Clinic +

+

+ Access your healthcare dashboard +

- {/* Password Input */} -
- - - {fieldErrors.password && ( -

{fieldErrors.password}

- )} + {/* Form */} +
+ + + + {/* Email */} +
+ + + {fieldErrors.email && ( +

{fieldErrors.email}

+ )} +
+ + {/* Password */} +
+
+ + + Forgot password? + +
+ +
+ + {/* Remember Me */} +
+ setRememberMe(e.target.checked)} + className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" + /> + +
+ + {/* Submit */} + + Sign in + +
- {/* Forgot Password Link */} -
- - Forgot password? + {/* Sign Up Link */} +

+ Don't have an account?{' '} + + Create account -

- - {/* Submit Button */} - - {isLoading ? 'Signing In...' : 'Sign In'} - +

+
+
- {/* Register Link */} -
-

- Don't have an account?{' '} - - Create one - -

+ {/* Right Side - Branding */} +
+
+

Welcome back to Quick Clinic

+

+ Manage appointments, access medical reports, and connect with your healthcare team—all + in one secure platform. +

+
+
+
+ Secure & encrypted +
+
+
+ 24/7 access to your health data +
+
+
+ HIPAA compliant +
- +
- +
); }; diff --git a/client/src/pages/public/RegisterPage.jsx b/client/src/pages/public/RegisterPage.jsx index 142302d..43b5395 100644 --- a/client/src/pages/public/RegisterPage.jsx +++ b/client/src/pages/public/RegisterPage.jsx @@ -1,8 +1,7 @@ -import { Check, Mail, Shield, Stethoscope, User } from 'lucide-react'; +import { Check, Mail, Shield, Stethoscope, User, Microscope, FlaskConical } from 'lucide-react'; import { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { AuthButton } from '../../components/auth/AuthButton.jsx'; -import { AuthLayout } from '../../components/auth/AuthLayout.jsx'; import { ErrorMessage } from '../../components/auth/ErrorMessage.jsx'; import { PasswordInput } from '../../components/auth/PasswordInput.jsx'; import { useAuth } from '../../context/authContext.jsx'; @@ -17,6 +16,7 @@ const RegisterPage = ({ error, setError }) => { confirmPassword: '', role: 'patient', }); + const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -31,19 +31,26 @@ const RegisterPage = ({ error, setError }) => { case 'admin': navigate('/admin/complete-profile'); break; + case 'lab_admin': + navigate('/quick-lab/lab-admin/dashboard'); + break; + case 'lab_staff': + navigate('/quick-lab/lab-staff/dashboard'); + break; default: navigate('/patient-dashboard'); } } - }, [isAuthenticated, navigate]); + }, [isAuthenticated, navigate, user]); const validateForm = () => { const errors = {}; if (!formData.email) errors.email = 'Email is required'; - else if (!/\S+@\S+\.\S+/.test(formData.email)) errors.email = 'Email is invalid'; + else if (!/\S+@\S+\.\S+/.test(formData.email)) errors.email = 'Invalid email address'; + if (!formData.password) errors.password = 'Password is required'; - else if (formData.password.length < 8) - errors.password = 'Password must be at least 8 characters'; + else if (formData.password.length < 8) errors.password = 'Must be at least 8 characters'; + if (formData.password !== formData.confirmPassword) errors.confirmPassword = 'Passwords do not match'; @@ -70,116 +77,251 @@ const RegisterPage = ({ error, setError }) => { password: formData.password, role: formData.role, }); + if (!result.success) { - console.log(result); setError(result.error || 'Registration failed. Please try again.'); } setIsLoading(false); }; const roleOptions = [ - { value: 'patient', label: 'Patient', icon: User, color: 'blue' }, - { value: 'doctor', label: 'Doctor', icon: Stethoscope, color: 'green' }, - { value: 'admin', label: 'Admin', icon: Shield, color: 'purple' }, + { + value: 'patient', + label: 'Patient', + icon: User, + description: 'Book appointments & manage health records', + }, + { + value: 'doctor', + label: 'Doctor', + icon: Stethoscope, + description: 'Manage consultations & patient care', + }, + { + value: 'admin', + label: 'Administrator', + icon: Shield, + description: 'Oversee clinic operations', + }, + { + value: 'lab_admin', + label: 'Lab Administrator', + icon: Microscope, + description: 'Manage laboratory operations', + }, + { + value: 'lab_staff', + label: 'Lab Staff', + icon: FlaskConical, + description: 'Process tests & upload results', + }, ]; return ( - -
- - -
- -
- {roleOptions.map((option) => ( - - ))} +
+ {/* Left Side - Branding */} +
+
+

Join Quick Clinic

+

+ One unified platform for patients, doctors, administrators, and laboratory staff. +

+
+
+
+ Role-based access control +
+
+
+ Enterprise-grade security +
+
+
+ Seamless healthcare management +
+
-
-
- + {/* Right Side - Form */} +
+
+ {/* Header - Compact */} +
+
+ +
+

+ Create your account +

+

+ Get started with Quick Clinic today +

- -
- {fieldErrors.email && ( -

{fieldErrors.email}

- )} - - - - - -
- - Create Account - -
-

- Already have an account?{' '} - - Sign In - -

- - + {/* Form - Compact */} +
+
+ + + {/* Role Selection - Compact Grid */} +
+ +
+ {roleOptions.map((role) => { + const Icon = role.icon; + const isSelected = formData.role === role.value; + + return ( + + ); + })} +
+
+ + {/* Email & Passwords in 2 Columns */} +
+ {/* Email */} +
+ + + {fieldErrors.email && ( +

+ {fieldErrors.email} +

+ )} +
+ + {/* Password */} +
+ + +
+ + {/* Confirm Password */} +
+ + +
+
+ + {/* Terms - Compact */} +

+ By creating an account, you agree to our{' '} + + Terms + {' '} + and{' '} + + Privacy Policy + + . +

+ + {/* Submit - Compact */} + + Create account + + +
+ + {/* Sign In Link - Compact */} +

+ Already have an account?{' '} + + Sign in + +

+
+
+
); }; 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} +
+ )} + +
+
+
+ +
+ handleInputChange('name', e.target.value)} + className="w-full pl-10 pr-4 py-3 rounded-lg border border-lab-black-200 text-lab-black-900 placeholder-lab-black-400 focus:outline-none focus:ring-2 focus:ring-lab-yellow-400 focus:border-transparent" + placeholder="QuickLab Diagnostics" + required + /> + +
+
+ +
+ +
+ handleInputChange('contact.phone', e.target.value)} + className="w-full pl-10 pr-4 py-3 rounded-lg border border-lab-black-200 text-lab-black-900 placeholder-lab-black-400 focus:outline-none focus:ring-2 focus:ring-lab-yellow-400 focus:border-transparent" + placeholder="+1 (555) 000-0000" + required + /> + +
+
+
+ +
+ +