diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 837d337b7..31d6a312a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -69,6 +69,9 @@ + diff --git a/app/src/main/java/deakin/gopher/guardian/WelcomeActivity.kt b/app/src/main/java/deakin/gopher/guardian/WelcomeActivity.kt index 4c2b2ab62..45ecec4b8 100644 --- a/app/src/main/java/deakin/gopher/guardian/WelcomeActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/WelcomeActivity.kt @@ -1,11 +1,67 @@ package deakin.gopher.guardian import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.View +import android.widget.Button +import deakin.gopher.guardian.components.EmptyView +import deakin.gopher.guardian.components.ErrorView +import deakin.gopher.guardian.components.LoadingView import androidx.appcompat.app.AppCompatActivity class WelcomeActivity : AppCompatActivity() { + + private lateinit var loadingView: LoadingView + private lateinit var emptyView: EmptyView + private lateinit var errorView: ErrorView + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_welcome) // This links to your activity_welcome.xml + + loadingView = findViewById(R.id.loadingView) + emptyView = findViewById(R.id.emptyView) + errorView = findViewById(R.id.errorView) + + val btnShowLoading = findViewById + + + {open && ( + + {options.map((opt) => ( +
  • handleSelect(opt.value)} + > + {opt.label} +
  • + ))} +
    + )} +
    + + {error && {error}} + + ); +} diff --git a/guardian-admin-dashboard/src/components/common/InputField.css b/guardian-admin-dashboard/src/components/common/InputField.css new file mode 100644 index 000000000..c6c04289d --- /dev/null +++ b/guardian-admin-dashboard/src/components/common/InputField.css @@ -0,0 +1,77 @@ +.field { + display: grid; + gap: 8px; +} + +.field-label { + font-size: 0.92rem; + font-weight: 700; + color: var(--primary-dark); +} + +.input-shell { + position: relative; + width: 100%; +} + +.field-input { + width: 100%; + border: 1px solid var(--border); + background: var(--white); + border-radius: 16px; + padding: 15px 16px; + color: var(--text); + outline: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} + +.field-input:focus { + border-color: rgba(79, 160, 200, 0.95); + box-shadow: 0 0 0 4px rgba(79, 160, 200, 0.14); +} + +.field-input--error { + border-color: #c73939; +} + +.field-error { + font-size: 0.84rem; + color: #c73939; + font-weight: 600; +} + +.password-input { + padding-right: 56px; +} + +.password-toggle { + position: absolute; + top: 50%; + right: 14px; + transform: translateY(-50%); + width: 32px; + height: 32px; + border: 0; + border-radius: 10px; + background: rgba(79, 160, 200, 0.08); + color: var(--primary-dark); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.2s ease, transform 0.2s ease, color 0.2s ease; +} + +.password-toggle:hover { + background: rgba(79, 160, 200, 0.16); + color: var(--primary-dark); +} + +.password-toggle:active { + transform: translateY(-50%) scale(0.96); +} + +.password-toggle:focus-visible { + outline: 2px solid rgba(79, 160, 200, 0.45); + outline-offset: 2px; +} \ No newline at end of file diff --git a/guardian-admin-dashboard/src/components/common/InputField.jsx b/guardian-admin-dashboard/src/components/common/InputField.jsx index 50507e50e..e69bcd6db 100644 --- a/guardian-admin-dashboard/src/components/common/InputField.jsx +++ b/guardian-admin-dashboard/src/components/common/InputField.jsx @@ -1,3 +1,7 @@ +import { useState } from "react"; +import { Eye, EyeOff } from "lucide-react"; +import "./InputField.css"; + export default function InputField({ label, type = "text", @@ -6,19 +10,41 @@ export default function InputField({ name, placeholder, autoComplete, + error, }) { + const [showPassword, setShowPassword] = useState(false); + const isPasswordField = type === "password"; + return ( ); } \ No newline at end of file diff --git a/guardian-admin-dashboard/src/components/common/Modal.jsx b/guardian-admin-dashboard/src/components/common/Modal.jsx new file mode 100644 index 000000000..bb38693cd --- /dev/null +++ b/guardian-admin-dashboard/src/components/common/Modal.jsx @@ -0,0 +1,37 @@ +import { X } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; + +export default function Modal({ open, onClose, title, children }) { + return ( + + {open && ( + <> + +
    + +
    +

    {title}

    + +
    +
    {children}
    +
    +
    + + )} +
    + ); +} diff --git a/guardian-admin-dashboard/src/components/common/Toast.jsx b/guardian-admin-dashboard/src/components/common/Toast.jsx new file mode 100644 index 000000000..7b0214486 --- /dev/null +++ b/guardian-admin-dashboard/src/components/common/Toast.jsx @@ -0,0 +1,74 @@ +import { motion, AnimatePresence } from 'framer-motion'; +import Button from './Button'; + +const VARIANT_STYLES = { + confirm: 'var(--color-danger, #e53e3e)', + success: 'var(--color-success, #38a169)', +}; + +export default function Toast({ + open, + variant = 'confirm', + title, + message, + confirmLabel, + cancelLabel = 'Cancel', + onConfirm, + onCancel, +}) { + const defaultTitle = variant === 'success' ? 'Success' : 'Are you sure?'; + const defaultConfirmLabel = variant === 'success' ? 'OK' : 'Confirm'; + + return ( + + {open && ( + <> + +
    + +
    +

    {title ?? defaultTitle}

    +
    +
    +

    {message}

    +
    + {variant === 'confirm' && ( + + )} + +
    +
    +
    +
    + + )} +
    + ); +} diff --git a/guardian-admin-dashboard/src/components/dashboard/Sidebar.jsx b/guardian-admin-dashboard/src/components/dashboard/Sidebar.jsx index ea3d2fc93..eb1991043 100644 --- a/guardian-admin-dashboard/src/components/dashboard/Sidebar.jsx +++ b/guardian-admin-dashboard/src/components/dashboard/Sidebar.jsx @@ -19,6 +19,7 @@ const iconMap = { "staff-management": Users, "org-assignment": Building2, patients: ShieldPlus, + "patient-overview": ClipboardList, reports: Bell, settings: Settings, "nurse-roster": ClipboardList @@ -93,7 +94,10 @@ export default function Sidebar({ `sidebar-link ${isActive ? "active" : ""} ${ collapsed && !isMobile ? "icon-only" : "" diff --git a/guardian-admin-dashboard/src/index.css b/guardian-admin-dashboard/src/index.css index fd883b28e..ca869395d 100644 --- a/guardian-admin-dashboard/src/index.css +++ b/guardian-admin-dashboard/src/index.css @@ -321,6 +321,10 @@ a { gap: 8px; } +.dropdown-wrapper { + position: relative; +} + .field-label { font-size: 0.92rem; font-weight: 700; @@ -337,12 +341,121 @@ a { outline: none; } +.field-select { + cursor: pointer; +} + +.dropdown-trigger { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + text-align: left; + background: var(--white); + border: 1px solid var(--border); + border-radius: 16px; + padding: 15px 16px; + color: var(--text); + cursor: pointer; +} + +.dropdown-trigger.placeholder-shown { + color: #94a3b8; +} + +.dropdown-trigger:focus { + outline: none; + border-color: rgba(79, 160, 200, 0.9); + box-shadow: 0 0 0 4px rgba(79, 160, 200, 0.14); +} + +.dropdown-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + z-index: 200; + padding: 6px; + list-style: none; + margin: 0; + background: var(--white); + border: 1px solid var(--border); + border-radius: 16px; + box-shadow: var(--shadow-lg); +} + +.dropdown-option { + padding: 10px 14px; + border-radius: 10px; + cursor: pointer; + font-size: 0.95rem; + color: var(--text); +} + +.dropdown-option:hover { + background: #f0f7fc; + color: var(--primary-dark); +} + +.dropdown-option.selected { + background: rgba(79, 160, 200, 0.12); + color: var(--primary-dark); + font-weight: 600; +} + +.field-select.placeholder-shown, +.field-select:invalid { + color: #94a3b8; +} + +.field-select option { + color: var(--text); +} + +.field-select option[value=""] { + color: #94a3b8; +} + .field-input:focus, .otp-input:focus { border-color: rgba(79, 160, 200, 0.9); box-shadow: 0 0 0 4px rgba(79, 160, 200, 0.14); } +.table-loader { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 48px 0; + color: var(--text-muted, #94a3b8); + font-size: 0.9rem; +} + +.table-loader-spinner { + width: 36px; + height: 36px; + border: 3px solid var(--border); + border-top-color: rgba(79, 160, 200, 0.9); + border-radius: 50%; + animation: table-spin 0.7s linear infinite; +} + +@keyframes table-spin { + to { transform: rotate(360deg); } +} + +.field-input--error { + border-color: #ef4444 !important; + box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.12) !important; +} + +.field-error { + font-size: 0.8rem; + color: #ef4444; + margin-top: -4px; +} + .auth-card-helper { display: flex; justify-content: space-between; @@ -954,6 +1067,127 @@ a { font-weight: 600; } +/* modal */ + +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(20, 61, 116, 0.18); + backdrop-filter: blur(4px); + z-index: 100; +} + +.modal-container { + position: fixed; + inset: 0; + z-index: 101; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + pointer-events: none; +} + +.modal { + width: 100%; + max-width: 480px; + background: var(--white); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + padding: 28px; + pointer-events: all; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + +.modal-title { + margin: 0; + color: var(--primary-dark); + font-size: 1.2rem; +} + +.modal-close { + border: 1px solid var(--border); + background: transparent; + color: var(--text-muted); + width: 34px; + height: 34px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.modal-close:hover { + background: #f5f8fb; + color: var(--primary-dark); +} + +.modal-body { + display: grid; + gap: 16px; +} + +.btn-deactivate { + border: 1px solid var(--danger); + background: transparent; + color: var(--danger); + font-weight: 600; + font-size: 0.85rem; + padding: 6px 14px; + border-radius: 10px; + cursor: pointer; +} + +.btn-deactivate:hover { + background: rgba(228, 98, 111, 0.08); +} + +.btn-activate { + border: 1px solid var(--success); + background: transparent; + color: var(--success); + font-weight: 600; + font-size: 0.85rem; + padding: 6px 14px; + border-radius: 10px; + cursor: pointer; +} + +.btn-activate:hover { + background: rgba(23, 166, 115, 0.08); +} + +.modal-footer { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; + margin-top: 8px; +} + +.btn-secondary { + border: 1px solid var(--border); + background: transparent; + color: var(--text-muted); + font-weight: 600; + padding: 10px 18px; + border-radius: 14px; + cursor: pointer; +} + +.btn-secondary:hover { + background: #f5f8fb; + color: var(--primary-dark); +} + @media (max-width: 1100px) { .auth-grid { grid-template-columns: 1fr; diff --git a/guardian-admin-dashboard/src/pages/OrgAssignmentPage.jsx b/guardian-admin-dashboard/src/pages/OrgAssignmentPage.jsx index c948897a9..2f5587c80 100644 --- a/guardian-admin-dashboard/src/pages/OrgAssignmentPage.jsx +++ b/guardian-admin-dashboard/src/pages/OrgAssignmentPage.jsx @@ -1,12 +1,316 @@ +import { useCallback, useEffect, useState } from "react"; +import { motion } from "framer-motion"; +import { + ArrowRight, + Building2, + CalendarDays, + CircleAlert, + FileText, + RefreshCcw, + ShieldCheck, + Users, +} from "lucide-react"; +import { Link } from "react-router-dom"; +import Button from "../components/common/Button"; +import Dropdown from "../components/common/Dropdown"; +import { getMyOrganizations } from "../services/orgService"; + +function formatDate(dateValue) { + if (!dateValue) return "-"; + + const date = new Date(dateValue); + if (Number.isNaN(date.getTime())) return "-"; + + return date.toLocaleDateString("en-AU", { + day: "2-digit", + month: "short", + year: "numeric", + }); +} + export default function OrgAssignmentPage() { + const [organizations, setOrganizations] = useState([]); + const [selectedOrgId, setSelectedOrgId] = useState(""); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchOrganizations = useCallback(async () => { + setLoading(true); + setError(""); + + try { + const response = await getMyOrganizations(); + const orgs = Array.isArray(response?.orgs) ? response.orgs : []; + + setOrganizations(orgs); + setSelectedOrgId((prev) => prev || orgs[0]?._id || ""); + } catch (err) { + console.error("Failed to fetch organizations:", err); + setError("Failed to fetch organizations."); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchOrganizations(); + }, [fetchOrganizations]); + + const selectedOrg = + organizations.find((org) => org._id === selectedOrgId) || organizations[0]; + + const organizationOptions = organizations.map((org) => ({ + value: org._id, + label: org.name, + })); + + const stats = selectedOrg + ? [ + { + title: "Staff Members", + value: selectedOrg.staff?.length ?? 0, + icon: Users, + description: "Staff currently linked with this organisation.", + }, + { + title: "Status", + value: selectedOrg.active ? "Active" : "Inactive", + icon: ShieldCheck, + description: "Current organisation status from backend data.", + }, + { + title: "Created On", + value: formatDate(selectedOrg.created_at), + icon: CalendarDays, + description: "Date this organisation record was created.", + }, + ] + : []; + + if (loading) { + return ( +
    +

    Organisation & Assignment

    +

    Loading organisation details...

    +
    + ); + } + + if (error) { + return ( +
    +
    + +
    +

    Organisation & Assignment

    +

    {error}

    +
    +
    + + +
    + ); + } + + if (!selectedOrg) { + return ( +
    +

    Organisation & Assignment

    +

    No organisation data found yet.

    +
    + ); + } + return ( -
    -

    Organisation & Assignment

    -

    - This route is prepared for organisation overview and assignment-related - admin work. The team can continue implementation here using the shared - shell layout. -

    +
    + +
    +
    +

    Organisation Overview

    +

    {selectedOrg.name}

    +

    + View organisation information, staff count, current status, and + prepare for organisation-related admin workflows. +

    +
    + +
    + + +
    + Staff Management + View and manage staff linked with this organisation +
    + + + + + +
    + Dashboard + Return to the main administrator workspace +
    + + +
    +
    +
    + +
    + {stats.map((item, index) => { + const Icon = item.icon; + + return ( + +
    +
    +

    {item.title}

    +

    {item.value}

    +
    + +
    + +
    +
    + +

    {item.description}

    +
    + ); + })} +
    + +
    + +
    +

    Organisation Details

    + + {organizationOptions.length > 1 && ( +
    + setSelectedOrgId(e.target.value)} + options={organizationOptions} + /> +
    + )} +
    + +
    +
    +

    Organisation ID

    +
    + {selectedOrg._id || "-"} +
    +
    + +
    +

    Name

    +
    {selectedOrg.name || "-"}
    +
    + +
    +

    Description

    +
    + {selectedOrg.description || "No description available."} +
    +
    + +
    +

    Active

    +
    + {selectedOrg.active ? "Yes" : "No"} +
    +
    + +
    +

    Created By

    +
    + {selectedOrg.createdBy || "-"} +
    +
    + +
    +

    Created At

    +
    + {formatDate(selectedOrg.created_at)} +
    +
    + +
    +

    Updated At

    +
    + {formatDate(selectedOrg.updated_at)} +
    +
    + +
    +

    Staff Count

    +
    + {selectedOrg.staff?.length ?? 0} +
    +
    +
    +
    + + +

    Overview Summary

    + +
    +
    + + Organisation data is connected to this admin account +
    + +
    + + {selectedOrg.staff?.length ?? 0} linked staff member(s) +
    + +
    + + Status: {selectedOrg.active ? "Active" : "Inactive"} +
    + +
    + + Ready for future organisation workflows and extensions +
    +
    + +
    + +
    +
    +
    ); } \ No newline at end of file diff --git a/guardian-admin-dashboard/src/pages/PatientOverviewPage.jsx b/guardian-admin-dashboard/src/pages/PatientOverviewPage.jsx new file mode 100644 index 000000000..5b49d43a9 --- /dev/null +++ b/guardian-admin-dashboard/src/pages/PatientOverviewPage.jsx @@ -0,0 +1,349 @@ +import { useEffect, useMemo, useState } from "react"; +import { getAllPatients, getPatientOverview } from "../services/patientService"; + + + +export default function PatientOverviewPage() { + const [patients, setPatients] = useState([]); + const [selectedPatientId, setSelectedPatientId] = useState(""); + const [searchTerm, setSearchTerm] = useState(""); + + const [overview, setOverview] = useState(null); + const [loadingPatients, setLoadingPatients] = useState(true); + const [loadingOverview, setLoadingOverview] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + const fetchPatients = async () => { + try { + setLoadingPatients(true); + setError(""); + + const data = await getAllPatients(); + const patientList = Array.isArray(data) ? data : data?.patients || []; + setPatients(patientList); + } catch (err) { + console.error("Failed to load patients:", err); + setError(err?.response?.data?.message || "Failed to load patients"); + } finally { + setLoadingPatients(false); + } + }; + + fetchPatients(); + }, []); + + const filteredPatients = useMemo(() => { + const term = searchTerm.trim().toLowerCase(); + + if (!term) return patients; + + return patients.filter((patient) => { + const fullname = patient?.fullname?.toLowerCase() || ""; + const id = patient?._id?.toLowerCase() || ""; + return fullname.includes(term) || id.includes(term); + }); + }, [patients, searchTerm]); + + const handleSelectPatient = async (patientId) => { + try { + setSelectedPatientId(patientId); + setLoadingOverview(true); + setError(""); + + const selectedPatient = patients.find(p => p._id === patientId); + const orgId = selectedPatient?.organization; + const data = await getPatientOverview(patientId, orgId); + + setOverview(data); + } catch (err) { + console.error("Failed to load patient overview:", err); + console.log("Backend error data:", err?.response?.data); + setError( + err?.response?.data?.message || "Failed to load patient overview" + ); + setOverview(null); + setSelectedPatientId(""); + } finally { + setLoadingOverview(false); + } + }; + + const handleChangePatient = () => { + setSelectedPatientId(""); + setOverview(null); + setError(""); + }; + + const patient = overview?.patient || {}; + const records = overview?.records || []; + const carePlans = overview?.carePlans || []; + const tasks = overview?.tasks || []; + const logs = overview?.logs || []; + + return ( +
    +
    +

    Patient Overview

    + +
    +
    + + setSearchTerm(e.target.value)} + style={styles.input} + /> +
    +
    + + {loadingPatients ? ( +

    Loading patients...

    + ) : error && !overview ? ( +

    {error}

    + ) : !selectedPatientId ? ( +
    + {filteredPatients.length === 0 ? ( +

    No patients found.

    + ) : ( + filteredPatients.map((item) => ( +
    +
    +

    {item.fullname}

    + {/*

    ID: {item._id}

    */} +

    + Gender: {item.gender || "-"} +

    +

    Age: {item.age ?? "-"}

    +
    + + +
    + )) + )} +
    + ) : null} + + {loadingOverview ? ( +

    Loading patient overview...

    + ) : overview ? ( + <> + + +
    +

    + {patient.fullname || "Unknown Patient"} +

    +

    Gender: {patient.gender || "-"}

    +

    Date of Birth: {new Date(patient.dateOfBirth).toLocaleDateString() || "-"}

    +

    Organization: {patient.organization?.name || "Guardian Moniter"}

    + +

    Caretaker

    +

    {patient.caretaker?.fullname || "-"}

    +

    {patient.caretaker?.email || "-"}

    + +

    Assigned Nurses

    + {patient.assignedNurses?.length ? ( + patient.assignedNurses.map((nurse) => ( +
    +

    {nurse.fullname}

    +

    {nurse.email}

    +
    + )) + ) : ( +

    No nurses assigned

    + )} + +

    Assigned Doctor

    +

    {patient.assignedDoctor?.fullname || "-"}

    +

    {patient.assignedDoctor?.email || "-"}

    +
    + +
    +
    +

    Records

    +

    {records.length}

    +
    +
    +

    Care Plans

    +

    {carePlans.length}

    +
    +
    +

    Tasks

    +

    {tasks.length}

    +
    +
    +

    Logs

    +

    {logs.length}

    +
    +
    + +
    +
    +
    +
    + + ) : null} +
    +
    + ); +} + +function Section({ title, items }) { + return ( +
    +

    {title}

    + {!items?.length ? ( +
    No {title.toLowerCase()} available.
    + ) : ( +
    + {items.map((item, index) => ( +
    +
    +                {JSON.stringify(item, null, 2)}
    +              
    +
    + ))} +
    + )} +
    + ); +} + +const styles = { + page: { + padding: "24px", + }, + card: { + background: "#f9fbfd", + borderRadius: "28px", + padding: "28px", + border: "1px solid #d9e4ee", + }, + title: { + color: "#1f4788", + fontSize: "2rem", + fontWeight: 700, + marginBottom: "24px", + }, + topSection: { + display: "flex", + gap: "16px", + marginBottom: "20px", + }, + searchBlock: { + width: "100%", + }, + label: { + display: "block", + marginBottom: "10px", + fontSize: "1.1rem", + fontWeight: 600, + color: "#1f4788", + }, + input: { + width: "100%", + maxWidth: "520px", + padding: "16px 20px", + borderRadius: "22px", + border: "1px solid #d9e4ee", + fontSize: "1rem", + outline: "none", + }, + listWrap: { + display: "grid", + gap: "14px", + marginBottom: "24px", + }, + patientRow: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + background: "#ffffff", + borderRadius: "18px", + padding: "18px", + border: "1px solid #d9e4ee", + }, + patientName: { + margin: "0 0 8px 0", + color: "#1f4788", + }, + patientMeta: { + margin: "4px 0", + color: "#5b6b84", + }, + button: { + background: "linear-gradient(90deg, #6fb2dc, #4f95c5)", + color: "#fff", + border: "none", + borderRadius: "16px", + padding: "12px 18px", + cursor: "pointer", + fontWeight: 600, + }, + secondaryButton: { + background: "#ffffff", + color: "#1f4788", + border: "1px solid #d9e4ee", + borderRadius: "14px", + padding: "10px 16px", + cursor: "pointer", + fontWeight: 600, + marginTop: "6px", + marginBottom: "14px", + }, + overviewCard: { + background: "#fff", + borderRadius: "20px", + padding: "20px", + marginTop: "10px", + border: "1px solid #d9e4ee", + }, + sectionTitle: { + color: "#1f4788", + marginBottom: "12px", + }, + subTitle: { + color: "#1f4788", + marginTop: "16px", + marginBottom: "8px", + }, + statsRow: { + display: "grid", + gridTemplateColumns: "repeat(4, 1fr)", + gap: "14px", + marginTop: "20px", + }, + statCard: { + background: "#fff", + borderRadius: "18px", + padding: "18px", + textAlign: "center", + border: "1px solid #d9e4ee", + }, + emptyBox: { + background: "#fff", + borderRadius: "14px", + padding: "16px", + border: "1px solid #d9e4ee", + }, + itemBox: { + background: "#fff", + borderRadius: "14px", + padding: "16px", + border: "1px solid #d9e4ee", + }, + message: { + marginTop: "18px", + color: "#2c3e50", + }, +}; \ No newline at end of file diff --git a/guardian-admin-dashboard/src/pages/PatientsPage.jsx b/guardian-admin-dashboard/src/pages/PatientsPage.jsx index 8a2899390..faa8e3db8 100644 --- a/guardian-admin-dashboard/src/pages/PatientsPage.jsx +++ b/guardian-admin-dashboard/src/pages/PatientsPage.jsx @@ -1,8 +1,814 @@ -export default function PatientsPage() { +import { useEffect, useState } from 'react'; +import { + getPatients, + createPatient, + deactivatePatient, +} from '../services/patientService'; + +const initialFormData = { + fullname: '', + gender: '', + dateOfBirth: '', + caretakerId: '', + nurseId: '', + doctorId: '', + image: '', + dateOfAdmitting: '', + description: '', +}; + +function formatDate(dateString) { + if (!dateString) return 'N/A'; + + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) return dateString; + + return date.toLocaleDateString(); +} + +function PatientsPage() { + const [patients, setPatients] = useState([]); + const [pagination, setPagination] = useState({ + total: 0, + page: 1, + pages: 1, + limit: 10, + }); + + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [deactivatingId, setDeactivatingId] = useState(''); + const [error, setError] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + const [showForm, setShowForm] = useState(false); + const [formData, setFormData] = useState(initialFormData); + + const loadPatients = async (page = 1) => { + try { + setLoading(true); + setError(''); + + const data = await getPatients({ page, limit: 10 }); + + setPatients(data?.patients || []); + setPagination( + data?.pagination || { + total: 0, + page: 1, + pages: 1, + limit: 10, + } + ); + } catch (err) { + console.error('Load patients error:', err); + setError( + err?.response?.data?.message || + err?.message || + 'Failed to load patients.' + ); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadPatients(); + }, []); + + const handleInputChange = (e) => { + const { name, value } = e.target; + + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const resetForm = () => { + setFormData(initialFormData); + }; + + const handleToggleForm = () => { + setShowForm((prev) => !prev); + setError(''); + setSuccessMessage(''); + + if (showForm) { + resetForm(); + } + }; + + const handleAddPatient = async (e) => { + e.preventDefault(); + + if ( + !formData.fullname || + !formData.gender || + !formData.dateOfBirth || + !formData.caretakerId || + !formData.nurseId || + !formData.doctorId || + !formData.dateOfAdmitting + ) { + setError( + 'Full name, gender, date of birth, caretaker ID, nurse ID, doctor ID, and date of admitting are required.' + ); + setSuccessMessage(''); + return; + } + + try { + setSubmitting(true); + setError(''); + setSuccessMessage(''); + + const payload = { + fullname: formData.fullname, + gender: formData.gender, + dateOfBirth: formData.dateOfBirth, + caretakerId: formData.caretakerId, + nurseId: formData.nurseId, + doctorId: formData.doctorId, + image: formData.image, + dateOfAdmitting: formData.dateOfAdmitting, + description: formData.description, + }; + + const response = await createPatient(payload); + + setSuccessMessage(response?.message || 'Patient created.'); + resetForm(); + setShowForm(false); + + await loadPatients(pagination.page || 1); + } catch (err) { + console.error('Add patient error:', err); + setError( + err?.response?.data?.message || + err?.message || + 'Failed to add patient.' + ); + setSuccessMessage(''); + } finally { + setSubmitting(false); + } + }; + + const handleDeactivate = async (id) => { + const confirmed = window.confirm( + 'Are you sure you want to deactivate this patient?' + ); + + if (!confirmed) return; + + try { + setDeactivatingId(id); + setError(''); + setSuccessMessage(''); + + const response = await deactivatePatient(id); + + setSuccessMessage(response?.message || 'Patient deactivated successfully.'); + await loadPatients(pagination.page || 1); + } catch (err) { + console.error('Deactivate patient error:', err); + setError( + err?.response?.data?.message || + err?.message || + 'Failed to deactivate patient.' + ); + setSuccessMessage(''); + } finally { + setDeactivatingId(''); + } + }; + + const handlePreviousPage = () => { + if (pagination.page > 1) { + loadPatients(pagination.page - 1); + } + }; + + const handleNextPage = () => { + if (pagination.page < pagination.pages) { + loadPatients(pagination.page + 1); + } + }; + return ( -
    -

    Patients

    -

    This placeholder route is ready for future patient-related admin work.

    +
    +
    +
    +
    +

    Patients

    +

    + Manage patients under your organisation. +

    +
    + + +
    + + {successMessage && ( +
    + {successMessage} +
    + )} + + {showForm && ( +
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + +
    + +