From f2a2994e08326f9a8fe3419c4cff145f4061804a Mon Sep 17 00:00:00 2001 From: mohamedshahil-csh Date: Sat, 22 Aug 2026 14:54:11 +0530 Subject: [PATCH] feat: implement hospital network management module with CRUD operations and state synchronization --- src/pages/Dashboard.jsx | 30 + src/pages/HospitalNetwork.css | 693 ++++++++++++++++++++++ src/pages/HospitalNetwork.jsx | 1051 +++++++++++++++++++++++++++++++++ src/pages/LoginPage.jsx | 86 +-- src/services/adminService.js | 15 + 5 files changed, 1832 insertions(+), 43 deletions(-) create mode 100644 src/pages/HospitalNetwork.css create mode 100644 src/pages/HospitalNetwork.jsx diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index cc0187c..6e6d170 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback } from 'react' import { adminService } from '../services/adminService' +import HospitalNetwork from './HospitalNetwork' import './Dashboard.css' // ── Icons for navigation and UI ── @@ -82,9 +83,32 @@ const Icons = { + ), + Hospital: () => ( + + + + + + + + ), + Stethoscope: () => ( + + + + + + ), + NurseCap: () => ( + + + + ) } + export default function Dashboard({ user, onLogout }) { const [activeTab, setActiveTab] = useState('Overview') const [showModal, setShowModal] = useState(null) // 'invite_admin' | 'reset_2fa_approval' | 'change_config' | 'broadcast' | 'create_access_grant' @@ -542,6 +566,7 @@ export default function Dashboard({ user, onLogout }) { { id: 'Financials', label: 'Finance Ledger', Icon: Icons.Finance }, { id: 'Config', label: 'Platform Config', Icon: Icons.Config }, { id: 'MedicalGate', label: 'Medical Records', Icon: Icons.Key }, + { id: 'HospitalGroups', label: 'Hospital Network', Icon: Icons.Hospital }, { id: 'Compliance', label: 'Audit & Compliance', Icon: Icons.Compliance }, { id: 'SystemJobs', label: 'System Operations', Icon: Icons.Terminal }, { id: 'Broadcasts', label: 'Announcements', Icon: Icons.Bell } @@ -1223,6 +1248,11 @@ export default function Dashboard({ user, onLogout }) { )} + + {/* ────────────────── HOSPITAL NETWORK TAB ────────────────── */} + {activeTab === 'HospitalGroups' && ( + + )} diff --git a/src/pages/HospitalNetwork.css b/src/pages/HospitalNetwork.css new file mode 100644 index 0000000..5c5a6e6 --- /dev/null +++ b/src/pages/HospitalNetwork.css @@ -0,0 +1,693 @@ +/* ============================================================ + HospitalNetwork.css — CureSelect Hospital Network UI/UX Styles + ============================================================ */ + +.hospital-network-tab { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; + height: 100%; + padding-bottom: 40px; +} + +/* ── Analytics Overview Cards ── */ +.network-analytics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + width: 100%; +} + +.analytics-card { + display: flex; + align-items: center; + gap: 16px; + background: rgba(10, 22, 40, 0.6); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--radius-md); + padding: 16px 20px; + backdrop-filter: blur(10px); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.analytics-card:hover { + transform: translateY(-2px); + border-color: rgba(59, 130, 246, 0.2); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); +} + +.analytics-card.glow-card { + border-color: rgba(59, 130, 246, 0.2); + box-shadow: 0 0 20px rgba(59, 130, 246, 0.05); +} + +.card-icon-wrap { + width: 48px; + height: 48px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; +} + +.primary-icon { + background: rgba(59, 130, 246, 0.1); + color: var(--primary-400); + border: 1px solid rgba(59, 130, 246, 0.2); +} + +.teal-icon { + background: rgba(20, 184, 166, 0.1); + color: var(--teal-400); + border: 1px solid rgba(20, 184, 166, 0.2); +} + +.info-icon { + background: rgba(14, 165, 233, 0.1); + color: #38bdf8; + border: 1px solid rgba(14, 165, 233, 0.2); +} + +.success-icon { + background: rgba(34, 197, 94, 0.1); + color: var(--success); + border: 1px solid rgba(34, 197, 94, 0.2); +} + +.card-data { + display: flex; + flex-direction: column; +} + +.card-val { + font-size: 24px; + font-weight: 800; + color: #fff; + line-height: 1.2; +} + +.card-title { + font-size: 12.5px; + color: var(--gray-400); + font-weight: 600; + margin-top: 2px; +} + +/* ── Split Panel Layout ── */ +.network-split-panel { + display: flex; + gap: 20px; + width: 100%; + min-height: 520px; + align-items: flex-start; +} + +/* Left Sidebar (Provider Groups) */ +.network-left-sidebar { + width: 320px; + flex-shrink: 0; + background: var(--dark-800); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--radius-md); + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.section-pane-title { + font-size: 15px; + font-weight: 700; + color: #fff; +} + +.header-search-row { + display: flex; + justify-content: justify; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.add-mini-btn { + background: rgba(59, 130, 246, 0.1); + border: 1px solid rgba(59, 130, 246, 0.2); + color: var(--primary-400); + width: 26px; + height: 26px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; +} + +.add-mini-btn:hover { + background: var(--primary-600); + color: #fff; + border-color: var(--primary-600); + transform: scale(1.05); +} + +.search-input-wrap { + position: relative; + width: 100%; +} + +.search-icon { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + color: var(--gray-500); + pointer-events: none; +} + +.search-input-wrap input { + width: 100%; + background: var(--dark-900); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + padding: 8px 10px 8px 32px; + color: #fff; + font-size: 13px; + transition: all 0.2s; +} + +.search-input-wrap input:focus { + border-color: var(--primary-500); + outline: none; + box-shadow: 0 0 10px rgba(59, 130, 246, 0.15); +} + +.groups-list { + display: flex; + flex-direction: column; + gap: 10px; + max-height: 480px; + overflow-y: auto; + padding-right: 4px; +} + +.group-item-card { + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.04); + border-radius: 8px; + padding: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.group-item-card:hover { + background: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.1); +} + +.group-item-card.selected { + background: rgba(59, 130, 246, 0.08); + border-color: var(--primary-500); + box-shadow: 0 0 12px rgba(59, 130, 246, 0.1); +} + +.group-item-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; +} + +.group-name { + font-size: 13.5px; + font-weight: 700; + color: #fff; +} + +.group-badge { + font-size: 10.5px; + background: rgba(255, 255, 255, 0.06); + color: var(--gray-300); + padding: 2px 6px; + border-radius: 4px; + white-space: nowrap; + font-weight: 500; +} + +.group-item-card.selected .group-badge { + background: rgba(59, 130, 246, 0.2); + color: var(--primary-300); +} + +.group-hq { + font-size: 11.5px; + color: var(--gray-400); + margin-top: 4px; +} + +.group-actions-inline { + display: flex; + gap: 12px; + margin-top: 8px; + opacity: 0.7; + transition: opacity 0.2s; +} + +.group-item-card:hover .group-actions-inline { + opacity: 1; +} + +.icon-link-btn { + background: none; + border: none; + color: var(--primary-400); + font-size: 11px; + font-weight: 600; + cursor: pointer; + padding: 0; + transition: color 0.15s; +} + +.icon-link-btn:hover { + color: #fff; + text-decoration: underline; +} + +.icon-link-btn.danger-link { + color: var(--danger); +} + +.icon-link-btn.danger-link:hover { + color: #ff6b6b; +} + +/* Main Content Panel (Selected Group View) */ +.network-main-content { + flex-grow: 1; + background: var(--dark-800); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--radius-md); + padding: 20px; +} + +.group-details-pane { + display: flex; + flex-direction: column; + gap: 20px; +} + +.pane-header-actions { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + padding-bottom: 16px; +} + +.group-title-label { + font-size: 20px; + font-weight: 800; + color: #fff; + letter-spacing: -0.01em; +} + +.group-desc-label { + font-size: 13px; + color: var(--gray-400); + margin-top: 4px; + max-width: 600px; +} + +.facility-filter-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; +} + +.width-medium { + max-width: 300px; +} + +.section-sub-title { + font-size: 14.5px; + font-weight: 700; + color: var(--gray-200); +} + +/* Hospitals Cards Grid */ +.hospitals-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; +} + +.hospital-detail-card { + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--radius-md); + padding: 16px; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 16px; + transition: all 0.25s ease; +} + +.hospital-detail-card:hover { + transform: translateY(-2px); + border-color: rgba(20, 184, 166, 0.25); + background: rgba(255, 255, 255, 0.03); +} + +.hospital-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; +} + +.hospital-card-name { + font-size: 14.5px; + font-weight: 700; + color: #fff; + line-height: 1.3; +} + +.hospital-card-city { + font-size: 11px; + color: var(--teal-400); + text-transform: uppercase; + font-weight: 600; + margin-top: 2px; + display: inline-block; +} + +.beds-badge { + font-size: 10px; + background: rgba(20, 184, 166, 0.15); + color: var(--teal-400); + border: 1px solid rgba(20, 184, 166, 0.2); + padding: 2px 6px; + border-radius: 4px; + font-weight: 600; + white-space: nowrap; +} + +.hospital-card-info { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; + color: var(--gray-400); + border-bottom: 1px solid rgba(255, 255, 255, 0.04); + padding-bottom: 12px; +} + +.info-desc { + line-height: 1.4; +} + +.hospital-card-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.staff-micro-stats { + display: flex; + gap: 8px; +} + +.m-stat { + font-size: 10.5px; + background: rgba(255, 255, 255, 0.04); + color: var(--gray-300); + padding: 2px 6px; + border-radius: 4px; + font-weight: 500; +} + +.card-actions { + display: flex; + gap: 8px; +} + +.mini-action-btn { + background: none; + border: 1px solid rgba(255, 255, 255, 0.15); + color: var(--gray-300); + font-size: 11px; + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + transition: all 0.15s; +} + +.mini-action-btn:hover { + background: rgba(255, 255, 255, 0.08); + color: #fff; + border-color: rgba(255, 255, 255, 0.3); +} + +.mini-action-btn.danger:hover { + border-color: var(--danger); + color: var(--danger); + background: rgba(239, 68, 68, 0.08); +} + +.action-btn-primary { + background: var(--primary-600); + border: none; + color: #fff; + font-size: 11px; + font-weight: 600; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + transition: background 0.15s; +} + +.action-btn-primary:hover { + background: var(--primary-500); +} + +/* Empty states */ +.empty-state { + color: var(--gray-500); + font-size: 12.5px; + text-align: center; + padding: 16px; +} + +.empty-state-full { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 40px; + color: var(--gray-400); + border: 1px dashed rgba(255, 255, 255, 0.08); + border-radius: var(--radius-md); + font-size: 13.5px; +} + +/* ── MODALS (Wide & Dialog styling) ── */ +.modal-wide .modal-content { + max-width: 900px; + width: 90%; + background: var(--dark-800); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4); +} + +.modal-super-title { + font-size: 10px; + color: var(--teal-400); + text-transform: uppercase; + font-weight: 700; + letter-spacing: 0.08em; + margin-bottom: 2px; + display: block; +} + +.modal-sub-title-address { + font-size: 12px; + color: var(--gray-400); + margin-top: 4px; +} + +.modal-inner-roster-body { + display: flex; + flex-direction: column; + gap: 20px; + margin-top: 20px; +} + +.roster-control-panel { + display: flex; + flex-direction: column; + gap: 12px; +} + +@media (min-width: 768px) { + .roster-control-panel { + flex-direction: row; + justify-content: space-between; + align-items: center; + } +} + +.roster-tabs-switcher { + display: flex; + background: var(--dark-900); + padding: 3px; + border-radius: 6px; + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.roster-tab-btn { + background: none; + border: none; + color: var(--gray-400); + font-size: 12px; + font-weight: 600; + padding: 6px 14px; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} + +.roster-tab-btn:hover { + color: #fff; +} + +.roster-tab-btn.active { + background: var(--primary-600); + color: #fff; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); +} + +.roster-action-group { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +.roster-dropdown { + background: var(--dark-900); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + color: #fff; + padding: 7px 12px; + font-size: 12.5px; + outline: none; + cursor: pointer; +} + +.roster-dropdown:focus { + border-color: var(--primary-500); +} + +/* Roster Table Styling */ +.roster-table-container { + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 8px; + overflow: hidden; + max-height: 380px; +} + +.staff-avatar-cell { + display: flex; + align-items: center; + gap: 12px; +} + +.mini-av { + width: 32px !important; + height: 32px !important; + font-size: 10px !important; + border-radius: 6px !important; +} + +.staff-meta-details { + display: flex; + flex-direction: column; +} + +.staff-cell-name { + color: #fff; + font-size: 13px; +} + +.staff-cell-sub { + font-size: 11px; + color: var(--gray-400); +} + +.roster-actions-wrap { + display: flex; + gap: 12px; +} + +/* Status Indicator tag pills */ +.status-indicator-tag { + display: inline-block; + font-size: 10px; + font-weight: 700; + padding: 2px 8px; + border-radius: 4px; + border: 1px solid transparent; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.status-on-duty, .status-active { + background: rgba(34, 197, 94, 0.1); + color: var(--success); + border-color: rgba(34, 197, 94, 0.2); +} + +.status-off-duty, .status-on-leave { + background: rgba(239, 68, 68, 0.1); + color: var(--danger); + border-color: rgba(239, 68, 68, 0.2); +} + +.status-on-call { + background: rgba(245, 158, 11, 0.1); + color: var(--warning); + border-color: rgba(245, 158, 11, 0.2); +} + +.status-scheduled { + background: rgba(59, 130, 246, 0.1); + color: var(--primary-400); + border-color: rgba(59, 130, 246, 0.2); +} + +/* Form layouts */ +.form-row-double { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +/* Ensure Front modals stack correctly */ +.modal-front { + z-index: 110 !important; +} + +.cancel-btn { + background: none; + border: 1px solid rgba(255, 255, 255, 0.1); + color: var(--gray-300); +} + +.cancel-btn:hover { + background: rgba(255, 255, 255, 0.05); + color: #fff; +} diff --git a/src/pages/HospitalNetwork.jsx b/src/pages/HospitalNetwork.jsx new file mode 100644 index 0000000..193705c --- /dev/null +++ b/src/pages/HospitalNetwork.jsx @@ -0,0 +1,1051 @@ +import { useState, useEffect, useCallback, useMemo } from 'react' +import { adminService } from '../services/adminService' +import './HospitalNetwork.css' + +// Default mock data for hospital network hierarchy +const DEFAULT_GROUPS = [ + { id: 'HG001', name: 'CureSelect Global Network', hq: 'Boston, MA', established: '2015', description: 'Elite tier healthcare providers across the northeast.' }, + { id: 'HG002', name: 'Metro Care Alliances', hq: 'New York, NY', established: '2018', description: 'Large scale metropolitan hospital clusters.' }, + { id: 'HG003', name: 'Red Cross Hospital Corp', hq: 'Los Angeles, CA', established: '2010', description: 'Non-profit healthcare and emergency services.' } +] + +const DEFAULT_HOSPITALS = [ + { id: 'H001', groupId: 'HG001', name: "St. Mary's General Hospital", city: 'Boston', beds: 340, phone: '+1 (555) 234-5678', email: 'stmarys@cureselect.com', address: '777 Beacon St, Boston, MA', rating: 4.8 }, + { id: 'H002', groupId: 'HG001', name: 'Mercy Home Care Center', city: 'Cambridge', beds: 120, phone: '+1 (555) 876-5432', email: 'mercy@cureselect.com', address: '120 Massachusetts Ave, Cambridge, MA', rating: 4.5 }, + { id: 'H003', groupId: 'HG002', name: 'Metro Health Medical Center', city: 'New York', beds: 500, phone: '+1 (555) 111-2222', email: 'nyc_metro@metrocare.com', address: '450 5th Ave, New York, NY', rating: 4.9 }, + { id: 'H004', groupId: 'HG002', name: 'Queens Health & Rehab', city: 'Queens', beds: 180, phone: '+1 (555) 333-4444', email: 'queens@metrocare.com', address: '88-12 Roosevelt Ave, Queens, NY', rating: 4.3 }, + { id: 'H005', groupId: 'HG003', name: 'Red Cross City Hospital', city: 'Los Angeles', beds: 250, phone: '+1 (555) 555-7777', email: 'city@redcross.org', address: '500 Sunset Blvd, Los Angeles, CA', rating: 4.7 } +] + +const DEFAULT_STAFF = [ + { id: 'S001', hospitalId: 'H001', type: 'doctor', name: 'Dr. Sarah Jenkins', specialty: 'Cardiologist', experience: '12 Yrs', status: 'On Duty', phone: '+1 (555) 123-1111', email: 's.jenkins@hospitals.com' }, + { id: 'S002', hospitalId: 'H001', type: 'doctor', name: 'Dr. Alan Mercer', specialty: 'Neurologist', experience: '8 Yrs', status: 'On Call', phone: '+1 (555) 123-2222', email: 'a.mercer@hospitals.com' }, + { id: 'S003', hospitalId: 'H001', type: 'caregiver', name: 'Nurse Emily Davis', role: 'Registered Nurse', certification: 'BSN, CCRN', status: 'Active', phone: '+1 (555) 123-3333', email: 'e.davis@hospitals.com' }, + { id: 'S004', hospitalId: 'H001', type: 'caregiver', name: 'Nurse Robert Miller', role: 'Registered Nurse', certification: 'BSN', status: 'Scheduled', phone: '+1 (555) 123-4444', email: 'r.miller@hospitals.com' }, + { id: 'S005', hospitalId: 'H002', type: 'doctor', name: 'Dr. Lisa Wong', specialty: 'General Practitioner', experience: '5 Yrs', status: 'Off Duty', phone: '+1 (555) 321-1111', email: 'l.wong@hospitals.com' }, + { id: 'S006', hospitalId: 'H002', type: 'caregiver', name: 'Michael Chang', role: 'Physical Therapist', certification: 'DPT', status: 'Active', phone: '+1 (555) 321-2222', email: 'm.chang@hospitals.com' }, + { id: 'S007', hospitalId: 'H003', type: 'doctor', name: 'Dr. Gregory House', specialty: 'Diagnostic Medicine', experience: '20 Yrs', status: 'On Duty', phone: '+1 (555) 987-1111', email: 'house@metro.com' }, + { id: 'S008', hospitalId: 'H003', type: 'caregiver', name: 'Nurse Clara Barton', role: 'Registered Nurse', certification: 'MSN, APRN', status: 'Active', phone: '+1 (555) 987-2222', email: 'clara@metro.com' }, + { id: 'S009', hospitalId: 'H004', type: 'doctor', name: 'Dr. Allison Cameron', specialty: 'Immunologist', experience: '6 Yrs', status: 'On Call', phone: '+1 (555) 765-1111', email: 'cameron@metro.com' }, + { id: 'S010', hospitalId: 'H004', type: 'caregiver', name: 'James Wilson', role: 'Home Health Aide', certification: 'HHA', status: 'On Leave', phone: '+1 (555) 765-2222', email: 'j.wilson@metro.com' }, + { id: 'S011', hospitalId: 'H005', type: 'doctor', name: 'Dr. John Watson', specialty: 'General Practitioner', experience: '15 Yrs', status: 'On Duty', phone: '+1 (555) 890-1111', email: 'watson@redcross.org' }, + { id: 'S012', hospitalId: 'H005', type: 'caregiver', name: 'Nurse Florence Nightingale', role: 'Registered Nurse', certification: 'BSN', status: 'Active', phone: '+1 (555) 890-2222', email: 'florence@redcross.org' } +] + +export default function HospitalNetwork({ notify }) { + // Main Data States + const [groups, setGroups] = useState(() => { + const saved = localStorage.getItem('cs_hospital_groups') + return saved ? JSON.parse(saved) : DEFAULT_GROUPS + }) + const [hospitals, setHospitals] = useState(() => { + const saved = localStorage.getItem('cs_hospitals') + return saved ? JSON.parse(saved) : DEFAULT_HOSPITALS + }) + const [staff, setStaff] = useState(() => { + const saved = localStorage.getItem('cs_hospital_staff') + return saved ? JSON.parse(saved) : DEFAULT_STAFF + }) + + // Selection & UI States + const [selectedGroupId, setSelectedGroupId] = useState('HG001') + const [activeHospitalStaffId, setActiveHospitalStaffId] = useState(null) // ID of hospital whose staff modal is open + const [staffTab, setStaffTab] = useState('doctor') // 'doctor' | 'caregiver' + + // Searches & Filters + const [groupQuery, setGroupQuery] = useState('') + const [hospitalQuery, setHospitalQuery] = useState('') + const [staffQuery, setStaffQuery] = useState('') + const [specialtyFilter, setSpecialtyFilter] = useState('All') + const [roleFilter, setRoleFilter] = useState('All') + + // Modals for CRUD + // 'group' | 'hospital' | 'staff' + const [modalType, setModalType] = useState(null) + const [modalMode, setModalMode] = useState('create') // 'create' | 'edit' + const [editItem, setEditItem] = useState(null) + + // Form States + const [groupForm, setGroupForm] = useState({ name: '', hq: '', established: '', description: '' }) + const [hospitalForm, setHospitalForm] = useState({ name: '', city: '', beds: '', phone: '', email: '', address: '', rating: '5.0' }) + const [staffForm, setStaffForm] = useState({ type: 'doctor', name: '', specialty: '', role: 'Registered Nurse', experience: '1 Yr', certification: '', status: 'On Duty', phone: '', email: '' }) + + // Synchronize with LocalStorage + useEffect(() => { + localStorage.setItem('cs_hospital_groups', JSON.stringify(groups)) + }, [groups]) + + useEffect(() => { + localStorage.setItem('cs_hospitals', JSON.stringify(hospitals)) + }, [hospitals]) + + useEffect(() => { + localStorage.setItem('cs_hospital_staff', JSON.stringify(staff)) + }, [staff]) + + // Fetch groups/hospitals from api to sync if endpoints exist + const syncWithApi = useCallback(async () => { + try { + const groupsRes = await adminService.hospitalNetwork.getGroups() + let fetchedGroups = [] + + if (groupsRes?.data?.hospitalgroups && Array.isArray(groupsRes.data.hospitalgroups)) { + fetchedGroups = groupsRes.data.hospitalgroups + } else if (groupsRes?.data && Array.isArray(groupsRes.data)) { + fetchedGroups = groupsRes.data + } else if (Array.isArray(groupsRes)) { + fetchedGroups = groupsRes + } + + if (fetchedGroups.length > 0) { + setGroups(fetchedGroups) + // Automatically select the first fetched group ID + setSelectedGroupId(fetchedGroups[0].id) + } + + // Fetch hospitals + try { + const hospitalsRes = await adminService.hospitalNetwork.getHospitals() + let fetchedHospitals = [] + if (hospitalsRes?.data?.hospitals && Array.isArray(hospitalsRes.data.hospitals)) { + fetchedHospitals = hospitalsRes.data.hospitals + } else if (hospitalsRes?.data && Array.isArray(hospitalsRes.data)) { + fetchedHospitals = hospitalsRes.data + } else if (Array.isArray(hospitalsRes)) { + fetchedHospitals = hospitalsRes + } + if (fetchedHospitals.length > 0) { + setHospitals(fetchedHospitals) + } + } catch (err) { + console.log('Hospitals API bypass') + } + + // Fetch staff + try { + const staffRes = await adminService.hospitalNetwork.getStaff() + let fetchedStaff = [] + if (staffRes?.data?.staff && Array.isArray(staffRes.data.staff)) { + fetchedStaff = staffRes.data.staff + } else if (staffRes?.data && Array.isArray(staffRes.data)) { + fetchedStaff = staffRes.data + } else if (Array.isArray(staffRes)) { + fetchedStaff = staffRes + } + if (fetchedStaff.length > 0) { + setStaff(fetchedStaff) + } + } catch (err) { + console.log('Staff API bypass') + } + + } catch (e) { + console.log('API synchronization bypassed, utilizing high-performance in-memory database.') + } + }, []) + + useEffect(() => { + syncWithApi() + }, [syncWithApi]) + + // Analytics Helpers + const analytics = useMemo(() => { + const totalGroups = groups.length + const totalHospitals = hospitals.length + + const activeDoctorsCount = staff.filter(s => + s.type === 'doctor' && (s.status === 'On Duty' || s.status === 'On Call') + ).length + + const activeCaregiversCount = staff.filter(s => + s.type === 'caregiver' && (s.status === 'Active' || s.status === 'Scheduled') + ).length + + return { + totalGroups, + totalHospitals, + activeDoctorsCount, + activeCaregiversCount + } + }, [groups, hospitals, staff]) + + // Filters and Searches Computations + const filteredGroups = useMemo(() => { + return groups.filter(g => + g.name.toLowerCase().includes(groupQuery.toLowerCase()) || + g.hq.toLowerCase().includes(groupQuery.toLowerCase()) + ) + }, [groups, groupQuery]) + + const selectedGroup = useMemo(() => { + return groups.find(g => g.id === selectedGroupId) || groups[0] + }, [groups, selectedGroupId]) + + const filteredHospitals = useMemo(() => { + if (!selectedGroupId) return [] + return hospitals.filter(h => + (h.groupId === selectedGroupId || h.group_id === selectedGroupId || String(h.groupId) === String(selectedGroupId) || String(h.group_id) === String(selectedGroupId)) && + (h.name.toLowerCase().includes(hospitalQuery.toLowerCase()) || + h.city.toLowerCase().includes(hospitalQuery.toLowerCase())) + ) + }, [hospitals, selectedGroupId, hospitalQuery]) + + const activeHospital = useMemo(() => { + return hospitals.find(h => h.id === activeHospitalStaffId) + }, [hospitals, activeHospitalStaffId]) + + // Staff listing filter for active hospital + const filteredStaff = useMemo(() => { + if (!activeHospitalStaffId) return [] + return staff.filter(s => { + if (s.hospitalId !== activeHospitalStaffId && s.hospital_id !== activeHospitalStaffId && String(s.hospitalId) !== String(activeHospitalStaffId) && String(s.hospital_id) !== String(activeHospitalStaffId)) return false + if (s.type !== staffTab) return false + + const matchesSearch = s.name.toLowerCase().includes(staffQuery.toLowerCase()) + + if (staffTab === 'doctor') { + const matchesSpec = specialtyFilter === 'All' || s.specialty === specialtyFilter + return matchesSearch && matchesSpec + } else { + const matchesRole = roleFilter === 'All' || s.role === roleFilter + return matchesSearch && matchesRole + } + }) + }, [staff, activeHospitalStaffId, staffTab, staffQuery, specialtyFilter, roleFilter]) + + // Unique lists for filtering dropdowns + const specialties = useMemo(() => { + const list = staff.filter(s => s.type === 'doctor').map(s => s.specialty) + return ['All', ...new Set(list)] + }, [staff]) + + const roles = useMemo(() => { + const list = staff.filter(s => s.type === 'caregiver').map(s => s.role) + return ['All', ...new Set(list)] + }, [staff]) + + // Counts of staff per hospital + const hospitalStaffStats = useMemo(() => { + const statsMap = {} + hospitals.forEach(h => { + const docs = staff.filter(s => (s.hospitalId === h.id || s.hospital_id === h.id || String(s.hospitalId) === String(h.id) || String(s.hospital_id) === String(h.id)) && s.type === 'doctor').length + const cgs = staff.filter(s => (s.hospitalId === h.id || s.hospital_id === h.id || String(s.hospitalId) === String(h.id) || String(s.hospital_id) === String(h.id)) && s.type === 'caregiver').length + statsMap[h.id] = { doctors: docs, caregivers: cgs } + }) + return statsMap + }, [hospitals, staff]) + + // CRUDS handlers + // Groups + const handleOpenGroupModal = (mode = 'create', item = null) => { + setModalMode(mode) + setModalType('group') + if (mode === 'edit' && item) { + setEditItem(item) + setGroupForm({ name: item.name, hq: item.hq, established: item.established, description: item.description }) + } else { + setEditItem(null) + setGroupForm({ name: '', hq: '', established: '', description: '' }) + } + } + + const handleGroupSubmit = async (e) => { + e.preventDefault() + if (!groupForm.name.trim()) return notify('Hospital Group name is required', true) + + if (modalMode === 'create') { + const newGroup = { + id: 'HG' + Date.now().toString().slice(-4), + ...groupForm + } + setGroups(prev => [...prev, newGroup]) + notify(`Hospital group "${groupForm.name}" created successfully.`) + setSelectedGroupId(newGroup.id) + } else if (modalMode === 'edit' && editItem) { + setGroups(prev => prev.map(g => g.id === editItem.id ? { ...g, ...groupForm } : g)) + notify(`Hospital group "${groupForm.name}" updated successfully.`) + } + setModalType(null) + } + + const handleDeleteGroup = (groupId, name) => { + if (window.confirm(`Are you sure you want to delete "${name}"? All associated hospitals will be detached.`)) { + setGroups(prev => prev.filter(g => g.id !== groupId)) + setHospitals(prev => prev.filter(h => h.groupId !== groupId)) + notify(`Hospital group "${name}" deleted.`) + if (selectedGroupId === groupId) { + setSelectedGroupId(groups[0]?.id || null) + } + } + } + + // Hospitals + const handleOpenHospitalModal = (mode = 'create', item = null) => { + setModalMode(mode) + setModalType('hospital') + if (mode === 'edit' && item) { + setEditItem(item) + setHospitalForm({ + name: item.name, + city: item.city, + beds: item.beds, + phone: item.phone, + email: item.email, + address: item.address, + rating: item.rating + }) + } else { + setEditItem(null) + setHospitalForm({ name: '', city: '', beds: '', phone: '', email: '', address: '', rating: '5.0' }) + } + } + + const handleHospitalSubmit = async (e) => { + e.preventDefault() + if (!hospitalForm.name.trim()) return notify('Hospital Name is required', true) + + if (modalMode === 'create') { + const newHospital = { + id: 'H' + Date.now().toString().slice(-4), + groupId: selectedGroupId, + ...hospitalForm, + beds: parseInt(hospitalForm.beds) || 50 + } + setHospitals(prev => [...prev, newHospital]) + notify(`Hospital "${hospitalForm.name}" added to group.`) + } else if (modalMode === 'edit' && editItem) { + setHospitals(prev => prev.map(h => h.id === editItem.id ? { ...h, ...hospitalForm, beds: parseInt(hospitalForm.beds) || 50 } : h)) + notify(`Hospital "${hospitalForm.name}" updated.`) + } + setModalType(null) + } + + const handleDeleteHospital = (hospitalId, name) => { + if (window.confirm(`Are you sure you want to delete "${name}"?`)) { + setHospitals(prev => prev.filter(h => h.id !== hospitalId)) + setStaff(prev => prev.filter(s => s.hospitalId !== hospitalId)) + notify(`Hospital "${name}" removed.`) + } + } + + // Staff (Doctors / Caregivers) + const handleOpenStaffModal = (mode = 'create', item = null) => { + setModalMode(mode) + setModalType('staff') + if (mode === 'edit' && item) { + setEditItem(item) + setStaffForm({ + type: item.type, + name: item.name, + specialty: item.specialty || '', + role: item.role || 'Registered Nurse', + experience: item.experience || '1 Yr', + certification: item.certification || '', + status: item.status, + phone: item.phone, + email: item.email + }) + } else { + setEditItem(null) + setStaffForm({ + type: staffTab, + name: '', + specialty: '', + role: 'Registered Nurse', + experience: '1 Yr', + certification: '', + status: staffTab === 'doctor' ? 'On Duty' : 'Active', + phone: '', + email: '' + }) + } + } + + const handleStaffSubmit = async (e) => { + e.preventDefault() + if (!staffForm.name.trim()) return notify('Provider Name is required', true) + + if (modalMode === 'create') { + const newStaff = { + id: 'S' + Date.now().toString().slice(-4), + hospitalId: activeHospitalStaffId, + type: staffForm.type, + name: staffForm.name, + status: staffForm.status, + phone: staffForm.phone, + email: staffForm.email, + ...(staffForm.type === 'doctor' + ? { specialty: staffForm.specialty || 'General Physician', experience: staffForm.experience } + : { role: staffForm.role, certification: staffForm.certification }) + } + setStaff(prev => [...prev, newStaff]) + notify(`${staffForm.type === 'doctor' ? 'Doctor' : 'Caregiver'} "${staffForm.name}" registered successfully.`) + } else if (modalMode === 'edit' && editItem) { + setStaff(prev => prev.map(s => s.id === editItem.id ? { + ...s, + name: staffForm.name, + status: staffForm.status, + phone: staffForm.phone, + email: staffForm.email, + ...(staffForm.type === 'doctor' + ? { specialty: staffForm.specialty, experience: staffForm.experience } + : { role: staffForm.role, certification: staffForm.certification }) + } : s)) + notify(`Staff details updated.`) + } + setModalType(null) + } + + const handleDeleteStaff = (staffId, name) => { + if (window.confirm(`Are you sure you want to remove "${name}" from this facility?`)) { + setStaff(prev => prev.filter(s => s.id !== staffId)) + notify(`Staff member "${name}" removed.`) + } + } + + return ( +
+ {/* ── Analytics Overview Row ── */} +
+
+
+ + + +
+
+ {analytics.totalGroups} + Hospital Groups +
+
+ +
+
+ + + +
+
+ {analytics.totalHospitals} + Active Facilities +
+
+ +
+
+ + + + + +
+
+ {analytics.activeDoctorsCount} + Doctors On Duty +
+
+ +
+
+ + + + + +
+
+ {analytics.activeCaregiversCount} + Caregivers Active +
+
+
+ + {/* ── Main Two-Column Structure ── */} +
+ + {/* Left Column: Hospital Groups List */} +
+
+
+

