diff --git a/guardian-admin-dashboard/src/App.css b/guardian-admin-dashboard/src/App.css index 2855bf594..b269776c1 100644 --- a/guardian-admin-dashboard/src/App.css +++ b/guardian-admin-dashboard/src/App.css @@ -132,4 +132,232 @@ body { .org-refresh-wrap { margin-top: 22px; +} +/* Doctor Assignments Page */ + +.page-shell { + display: flex; + flex-direction: column; + gap: 24px; + padding: 24px 28px; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.eyebrow { + margin: 0 0 8px; + font-size: 12px; + font-weight: 700; + letter-spacing: 1.5px; + color: #2d8fca; + text-transform: uppercase; +} + +.page-header h1 { + margin: 0; + color: #07336f; + font-size: 30px; + font-weight: 800; +} + +.page-subtitle, +.card-muted { + color: #61708a; + font-size: 14px; + line-height: 1.5; +} + +.dashboard-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.dashboard-card { + background: #ffffff; + border: 1px solid #d8e5f0; + border-radius: 18px; + padding: 22px; + box-shadow: 0 8px 20px rgba(15, 42, 70, 0.05); +} + +.dashboard-card h2 { + margin-top: 0; + margin-bottom: 8px; + color: #07336f; + font-size: 20px; + font-weight: 800; +} + +.full-width-card { + width: auto; +} + +.assignment-form { + display: flex; + flex-direction: column; + gap: 14px; + margin-top: 18px; +} + +.assignment-form label { + display: flex; + flex-direction: column; + gap: 6px; + font-weight: 600; + color: #07336f; + font-size: 14px; +} + +.form-control { + width: 100%; + box-sizing: border-box; + border: 1px solid #d7e4ef; + border-radius: 12px; + padding: 12px 14px; + font-size: 14px; + outline: none; + background: #ffffff; + color: #0b1f3a; +} + +.form-control:focus { + border-color: #4aa3c7; + box-shadow: 0 0 0 3px rgba(74, 163, 199, 0.15); +} + +.primary-button, +.secondary-button, +.danger-button { + border: none; + border-radius: 12px; + padding: 11px 16px; + font-weight: 700; + cursor: pointer; + transition: 0.2s ease; +} + +.primary-button { + background: #07336f; + color: #ffffff; +} + +.primary-button:hover { + background: #052858; +} + +.secondary-button { + background: #eef6fb; + color: #07336f; +} + +.secondary-button:hover { + background: #dceff8; +} + +.danger-button { + background: #ffecec; + color: #c62828; +} + +.danger-button:hover { + background: #ffdada; +} + +.primary-button:disabled, +.secondary-button:disabled, +.danger-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.alert { + padding: 14px 16px; + border-radius: 12px; + font-weight: 600; + font-size: 14px; +} + +.alert-error { + background: #ffecec; + color: #b3261e; + border: 1px solid #ffc9c9; +} + +.alert-success { + background: #eaf8ef; + color: #1b7f3a; + border: 1px solid #bfe8cb; +} + +.summary-box { + margin-top: 16px; + padding: 14px; + border-radius: 12px; + background: #f4f9fd; + display: flex; + flex-direction: column; + gap: 4px; + color: #07336f; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.empty-state { + margin-top: 16px; + padding: 28px; + border: 1px dashed #cddcea; + border-radius: 14px; + color: #61708a; + text-align: center; + background: #f8fbfd; +} + +.table-wrapper { + overflow-x: auto; + margin-top: 16px; +} + +.admin-table { + width: 100%; + border-collapse: collapse; + background: #ffffff; +} + +.admin-table th, +.admin-table td { + text-align: left; + padding: 14px; + border-bottom: 1px solid #edf2f7; + font-size: 14px; +} + +.admin-table th { + color: #07336f; + background: #f7fbfe; + font-weight: 800; +} + +.admin-table td { + color: #253858; +} + +@media (max-width: 900px) { + .dashboard-grid { + grid-template-columns: 1fr; + } + + .section-header { + flex-direction: column; + align-items: flex-start; + } } \ No newline at end of file diff --git a/guardian-admin-dashboard/src/App.jsx b/guardian-admin-dashboard/src/App.jsx index 649ed6b62..d744191ea 100644 --- a/guardian-admin-dashboard/src/App.jsx +++ b/guardian-admin-dashboard/src/App.jsx @@ -10,6 +10,7 @@ import PatientsPage from "./pages/PatientsPage"; import NurseRosterPage from "./pages/NurseRosterPage"; import ReportsPage from "./pages/ReportsPage"; import SettingsPage from "./pages/SettingsPage"; +import DoctorAssignmentsPage from "./pages/DoctorAssignmentsPage"; import "./App.css"; import PatientOverviewPage from "./pages/PatientOverviewPage"; @@ -37,6 +38,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/guardian-admin-dashboard/src/components/dashboard/Sidebar.jsx b/guardian-admin-dashboard/src/components/dashboard/Sidebar.jsx index eb1991043..89309a2a2 100644 --- a/guardian-admin-dashboard/src/components/dashboard/Sidebar.jsx +++ b/guardian-admin-dashboard/src/components/dashboard/Sidebar.jsx @@ -9,20 +9,64 @@ import { Users, Building2, X, + Stethoscope, } from "lucide-react"; import Logo from "../common/Logo"; -import { ADMIN_NAV_ITEMS } from "../../utils/constants"; import { clearAuthStorage } from "../../utils/storage"; +const ADMIN_NAV_ITEMS = [ + { + id: "dashboard", + label: "Dashboard", + path: "/dashboard", + }, + { + id: "staff-management", + label: "Staff Management", + path: "/dashboard/staff-management", + }, + { + id: "org-assignment", + label: "Organisation", + path: "/dashboard/org-assignment", + }, + { + id: "patients", + label: "Patients", + path: "/dashboard/patients", + }, + { + id: "doctor-assignments", + label: "Doctor Assignments", + path: "/dashboard/doctor-assignments", + }, + { + id: "nurse-roster", + label: "Nurse Roster", + path: "/dashboard/nurse-roster", + }, + { + id: "reports", + label: "Reports", + path: "/dashboard/reports", + }, + { + id: "settings", + label: "Settings", + path: "/dashboard/settings", + }, +]; + const iconMap = { dashboard: LayoutDashboard, "staff-management": Users, "org-assignment": Building2, patients: ShieldPlus, - "patient-overview": ClipboardList, +"doctor-assignments": Stethoscope, +"patient-overview": ClipboardList, reports: Bell, settings: Settings, - "nurse-roster": ClipboardList + "nurse-roster": ClipboardList, }; export default function Sidebar({ diff --git a/guardian-admin-dashboard/src/pages/DoctorAssignmentsPage.jsx b/guardian-admin-dashboard/src/pages/DoctorAssignmentsPage.jsx new file mode 100644 index 000000000..6a4acfb86 --- /dev/null +++ b/guardian-admin-dashboard/src/pages/DoctorAssignmentsPage.jsx @@ -0,0 +1,328 @@ +import { useEffect, useMemo, useState } from "react"; +import { + assignDoctorToPatient, + getDoctors, + getPatientsByDoctor, + unassignDoctorFromPatient, +} from "../services/doctorAssignmentService"; + +export default function DoctorAssignmentsPage() { + const [doctors, setDoctors] = useState([]); + const [selectedDoctorId, setSelectedDoctorId] = useState(""); + const [patients, setPatients] = useState([]); + + const [patientId, setPatientId] = useState(""); + const [newDoctorId, setNewDoctorId] = useState(""); + + const [loadingDoctors, setLoadingDoctors] = useState(false); + const [loadingPatients, setLoadingPatients] = useState(false); + const [actionLoading, setActionLoading] = useState(false); + + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + + const selectedDoctor = useMemo(() => { + return doctors.find((doctor) => getId(doctor) === selectedDoctorId); + }, [doctors, selectedDoctorId]); + + useEffect(() => { + loadDoctors(); + }, []); + + useEffect(() => { + if (selectedDoctorId) { + loadPatientsForDoctor(selectedDoctorId); + } else { + setPatients([]); + } + }, [selectedDoctorId]); + + async function loadDoctors() { + try { + setLoadingDoctors(true); + setError(""); + + const data = await getDoctors(); + + const doctorList = + data?.doctors || + data?.data || + data?.items || + data?.results || + (Array.isArray(data) ? data : []); + + setDoctors(doctorList); + } catch (err) { + console.error(err); + setError("Failed to load doctors. Please try again."); + } finally { + setLoadingDoctors(false); + } + } + + async function loadPatientsForDoctor(doctorId) { + try { + setLoadingPatients(true); + setError(""); + setSuccess(""); + + const data = await getPatientsByDoctor(doctorId); + + const patientList = + data?.patients || + data?.data || + data?.items || + data?.results || + (Array.isArray(data) ? data : []); + + setPatients(patientList); + } catch (err) { + console.error(err); + setError("Failed to load patients for this doctor."); + } finally { + setLoadingPatients(false); + } + } + + async function handleAssignDoctor(event) { + event.preventDefault(); + + if (!patientId || !newDoctorId) { + setError("Please enter/select a patient and doctor before assigning."); + return; + } + + try { + setActionLoading(true); + setError(""); + setSuccess(""); + + await assignDoctorToPatient(patientId, newDoctorId); + + setSuccess("Doctor assigned successfully."); + setPatientId(""); + setNewDoctorId(""); + + if (selectedDoctorId) { + await loadPatientsForDoctor(selectedDoctorId); + } + } catch (err) { + console.error(err); + setError("Failed to assign doctor. Please check patient ID and try again."); + } finally { + setActionLoading(false); + } + } + + async function handleUnassignPatient(targetPatientId) { + const confirmed = window.confirm( + "Are you sure you want to unassign this doctor from the selected patient?" + ); + + if (!confirmed) return; + + try { + setActionLoading(true); + setError(""); + setSuccess(""); + + await unassignDoctorFromPatient(targetPatientId); + + setSuccess("Doctor unassigned successfully."); + + if (selectedDoctorId) { + await loadPatientsForDoctor(selectedDoctorId); + } + } catch (err) { + console.error(err); + setError("Failed to unassign doctor. Please try again."); + } finally { + setActionLoading(false); + } + } + + function getId(item) { + return item?._id || item?.id || item?.doctorId || item?.patientId || ""; + } + + function getDoctorName(doctor) { + return ( + doctor?.name || + doctor?.fullName || + doctor?.user?.name || + `${doctor?.firstName || ""} ${doctor?.lastName || ""}`.trim() || + "Unnamed Doctor" + ); + } + + function getPatientName(patient) { + return ( + patient?.name || + patient?.fullName || + patient?.user?.name || + `${patient?.firstName || ""} ${patient?.lastName || ""}`.trim() || + "Unnamed Patient" + ); + } + + return ( +
+
+
+

ADMIN DASHBOARD

+

Doctor Assignments

+

+ View doctors, check assigned patients, and manage doctor-patient + assignments from one place. +

+
+
+ + {error ?
{error}
: null} + {success ?
{success}
: null} + +
+
+

Select Doctor

+

+ Choose a doctor to view the patients currently assigned to them. +

+ + {loadingDoctors ? ( +

Loading doctors...

+ ) : ( + + )} + + {selectedDoctor ? ( +
+ Selected Doctor: + {getDoctorName(selectedDoctor)} +
+ ) : null} +
+ +
+

Assign / Change Doctor

+

+ Enter the patient ID and select the doctor to assign or change the + doctor assignment. +

+ +
+ + + + + +
+
+
+ +
+
+
+

Assigned Patients

+

+ Patients assigned to the selected doctor will appear here. +

+
+ + {selectedDoctorId ? ( + + ) : null} +
+ + {!selectedDoctorId ? ( +
Please select a doctor first.
+ ) : loadingPatients ? ( +
Loading assigned patients...
+ ) : patients.length === 0 ? ( +
+ No patients are currently assigned to this doctor. +
+ ) : ( +
+ + + + + + + + + + + + + {patients.map((patient) => { + const id = getId(patient); + + return ( + + + + + + + + ); + })} + +
Patient NamePatient IDAgeGenderAction
{getPatientName(patient)}{id || "N/A"}{patient?.age || patient?.dob || "N/A"}{patient?.gender || "N/A"} + +
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/guardian-admin-dashboard/src/services/doctorAssignmentService.js b/guardian-admin-dashboard/src/services/doctorAssignmentService.js new file mode 100644 index 000000000..6b6c0ac05 --- /dev/null +++ b/guardian-admin-dashboard/src/services/doctorAssignmentService.js @@ -0,0 +1,142 @@ +// const mockDoctors = [ +// { +// _id: "doc001", +// name: "Dr. John Smith", +// email: "john.smith@guardianhealth.com", +// specialization: "General Physician", +// }, +// { +// _id: "doc002", +// name: "Dr. Emily Brown", +// email: "emily.brown@guardianhealth.com", +// specialization: "Cardiologist", +// }, +// { +// _id: "doc003", +// name: "Dr. Michael Wilson", +// email: "michael.wilson@guardianhealth.com", +// specialization: "Neurologist", +// }, +// ]; + +// let mockDoctorPatients = { +// doc001: [ +// { +// _id: "pat001", +// name: "Robert Green", +// age: 72, +// gender: "Male", +// condition: "Diabetes", +// }, +// { +// _id: "pat002", +// name: "Mary Johnson", +// age: 68, +// gender: "Female", +// condition: "High Blood Pressure", +// }, +// ], +// doc002: [ +// { +// _id: "pat003", +// name: "William Taylor", +// age: 75, +// gender: "Male", +// condition: "Heart Disease", +// }, +// ], +// doc003: [], +// }; + +// function delay(ms = 500) { +// return new Promise((resolve) => setTimeout(resolve, ms)); +// } + +// export async function getDoctors() { +// await delay(); +// return { +// doctors: mockDoctors, +// }; +// } + +// export async function getPatientsByDoctor(doctorId) { +// await delay(); +// return { +// patients: mockDoctorPatients[doctorId] || [], +// }; +// } + +// export async function assignDoctorToPatient(patientId, doctorId) { +// await delay(); + +// const allPatients = Object.values(mockDoctorPatients).flat(); +// const existingPatient = allPatients.find((patient) => patient._id === patientId); + +// let patientToAssign = +// existingPatient || { +// _id: patientId, +// name: `Patient ${patientId}`, +// age: "N/A", +// gender: "N/A", +// condition: "Not available", +// }; + +// Object.keys(mockDoctorPatients).forEach((doctorKey) => { +// mockDoctorPatients[doctorKey] = mockDoctorPatients[doctorKey].filter( +// (patient) => patient._id !== patientId +// ); +// }); + +// if (!mockDoctorPatients[doctorId]) { +// mockDoctorPatients[doctorId] = []; +// } + +// mockDoctorPatients[doctorId].push(patientToAssign); + +// return { +// success: true, +// message: "Doctor assigned successfully", +// }; +// } + +// export async function unassignDoctorFromPatient(patientId) { +// await delay(); + +// Object.keys(mockDoctorPatients).forEach((doctorKey) => { +// mockDoctorPatients[doctorKey] = mockDoctorPatients[doctorKey].filter( +// (patient) => patient._id !== patientId +// ); +// }); + +// return { +// success: true, +// message: "Doctor unassigned successfully", +// }; +// } +import api from "./api"; + +export async function getDoctors() { + const response = await api.get("/api/v1/doctors"); + return response.data; +} + +export async function getPatientsByDoctor(doctorId) { + const response = await api.get(`/api/v1/doctors/${doctorId}/patients`); + return response.data; +} + +export async function assignDoctorToPatient(patientId, doctorId) { + const response = await api.post( + `/api/v1/patients/${patientId}/assign-doctor`, + { doctorId } + ); + return response.data; +} + +export async function unassignDoctorFromPatient(patientId) { + const response = await api.post( + `/api/v1/patients/${patientId}/assign-doctor`, + { doctorId: null } + ); + return response.data; +} \ No newline at end of file