From d2e9942edd45be423baa02b18779bbec7f4a0d54 Mon Sep 17 00:00:00 2001 From: munnokd Date: Tue, 31 Mar 2026 22:40:24 +1100 Subject: [PATCH 01/10] Doctor role has been added to the frontend code --- app/src/main/AndroidManifest.xml | 3 +++ .../java/deakin/gopher/guardian/model/login/Role.kt | 6 ++++++ .../gopher/guardian/services/NavigationService.kt | 10 ++++++++++ .../gopher/guardian/view/general/MainActivity.kt | 3 +++ 4 files changed, 22 insertions(+) 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/model/login/Role.kt b/app/src/main/java/deakin/gopher/guardian/model/login/Role.kt index b868e8fe4..ca2e86634 100644 --- a/app/src/main/java/deakin/gopher/guardian/model/login/Role.kt +++ b/app/src/main/java/deakin/gopher/guardian/model/login/Role.kt @@ -16,16 +16,22 @@ sealed class Role(val name: String) : Serializable { private fun readResolve(): Any = Nurse } + data object Doctor : Role("doctor") { + private fun readResolve(): Any = Doctor + } + companion object { private const val CARETAKER_ROLE = "caretaker" private const val ADMIN_ROLE = "admin" private const val NURSE_ROLE = "nurse" + private const val DOCTOR_ROLE = "doctor" fun create(name: String): Role { return when (name.lowercase()) { CARETAKER_ROLE.lowercase() -> Caretaker ADMIN_ROLE.lowercase() -> Admin NURSE_ROLE.lowercase() -> Nurse + DOCTOR_ROLE.lowercase() -> Doctor else -> throw IllegalArgumentException("Unknown role: $name") } } diff --git a/app/src/main/java/deakin/gopher/guardian/services/NavigationService.kt b/app/src/main/java/deakin/gopher/guardian/services/NavigationService.kt index 255c2e34f..c0b2775d5 100644 --- a/app/src/main/java/deakin/gopher/guardian/services/NavigationService.kt +++ b/app/src/main/java/deakin/gopher/guardian/services/NavigationService.kt @@ -5,6 +5,7 @@ import android.content.Intent import deakin.gopher.guardian.model.login.Role import deakin.gopher.guardian.view.general.Homepage4admin import deakin.gopher.guardian.view.general.Homepage4caretaker +import deakin.gopher.guardian.view.general.Homepage4doctor import deakin.gopher.guardian.view.general.Homepage4nurse import deakin.gopher.guardian.view.general.LoginActivity import deakin.gopher.guardian.view.general.PatientListActivity @@ -43,6 +44,15 @@ class NavigationService(val activity: Activity) { ), ) } + + Role.Doctor -> { + activity.startActivity( + Intent( + activity.applicationContext, + Homepage4doctor::class.java, + ), + ) + } } } diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/MainActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/MainActivity.kt index 5304cba68..3636d34d2 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/MainActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/MainActivity.kt @@ -47,6 +47,9 @@ class MainActivity : BaseActivity() { Role.Nurse -> { startActivity(Intent(this@MainActivity, Homepage4nurse::class.java)) } + Role.Doctor -> { + startActivity(Intent(this@MainActivity, Homepage4doctor::class.java)) + } } } } From 711d33ae46a899bd814bd413dff4a09eb5cb0fcc Mon Sep 17 00:00:00 2001 From: munnokd Date: Tue, 31 Mar 2026 22:44:16 +1100 Subject: [PATCH 02/10] Doctor role has been added to the frontend code --- .../guardian/view/general/Homepage4doctor.kt | 20 +++++++++++++ .../res/layout/activity_homepage4doctor.xml | 29 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 app/src/main/java/deakin/gopher/guardian/view/general/Homepage4doctor.kt create mode 100644 app/src/main/res/layout/activity_homepage4doctor.xml diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/Homepage4doctor.kt b/app/src/main/java/deakin/gopher/guardian/view/general/Homepage4doctor.kt new file mode 100644 index 000000000..fb2461467 --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/view/general/Homepage4doctor.kt @@ -0,0 +1,20 @@ +package deakin.gopher.guardian.view.general + +import android.os.Bundle +import android.widget.Button +import androidx.appcompat.app.AppCompatActivity +import deakin.gopher.guardian.R +import deakin.gopher.guardian.services.EmailPasswordAuthService + +class Homepage4doctor : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_homepage4doctor) + + val signOutButton: Button = findViewById(R.id.signOutButton_doctor) + signOutButton.setOnClickListener { + EmailPasswordAuthService.signOut(this) + finish() + } + } +} diff --git a/app/src/main/res/layout/activity_homepage4doctor.xml b/app/src/main/res/layout/activity_homepage4doctor.xml new file mode 100644 index 000000000..6e7936f35 --- /dev/null +++ b/app/src/main/res/layout/activity_homepage4doctor.xml @@ -0,0 +1,29 @@ + + + + + + + + + {open && ( + + {options.map((opt) => ( +
  • handleSelect(opt.value)} + > + {opt.label} +
  • + ))} +
    + )} +
    + + + ); +} 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/index.css b/guardian-admin-dashboard/src/index.css index fd883b28e..577413883 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,6 +341,81 @@ 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); @@ -954,6 +1033,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/StaffManagementPage.jsx b/guardian-admin-dashboard/src/pages/StaffManagementPage.jsx index 03fd595a5..ae06b43ae 100644 --- a/guardian-admin-dashboard/src/pages/StaffManagementPage.jsx +++ b/guardian-admin-dashboard/src/pages/StaffManagementPage.jsx @@ -1,11 +1,166 @@ +import { useState } from 'react'; +import { UserRoundPlus } from 'lucide-react'; +import DataTable from 'react-data-table-component'; +import Modal from '../components/common/Modal'; +import Button from '../components/common/Button'; +import InputField from '../components/common/InputField'; +import Dropdown from '../components/common/Dropdown'; + +const ROLE_OPTIONS = [ + { value: 'Doctor', label: 'Doctor' }, + { value: 'Nurse', label: 'Nurse' }, + { value: 'Carer', label: 'Carer' }, + { value: 'Administrator', label: 'Administrator' }, +]; + +const ORGANIZATION_OPTIONS = [ + { value: 'Guardian Health', label: 'Guardian Health' }, + { value: 'Deakin Medical Centre', label: 'Deakin Medical Centre' }, + { value: 'Sunrise Aged Care', label: 'Sunrise Aged Care' }, +]; + +const initialData = [ + { id: 1, fullName: 'Alice Johnson', email: 'alice.johnson@guardian.com', role: 'Nurse', organization: 'Guardian Health', active: true }, + { id: 2, fullName: 'Bob Smith', email: 'bob.smith@guardian.com', role: 'Doctor', organization: 'Guardian Health', active: true }, + { id: 3, fullName: 'Carol White', email: 'carol.white@deakin.com', role: 'Administrator', organization: 'Deakin Medical Centre', active: true }, + { id: 4, fullName: 'David Lee', email: 'david.lee@sunrise.com', role: 'Carer', organization: 'Sunrise Aged Care', active: true }, + { id: 5, fullName: 'Emma Brown', email: 'emma.brown@guardian.com', role: 'Nurse', organization: 'Guardian Health', active: false }, + { id: 6, fullName: 'Frank Wilson', email: 'frank.wilson@deakin.com', role: 'Doctor', organization: 'Deakin Medical Centre', active: true }, + { id: 7, fullName: 'Grace Hall', email: 'grace.hall@sunrise.com', role: 'Carer', organization: 'Sunrise Aged Care', active: true }, + { id: 8, fullName: 'Henry Adams', email: 'henry.adams@guardian.com', role: 'Administrator', organization: 'Guardian Health', active: false }, + { id: 9, fullName: 'Iris Martinez', email: 'iris.martinez@deakin.com', role: 'Nurse', organization: 'Deakin Medical Centre', active: true }, + { id: 10, fullName: 'James Taylor', email: 'james.taylor@sunrise.com', role: 'Doctor', organization: 'Sunrise Aged Care', active: true }, + { id: 11, fullName: 'Karen Thomas', email: 'karen.thomas@guardian.com', role: 'Carer', organization: 'Guardian Health', active: true }, + { id: 12, fullName: 'Liam Jackson', email: 'liam.jackson@deakin.com', role: 'Nurse', organization: 'Deakin Medical Centre', active: false }, + { id: 13, fullName: 'Mia Harris', email: 'mia.harris@sunrise.com', role: 'Doctor', organization: 'Sunrise Aged Care', active: true }, + { id: 14, fullName: 'Noah Clark', email: 'noah.clark@guardian.com', role: 'Administrator', organization: 'Guardian Health', active: true }, + { id: 15, fullName: 'Olivia Lewis', email: 'olivia.lewis@deakin.com', role: 'Carer', organization: 'Deakin Medical Centre', active: true }, + { id: 16, fullName: 'Peter Robinson', email: 'peter.robinson@sunrise.com', role: 'Nurse', organization: 'Sunrise Aged Care', active: false }, + { id: 17, fullName: 'Quinn Walker', email: 'quinn.walker@guardian.com', role: 'Doctor', organization: 'Guardian Health', active: true }, + { id: 18, fullName: 'Rachel Young', email: 'rachel.young@deakin.com', role: 'Administrator', organization: 'Deakin Medical Centre', active: true }, + { id: 19, fullName: 'Samuel King', email: 'samuel.king@sunrise.com', role: 'Carer', organization: 'Sunrise Aged Care', active: true }, + { id: 20, fullName: 'Tara Scott', email: 'tara.scott@guardian.com', role: 'Nurse', organization: 'Guardian Health', active: false }, +]; + +const emptyForm = { fullName: '', email: '', role: '', organization: '' }; + export default function StaffManagementPage() { + const [modalOpen, setModalOpen] = useState(false); + const [form, setForm] = useState(emptyForm); + const [staff, setStaff] = useState(initialData); + + function handleChange(e) { + const { name, value } = e.target; + setForm((prev) => ({ ...prev, [name]: value })); + } + + function handleClose() { + setModalOpen(false); + setForm(emptyForm); + } + + function handleSave() { + if (!form.fullName || !form.email || !form.role || !form.organization) return; + setStaff((prev) => ([ + ...prev, + { id: prev.length + 1, ...form, active: true }, + ])); + handleClose(); + } + + function toggleActive(id) { + setStaff((prev) => + prev.map((s) => (s.id === id ? { ...s, active: !s.active } : s)) + ); + } + + const columns = [ + { + name: 'ID', + selector: (row) => row.id, + width: '70px', + }, + { + name: 'Full Name', + selector: (row) => row.fullName, + sortable: true, + }, + { + name: 'Email', + selector: (row) => row.email, + }, + { + name: 'Role', + selector: (row) => row.role, + sortable: true, + }, + { + name: 'Organization', + selector: (row) => row.organization, + sortable: true, + }, + { + name: 'Actions', + cell: (row) => ( + + ), + }, + ]; + return ( -
    -

    Staff Management

    -

    - This route is prepared for admin-facing staff management work. The team - can continue implementation here using the shared shell layout. -

    +
    +
    +

    Staff Management

    + +
    + + + + + + + + +
    + + +
    +
    ); -} \ No newline at end of file +} From 4f667501df1bb051f465eae4290bf6e0eb6d867b Mon Sep 17 00:00:00 2001 From: Thisara Jayamuni Date: Sat, 4 Apr 2026 00:40:48 +1100 Subject: [PATCH 05/10] Staff Management Form Validation, Loaders Org Staff API Call --- .../src/components/common/Dropdown.jsx | 8 +- .../src/components/common/InputField.jsx | 4 +- guardian-admin-dashboard/src/index.css | 34 ++ .../src/pages/StaffManagementPage.jsx | 353 +++++++++++++++--- .../src/services/orgService.js | 6 + .../src/utils/constants.js | 9 +- 6 files changed, 350 insertions(+), 64 deletions(-) create mode 100644 guardian-admin-dashboard/src/services/orgService.js diff --git a/guardian-admin-dashboard/src/components/common/Dropdown.jsx b/guardian-admin-dashboard/src/components/common/Dropdown.jsx index 27e84d6af..ec0d4e7bc 100644 --- a/guardian-admin-dashboard/src/components/common/Dropdown.jsx +++ b/guardian-admin-dashboard/src/components/common/Dropdown.jsx @@ -5,7 +5,7 @@ import { motion, AnimatePresence } from 'framer-motion'; const MotionSpan = motion.span; const MotionUl = motion.ul; -export default function Dropdown({ label, name, value, onChange, options = [], placeholder }) { +export default function Dropdown({ label, name, value, onChange, options = [], placeholder, error, disabled }) { const [open, setOpen] = useState(false); const ref = useRef(null); @@ -30,8 +30,9 @@ export default function Dropdown({ label, name, value, onChange, options = [], p
    + {error && {error}}
    ); } diff --git a/guardian-admin-dashboard/src/components/common/InputField.jsx b/guardian-admin-dashboard/src/components/common/InputField.jsx index 50507e50e..d5893f0ba 100644 --- a/guardian-admin-dashboard/src/components/common/InputField.jsx +++ b/guardian-admin-dashboard/src/components/common/InputField.jsx @@ -6,12 +6,13 @@ export default function InputField({ name, placeholder, autoComplete, + error, }) { return ( ); } \ No newline at end of file diff --git a/guardian-admin-dashboard/src/index.css b/guardian-admin-dashboard/src/index.css index 577413883..ca869395d 100644 --- a/guardian-admin-dashboard/src/index.css +++ b/guardian-admin-dashboard/src/index.css @@ -422,6 +422,40 @@ a { 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; diff --git a/guardian-admin-dashboard/src/pages/StaffManagementPage.jsx b/guardian-admin-dashboard/src/pages/StaffManagementPage.jsx index ae06b43ae..e7ef8b383 100644 --- a/guardian-admin-dashboard/src/pages/StaffManagementPage.jsx +++ b/guardian-admin-dashboard/src/pages/StaffManagementPage.jsx @@ -1,76 +1,281 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { UserRoundPlus } from 'lucide-react'; import DataTable from 'react-data-table-component'; import Modal from '../components/common/Modal'; import Button from '../components/common/Button'; import InputField from '../components/common/InputField'; import Dropdown from '../components/common/Dropdown'; - -const ROLE_OPTIONS = [ - { value: 'Doctor', label: 'Doctor' }, - { value: 'Nurse', label: 'Nurse' }, - { value: 'Carer', label: 'Carer' }, - { value: 'Administrator', label: 'Administrator' }, -]; - -const ORGANIZATION_OPTIONS = [ - { value: 'Guardian Health', label: 'Guardian Health' }, - { value: 'Deakin Medical Centre', label: 'Deakin Medical Centre' }, - { value: 'Sunrise Aged Care', label: 'Sunrise Aged Care' }, -]; +import { getMyOrganizations } from '../services/orgService'; +import { ROLE_OPTIONS } from '../utils/constants'; const initialData = [ - { id: 1, fullName: 'Alice Johnson', email: 'alice.johnson@guardian.com', role: 'Nurse', organization: 'Guardian Health', active: true }, - { id: 2, fullName: 'Bob Smith', email: 'bob.smith@guardian.com', role: 'Doctor', organization: 'Guardian Health', active: true }, - { id: 3, fullName: 'Carol White', email: 'carol.white@deakin.com', role: 'Administrator', organization: 'Deakin Medical Centre', active: true }, - { id: 4, fullName: 'David Lee', email: 'david.lee@sunrise.com', role: 'Carer', organization: 'Sunrise Aged Care', active: true }, - { id: 5, fullName: 'Emma Brown', email: 'emma.brown@guardian.com', role: 'Nurse', organization: 'Guardian Health', active: false }, - { id: 6, fullName: 'Frank Wilson', email: 'frank.wilson@deakin.com', role: 'Doctor', organization: 'Deakin Medical Centre', active: true }, - { id: 7, fullName: 'Grace Hall', email: 'grace.hall@sunrise.com', role: 'Carer', organization: 'Sunrise Aged Care', active: true }, - { id: 8, fullName: 'Henry Adams', email: 'henry.adams@guardian.com', role: 'Administrator', organization: 'Guardian Health', active: false }, - { id: 9, fullName: 'Iris Martinez', email: 'iris.martinez@deakin.com', role: 'Nurse', organization: 'Deakin Medical Centre', active: true }, - { id: 10, fullName: 'James Taylor', email: 'james.taylor@sunrise.com', role: 'Doctor', organization: 'Sunrise Aged Care', active: true }, - { id: 11, fullName: 'Karen Thomas', email: 'karen.thomas@guardian.com', role: 'Carer', organization: 'Guardian Health', active: true }, - { id: 12, fullName: 'Liam Jackson', email: 'liam.jackson@deakin.com', role: 'Nurse', organization: 'Deakin Medical Centre', active: false }, - { id: 13, fullName: 'Mia Harris', email: 'mia.harris@sunrise.com', role: 'Doctor', organization: 'Sunrise Aged Care', active: true }, - { id: 14, fullName: 'Noah Clark', email: 'noah.clark@guardian.com', role: 'Administrator', organization: 'Guardian Health', active: true }, - { id: 15, fullName: 'Olivia Lewis', email: 'olivia.lewis@deakin.com', role: 'Carer', organization: 'Deakin Medical Centre', active: true }, - { id: 16, fullName: 'Peter Robinson', email: 'peter.robinson@sunrise.com', role: 'Nurse', organization: 'Sunrise Aged Care', active: false }, - { id: 17, fullName: 'Quinn Walker', email: 'quinn.walker@guardian.com', role: 'Doctor', organization: 'Guardian Health', active: true }, - { id: 18, fullName: 'Rachel Young', email: 'rachel.young@deakin.com', role: 'Administrator', organization: 'Deakin Medical Centre', active: true }, - { id: 19, fullName: 'Samuel King', email: 'samuel.king@sunrise.com', role: 'Carer', organization: 'Sunrise Aged Care', active: true }, - { id: 20, fullName: 'Tara Scott', email: 'tara.scott@guardian.com', role: 'Nurse', organization: 'Guardian Health', active: false }, + { + id: 1, + fullName: 'Alice Johnson', + email: 'alice.johnson@guardian.com', + role: 'Nurse', + organization: 'Guardian Health', + active: true, + }, + { + id: 2, + fullName: 'Bob Smith', + email: 'bob.smith@guardian.com', + role: 'Doctor', + organization: 'Guardian Health', + active: true, + }, + { + id: 3, + fullName: 'Carol White', + email: 'carol.white@deakin.com', + role: 'Administrator', + organization: 'Deakin Medical Centre', + active: true, + }, + { + id: 4, + fullName: 'David Lee', + email: 'david.lee@sunrise.com', + role: 'Carer', + organization: 'Sunrise Aged Care', + active: true, + }, + { + id: 5, + fullName: 'Emma Brown', + email: 'emma.brown@guardian.com', + role: 'Nurse', + organization: 'Guardian Health', + active: false, + }, + { + id: 6, + fullName: 'Frank Wilson', + email: 'frank.wilson@deakin.com', + role: 'Doctor', + organization: 'Deakin Medical Centre', + active: true, + }, + { + id: 7, + fullName: 'Grace Hall', + email: 'grace.hall@sunrise.com', + role: 'Carer', + organization: 'Sunrise Aged Care', + active: true, + }, + { + id: 8, + fullName: 'Henry Adams', + email: 'henry.adams@guardian.com', + role: 'Administrator', + organization: 'Guardian Health', + active: false, + }, + { + id: 9, + fullName: 'Iris Martinez', + email: 'iris.martinez@deakin.com', + role: 'Nurse', + organization: 'Deakin Medical Centre', + active: true, + }, + { + id: 10, + fullName: 'James Taylor', + email: 'james.taylor@sunrise.com', + role: 'Doctor', + organization: 'Sunrise Aged Care', + active: true, + }, + { + id: 11, + fullName: 'Karen Thomas', + email: 'karen.thomas@guardian.com', + role: 'Carer', + organization: 'Guardian Health', + active: true, + }, + { + id: 12, + fullName: 'Liam Jackson', + email: 'liam.jackson@deakin.com', + role: 'Nurse', + organization: 'Deakin Medical Centre', + active: false, + }, + { + id: 13, + fullName: 'Mia Harris', + email: 'mia.harris@sunrise.com', + role: 'Doctor', + organization: 'Sunrise Aged Care', + active: true, + }, + { + id: 14, + fullName: 'Noah Clark', + email: 'noah.clark@guardian.com', + role: 'Administrator', + organization: 'Guardian Health', + active: true, + }, + { + id: 15, + fullName: 'Olivia Lewis', + email: 'olivia.lewis@deakin.com', + role: 'Carer', + organization: 'Deakin Medical Centre', + active: true, + }, + { + id: 16, + fullName: 'Peter Robinson', + email: 'peter.robinson@sunrise.com', + role: 'Nurse', + organization: 'Sunrise Aged Care', + active: false, + }, + { + id: 17, + fullName: 'Quinn Walker', + email: 'quinn.walker@guardian.com', + role: 'Doctor', + organization: 'Guardian Health', + active: true, + }, + { + id: 18, + fullName: 'Rachel Young', + email: 'rachel.young@deakin.com', + role: 'Administrator', + organization: 'Deakin Medical Centre', + active: true, + }, + { + id: 19, + fullName: 'Samuel King', + email: 'samuel.king@sunrise.com', + role: 'Carer', + organization: 'Sunrise Aged Care', + active: true, + }, + { + id: 20, + fullName: 'Tara Scott', + email: 'tara.scott@guardian.com', + role: 'Nurse', + organization: 'Guardian Health', + active: false, + }, ]; const emptyForm = { fullName: '', email: '', role: '', organization: '' }; +const emptyErrors = { fullName: '', email: '', role: '', organization: '' }; + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const NAME_REGEX = /^[a-zA-Z\s'-]+$/; + +function TableLoader() { + return ( +
    +
    + Loading staff... +
    + ); +} export default function StaffManagementPage() { const [modalOpen, setModalOpen] = useState(false); const [form, setForm] = useState(emptyForm); - const [staff, setStaff] = useState(initialData); + const [errors, setErrors] = useState(emptyErrors); + const [staff, setStaff] = useState([]); + const [loading, setLoading] = useState(true); + const [orgOptions, setOrgOptions] = useState([]); + const [orgLoading, setOrgLoading] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setStaff(initialData); + setLoading(false); + }, 1200); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + const fetchOrgs = async () => { + setOrgLoading(true); + try { + const orgs = await getMyOrganizations(); + setOrgOptions(orgs.map((o) => ({ value: o._id, label: o.name }))); + } catch (err) { + console.error('Failed to load organizations:', err); + } finally { + setOrgLoading(false); + } + }; + fetchOrgs(); + }, []); + + function validate(fields) { + const errs = { ...emptyErrors }; + if (!fields.fullName.trim()) { + errs.fullName = 'Full name is required.'; + } else if (fields.fullName.trim().length < 2) { + errs.fullName = 'Full name must be at least 2 characters.'; + } else if (!NAME_REGEX.test(fields.fullName.trim())) { + errs.fullName = + 'Full name can only contain letters, spaces, hyphens, and apostrophes.'; + } + + if (!fields.email.trim()) { + errs.email = 'Email is required.'; + } else if (!EMAIL_REGEX.test(fields.email.trim())) { + errs.email = 'Enter a valid email address.'; + } else if ( + staff.some( + (s) => s.email.toLowerCase() === fields.email.trim().toLowerCase(), + ) + ) { + errs.email = 'A staff member with this email already exists.'; + } + + if (!fields.role) errs.role = 'Please select a role.'; + if (!fields.organization) + errs.organization = 'Please select an organization.'; + + return errs; + } function handleChange(e) { const { name, value } = e.target; setForm((prev) => ({ ...prev, [name]: value })); + setErrors((prev) => ({ ...prev, [name]: '' })); } function handleClose() { setModalOpen(false); setForm(emptyForm); + setErrors(emptyErrors); } function handleSave() { - if (!form.fullName || !form.email || !form.role || !form.organization) return; - setStaff((prev) => ([ + const errs = validate(form); + if (Object.values(errs).some(Boolean)) { + setErrors(errs); + return; + } + setStaff((prev) => [ ...prev, { id: prev.length + 1, ...form, active: true }, - ])); + ]); handleClose(); } function toggleActive(id) { setStaff((prev) => - prev.map((s) => (s.id === id ? { ...s, active: !s.active } : s)) + prev.map((s) => (s.id === id ? { ...s, active: !s.active } : s)), ); } @@ -114,7 +319,14 @@ export default function StaffManagementPage() { return (
    -
    +

    Staff Management

    - + } + pagination + paginationPerPage={10} + paginationRowsPerPageOptions={[10, 20]} + /> - + -
    - - +
    + +
    diff --git a/guardian-admin-dashboard/src/services/orgService.js b/guardian-admin-dashboard/src/services/orgService.js new file mode 100644 index 000000000..d4cad456c --- /dev/null +++ b/guardian-admin-dashboard/src/services/orgService.js @@ -0,0 +1,6 @@ +import api from "./api"; + +export async function getMyOrganizations() { + const response = await api.get("/api/v1/orgs/mine"); + return response.data?.orgs ?? []; +} diff --git a/guardian-admin-dashboard/src/utils/constants.js b/guardian-admin-dashboard/src/utils/constants.js index 68bc3a8f1..593526c88 100644 --- a/guardian-admin-dashboard/src/utils/constants.js +++ b/guardian-admin-dashboard/src/utils/constants.js @@ -48,4 +48,11 @@ export const STORAGE_KEYS = { email: "guardian_admin_email", pendingToken: "guardian_admin_pending_token", pendingUser: "guardian_admin_pending_user", -}; \ No newline at end of file +}; + +export const ROLE_OPTIONS = [ + { value: 'doctor', label: 'Doctor' }, + { value: 'nurse', label: 'Nurse' }, + { value: 'caretaker', label: 'Caretaker' }, + { value: 'admin', label: 'Admin' }, +]; \ No newline at end of file From 1993cf0c6e54e1289919424be8521fb00ee787f5 Mon Sep 17 00:00:00 2001 From: Thisara Jayamuni Date: Tue, 7 Apr 2026 08:35:42 +1000 Subject: [PATCH 06/10] Staff Management Create Form, Filters and API Integration --- .../src/components/common/DataTable.jsx | 47 ++ .../src/components/common/Toast.jsx | 74 +++ .../src/pages/StaffManagementPage.jsx | 428 +++++++----------- .../src/services/staffService.js | 27 ++ .../src/utils/constants.js | 2 - 5 files changed, 311 insertions(+), 267 deletions(-) create mode 100644 guardian-admin-dashboard/src/components/common/DataTable.jsx create mode 100644 guardian-admin-dashboard/src/components/common/Toast.jsx create mode 100644 guardian-admin-dashboard/src/services/staffService.js diff --git a/guardian-admin-dashboard/src/components/common/DataTable.jsx b/guardian-admin-dashboard/src/components/common/DataTable.jsx new file mode 100644 index 000000000..037df172c --- /dev/null +++ b/guardian-admin-dashboard/src/components/common/DataTable.jsx @@ -0,0 +1,47 @@ +import ReactDataTable from 'react-data-table-component'; + +function TableLoader() { + return ( +
    +
    + Loading... +
    + ); +} + +export default function DataTable({ + columns, + data, + loading = false, + totalRows = 0, + onChangePage, + perPage = 10, + perPageOptions = [10, 20], + loadingMessage, + ...rest +}) { + const progressComponent = loadingMessage ? ( +
    +
    + {loadingMessage} +
    + ) : ( + + ); + + return ( + + ); +} 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/pages/StaffManagementPage.jsx b/guardian-admin-dashboard/src/pages/StaffManagementPage.jsx index e7ef8b383..0bd6a6fdf 100644 --- a/guardian-admin-dashboard/src/pages/StaffManagementPage.jsx +++ b/guardian-admin-dashboard/src/pages/StaffManagementPage.jsx @@ -1,207 +1,81 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { UserRoundPlus } from 'lucide-react'; -import DataTable from 'react-data-table-component'; +import DataTable from '../components/common/DataTable'; import Modal from '../components/common/Modal'; +import Toast from '../components/common/Toast'; import Button from '../components/common/Button'; import InputField from '../components/common/InputField'; import Dropdown from '../components/common/Dropdown'; import { getMyOrganizations } from '../services/orgService'; +import { + getStaff, + createStaff, + deactivateStaff, +} from '../services/staffService'; import { ROLE_OPTIONS } from '../utils/constants'; -const initialData = [ - { - id: 1, - fullName: 'Alice Johnson', - email: 'alice.johnson@guardian.com', - role: 'Nurse', - organization: 'Guardian Health', - active: true, - }, - { - id: 2, - fullName: 'Bob Smith', - email: 'bob.smith@guardian.com', - role: 'Doctor', - organization: 'Guardian Health', - active: true, - }, - { - id: 3, - fullName: 'Carol White', - email: 'carol.white@deakin.com', - role: 'Administrator', - organization: 'Deakin Medical Centre', - active: true, - }, - { - id: 4, - fullName: 'David Lee', - email: 'david.lee@sunrise.com', - role: 'Carer', - organization: 'Sunrise Aged Care', - active: true, - }, - { - id: 5, - fullName: 'Emma Brown', - email: 'emma.brown@guardian.com', - role: 'Nurse', - organization: 'Guardian Health', - active: false, - }, - { - id: 6, - fullName: 'Frank Wilson', - email: 'frank.wilson@deakin.com', - role: 'Doctor', - organization: 'Deakin Medical Centre', - active: true, - }, - { - id: 7, - fullName: 'Grace Hall', - email: 'grace.hall@sunrise.com', - role: 'Carer', - organization: 'Sunrise Aged Care', - active: true, - }, - { - id: 8, - fullName: 'Henry Adams', - email: 'henry.adams@guardian.com', - role: 'Administrator', - organization: 'Guardian Health', - active: false, - }, - { - id: 9, - fullName: 'Iris Martinez', - email: 'iris.martinez@deakin.com', - role: 'Nurse', - organization: 'Deakin Medical Centre', - active: true, - }, - { - id: 10, - fullName: 'James Taylor', - email: 'james.taylor@sunrise.com', - role: 'Doctor', - organization: 'Sunrise Aged Care', - active: true, - }, - { - id: 11, - fullName: 'Karen Thomas', - email: 'karen.thomas@guardian.com', - role: 'Carer', - organization: 'Guardian Health', - active: true, - }, - { - id: 12, - fullName: 'Liam Jackson', - email: 'liam.jackson@deakin.com', - role: 'Nurse', - organization: 'Deakin Medical Centre', - active: false, - }, - { - id: 13, - fullName: 'Mia Harris', - email: 'mia.harris@sunrise.com', - role: 'Doctor', - organization: 'Sunrise Aged Care', - active: true, - }, - { - id: 14, - fullName: 'Noah Clark', - email: 'noah.clark@guardian.com', - role: 'Administrator', - organization: 'Guardian Health', - active: true, - }, - { - id: 15, - fullName: 'Olivia Lewis', - email: 'olivia.lewis@deakin.com', - role: 'Carer', - organization: 'Deakin Medical Centre', - active: true, - }, - { - id: 16, - fullName: 'Peter Robinson', - email: 'peter.robinson@sunrise.com', - role: 'Nurse', - organization: 'Sunrise Aged Care', - active: false, - }, - { - id: 17, - fullName: 'Quinn Walker', - email: 'quinn.walker@guardian.com', - role: 'Doctor', - organization: 'Guardian Health', - active: true, - }, - { - id: 18, - fullName: 'Rachel Young', - email: 'rachel.young@deakin.com', - role: 'Administrator', - organization: 'Deakin Medical Centre', - active: true, - }, - { - id: 19, - fullName: 'Samuel King', - email: 'samuel.king@sunrise.com', - role: 'Carer', - organization: 'Sunrise Aged Care', - active: true, - }, - { - id: 20, - fullName: 'Tara Scott', - email: 'tara.scott@guardian.com', - role: 'Nurse', - organization: 'Guardian Health', - active: false, - }, -]; - -const emptyForm = { fullName: '', email: '', role: '', organization: '' }; -const emptyErrors = { fullName: '', email: '', role: '', organization: '' }; +const emptyForm = { userId: '' }; +const emptyErrors = { userId: '' }; -const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -const NAME_REGEX = /^[a-zA-Z\s'-]+$/; +const ROLE_FILTER_OPTIONS = [ + { value: '', label: 'All Roles' }, + ...ROLE_OPTIONS, +]; -function TableLoader() { - return ( -
    -
    - Loading staff... -
    - ); +function formatStaff(raw) { + return raw.map((s) => ({ + id: s._id, + fullName: s.fullname, + email: s.email, + role: + ROLE_OPTIONS.find((r) => r.value === s.role?.name?.toLowerCase()) + ?.label ?? '-', + organization: s.organization?.name ?? '-', + })); } export default function StaffManagementPage() { + const [staff, setStaff] = useState([]); + const [loading, setLoading] = useState(true); + const [page, setPage] = useState(1); + const [totalRows, setTotalRows] = useState(0); const [modalOpen, setModalOpen] = useState(false); const [form, setForm] = useState(emptyForm); const [errors, setErrors] = useState(emptyErrors); - const [staff, setStaff] = useState([]); - const [loading, setLoading] = useState(true); const [orgOptions, setOrgOptions] = useState([]); const [orgLoading, setOrgLoading] = useState(true); + const [orgFilter, setOrgFilter] = useState(''); + const [roleFilter, setRoleFilter] = useState(''); + const [search, setSearch] = useState(''); + const [confirmId, setConfirmId] = useState(null); + const [successOpen, setSuccessOpen] = useState(false); - useEffect(() => { - const timer = setTimeout(() => { - setStaff(initialData); + const fetchStaff = useCallback(async () => { + setLoading(true); + try { + const data = await getStaff({ + page, + limit: 10, + role: roleFilter, + orgId: orgFilter, + search, + }); + const normalized = formatStaff(data.staff ?? []); + const filtered = normalized.filter((s) => + ROLE_OPTIONS.some((r) => r.value === s.role.toLowerCase()), + ); + setStaff(filtered); + setTotalRows(data.pagination?.total ?? 0); + } catch (err) { + console.error('Failed to load staff:', err); + } finally { setLoading(false); - }, 1200); - return () => clearTimeout(timer); - }, []); + } + }, [page, roleFilter, orgFilter, search]); + + useEffect(() => { + fetchStaff(); + }, [fetchStaff]); useEffect(() => { const fetchOrgs = async () => { @@ -220,31 +94,9 @@ export default function StaffManagementPage() { function validate(fields) { const errs = { ...emptyErrors }; - if (!fields.fullName.trim()) { - errs.fullName = 'Full name is required.'; - } else if (fields.fullName.trim().length < 2) { - errs.fullName = 'Full name must be at least 2 characters.'; - } else if (!NAME_REGEX.test(fields.fullName.trim())) { - errs.fullName = - 'Full name can only contain letters, spaces, hyphens, and apostrophes.'; - } - - if (!fields.email.trim()) { - errs.email = 'Email is required.'; - } else if (!EMAIL_REGEX.test(fields.email.trim())) { - errs.email = 'Enter a valid email address.'; - } else if ( - staff.some( - (s) => s.email.toLowerCase() === fields.email.trim().toLowerCase(), - ) - ) { - errs.email = 'A staff member with this email already exists.'; + if (!fields.userId.trim()) { + errs.userId = 'User ID is required.'; } - - if (!fields.role) errs.role = 'Please select a role.'; - if (!fields.organization) - errs.organization = 'Please select an organization.'; - return errs; } @@ -260,30 +112,38 @@ export default function StaffManagementPage() { setErrors(emptyErrors); } - function handleSave() { + async function handleSave() { const errs = validate(form); if (Object.values(errs).some(Boolean)) { setErrors(errs); return; } - setStaff((prev) => [ - ...prev, - { id: prev.length + 1, ...form, active: true }, - ]); - handleClose(); + try { + await createStaff(form.userId); + handleClose(); + setSuccessOpen(true); + fetchStaff(); + } catch (err) { + console.error('Failed to add staff:', err); + } } - function toggleActive(id) { - setStaff((prev) => - prev.map((s) => (s.id === id ? { ...s, active: !s.active } : s)), - ); + async function handleConfirmDeactivate() { + try { + await deactivateStaff(confirmId); + setStaff((prev) => prev.filter((s) => s.id !== confirmId)); + } catch (err) { + console.error('Failed to deactivate staff:', err); + } finally { + setConfirmId(null); + } } const columns = [ { name: 'ID', selector: (row) => row.id, - width: '70px', + sortable: true, }, { name: 'Full Name', @@ -307,11 +167,8 @@ export default function StaffManagementPage() { { name: 'Actions', cell: (row) => ( - ), }, @@ -324,7 +181,7 @@ export default function StaffManagementPage() { display: 'flex', justifyContent: 'space-between', alignItems: 'center', - marginBottom: '14px', + marginBottom: '28px', }} >

    Staff Management

    @@ -334,54 +191,86 @@ export default function StaffManagementPage() {
    +
    +
    + { + setSearch(e.target.value); + setPage(1); + }} + placeholder='Search by name or email...' + /> +
    +
    +
    + { + setRoleFilter(e.target.value); + setPage(1); + }} + options={ROLE_FILTER_OPTIONS} + /> +
    +
    + { + setOrgFilter(e.target.value); + setPage(1); + }} + options={[ + { value: '', label: 'All Organizations' }, + ...orgOptions, + ]} + placeholder={orgLoading ? 'Loading...' : 'All Organizations'} + disabled={orgLoading} + /> +
    +
    +
    + } - pagination - paginationPerPage={10} - paginationRowsPerPageOptions={[10, 20]} + loading={loading} + loadingMessage='Loading staff...' + totalRows={totalRows} + onChangePage={(newPage) => setPage(newPage)} + /> + + setConfirmId(null)} /> - - -
    + + setSuccessOpen(false)} + />
    ); } diff --git a/guardian-admin-dashboard/src/services/staffService.js b/guardian-admin-dashboard/src/services/staffService.js new file mode 100644 index 000000000..dacb7724c --- /dev/null +++ b/guardian-admin-dashboard/src/services/staffService.js @@ -0,0 +1,27 @@ +import api from './api'; + +export async function getStaff({ + page = 1, + limit = 10, + search = '', + orgId = '', + role = '', +} = {}) { + const params = { page, limit }; + if (search) params.search = search; + if (orgId) params.orgId = orgId; + if (role) params.role = role; + + const response = await api.get('/api/v1/admin/staff', { params }); + return response.data; +} + +export async function createStaff(userId) { + const response = await api.post('/api/v1/admin/staff', { userId }); + return response.data; +} + +export async function deactivateStaff(id) { + const response = await api.put(`/api/v1/admin/staff/${id}/deactivate`); + return response.data; +} diff --git a/guardian-admin-dashboard/src/utils/constants.js b/guardian-admin-dashboard/src/utils/constants.js index 593526c88..5b4852dd1 100644 --- a/guardian-admin-dashboard/src/utils/constants.js +++ b/guardian-admin-dashboard/src/utils/constants.js @@ -53,6 +53,4 @@ export const STORAGE_KEYS = { export const ROLE_OPTIONS = [ { value: 'doctor', label: 'Doctor' }, { value: 'nurse', label: 'Nurse' }, - { value: 'caretaker', label: 'Caretaker' }, - { value: 'admin', label: 'Admin' }, ]; \ No newline at end of file From b85d342488944c31716be7ad1256453b99dd3f80 Mon Sep 17 00:00:00 2001 From: Shubham Rana Date: Sun, 12 Apr 2026 17:48:53 +1000 Subject: [PATCH 07/10] added admin dashboard overview page --- guardian-admin-dashboard/src/App.css | 92 +++++ .../src/pages/OrgAssignmentPage.jsx | 318 +++++++++++++++++- .../src/services/orgService.js | 30 +- package-lock.json | 6 + 4 files changed, 434 insertions(+), 12 deletions(-) create mode 100644 package-lock.json diff --git a/guardian-admin-dashboard/src/App.css b/guardian-admin-dashboard/src/App.css index 83c74d68e..2855bf594 100644 --- a/guardian-admin-dashboard/src/App.css +++ b/guardian-admin-dashboard/src/App.css @@ -41,3 +41,95 @@ body { .login-form button:hover { background-color: #303f9f; } +/* overview */ +.org-page-title { + margin-top: 0; + margin-bottom: 10px; +} + +.org-section-title { + margin: 0; +} + +.org-error-title { + margin: 0 0 6px 0; +} + +.org-muted-text { + margin: 0; + color: #6b7280; +} + +.org-error-header { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 20px; +} + +.org-stat-top { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16px; +} + +.org-stat-label { + margin: 0 0 6px 0; + font-size: 14px; + color: #6b7280; +} + +.org-stat-value { + margin: 0; +} + +.org-stat-icon { + width: 42px; + height: 42px; + border-radius: 12px; + display: grid; + place-items: center; + background: rgba(63, 81, 181, 0.08); +} + +.org-details-header { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 16px; + margin-bottom: 24px; + flex-wrap: wrap; +} + +.org-dropdown-wrap { + width: 260px; +} + +.org-details-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 18px; +} + +.org-detail-label { + margin: 0 0 8px 0; + font-size: 13px; + color: #6b7280; +} + +.org-detail-value { + font-weight: 500; +} + +.org-break-word { + word-break: break-word; +} + +.org-summary-list { + margin-top: 18px; +} + +.org-refresh-wrap { + margin-top: 22px; +} \ No newline at end of file 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/services/orgService.js b/guardian-admin-dashboard/src/services/orgService.js index d4cad456c..81c5ca8ed 100644 --- a/guardian-admin-dashboard/src/services/orgService.js +++ b/guardian-admin-dashboard/src/services/orgService.js @@ -1,6 +1,26 @@ -import api from "./api"; - export async function getMyOrganizations() { - const response = await api.get("/api/v1/orgs/mine"); - return response.data?.orgs ?? []; -} + return { + orgs: [ + { + _id: "1", + name: "Guardian Health Org", + description: "Primary org for testing", + active: true, + createdBy: "admin001", + staff: ["u1", "u2", "u3"], + created_at: "2026-04-11T08:43:07.933Z", + updated_at: "2026-04-11T08:43:07.933Z" + }, + { + _id: "2", + name: "Care Support Org", + description: "Secondary organisation", + active: false, + createdBy: "admin002", + staff: ["u4"], + created_at: "2026-03-02T10:20:00.000Z", + updated_at: "2026-03-15T11:10:00.000Z" + } + ] + }; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..b0578dffa --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Guardian", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From f6c8ec5a5a0cd09824f264884662705604273d09 Mon Sep 17 00:00:00 2001 From: Palak Deep Kaur Date: Mon, 13 Apr 2026 11:22:50 +1000 Subject: [PATCH 08/10] Patient Overview page improvements and API integration --- guardian-admin-dashboard/src/App.jsx | 3 + .../src/components/dashboard/Sidebar.jsx | 6 +- .../src/pages/PatientOverviewPage.jsx | 349 ++++++++++++++++++ guardian-admin-dashboard/src/services/api.js | 2 +- .../src/services/authService.js | 19 +- .../src/services/patientService.js | 13 + .../src/utils/constants.js | 6 + package-lock.json | 6 + 8 files changed, 389 insertions(+), 15 deletions(-) create mode 100644 guardian-admin-dashboard/src/pages/PatientOverviewPage.jsx create mode 100644 guardian-admin-dashboard/src/services/patientService.js create mode 100644 package-lock.json diff --git a/guardian-admin-dashboard/src/App.jsx b/guardian-admin-dashboard/src/App.jsx index 750e86118..4b1cc6554 100644 --- a/guardian-admin-dashboard/src/App.jsx +++ b/guardian-admin-dashboard/src/App.jsx @@ -11,6 +11,7 @@ import NurseRosterPage from "./pages/NurseRosterPage"; import ReportsPage from "./pages/ReportsPage"; import SettingsPage from "./pages/SettingsPage"; import "./App.css"; +import PatientOverviewPage from "./pages/PatientOverviewPage"; function ProtectedRoute({ children }) { const token = getAuthToken(); @@ -36,9 +37,11 @@ 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 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/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/services/api.js b/guardian-admin-dashboard/src/services/api.js index 7d56f1bb6..eb1419540 100644 --- a/guardian-admin-dashboard/src/services/api.js +++ b/guardian-admin-dashboard/src/services/api.js @@ -4,7 +4,7 @@ import { getAuthToken } from "../utils/storage"; const api = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || - "https://guardian-backend-ashen.vercel.app/", + "https://guardian-backend-git-fix-cors-patelrudra2306-5873s-projects.vercel.app/api/v1", headers: { "Content-Type": "application/json", }, diff --git a/guardian-admin-dashboard/src/services/authService.js b/guardian-admin-dashboard/src/services/authService.js index 9ac70474b..52a65daeb 100644 --- a/guardian-admin-dashboard/src/services/authService.js +++ b/guardian-admin-dashboard/src/services/authService.js @@ -29,22 +29,15 @@ // } // Mock login and otp fallback +import api from "./api"; export async function loginAdmin({ email, password }) { - return new Promise((resolve) => { - setTimeout(() => { - resolve({ - user: { - id: "demo-admin-id", - fullname: "Alice Smith", - email, - role: "admin", - twoFactorRequired: true, - }, - token: "demo-admin-token", - }); - }, 700); + const response = await api.post("/auth/login", { + email, + password, }); + console.log("LOGIN RESPONSE:", response.data); + return response.data; } export async function sendPin(email) { diff --git a/guardian-admin-dashboard/src/services/patientService.js b/guardian-admin-dashboard/src/services/patientService.js new file mode 100644 index 000000000..0f32ba99d --- /dev/null +++ b/guardian-admin-dashboard/src/services/patientService.js @@ -0,0 +1,13 @@ +import api from "./api"; + +export const getAllPatients = async () => { + const response = await api.get("/admin/patients"); + return response.data; +}; + +export const getPatientOverview = async (patientId, orgId) => { + const response = await api.get(`/admin/patients/${patientId}/overview`, { + params: { orgId }, + }); + return response.data; +}; \ No newline at end of file diff --git a/guardian-admin-dashboard/src/utils/constants.js b/guardian-admin-dashboard/src/utils/constants.js index 5b4852dd1..dba88d487 100644 --- a/guardian-admin-dashboard/src/utils/constants.js +++ b/guardian-admin-dashboard/src/utils/constants.js @@ -11,9 +11,15 @@ export const ADMIN_NAV_ITEMS = [ path: "/dashboard/org-assignment", }, { id: "patients", label: "Patients", path: "/dashboard/patients" }, + { + id: "patient-overview", + label: "Patient Overview", + path: "/dashboard/patient-overview", +}, { id: "nurse-roster", label: "Nurse Roster", path: "/dashboard/nurse-roster" }, { id: "reports", label: "Reports", path: "/dashboard/reports" }, { id: "settings", label: "Settings", path: "/dashboard/settings" }, + ]; export const DASHBOARD_STATS = [ { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..b0578dffa --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Guardian", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 3960a9a14028415e7bdcab81acb3f1402b8f772f Mon Sep 17 00:00:00 2001 From: Nishiki-Asumi97 Date: Wed, 15 Apr 2026 15:26:11 +1000 Subject: [PATCH 09/10] Patient management implementation --- guardian-admin-dashboard/package-lock.json | 587 ++++++++----- guardian-admin-dashboard/package.json | 1 + .../src/pages/PatientsPage.jsx | 816 +++++++++++++++++- guardian-admin-dashboard/src/services/api.js | 3 +- .../src/services/patientService.js | 26 + 5 files changed, 1192 insertions(+), 241 deletions(-) create mode 100644 guardian-admin-dashboard/src/services/patientService.js diff --git a/guardian-admin-dashboard/package-lock.json b/guardian-admin-dashboard/package-lock.json index 334a74566..65f92b365 100644 --- a/guardian-admin-dashboard/package-lock.json +++ b/guardian-admin-dashboard/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "axios": "^1.13.6", + "cors": "^2.8.6", "framer-motion": "^12.38.0", "lucide-react": "^1.6.0", "react": "^19.1.0", @@ -347,9 +348,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -364,9 +365,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -381,9 +382,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -398,9 +399,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -415,9 +416,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -432,9 +433,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -449,9 +450,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -466,9 +467,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -483,9 +484,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -500,9 +501,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -517,9 +518,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -534,9 +535,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -551,9 +552,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -568,9 +569,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -585,9 +586,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -602,9 +603,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -619,9 +620,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -636,9 +637,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -653,9 +654,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -670,9 +671,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -687,9 +688,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -704,9 +705,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -721,9 +722,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -738,9 +739,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -755,9 +756,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -772,9 +773,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -1055,9 +1056,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", "cpu": [ "arm" ], @@ -1069,9 +1070,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", "cpu": [ "arm64" ], @@ -1083,9 +1084,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", "cpu": [ "arm64" ], @@ -1097,9 +1098,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", "cpu": [ "x64" ], @@ -1111,9 +1112,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", "cpu": [ "arm64" ], @@ -1125,9 +1126,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", "cpu": [ "x64" ], @@ -1139,9 +1140,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", "cpu": [ "arm" ], @@ -1153,9 +1154,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", "cpu": [ "arm" ], @@ -1167,9 +1168,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", "cpu": [ "arm64" ], @@ -1181,9 +1182,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", "cpu": [ "arm64" ], @@ -1194,10 +1195,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", "cpu": [ "loong64" ], @@ -1209,9 +1224,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", "cpu": [ "ppc64" ], @@ -1223,9 +1252,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", "cpu": [ "riscv64" ], @@ -1237,9 +1266,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", "cpu": [ "riscv64" ], @@ -1251,9 +1280,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", "cpu": [ "s390x" ], @@ -1265,9 +1294,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "cpu": [ "x64" ], @@ -1279,9 +1308,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", "cpu": [ "x64" ], @@ -1292,10 +1321,38 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", "cpu": [ "arm64" ], @@ -1307,9 +1364,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", "cpu": [ "ia32" ], @@ -1320,10 +1377,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", "cpu": [ "x64" ], @@ -1464,9 +1535,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1510,14 +1581,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/balanced-match": { @@ -1528,9 +1599,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -1700,6 +1771,23 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1851,9 +1939,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1864,32 +1952,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -2115,11 +2203,14 @@ "license": "MIT" }, "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -2174,16 +2265,16 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -2476,9 +2567,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -2632,9 +2723,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2698,6 +2789,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2788,9 +2888,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -2846,10 +2946,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -2959,9 +3062,9 @@ } }, "node_modules/rollup": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", - "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "dev": true, "license": "MIT", "dependencies": { @@ -2975,26 +3078,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" } }, @@ -3152,14 +3260,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -3228,19 +3336,28 @@ "punycode": "^2.1.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", - "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.6", + "esbuild": "^0.27.0", + "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", - "rollup": "^4.40.0", - "tinyglobby": "^0.2.14" + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" diff --git a/guardian-admin-dashboard/package.json b/guardian-admin-dashboard/package.json index 9547841e6..71dc84c61 100644 --- a/guardian-admin-dashboard/package.json +++ b/guardian-admin-dashboard/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "axios": "^1.13.6", + "cors": "^2.8.6", "framer-motion": "^12.38.0", "lucide-react": "^1.6.0", "react": "^19.1.0", 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 && ( +
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + +
    + +