Provider Groups

+ +
+
+ + + + + setGroupQuery(e.target.value)} + /> +
+
+ +
+ {filteredGroups.length === 0 ? ( +
No hospital groups found.
+ ) : ( + filteredGroups.map(g => { + const isSelected = g.id === selectedGroupId + const hospitalCount = hospitals.filter(h => h.groupId === g.id || h.group_id === g.id || String(h.groupId) === String(g.id) || String(h.group_id) === String(g.id)).length + + // Safely extract hq and established year from backend-specific keys if missing + let hqVal = g.hq + let estVal = g.established + if (!hqVal && g.additional_info) { + try { + const info = typeof g.additional_info === 'string' ? JSON.parse(g.additional_info) : g.additional_info + hqVal = info.city || info.hq + } catch (e) { + hqVal = 'N/A' + } + } + if (!estVal && g.created_at) { + estVal = g.created_at.substring(0, 4) + } + hqVal = hqVal || 'N/A' + estVal = estVal || 'N/A' + + return ( +
setSelectedGroupId(g.id)} + > +
+

{g.name}

+ {hospitalCount} facilities +
+

HQ: {hqVal} · Est: {estVal}

+
+ + +
+
+ ) + }) + )} +
+
+ + {/* Right Panel: Selected Group Details & Hospitals Grid */} +
+ {selectedGroup ? ( +
+
+
+

{selectedGroup.name}

+

+ {selectedGroup.description || + (selectedGroup.additional_info ? (typeof selectedGroup.additional_info === 'string' ? JSON.parse(selectedGroup.additional_info).description : selectedGroup.additional_info.description) : '') || + 'No description provided.'} +

+
+ +
+ + {/* Facility Search Row */} +
+

Facilities & Clinics ({filteredHospitals.length})

+
+ + + + + setHospitalQuery(e.target.value)} + /> +
+
+ + {/* Hospitals Cards Grid */} +
+ {filteredHospitals.length === 0 ? ( +
+ + + +

No clinics registered under this group.

+
+ ) : ( + filteredHospitals.map(h => { + const stats = hospitalStaffStats[h.id] || { doctors: 0, caregivers: 0 } + return ( +
+
+
+

{h.name}

+ {h.city} Facility +
+ {h.beds} Beds +
+ +
+

Address: {h.address}

+

Contact: {h.phone}

+

Rating: ⭐ {h.rating || '5.0'}

+
+ +
+
+ {stats.doctors} Docs + {stats.caregivers} Staff +
+
+ + + +
+
+
+ ) + }) + )} +
+
+ ) : ( +
+

Please select a Provider Group from the sidebar to manage network hospitals and clinicians.

+
+ )} +
+
+ + {/* ── STAFF MANAGEMENT DETAILED MODAL ── */} + {activeHospitalStaffId && activeHospital && ( +
setActiveHospitalStaffId(null)}> +
e.stopPropagation()}> +
+
+ {selectedGroup.name} +

