feat: implement hospital network management module with CRUD operations and state synchronization

This commit is contained in:
2026-08-22 14:54:11 +05:30
parent b23a8b7edd
commit f2a2994e08
5 changed files with 1832 additions and 43 deletions
+30
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { adminService } from '../services/adminService' import { adminService } from '../services/adminService'
import HospitalNetwork from './HospitalNetwork'
import './Dashboard.css' import './Dashboard.css'
// ── Icons for navigation and UI ── // ── Icons for navigation and UI ──
@@ -82,9 +83,32 @@ const Icons = {
<polyline points="1 20 1 14 7 14"/> <polyline points="1 20 1 14 7 14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/> <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg> </svg>
),
Hospital: () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 22V8a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14" />
<path d="M4 22h16" />
<path d="M10 16h4" />
<path d="M12 14v4" />
<path d="M10 10h4" />
</svg>
),
Stethoscope: () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4.8 2.3A2.5 2.5 0 0 0 2.5 4.8v5A7.7 7.7 0 0 0 10 17.5h.3M19.2 2.3a2.5 2.5 0 0 1 2.3 2.5v5a7.7 7.7 0 0 1-7.5 7.7h-.3" />
<path d="M12 17v4" />
<circle cx="12" cy="21" r="2" />
</svg>
),
NurseCap: () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M2 22h20M6 12V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6" />
<path d="M12 3v19M6 12c0 2.5 2 4.5 4.5 4.5h3c2.5 0 4.5-2 4.5-4.5" />
</svg>
) )
} }
export default function Dashboard({ user, onLogout }) { export default function Dashboard({ user, onLogout }) {
const [activeTab, setActiveTab] = useState('Overview') const [activeTab, setActiveTab] = useState('Overview')
const [showModal, setShowModal] = useState(null) // 'invite_admin' | 'reset_2fa_approval' | 'change_config' | 'broadcast' | 'create_access_grant' 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: 'Financials', label: 'Finance Ledger', Icon: Icons.Finance },
{ id: 'Config', label: 'Platform Config', Icon: Icons.Config }, { id: 'Config', label: 'Platform Config', Icon: Icons.Config },
{ id: 'MedicalGate', label: 'Medical Records', Icon: Icons.Key }, { 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: 'Compliance', label: 'Audit & Compliance', Icon: Icons.Compliance },
{ id: 'SystemJobs', label: 'System Operations', Icon: Icons.Terminal }, { id: 'SystemJobs', label: 'System Operations', Icon: Icons.Terminal },
{ id: 'Broadcasts', label: 'Announcements', Icon: Icons.Bell } { id: 'Broadcasts', label: 'Announcements', Icon: Icons.Bell }
@@ -1223,6 +1248,11 @@ export default function Dashboard({ user, onLogout }) {
</div> </div>
</div> </div>
)} )}
{/* ────────────────── HOSPITAL NETWORK TAB ────────────────── */}
{activeTab === 'HospitalGroups' && (
<HospitalNetwork notify={notify} />
)}
</main> </main>
</div> </div>
+693
View File
@@ -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;
}
File diff suppressed because it is too large Load Diff
+43 -43
View File
@@ -23,14 +23,14 @@ function GridLines() {
function EyeIcon({ open }) { function EyeIcon({ open }) {
return open ? ( return open ? (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/> <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3" />
</svg> </svg>
) : ( ) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/> <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94" />
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/> <path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19" />
<line x1="1" y1="23" x2="23" y2="1"/> <line x1="1" y1="23" x2="23" y2="1" />
</svg> </svg>
) )
} }
@@ -41,14 +41,14 @@ function ShieldLogo() {
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<linearGradient id="shieldGrad" x1="0" y1="0" x2="44" y2="44" gradientUnits="userSpaceOnUse"> <linearGradient id="shieldGrad" x1="0" y1="0" x2="44" y2="44" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#3b82f6"/> <stop offset="0%" stopColor="#3b82f6" />
<stop offset="100%" stopColor="#14b8a6"/> <stop offset="100%" stopColor="#14b8a6" />
</linearGradient> </linearGradient>
</defs> </defs>
<path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z" fill="url(#shieldGrad)" opacity="0.15"/> <path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z" fill="url(#shieldGrad)" opacity="0.15" />
<path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z" stroke="url(#shieldGrad)" strokeWidth="1.5" fill="none"/> <path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z" stroke="url(#shieldGrad)" strokeWidth="1.5" fill="none" />
<line x1="22" y1="13" x2="22" y2="31" stroke="url(#shieldGrad)" strokeWidth="2.5" strokeLinecap="round"/> <line x1="22" y1="13" x2="22" y2="31" stroke="url(#shieldGrad)" strokeWidth="2.5" strokeLinecap="round" />
<line x1="13" y1="22" x2="31" y2="22" stroke="url(#shieldGrad)" strokeWidth="2.5" strokeLinecap="round"/> <line x1="13" y1="22" x2="31" y2="22" stroke="url(#shieldGrad)" strokeWidth="2.5" strokeLinecap="round" />
</svg> </svg>
) )
} }
@@ -58,8 +58,8 @@ function Spinner() {
return ( return (
<span className="spinner" aria-hidden="true"> <span className="spinner" aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<circle cx="10" cy="10" r="8" stroke="rgba(255,255,255,0.3)" strokeWidth="2"/> <circle cx="10" cy="10" r="8" stroke="rgba(255,255,255,0.3)" strokeWidth="2" />
<path d="M10 2a8 8 0 0 1 8 8" stroke="#fff" strokeWidth="2" strokeLinecap="round"/> <path d="M10 2a8 8 0 0 1 8 8" stroke="#fff" strokeWidth="2" strokeLinecap="round" />
</svg> </svg>
</span> </span>
) )
@@ -69,17 +69,17 @@ function Spinner() {
/* Main Login Page Component */ /* Main Login Page Component */
/* ─────────────────────────────────────────────── */ /* ─────────────────────────────────────────────── */
export default function LoginPage({ onLoginSuccess }) { export default function LoginPage({ onLoginSuccess }) {
const [form, setForm] = useState({ email: 'admin@cureselect.com', password: 'CureSelect@2025', remember: true }) const [form, setForm] = useState({ email: 'pvsrinath@gmail.com', password: 'ThisIsAStrongBootstrapSecret123', remember: true })
const [showPwd, setShowPwd] = useState(false) const [showPwd, setShowPwd] = useState(false)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const [shake, setShake] = useState(false) const [shake, setShake] = useState(false)
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
const [requires2fa, setRequires2fa] = useState(false) const [requires2fa, setRequires2fa] = useState(false)
const [sessionToken, setSessionToken] = useState('') const [sessionToken, setSessionToken] = useState('')
const [totpCode, setTotpCode] = useState('') const [totpCode, setTotpCode] = useState('')
const emailRef = useRef(null) const emailRef = useRef(null)
const totpRef = useRef(null) const totpRef = useRef(null)
useEffect(() => { useEffect(() => {
const t = setTimeout(() => setMounted(true), 50) const t = setTimeout(() => setMounted(true), 50)
@@ -202,16 +202,16 @@ export default function LoginPage({ onLoginSuccess }) {
<svg width="64" height="64" viewBox="0 0 44 44" fill="none"> <svg width="64" height="64" viewBox="0 0 44 44" fill="none">
<defs> <defs>
<linearGradient id="hg1" x1="0" y1="0" x2="44" y2="44" gradientUnits="userSpaceOnUse"> <linearGradient id="hg1" x1="0" y1="0" x2="44" y2="44" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#3b82f6"/> <stop offset="0%" stopColor="#3b82f6" />
<stop offset="100%" stopColor="#14b8a6"/> <stop offset="100%" stopColor="#14b8a6" />
</linearGradient> </linearGradient>
</defs> </defs>
<path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z" <path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z"
fill="url(#hg1)" opacity="0.2"/> fill="url(#hg1)" opacity="0.2" />
<path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z" <path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z"
stroke="url(#hg1)" strokeWidth="1.5" fill="none"/> stroke="url(#hg1)" strokeWidth="1.5" fill="none" />
<line x1="22" y1="13" x2="22" y2="31" stroke="url(#hg1)" strokeWidth="2.5" strokeLinecap="round"/> <line x1="22" y1="13" x2="22" y2="31" stroke="url(#hg1)" strokeWidth="2.5" strokeLinecap="round" />
<line x1="13" y1="22" x2="31" y2="22" stroke="url(#hg1)" strokeWidth="2.5" strokeLinecap="round"/> <line x1="13" y1="22" x2="31" y2="22" stroke="url(#hg1)" strokeWidth="2.5" strokeLinecap="round" />
</svg> </svg>
</div> </div>
</div> </div>
@@ -248,9 +248,9 @@ export default function LoginPage({ onLoginSuccess }) {
{error && ( {error && (
<div className="error-banner" role="alert"> <div className="error-banner" role="alert">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10"/> <circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12"/> <line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16"/> <line x1="12" y1="16" x2="12.01" y2="16" />
</svg> </svg>
{error} {error}
</div> </div>
@@ -270,8 +270,8 @@ export default function LoginPage({ onLoginSuccess }) {
<div className="field-wrapper"> <div className="field-wrapper">
<span className="field-icon"> <span className="field-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/> <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4"/> <path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg> </svg>
</span> </span>
<input <input
@@ -319,8 +319,8 @@ export default function LoginPage({ onLoginSuccess }) {
<div className="field-wrapper"> <div className="field-wrapper">
<span className="field-icon"> <span className="field-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/> <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<polyline points="22,6 12,13 2,6"/> <polyline points="22,6 12,13 2,6" />
</svg> </svg>
</span> </span>
<input <input
@@ -346,8 +346,8 @@ export default function LoginPage({ onLoginSuccess }) {
<div className="field-wrapper"> <div className="field-wrapper">
<span className="field-icon"> <span className="field-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/> <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4"/> <path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg> </svg>
</span> </span>
<input <input
@@ -404,9 +404,9 @@ export default function LoginPage({ onLoginSuccess }) {
) : ( ) : (
<> <>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/> <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7"/> <polyline points="10 17 15 12 10 7" />
<line x1="15" y1="12" x2="3" y2="12"/> <line x1="15" y1="12" x2="3" y2="12" />
</svg> </svg>
Sign In to Dashboard Sign In to Dashboard
</> </>
@@ -424,8 +424,8 @@ export default function LoginPage({ onLoginSuccess }) {
{/* Security badge */} {/* Security badge */}
<div className="security-badge"> <div className="security-badge">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="3" y="11" width="18" height="11" rx="2"/> <rect x="3" y="11" width="18" height="11" rx="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4"/> <path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg> </svg>
256-bit SSL encrypted · Direct API Integration 256-bit SSL encrypted · Direct API Integration
</div> </div>
+15
View File
@@ -138,6 +138,21 @@ export const adminService = {
create: (data) => api.post('/api/admin/announcements', data), create: (data) => api.post('/api/admin/announcements', data),
cancel: (id) => api.post(`/api/admin/announcements/${id}/cancel`), cancel: (id) => api.post(`/api/admin/announcements/${id}/cancel`),
}, },
// ── Hospital & Caregiver Network ──
hospitalNetwork: {
getGroups: () => api.get('/v1/resource/hospitalgroups'),
saveGroup: (data) => data.id ? api.put(`/v1/resource/hospitalgroups/${data.id}`, data) : api.post('/v1/resource/hospitalgroups', data),
deleteGroup: (id) => api.delete(`/v1/resource/hospitalgroups/${id}`),
getHospitals: () => api.get('/v1/resource/hospitals'),
saveHospital: (data) => data.id ? api.put(`/v1/resource/hospitals/${data.id}`, data) : api.post('/v1/resource/hospitals', data),
deleteHospital: (id) => api.delete(`/v1/resource/hospitals/${id}`),
getStaff: () => api.get('/v1/resource/staff'),
saveStaff: (data) => data.id ? api.put(`/v1/resource/staff/${data.id}`, data) : api.post('/v1/resource/staff', data),
deleteStaff: (id) => api.delete(`/v1/resource/staff/${id}`),
},
} }
export default adminService export default adminService