implement TeleEMS platform architecture with centralized API client and master data management system
This commit is contained in:
893
src/pages/HospitalsNetwork.tsx
Normal file
893
src/pages/HospitalsNetwork.tsx
Normal file
@@ -0,0 +1,893 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Hospital,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Plus,
|
||||
Phone,
|
||||
Settings,
|
||||
CheckCircle2,
|
||||
MoreVertical,
|
||||
Shield,
|
||||
User as UserIcon,
|
||||
Navigation2,
|
||||
XCircle,
|
||||
Stethoscope,
|
||||
Lock,
|
||||
Edit2,
|
||||
Trash2,
|
||||
Eye,
|
||||
EyeOff
|
||||
} from 'lucide-react';
|
||||
import { Card } from '../components/Common';
|
||||
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { authApi } from '../api/auth';
|
||||
import { incidentsApi } from '../api/incidents';
|
||||
|
||||
|
||||
|
||||
|
||||
type ViewMode = 'NETWORK_OVERVIEW' | 'HOSPITAL_MGMT' | 'APPROVAL_QUEUE' | 'ANALYTICS';
|
||||
|
||||
export const HospitalsNetwork: React.FC = () => {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('NETWORK_OVERVIEW');
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [realHospitals, setRealHospitals] = useState<any[]>([]);
|
||||
const [incidents, setIncidents] = useState<any[]>([]);
|
||||
const [issues, setIssues] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [editingHospital, setEditingHospital] = useState<any | null>(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const loadIncidents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('teleems_token') || '';
|
||||
if (!token) return;
|
||||
const res = await incidentsApi.getIncidents({ limit: 10 }, token);
|
||||
if (res && res.data) {
|
||||
setIncidents(res.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load incidents:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const loadHospitals = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('teleems_token') || '';
|
||||
if (!token) return;
|
||||
|
||||
const response = await authApi.getUsers(token);
|
||||
|
||||
if (response && (response.status === 401 || response.message?.toLowerCase().includes('expired'))) {
|
||||
localStorage.removeItem('teleems_auth');
|
||||
localStorage.removeItem('teleems_token');
|
||||
localStorage.removeItem('teleems_user');
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
if (response && response.data) {
|
||||
// Robust Hospital Node extraction
|
||||
const filteredAdmins = response.data.filter((u: any) => {
|
||||
const roles = Array.isArray(u.roles) ? u.roles.map((r: any) => String(r).toUpperCase()) : [];
|
||||
return roles.includes('HOSPITAL ADMIN') || roles.includes('HOSPITAL_ADMIN');
|
||||
});
|
||||
|
||||
const hospitalNodes = filteredAdmins.map((u: any) => {
|
||||
const metaHosp = u.metadata?.hospital || {};
|
||||
const metaOrg = u.metadata?.organization || {};
|
||||
|
||||
// Determine activity status
|
||||
const activeInc = incidents.filter(i => i.hospital_id === u.id && i.status !== 'RESOLVED').length;
|
||||
const [available] = (metaHosp.beds || '0/0').split('/').map((n: string) => parseInt(n) || 0);
|
||||
|
||||
let activityStatus = 'IDLE';
|
||||
if (activeInc > 5) activityStatus = 'CRITICAL LOAD';
|
||||
else if (activeInc > 0) activityStatus = `HANDLING ${activeInc} INCIDENTS`;
|
||||
else if (available < 5) activityStatus = 'NEAR CAPACITY';
|
||||
|
||||
return {
|
||||
id: u.id,
|
||||
name: metaHosp.name || metaOrg.company_name || u.name || u.username || 'Unknown Hospital',
|
||||
type: metaHosp.type || metaHosp.specialization || 'Multi-Specialty',
|
||||
beds: metaHosp.beds || '15/60',
|
||||
status: u.status || 'ACTIVE',
|
||||
activity: activityStatus,
|
||||
accreditation: metaHosp.accreditation || 'NABH',
|
||||
admin: u.name || u.username,
|
||||
phone: u.phone || 'Contact Support',
|
||||
email: u.email,
|
||||
city: metaHosp.city || metaOrg.city || 'Chennai',
|
||||
radius: metaHosp.radius || '15km',
|
||||
zones: [metaHosp.city || metaOrg.city || 'Chennai'],
|
||||
rawMetadata: u.metadata,
|
||||
roles: u.roles || []
|
||||
};
|
||||
});
|
||||
|
||||
setRealHospitals(hospitalNodes);
|
||||
|
||||
// Derive current issues
|
||||
const newIssues = hospitalNodes.map(h => {
|
||||
const [available] = (h.beds || '0/0').split('/').map((n: string) => parseInt(n) || 0);
|
||||
if (available === 0) return { type: 'CRITICAL', msg: `${h.name}: Zero bed capacity`, hospital: h.name };
|
||||
if (available < 5) return { type: 'WARNING', msg: `${h.name}: Low bed availability`, hospital: h.name };
|
||||
return null;
|
||||
}).filter(Boolean);
|
||||
|
||||
setIssues(newIssues as any[]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch hospitals:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadHospitals();
|
||||
loadIncidents();
|
||||
const interval = setInterval(() => {
|
||||
loadHospitals();
|
||||
loadIncidents();
|
||||
}, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
|
||||
const hospitalStats = useMemo(() => {
|
||||
return realHospitals.map(h => {
|
||||
const [available, total] = (h.beds || '0/0').split('/').map((n: string) => parseInt(n) || 0);
|
||||
return {
|
||||
name: h.name,
|
||||
total: total || 100,
|
||||
available: available || 0
|
||||
};
|
||||
});
|
||||
}, [realHospitals]);
|
||||
|
||||
const handleHospitalSubmit = async (data: any) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const token = localStorage.getItem('teleems_token') || '';
|
||||
if (!token) {
|
||||
throw new Error('No authentication token found. Please login again.');
|
||||
}
|
||||
|
||||
const result = await authApi.registerUser(data, token);
|
||||
|
||||
|
||||
if (result.error || result.status === 401) {
|
||||
throw new Error(result.error?.message || result.message || 'Unauthorized');
|
||||
}
|
||||
|
||||
console.log('Hospital Registration Success:', result);
|
||||
alert('Hospital registered successfully!');
|
||||
setIsModalOpen(false);
|
||||
loadHospitals();
|
||||
} catch (error: any) {
|
||||
console.error('Registration failed:', error);
|
||||
const isExpired = error.message.includes('expired');
|
||||
alert(`Registration failed: ${error.message}${isExpired ? '. Your session has expired, redirecting to login...' : ''}`);
|
||||
|
||||
if (isExpired) {
|
||||
localStorage.removeItem('teleems_auth');
|
||||
localStorage.removeItem('teleems_token');
|
||||
localStorage.removeItem('teleems_user');
|
||||
navigate('/login');
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusToggle = async (hospital: any) => {
|
||||
try {
|
||||
const newStatus = hospital.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE';
|
||||
const token = localStorage.getItem('teleems_token') || '';
|
||||
|
||||
const payload = {
|
||||
name: hospital.admin,
|
||||
email: hospital.email,
|
||||
phone: hospital.phone || '',
|
||||
status: newStatus,
|
||||
role: 'HOSPITAL_ADMIN',
|
||||
metadata: hospital.rawMetadata
|
||||
};
|
||||
|
||||
const res = await authApi.updateUser(hospital.id, payload, token);
|
||||
if (res.status === 401) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
loadHospitals();
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditHospitalSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!editingHospital) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const token = localStorage.getItem('teleems_token') || '';
|
||||
const payload = {
|
||||
name: editingHospital.admin,
|
||||
email: editingHospital.email,
|
||||
phone: editingHospital.phone || '',
|
||||
status: editingHospital.status,
|
||||
role: 'HOSPITAL_ADMIN',
|
||||
metadata: {
|
||||
...editingHospital.rawMetadata,
|
||||
hospital: {
|
||||
...editingHospital.rawMetadata?.hospital,
|
||||
name: editingHospital.name,
|
||||
city: editingHospital.city
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const res = await authApi.updateUser(editingHospital.id, payload, token);
|
||||
if (res.status === 401) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
setEditingHospital(null);
|
||||
loadHospitals();
|
||||
} catch (error) {
|
||||
console.error('Update failed:', error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const triggerSubmit = () => {
|
||||
const form = document.getElementById('hospital-reg-form') as HTMLFormElement;
|
||||
if (form) form.requestSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page-container" style={{ padding: '0 40px 40px 40px' }}>
|
||||
<header className="network-header-premium" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<h2 style={{ fontSize: '2.5rem', fontWeight: 900, background: 'linear-gradient(90deg, var(--accent-blue), var(--text-primary))', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', letterSpacing: '-1.5px' }}>
|
||||
Hospital Governance
|
||||
</h2>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginTop: '4px', fontWeight: 700, letterSpacing: '0.5px' }}>
|
||||
NETWORK OPERATIONAL CONTROL • {realHospitals.length} ACTIVE NODES
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="glass" style={{ padding: '6px', borderRadius: '12px', display: 'flex', gap: '4px', background: 'rgba(0,0,0,0.03)' }}>
|
||||
<button
|
||||
onClick={() => setViewMode('NETWORK_OVERVIEW')}
|
||||
style={{
|
||||
padding: '10px 20px', borderRadius: '8px', border: 'none',
|
||||
background: viewMode === 'NETWORK_OVERVIEW' ? 'var(--accent-cyan)' : 'transparent',
|
||||
color: viewMode === 'NETWORK_OVERVIEW' ? '#fff' : 'var(--text-secondary)',
|
||||
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
|
||||
}}>
|
||||
<Shield size={16} /> NETWORK OVERVIEW
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('HOSPITAL_MGMT')}
|
||||
style={{
|
||||
padding: '10px 20px', borderRadius: '8px', border: 'none',
|
||||
background: viewMode === 'HOSPITAL_MGMT' ? 'var(--accent-cyan)' : 'transparent',
|
||||
color: viewMode === 'HOSPITAL_MGMT' ? '#fff' : 'var(--text-secondary)',
|
||||
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
|
||||
}}>
|
||||
<Hospital size={16} /> ACCOUNTS
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('APPROVAL_QUEUE')}
|
||||
style={{
|
||||
padding: '10px 20px', borderRadius: '8px', border: 'none',
|
||||
background: viewMode === 'APPROVAL_QUEUE' ? 'var(--accent-cyan)' : 'transparent',
|
||||
color: viewMode === 'APPROVAL_QUEUE' ? '#fff' : 'var(--text-secondary)',
|
||||
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
|
||||
}}>
|
||||
<CheckCircle2 size={16} /> APPROVALS <span style={{ background: 'var(--alert-red)', color: '#fff', fontSize: '0.6rem', padding: '2px 6px', borderRadius: '10px' }}>2</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('ANALYTICS')}
|
||||
style={{
|
||||
padding: '10px 20px', borderRadius: '8px', border: 'none',
|
||||
background: viewMode === 'ANALYTICS' ? 'var(--accent-cyan)' : 'transparent',
|
||||
color: viewMode === 'ANALYTICS' ? '#fff' : 'var(--text-secondary)',
|
||||
fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8rem'
|
||||
}}>
|
||||
<Stethoscope size={16} /> REPORTS
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{viewMode === 'NETWORK_OVERVIEW' && (
|
||||
<motion.div key="overview" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
||||
|
||||
{/* NETWORK PULSE STRIP */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '20px' }}>
|
||||
<Card style={{ padding: '20px', background: 'rgba(59, 130, 246, 0.03)', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', textTransform: 'uppercase', fontWeight: 800 }}>Live Incidents</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: '10px', marginTop: '8px' }}>
|
||||
<div style={{ fontSize: '2.5rem', fontWeight: 900, color: 'var(--accent-cyan)' }}>{incidents.filter(i => i.status !== 'RESOLVED').length}</div>
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--accent-green)', fontWeight: 700 }}>ACTIVE</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{ padding: '20px', background: 'rgba(255, 82, 82, 0.03)', border: '1px solid rgba(255, 82, 82, 0.2)' }}>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', textTransform: 'uppercase', fontWeight: 800 }}>Critical Issues</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: '10px', marginTop: '8px' }}>
|
||||
<div style={{ fontSize: '2.5rem', fontWeight: 900, color: 'var(--alert-red)' }}>{issues.filter(i => i.type === 'CRITICAL').length}</div>
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--alert-red)', fontWeight: 700 }}>ALERTS</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{ padding: '20px' }}>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', textTransform: 'uppercase', fontWeight: 800 }}>Network Sync</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: '10px', marginTop: '8px' }}>
|
||||
<div style={{ fontSize: '2.5rem', fontWeight: 900, color: 'var(--text-primary)' }}>98%</div>
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--accent-cyan)', fontWeight: 700 }}>STABLE</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{ padding: '20px' }}>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', textTransform: 'uppercase', fontWeight: 800 }}>System Health</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginTop: '12px' }}>
|
||||
<div style={{ width: '12px', height: '12px', borderRadius: '50%', background: 'var(--accent-green)', boxShadow: '0 0 10px var(--accent-green)' }}></div>
|
||||
<div style={{ fontSize: '1.2rem', fontWeight: 800 }}>OPTIMAL</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 2fr) 1fr', gap: '24px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h3 style={{ fontSize: '1.2rem', fontWeight: 800, display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<Hospital size={20} color="var(--accent-cyan)" /> HOSPITAL NODES
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '20px' }}>
|
||||
{realHospitals.length === 0 && !isLoading && (
|
||||
<div style={{ gridColumn: '1/-1', textAlign: 'center', padding: '40px', color: 'var(--text-secondary)' }}>
|
||||
No hospital nodes registered in network.
|
||||
</div>
|
||||
)}
|
||||
{realHospitals.map((h) => (
|
||||
<Card key={h.id} className="hover-glow" style={{ padding: '20px', border: issues.some(i => i.hospital === h.name && i.type === 'CRITICAL') ? '1px solid var(--alert-red)' : '1px solid var(--card-border)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ width: '44px', height: '44px', background: 'rgba(59, 130, 246, 0.1)', borderRadius: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Hospital size={22} color="var(--accent-cyan)" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '4px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.65rem', fontWeight: 800, color: h.status === 'ACTIVE' ? 'var(--accent-green)' : 'var(--alert-red)', textTransform: 'uppercase' }}>
|
||||
<div style={{ width: '6px', height: '6px', borderRadius: '50%', background: h.status === 'ACTIVE' ? 'var(--accent-green)' : 'var(--alert-red)' }} />
|
||||
{h.status}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', fontWeight: 700 }}>{h.activity}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<div style={{ fontSize: '1.2rem', fontWeight: 800 }}>{h.name}</div>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
|
||||
<span style={{ fontSize: '0.65rem', color: 'var(--accent-cyan)', fontWeight: 700 }}>{h.type}</span>
|
||||
<span style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>• {h.city}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '24px', borderTop: '1px solid var(--card-border)', paddingTop: '16px' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Available Beds</div>
|
||||
<div className="mono" style={{ fontSize: '1rem', fontWeight: 800, color: h.beds.startsWith('0') ? 'var(--alert-red)' : 'var(--accent-green)' }}>{h.beds}</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Active Cases</div>
|
||||
<div className="mono" style={{ fontSize: '1rem', fontWeight: 800, color: 'var(--text-primary)' }}>{incidents.filter(i => i.hospital_id === h.id && i.status !== 'RESOLVED').length}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
||||
<Card title="CRITICAL ISSUES" subtitle="Real-time network alerts" style={{ border: '1px solid rgba(255, 82, 82, 0.3)' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '15px' }}>
|
||||
{issues.length === 0 ? (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: 'var(--text-secondary)', fontSize: '0.8rem' }}>No critical issues detected.</div>
|
||||
) : (
|
||||
issues.map((issue, idx) => (
|
||||
<div key={idx} style={{ padding: '12px', background: issue.type === 'CRITICAL' ? 'rgba(255, 82, 82, 0.1)' : 'rgba(255, 183, 77, 0.1)', borderLeft: `4px solid ${issue.type === 'CRITICAL' ? 'var(--alert-red)' : 'var(--warning-amber)'}`, borderRadius: '4px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
||||
<AlertCircle size={14} color={issue.type === 'CRITICAL' ? 'var(--alert-red)' : 'var(--warning-amber)'} />
|
||||
<span style={{ fontSize: '0.75rem', fontWeight: 800, color: issue.type === 'CRITICAL' ? 'var(--alert-red)' : 'var(--warning-amber)' }}>{issue.type}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', fontWeight: 600 }}>{issue.msg}</div>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', marginTop: '4px' }}>Identified 2m ago</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="LIVE ACTIVITY" subtitle="Latest incidents across network">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px', marginTop: '15px' }}>
|
||||
{incidents.length === 0 ? (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: 'var(--text-secondary)', fontSize: '0.8rem' }}>Monitoring network traffic...</div>
|
||||
) : (
|
||||
incidents.slice(0, 5).map((inc, i) => (
|
||||
<div key={i} style={{ paddingBottom: '12px', borderBottom: '1px solid var(--card-border)', display: 'flex', gap: '12px' }}>
|
||||
<div style={{ width: '32px', height: '32px', background: 'rgba(0,0,0,0.03)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<Navigation2 size={16} color="var(--accent-cyan)" />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{inc.patient_name || 'Emergency Call'}</div>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)' }}>{inc.status.toUpperCase()} • {inc.priority || 'P1'}</div>
|
||||
<div style={{ fontSize: '0.65rem', color: 'var(--accent-cyan)', marginTop: '4px' }}>{new Date(inc.created_at).toLocaleTimeString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{incidents.length > 5 && (
|
||||
<button style={{ width: '100%', padding: '10px', background: 'transparent', border: 'none', color: 'var(--accent-cyan)', fontSize: '0.75rem', fontWeight: 700, cursor: 'pointer', marginTop: '10px' }}>VIEW ALL ACTIVITY</button>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
|
||||
<Card title="Regional Bed Capacity" subtitle="Real-time availability by hospital node">
|
||||
<div style={{ height: '300px', marginTop: '20px' }}>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={hospitalStats} layout="vertical">
|
||||
<XAxis type="number" hide />
|
||||
<YAxis dataKey="name" type="category" width={100} stroke="var(--text-secondary)" fontSize={11} tick={{fontWeight: 600}} />
|
||||
<Tooltip cursor={{fill: 'rgba(0,0,0,0.02)'}} contentStyle={{ background: 'var(--base-bg)', border: '1px solid var(--card-border)', borderRadius: '8px' }} />
|
||||
<Bar dataKey="total" fill="rgba(0,0,0,0.03)" radius={[0, 4, 4, 0]} barSize={20} />
|
||||
<Bar dataKey="available" fill="var(--accent-cyan)" radius={[0, 4, 4, 0]} barSize={20} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="HMIS Data Exchange Health" subtitle="Integration status of hospital information systems">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{[
|
||||
{ name: 'Apollo Main', api: 'REST/FHIR', status: 'Healthy', latency: '45ms', lastSync: '12s ago' },
|
||||
{ name: 'MGM Healthcare', api: 'REST/FHIR', status: 'Healthy', latency: '38ms', lastSync: '5s ago' },
|
||||
{ name: 'MIOT Int.', api: 'REST/FHIR', status: 'Healthy', latency: '42ms', lastSync: '8s ago' },
|
||||
{ name: 'Global Health', api: 'HL7v2', status: 'Syncing', latency: '120ms', lastSync: '1m ago' },
|
||||
{ name: 'Stanley Medical', api: 'Direct SQL', status: 'Critical', latency: '--', lastSync: '14h ago' },
|
||||
].map((item, i) => (
|
||||
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px', background: 'rgba(0,0,0,0.02)', borderRadius: '10px', border: '1px solid var(--card-border)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: item.status === 'Healthy' ? 'var(--accent-green)' : (item.status === 'Syncing' ? 'var(--warning-amber)' : 'var(--alert-red)') }} />
|
||||
<div>
|
||||
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{item.name}</div>
|
||||
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>{item.api} Protocol</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div className="mono" style={{ fontSize: '0.75rem', color: 'var(--accent-cyan)' }}>{item.latency}</div>
|
||||
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>{item.lastSync}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{viewMode === 'HOSPITAL_MGMT' && (
|
||||
<HospitalManagement
|
||||
key="management"
|
||||
hospitals={realHospitals}
|
||||
onRegister={() => setIsModalOpen(true)}
|
||||
onEdit={(h) => setEditingHospital(h)}
|
||||
onToggleStatus={handleStatusToggle}
|
||||
/>
|
||||
)}
|
||||
|
||||
{viewMode === 'APPROVAL_QUEUE' && (
|
||||
<motion.div key="approvals" initial={{ opacity: 0 }} animate={{ opacity: 1 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
||||
<Card title="Pending Hospital Registrations" subtitle="Review and approve new hospital node requests.">
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '20px' }}>
|
||||
<thead>
|
||||
<tr style={{ textAlign: 'left', borderBottom: '1px solid var(--card-border)' }}>
|
||||
<th style={{ padding: '16px' }}>Request Details</th>
|
||||
<th style={{ padding: '16px' }}>Admin Info</th>
|
||||
<th style={{ padding: '16px' }}>Submitted</th>
|
||||
<th style={{ padding: '16px', textAlign: 'right' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
{ id: 'REQ-9901', name: 'Global Health City', city: 'Chennai', admin: 'Dr. S. Karthik', email: 'sk@globalhealth.com', date: '2026-04-16' },
|
||||
{ id: 'REQ-9905', name: 'Fortis Malar', city: 'Chennai', admin: 'Pavitra M.', email: 'p.malar@fortis.com', date: '2026-04-15' }
|
||||
].map(req => (
|
||||
<tr key={req.id} style={{ borderBottom: '1px solid rgba(0,0,0,0.02)' }}>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<div style={{ fontWeight: 700 }}>{req.name}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{req.city} Node</div>
|
||||
</td>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<div style={{ fontSize: '0.85rem' }}>{req.admin}</div>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--accent-cyan)' }}>{req.email}</div>
|
||||
</td>
|
||||
<td style={{ padding: '16px', fontSize: '0.85rem' }}>{req.date}</td>
|
||||
<td style={{ padding: '16px', textAlign: 'right' }}>
|
||||
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
|
||||
<button style={{ padding: '8px 16px', background: 'var(--accent-green)', color: '#fff', border: 'none', borderRadius: '4px', fontWeight: 700, cursor: 'pointer' }}>APPROVE</button>
|
||||
<button style={{ padding: '8px 16px', background: 'rgba(0,0,0,0.02)', color: 'var(--alert-red)', border: '1px solid var(--alert-red)', borderRadius: '4px', fontWeight: 700, cursor: 'pointer' }}>REJECT</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{viewMode === 'ANALYTICS' && (
|
||||
<motion.div key="analytics" initial={{ opacity: 0 }} animate={{ opacity: 1 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '24px' }}>
|
||||
<Card title="Total Incidents Handled" subtitle="Last 30 days total volume">
|
||||
<div style={{ fontSize: '3rem', fontWeight: 900, color: 'var(--accent-cyan)', margin: '20px 0' }}>1,248</div>
|
||||
<div style={{ color: 'var(--accent-green)', fontWeight: 700 }}>↑ 14% vs last month</div>
|
||||
</Card>
|
||||
<Card title="Avg Handover Time" subtitle="Ambulance arrival to ED handoff">
|
||||
<div style={{ fontSize: '3rem', fontWeight: 900, color: 'var(--text-primary)', margin: '20px 0' }}>12.4m</div>
|
||||
<div style={{ color: 'var(--accent-green)', fontWeight: 700 }}>-1.2m improvement</div>
|
||||
</Card>
|
||||
<Card title="Clinical Escalation Rate" subtitle="TeleLink sessions requiring specialist">
|
||||
<div style={{ fontSize: '3rem', fontWeight: 900, color: 'var(--alert-red)', margin: '20px 0' }}>8.2%</div>
|
||||
<div style={{ color: 'var(--text-secondary)', fontWeight: 700 }}>Target: < 10%</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card title="Network Volume Trends">
|
||||
<div style={{ height: '300px', display: 'flex', alignItems: 'flex-end', gap: '10px', padding: '40px 0' }}>
|
||||
{[40, 65, 52, 88, 70, 95, 82, 60, 75, 90, 110, 85].map((h, i) => (
|
||||
<div key={i} style={{ flex: 1, background: 'var(--accent-cyan)', height: `${h}%`, borderRadius: '4px 4px 0 0', opacity: 0.6 + (h/200), position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', top: '-25px', left: '50%', transform: 'translateX(-50%)', fontSize: '0.6rem', fontWeight: 900 }}>{h}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.7rem', color: 'var(--text-secondary)', marginTop: '10px' }}>
|
||||
<span>APR 01</span>
|
||||
<span>APR 07</span>
|
||||
<span>APR 14</span>
|
||||
<span>APR 21</span>
|
||||
<span>APR 28</span>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
title="Register New Hospital"
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onSubmit={triggerSubmit}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<HospitalRegistrationForm onSubmit={handleHospitalSubmit} loading={isSubmitting} />
|
||||
</Modal>
|
||||
|
||||
{/* EDIT HOSPITAL MODAL */}
|
||||
<Modal
|
||||
isOpen={!!editingHospital}
|
||||
title={`Edit ${editingHospital?.name}`}
|
||||
onClose={() => setEditingHospital(null)}
|
||||
onSubmit={() => {
|
||||
const form = document.getElementById('hospital-edit-form') as HTMLFormElement;
|
||||
if (form) form.requestSubmit();
|
||||
}}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{editingHospital && (
|
||||
<form id="hospital-edit-form" onSubmit={handleEditHospitalSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 700 }}>HOSPITAL FULL NAME</label>
|
||||
<input name="name" type="text" required value={editingHospital.name} onChange={(e) => setEditingHospital({...editingHospital, name: e.target.value})} style={{ width: '100%', padding: '12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 700 }}>PRIMARY CITY</label>
|
||||
<input name="city" type="text" required value={editingHospital.city} onChange={(e) => setEditingHospital({...editingHospital, city: e.target.value})} style={{ width: '100%', padding: '12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: '20px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 700 }}>ADMINISTRATOR NAME</label>
|
||||
<input name="admin" type="text" required value={editingHospital.admin} onChange={(e) => setEditingHospital({...editingHospital, admin: e.target.value})} style={{ width: '100%', padding: '12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 700 }}>CONTACT PHONE</label>
|
||||
<input name="phone" type="text" required value={editingHospital.phone} onChange={(e) => setEditingHospital({...editingHospital, phone: e.target.value})} style={{ width: '100%', padding: '12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.7rem', color: 'var(--accent-cyan)', marginBottom: '8px', fontWeight: 700 }}>OFFICIAL EMAIL</label>
|
||||
<input name="email" type="email" required value={editingHospital.email} onChange={(e) => setEditingHospital({...editingHospital, email: e.target.value})} style={{ width: '100%', padding: '12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '8px', color: 'var(--text-primary)' }} />
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 5.3 Hospital Management (CRUD Sub-page)
|
||||
const HospitalManagement: React.FC<{
|
||||
hospitals: any[];
|
||||
onRegister: () => void;
|
||||
onEdit: (h: any) => void;
|
||||
onToggleStatus: (h: any) => void;
|
||||
}> = ({ hospitals, onRegister, onEdit, onToggleStatus }) => {
|
||||
const filteredHospitals = hospitals; // realHospitals already filtered in loadHospitals for Admine role
|
||||
|
||||
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h3 style={{ fontSize: '1.25rem', display: 'flex', alignItems: 'center', gap: '12px', fontWeight: 700 }}>
|
||||
<Settings size={24} color="var(--accent-cyan)" /> Hospital Account Management
|
||||
</h3>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button onClick={onRegister} className="glass" style={{ padding: '10px 20px', background: 'var(--accent-cyan)', color: '#fff', border: 'none', borderRadius: '6px', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<Plus size={18} /> REGISTER NEW HOSPITAL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'rgba(0,0,0,0.02)', textAlign: 'left' }}>
|
||||
<th style={{ padding: '16px' }}>Hospital Details</th>
|
||||
<th style={{ padding: '16px' }}>Leadership & Contact</th>
|
||||
<th style={{ padding: '16px' }}>Service Area</th>
|
||||
<th style={{ padding: '16px' }}>Accreditation</th>
|
||||
<th style={{ padding: '16px' }}>Status</th>
|
||||
<th style={{ padding: '16px', textAlign: 'right' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredHospitals.map((h, i) => (
|
||||
<tr key={i} style={{ borderBottom: '1px solid rgba(0,0,0,0.02)' }}>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<div style={{ fontWeight: 800 }}>{h.name}</div>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--accent-cyan)', marginTop: '2px', fontWeight: 700 }}>{h.type.toUpperCase()}</div>
|
||||
</td>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<UserIcon size={14} color="var(--text-secondary)" />
|
||||
<span>{h.admin}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '4px', fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
|
||||
<Phone size={14} color="var(--text-secondary)" />
|
||||
<span>{h.phone}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Navigation2 size={14} color="var(--accent-cyan)" />
|
||||
<span className="mono" style={{ fontWeight: 700 }}>{h.radius}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', marginTop: '4px' }}>
|
||||
{h.zones.slice(0, 2).join(', ')}{h.zones.length > 2 ? '...' : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<span style={{ fontSize: '0.7rem', padding: '3px 8px', background: 'rgba(0,0,0,0.02)', borderRadius: '4px', border: '1px solid var(--card-border)' }}>{h.acc}</span>
|
||||
</td>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: h.status === 'ACTIVE' ? 'var(--accent-green)' : 'var(--alert-red)', fontWeight: 800 }}>
|
||||
{h.status === 'ACTIVE' ? <CheckCircle2 size={14} /> : <XCircle size={14} />}
|
||||
{h.status}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '16px', textAlign: 'right' }}>
|
||||
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
onClick={() => onEdit(h)}
|
||||
style={{ background: 'transparent', border: 'none', color: 'var(--accent-cyan)', cursor: 'pointer' }}
|
||||
>
|
||||
<Edit2 size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onToggleStatus(h)}
|
||||
style={{
|
||||
padding: '6px 12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)',
|
||||
borderRadius: '4px', fontSize: '0.65rem', color: h.status === 'ACTIVE' ? 'var(--alert-red)' : 'var(--accent-green)',
|
||||
fontWeight: 800, cursor: 'pointer'
|
||||
}}>
|
||||
{h.status === 'ACTIVE' ? 'DEACTIVATE' : 'ACTIVATE'}
|
||||
</button>
|
||||
<button style={{ background: 'transparent', border: 'none', color: 'var(--alert-red)', cursor: 'pointer' }}>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(400px, 1fr) 400px', gap: '24px' }}>
|
||||
<Card title="Teleconsult Routing Rules">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{[
|
||||
{ trigger: 'Major Cardiac (Red)', target: 'Cath Lab / Cardiac Centre', priority: 'P0' },
|
||||
{ trigger: 'Severe Burns (Red)', target: 'Burns Specialty Unit', priority: 'P0' },
|
||||
{ trigger: 'Pediatric Emergency', target: 'Pediatric ED Node', priority: 'P1' },
|
||||
].map((rule, i) => (
|
||||
<div key={i} style={{ padding: '16px', background: 'rgba(0,0,0,0.01)', border: '1px solid var(--card-border)', borderRadius: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Stethoscope size={20} color="var(--accent-cyan)" />
|
||||
<div>
|
||||
<div style={{ fontWeight: 700 }}>{rule.trigger}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Route to: {rule.target}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mono" style={{ padding: '4px 8px', background: 'rgba(59, 130, 246, 0.1)', color: 'var(--accent-cyan)', fontWeight: 800, borderRadius: '4px' }}>{rule.priority}</div>
|
||||
</div>
|
||||
))}
|
||||
<button style={{ padding: '12px', background: 'transparent', border: '1px dashed var(--card-border)', color: 'var(--text-secondary)', borderRadius: '10px', fontSize: '0.8rem', cursor: 'pointer' }}>
|
||||
+ DEFINE NEW ROUTING RULE
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Accreditation Compliance">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '0.85rem' }}>NABH Verification</span>
|
||||
<div style={{ color: 'var(--accent-green)', fontWeight: 700, fontSize: '0.75rem' }}>VALID</div>
|
||||
</div>
|
||||
<div style={{ width: '100%', height: '4px', background: 'rgba(0,0,0,0.02)', borderRadius: '2px' }}>
|
||||
<div style={{ width: '92%', height: '100%', background: 'var(--accent-green)', borderRadius: '2px' }}></div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '8px' }}>
|
||||
<span style={{ fontSize: '0.85rem' }}>Quality Assurance Audit</span>
|
||||
<div style={{ color: 'var(--warning-amber)', fontWeight: 700, fontSize: '0.75rem' }}>PENDING</div>
|
||||
</div>
|
||||
<p style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', lineHeight: '1.5' }}>
|
||||
Automatic compliance checks run every 30 days. Last verification was successful for 88% of the hospital network.
|
||||
</p>
|
||||
<button style={{ marginTop: '10px', padding: '10px', background: 'rgba(59, 130, 246, 0.1)', border: '1px solid var(--accent-cyan)', color: 'var(--accent-cyan)', borderRadius: '8px', fontSize: '0.8rem', fontWeight: 700, cursor: 'pointer' }}>
|
||||
RUN FULL COMPLIANCE SYNC
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- COMPONENTS ---
|
||||
|
||||
const Modal: React.FC<{ isOpen: boolean; title: string; onClose: () => void; children: React.ReactNode; onSubmit?: () => void; loading?: boolean }> = ({ isOpen, title, onClose, children, onSubmit, loading }) => {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div className="modal-overlay" style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(255,255,255,0.85)', backdropFilter: 'blur(8px)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '40px' }}>
|
||||
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className="glass" style={{ width: '100%', maxWidth: '800px', maxHeight: '90vh', overflowY: 'auto', background: 'var(--card-bg)', padding: '32px', borderRadius: '20px', border: '1px solid var(--accent-cyan)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
|
||||
<h3 style={{ fontSize: '1.5rem', fontWeight: 800 }}>{title}</h3>
|
||||
<button onClick={onClose} style={{ background: 'transparent', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer' }}><XCircle size={24} /></button>
|
||||
</div>
|
||||
{children}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '16px', marginTop: '32px' }}>
|
||||
<button onClick={onClose} style={{ padding: '10px 20px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', color: 'var(--text-primary)', borderRadius: '6px', fontWeight: 700, cursor: 'pointer' }} disabled={loading}>CANCEL</button>
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
className="btn-primary"
|
||||
disabled={loading}
|
||||
style={{ padding: '10px 20px', background: 'var(--accent-cyan)', border: 'none', color: '#fff', borderRadius: '6px', fontWeight: 700, cursor: 'pointer', opacity: loading ? 0.7 : 1 }}
|
||||
>
|
||||
{loading ? 'PROCESSING...' : 'REGISTER HOSPITAL'}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const HospitalRegistrationForm: React.FC<{ onSubmit: (data: any) => void }> = ({ onSubmit }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
admin_name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
hospital_name: '',
|
||||
city: '',
|
||||
lat: '13.0827',
|
||||
lon: '80.2707'
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleFormSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const payload = {
|
||||
role: "HOSPITAL_ADMIN",
|
||||
name: formData.admin_name,
|
||||
phone: formData.phone,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
metadata: {
|
||||
hospital: {
|
||||
name: formData.hospital_name,
|
||||
city: formData.city,
|
||||
lat: parseFloat(formData.lat),
|
||||
lon: parseFloat(formData.lon)
|
||||
}
|
||||
}
|
||||
};
|
||||
onSubmit(payload);
|
||||
};
|
||||
|
||||
return (
|
||||
<form id="hospital-reg-form" onSubmit={handleFormSubmit} style={{ display: 'flex', flexWrap: 'wrap', gap: '20px' }}>
|
||||
<h4 style={{ width: '100%', color: 'var(--accent-cyan)', borderBottom: '1px solid rgba(59, 130, 246, 0.1)', paddingBottom: '8px' }}>ADMINISTRATOR DETAILS</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
|
||||
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Full Name</label>
|
||||
<input name="admin_name" value={formData.admin_name} onChange={handleChange} required placeholder="Dr. Administrator Name" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
|
||||
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Email Address</label>
|
||||
<input name="email" value={formData.email} onChange={handleChange} required type="email" placeholder="hospital@example.com" autoComplete="new-password" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
|
||||
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Phone Number</label>
|
||||
<input name="phone" value={formData.phone} onChange={handleChange} required placeholder="9876543210" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
|
||||
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Access Password</label>
|
||||
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
|
||||
<input name="password" value={formData.password} onChange={handleChange} required type={showPassword ? "text" : "password"} autoComplete="new-password" placeholder="••••••••" className="glass" style={{ padding: '10px 14px', paddingLeft: '36px', paddingRight: '40px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem', width: '100%' }} />
|
||||
<Lock size={14} style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-secondary)' }} />
|
||||
<button type="button" onClick={() => setShowPassword(!showPassword)} style={{ position: 'absolute', right: '14px', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer', padding: 0, display: 'flex' }}>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style={{ width: '100%', color: 'var(--accent-cyan)', borderBottom: '1px solid rgba(59, 130, 246, 0.1)', paddingBottom: '8px', marginTop: '10px' }}>HOSPITAL PROFILE</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 100%' }}>
|
||||
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Hospital Name</label>
|
||||
<input name="hospital_name" value={formData.hospital_name} onChange={handleChange} required placeholder="Apollo Hospital Chennai" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
|
||||
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>City</label>
|
||||
<input name="city" value={formData.city} onChange={handleChange} required placeholder="Chennai" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
|
||||
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Latitude</label>
|
||||
<input name="lat" value={formData.lat} onChange={handleChange} required type="number" step="0.0001" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: '1 1 200px' }}>
|
||||
<label style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' }}>Longitude</label>
|
||||
<input name="lon" value={formData.lon} onChange={handleChange} required type="number" step="0.0001" className="glass" style={{ padding: '10px 14px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--card-border)', borderRadius: '6px', color: 'var(--text-primary)', outline: 'none', fontSize: '0.85rem' }} />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user