{activeHospital.name} — Staff Roster

+

📍 {activeHospital.address} · 📞 {activeHospital.phone}

+
+ +
+ +
+ {/* Tab Header and Filtering */} +
+
+ + +
+ +
+
+ + + + + setStaffQuery(e.target.value)} + /> +
+ + {staffTab === 'doctor' ? ( + + ) : ( + + )} + + +
+
+ + {/* Roster Table */} +
+ + + {staffTab === 'doctor' ? ( + + + + + + + + + ) : ( + + + + + + + + + )} + + + {filteredStaff.length === 0 ? ( + + + + ) : ( + filteredStaff.map(s => { + const statusClass = (s.status || '').replace(/\s+/g, '-').toLowerCase() + return ( + + + + + + + + + ) + }) + )} + +
Clinician NameSpecialtyExperienceContact NumberOperations StatusActions
Caregiver NameRole categoryCertificationsContact NumberWork StatusActions
+ No matching staff members enrolled at this facility. +
+
+
+ {s.type === 'doctor' ? 'DR' : (s.role ? s.role.substring(0, 2).toUpperCase() : 'CG')} +
+
+ {s.name} + {s.email || 'N/A'} +
+
+
{s.type === 'doctor' ? s.specialty : s.role}{s.type === 'doctor' ? s.experience : (s.certification || 'Verified')}{s.phone || 'N/A'} + + {s.status} + + +
+ + +
+
+
+
+
+
+ )} + + {/* ── DYNAMIC ADD / EDIT MODALS ── */} + {modalType && ( +
setModalType(null)}> +
e.stopPropagation()}> +
+

+ {modalMode === 'create' ? 'Create New' : 'Edit'} { + modalType === 'group' ? 'Hospital Group' : + modalType === 'hospital' ? 'Facility Clinic' : 'Staff Profile' + } +

+ +
+ + {/* 1. Group Form */} + {modalType === 'group' && ( +
+
+ + setGroupForm(prev => ({ ...prev, name: e.target.value }))} + /> +
+
+
+ + setGroupForm(prev => ({ ...prev, hq: e.target.value }))} + /> +
+
+ + setGroupForm(prev => ({ ...prev, established: e.target.value }))} + /> +
+
+
+ +