Files
TeleEms-Dashboard/src/pages/fleet/FleetPersonnel.tsx
2026-05-06 17:09:54 +05:30

434 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useCallback } from 'react';
import { UserPlus, Search, ShieldCheck, ChevronLeft, ChevronRight, Loader2, AlertCircle, Phone, Mail, X, Eye, EyeOff } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
interface Staff {
id: string; type?: string; status?: string; createdAt?: string; aadhaar_number?: string;
professional_details?: { qualification?: string; certificate_expiry?: string; certificate_number?: string; certification_body?: string; license_expiry?: string; license_number?: string; license_category?: string; };
user?: { name?: string; phone?: string; email?: string | null; status?: string; isAvailable?: boolean; };
}
const ROLE_COLORS: Record<string, { color: string; bg: string }> = {
DRIVER: { color: '#06B6D4', bg: 'rgba(6,182,212,0.12)' },
EMT: { color: '#3B82F6', bg: 'rgba(59,130,246,0.12)' },
DOCTOR: { color: '#10B981', bg: 'rgba(16,185,129,0.12)' },
};
const DEF = { color: '#94A3B8', bg: 'rgba(148,163,184,0.1)' };
const STATUS_CFG: Record<string, { label: string; color: string }> = {
ON_DUTY: { label: 'On Duty', color: '#10B981' }, OFF_DUTY: { label: 'Off Duty', color: '#64748B' },
ON_LEAVE: { label: 'On Leave', color: '#F59E0B' }, ACTIVE: { label: 'Active', color: '#10B981' }, INACTIVE: { label: 'Inactive', color: '#EF4444' },
};
const FILTERS = ['ALL', 'DRIVER', 'EMT', 'DOCTOR'] as const;
const PAGE_SIZE = 5;
const th: React.CSSProperties = { padding: '14px 18px', fontSize: '0.68rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, textAlign: 'left', borderBottom: '1px solid rgba(255,255,255,0.06)', background: 'rgba(255,255,255,0.02)', whiteSpace: 'nowrap' };
const norm = (s: Staff) => {
const pd = s.professional_details;
return {
id: s.id, name: s.user?.name || '—', role: (s.type || '').toUpperCase(),
status: (s.status || s.user?.status || 'ACTIVE').toUpperCase(),
phone: s.user?.phone || '—', email: s.user?.email || null,
joinedDate: s.createdAt ? s.createdAt.substring(0, 10) : '',
specialization: pd?.qualification || pd?.license_category || pd?.certification_body || '',
certExpiry: pd?.certificate_expiry || pd?.license_expiry || '',
isAvailable: s.user?.isAvailable ?? true,
};
};
export const FleetPersonnel: React.FC = () => {
const [staffList, setStaffList] = useState<Staff[]>([]);
const [loading, setLoading] = useState(false);
const [fetchError, setFetchError] = useState('');
const [filter, setFilter] = useState<typeof FILTERS[number]>('ALL');
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [selectedRaw, setSelectedRaw] = useState<Staff | null>(null);
const [showModal, setShowModal] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [submitErr, setSubmitErr] = useState('');
const [submitOk, setSubmitOk] = useState('');
const [showPw, setShowPw] = useState(false);
const [staffType, setStaffType] = useState<'DRIVER'|'EMT'|'DOCTOR'>('DRIVER');
const [form, setForm] = useState({ name:'', phone:'', password:'', aadhaar_number:'',
license_number:'', license_category:'', license_expiry:'',
qualification:'', certification_body:'', certificate_number:'', certificate_expiry:'',
medical_registration_number:'', specialization:'', teleconsult_available: false,
});
const setF = (k: string, v: string | boolean) => setForm(p => ({ ...p, [k]: v }));
const resetModal = () => { setForm({ name:'', phone:'', password:'', aadhaar_number:'', license_number:'', license_category:'', license_expiry:'', qualification:'', certification_body:'', certificate_number:'', certificate_expiry:'', medical_registration_number:'', specialization:'', teleconsult_available: false }); setStaffType('DRIVER'); setSubmitErr(''); setSubmitOk(''); };
const token = localStorage.getItem('teleems_token') || '';
const fetchStaff = useCallback(async () => {
setLoading(true); setFetchError('');
try {
const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/staff', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } });
const json = await res.json();
if (res.status === 401 || res.status === 403) { setFetchError('Session expired.'); return; }
if (!res.ok) { setFetchError(json?.message || `Error ${res.status}`); return; }
let list: Staff[] = [];
if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data;
else if (json?.data && Array.isArray(json.data)) list = json.data;
else if (Array.isArray(json)) list = json;
setStaffList(list);
} catch (e) { setFetchError('Network error.'); } finally { setLoading(false); }
}, [token]);
const submitStaff = async () => {
if (!form.name || !form.phone) { setSubmitErr('Name and phone are required.'); return; }
setSubmitting(true); setSubmitErr(''); setSubmitOk('');
let professional_details: Record<string, string | boolean> = {};
if (staffType === 'DRIVER') professional_details = { license_number: form.license_number, license_category: form.license_category, license_expiry: form.license_expiry };
if (staffType === 'EMT') professional_details = { qualification: form.qualification, certification_body: form.certification_body, certificate_number: form.certificate_number, certificate_expiry: form.certificate_expiry };
if (staffType === 'DOCTOR') professional_details = { medical_registration_number: form.medical_registration_number, specialization: form.specialization, qualification: form.qualification, teleconsult_available: form.teleconsult_available };
const body = { name: form.name, phone: form.phone, password: form.password, type: staffType, aadhaar_number: form.aadhaar_number, professional_details };
try {
const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/staff', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const json = await res.json();
if (!res.ok) { setSubmitErr(json?.message || `Error ${res.status}`); return; }
setSubmitOk(`${staffType} registered successfully!`);
resetModal();
fetchStaff();
setTimeout(() => { setShowModal(false); setSubmitOk(''); }, 1500);
} catch { setSubmitErr('Network error.'); } finally { setSubmitting(false); }
};
useEffect(() => { fetchStaff(); }, [fetchStaff]);
const normalized = staffList.map(norm);
const filtered = normalized.filter(s => (filter === 'ALL' || s.role === filter) && (s.name.toLowerCase().includes(search.toLowerCase()) || s.role.toLowerCase().includes(search.toLowerCase())));
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
const safePage = Math.min(page, totalPages);
const pageData = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);
const onDuty = normalized.filter(s => ['ON_DUTY', 'ACTIVE'].includes(s.status)).length;
const offDuty = normalized.filter(s => s.status === 'OFF_DUTY').length;
const onLeave = normalized.filter(s => s.status === 'ON_LEAVE').length;
// ── DETAIL PAGE ──
if (selectedRaw) {
const s = norm(selectedRaw);
const pd = selectedRaw.professional_details;
const rc = ROLE_COLORS[s.role] || DEF;
const sc = STATUS_CFG[s.status] || { label: s.status, color: '#94A3B8' };
const ini = s.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
const cw = s.certExpiry ? new Date(s.certExpiry) < new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) : false;
const row = (label: string, value: string, mono = false) => (
<div style={{ padding: '14px 16px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: '12px' }}>
<div style={{ fontSize: '0.62rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '5px' }}>{label}</div>
<div style={{ fontSize: '0.9rem', color: '#E2E8F0', fontWeight: 600, fontFamily: mono ? 'monospace' : undefined, wordBreak: 'break-all' }}>{value || '—'}</div>
</div>
);
return (
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} style={{ color: '#F8FAFC' }}>
<button onClick={() => setSelectedRaw(null)} style={{ display: 'flex', alignItems: 'center', gap: '8px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '10px', padding: '9px 16px', color: '#94A3B8', cursor: 'pointer', fontSize: '0.82rem', fontWeight: 600, marginBottom: '24px' }}>
<ChevronLeft size={16} /> Back to Staff List
</button>
{/* Hero */}
<div style={{ background: `linear-gradient(135deg,${rc.color}18,rgba(255,255,255,0.02))`, border: `1px solid ${rc.color}30`, borderRadius: '20px', padding: '32px', marginBottom: '20px', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: -40, right: -40, width: 180, height: 180, borderRadius: '50%', background: `${rc.color}10` }} />
<div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}>
<div style={{ width: 80, height: 80, borderRadius: 22, background: rc.bg, border: `2px solid ${rc.color}60`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '1.6rem', fontWeight: 900, color: rc.color, boxShadow: `0 0 28px ${rc.color}30`, flexShrink: 0 }}>{ini}</div>
<div>
<h1 style={{ fontSize: '1.6rem', fontWeight: 900, color: '#fff', margin: '0 0 10px' }}>{s.name}</h1>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<span style={{ padding: '4px 12px', borderRadius: 7, fontSize: '0.72rem', fontWeight: 800, background: rc.bg, color: rc.color, border: `1px solid ${rc.color}40` }}>{s.role}</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 5, padding: '4px 12px', borderRadius: 7, fontSize: '0.72rem', fontWeight: 700, background: `${sc.color}18`, color: sc.color }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: sc.color, display: 'inline-block', boxShadow: `0 0 6px ${sc.color}` }} />{sc.label}
</span>
{s.isAvailable && <span style={{ padding: '4px 12px', borderRadius: 7, fontSize: '0.72rem', fontWeight: 700, background: 'rgba(16,185,129,0.12)', color: '#10B981', border: '1px solid rgba(16,185,129,0.2)' }}> Available</span>}
</div>
</div>
</div>
</div>
{/* Grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
{/* Contact */}
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 16, padding: 22 }}>
<div style={{ fontSize: '0.65rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 14 }}>📞 Contact</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 10 }}>
<Phone size={16} color="#06B6D4" />
<div><div style={{ fontSize: '0.62rem', color: '#64748B', marginBottom: 2 }}>Phone</div><div style={{ fontSize: '0.9rem', color: '#E2E8F0', fontWeight: 600 }}>{s.phone}</div></div>
</div>
{s.email && (
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 10 }}>
<Mail size={16} color="#06B6D4" />
<div><div style={{ fontSize: '0.62rem', color: '#64748B', marginBottom: 2 }}>Email</div><div style={{ fontSize: '0.88rem', color: '#E2E8F0', fontWeight: 600 }}>{s.email}</div></div>
</div>
)}
</div>
</div>
{/* Identity */}
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 16, padding: 22 }}>
<div style={{ fontSize: '0.65rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 14 }}>🪪 Identity</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{row('Staff ID', s.id, true)}
{selectedRaw.aadhaar_number && row('Aadhaar', selectedRaw.aadhaar_number, true)}
{row('Joined Date', s.joinedDate)}
</div>
</div>
{/* Professional — full width */}
{pd && (
<div style={{ gridColumn: '1 / -1', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 16, padding: 22 }}>
<div style={{ fontSize: '0.65rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 14 }}>🎓 Professional Details</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(240px,1fr))', gap: 10 }}>
{pd.qualification && row('Qualification', pd.qualification)}
{pd.certification_body && row('Certification Body', pd.certification_body)}
{(pd.certificate_number || pd.license_number) && row('Cert / License No.', pd.certificate_number || pd.license_number || '', true)}
{pd.license_category && row('License Category', pd.license_category)}
{s.specialization && row('Specialization', s.specialization)}
{s.certExpiry && (
<div style={{ padding: '14px 16px', background: cw ? 'rgba(245,158,11,0.08)' : 'rgba(16,185,129,0.07)', border: `1px solid ${cw ? 'rgba(245,158,11,0.25)' : 'rgba(16,185,129,0.2)'}`, borderRadius: 12 }}>
<div style={{ fontSize: '0.62rem', color: '#64748B', marginBottom: 5, display: 'flex', alignItems: 'center', gap: 5, textTransform: 'uppercase' }}>
<ShieldCheck size={11} color={cw ? '#F59E0B' : '#10B981'} /> Cert Expiry
</div>
<div style={{ fontSize: '0.95rem', fontWeight: 800, color: cw ? '#F59E0B' : '#10B981' }}>{s.certExpiry}</div>
</div>
)}
</div>
</div>
)}
</div>
<style>{`@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}`}</style>
</motion.div>
);
}
// ── TABLE VIEW ──
return (
<div style={{ color: '#F8FAFC' }}>
{/* ── Add Staff Modal ── */}
<AnimatePresence>
{showModal && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20, backdropFilter: 'blur(6px)' }}
onClick={e => { if (e.target === e.currentTarget) setShowModal(false); }}
>
<motion.div initial={{ scale: 0.93, y: 20, opacity: 0 }} animate={{ scale: 1, y: 0, opacity: 1 }} exit={{ scale: 0.93, y: 20, opacity: 0 }}
style={{ background: '#0D1526', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 24, width: 780, maxWidth: '96vw', position: 'relative' }}
>
{/* Modal Header */}
<div style={{ background: 'linear-gradient(135deg,rgba(6,182,212,0.12),transparent)', borderBottom: '1px solid rgba(255,255,255,0.07)', padding: '20px 28px 16px', borderRadius: '24px 24px 0 0' }}>
<button onClick={() => setShowModal(false)} style={{ position: 'absolute', top: 14, right: 14, background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, padding: 6, color: '#94A3B8', cursor: 'pointer', display: 'flex' }}><X size={15} /></button>
<h3 style={{ margin: 0, fontSize: '1.05rem', fontWeight: 900, color: '#fff' }}>Register New Staff</h3>
<p style={{ margin: '4px 0 0', fontSize: '0.75rem', color: '#64748B' }}>Fill details based on staff type</p>
</div>
<div style={{ padding: '20px 28px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Type Selector */}
<div style={{ display: 'flex', gap: 8 }}>
{(['DRIVER','EMT','DOCTOR'] as const).map(t => {
const rc = ROLE_COLORS[t];
return (
<button key={t} onClick={() => setStaffType(t)}
style={{ flex: 1, padding: '9px 0', borderRadius: 10, border: `2px solid ${staffType === t ? rc.color : 'rgba(255,255,255,0.08)'}`, background: staffType === t ? rc.bg : 'rgba(255,255,255,0.03)', color: staffType === t ? rc.color : '#64748B', fontWeight: 800, fontSize: '0.8rem', cursor: 'pointer', transition: 'all 0.2s' }}>
{t}
</button>
);
})}
</div>
{/* Common Fields — 2 col grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{[{ k: 'name', label: 'Full Name', ph: 'e.g. Driver 2' }, { k: 'phone', label: 'Phone', ph: '9999999998' }, { k: 'aadhaar_number', label: 'Aadhaar Number', ph: '1234-5678-9001' }].map(f => (
<div key={f.k}>
<div style={{ fontSize: '0.6rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 5 }}>{f.label}</div>
<input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph}
autoComplete="off"
style={{ width: '100%', padding: '9px 12px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 9, color: '#fff', fontSize: '0.85rem', outline: 'none', boxSizing: 'border-box' }} />
</div>
))}
{/* Password — same row as Aadhaar */}
<div>
<div style={{ fontSize: '0.6rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, marginBottom: 5 }}>Password <span style={{ color: '#334155', fontWeight: 400, textTransform: 'none', letterSpacing: 0 }}>(optional)</span></div>
<div style={{ position: 'relative' }}>
<input type={showPw ? 'text' : 'password'} value={form.password} onChange={e => setF('password', e.target.value)} placeholder="Min 8 chars"
autoComplete="new-password"
style={{ width: '100%', padding: '9px 36px 9px 12px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 9, color: '#fff', fontSize: '0.85rem', outline: 'none', boxSizing: 'border-box' }} />
<button onClick={() => setShowPw(p => !p)} style={{ position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: '#64748B', cursor: 'pointer', display: 'flex' }}>
{showPw ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
</div>
</div>
</div>
{/* DRIVER fields */}
{staffType === 'DRIVER' && (
<div style={{ padding: '14px 16px', background: 'rgba(6,182,212,0.05)', border: '1px solid rgba(6,182,212,0.15)', borderRadius: 12 }}>
<div style={{ fontSize: '0.6rem', color: '#06B6D4', textTransform: 'uppercase', fontWeight: 700, marginBottom: 10 }}>Driver Professional Details</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
{[{ k:'license_number', label:'License Number', ph:'DL-KA-2024-DR1' }, { k:'license_category', label:'License Category', ph:'MCWG/LMV' }, { k:'license_expiry', label:'License Expiry', ph:'2034-01-01' }].map(f => (
<div key={f.k}>
<div style={{ fontSize: '0.58rem', color: '#475569', textTransform: 'uppercase', fontWeight: 700, marginBottom: 4 }}>{f.label}</div>
<input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph}
style={{ width: '100%', padding: '8px 10px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, color: '#fff', fontSize: '0.82rem', outline: 'none', boxSizing: 'border-box' }} />
</div>
))}
</div>
</div>
)}
{/* EMT fields */}
{staffType === 'EMT' && (
<div style={{ padding: '14px 16px', background: 'rgba(59,130,246,0.05)', border: '1px solid rgba(59,130,246,0.15)', borderRadius: 12 }}>
<div style={{ fontSize: '0.6rem', color: '#3B82F6', textTransform: 'uppercase', fontWeight: 700, marginBottom: 10 }}>EMT Professional Details</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{[{ k:'qualification', label:'Qualification', ph:'Diploma in EMT' }, { k:'certification_body', label:'Certification Body', ph:'Indian Resuscitation Council' }, { k:'certificate_number', label:'Certificate Number', ph:'EMT-CERT-9901' }, { k:'certificate_expiry', label:'Certificate Expiry', ph:'2029-08-15' }].map(f => (
<div key={f.k}>
<div style={{ fontSize: '0.58rem', color: '#475569', textTransform: 'uppercase', fontWeight: 700, marginBottom: 4 }}>{f.label}</div>
<input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph}
style={{ width: '100%', padding: '8px 10px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, color: '#fff', fontSize: '0.82rem', outline: 'none', boxSizing: 'border-box' }} />
</div>
))}
</div>
</div>
)}
{/* DOCTOR fields */}
{staffType === 'DOCTOR' && (
<div style={{ padding: '14px 16px', background: 'rgba(16,185,129,0.05)', border: '1px solid rgba(16,185,129,0.15)', borderRadius: 12 }}>
<div style={{ fontSize: '0.6rem', color: '#10B981', textTransform: 'uppercase', fontWeight: 700, marginBottom: 10 }}>Doctor Professional Details</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{[{ k:'medical_registration_number', label:'Medical Reg. Number', ph:'MCI/DMC/2024/77' }, { k:'specialization', label:'Specialization', ph:'Emergency Medicine' }, { k:'qualification', label:'Qualification', ph:'MBBS, MD (Emergency)' }].map(f => (
<div key={f.k}>
<div style={{ fontSize: '0.58rem', color: '#475569', textTransform: 'uppercase', fontWeight: 700, marginBottom: 4 }}>{f.label}</div>
<input value={(form as Record<string, string | boolean>)[f.k] as string} onChange={e => setF(f.k, e.target.value)} placeholder={f.ph}
style={{ width: '100%', padding: '8px 10px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, color: '#fff', fontSize: '0.82rem', outline: 'none', boxSizing: 'border-box' }} />
</div>
))}
<label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: '0.82rem', color: '#94A3B8', gridColumn: '1 / -1' }}>
<input type="checkbox" checked={form.teleconsult_available} onChange={e => setF('teleconsult_available', e.target.checked)} style={{ accentColor: '#10B981', width: 15, height: 15 }} />
Teleconsult Available
</label>
</div>
</div>
)}
{submitErr && <div style={{ padding: '9px 14px', background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.2)', borderRadius: 9, color: '#EF4444', fontSize: '0.8rem' }}>{submitErr}</div>}
{submitOk && <div style={{ padding: '9px 14px', background: 'rgba(16,185,129,0.1)', border: '1px solid rgba(16,185,129,0.2)', borderRadius: 9, color: '#10B981', fontSize: '0.8rem', fontWeight: 700 }}> {submitOk}</div>}
<button onClick={submitStaff} disabled={submitting}
style={{ padding: '11px', background: 'linear-gradient(135deg,#06B6D4,#3B82F6)', border: 'none', borderRadius: 11, color: '#fff', fontWeight: 800, fontSize: '0.88rem', cursor: submitting ? 'not-allowed' : 'pointer', opacity: submitting ? 0.7 : 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
{submitting ? <><Loader2 size={15} style={{ animation: 'spin 1s linear infinite' }} /> Registering...</> : <><UserPlus size={15} /> Register {staffType}</>}
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* Stats */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 14, marginBottom: 24 }}>
{[{ label: 'Total Staff', value: normalized.length, color: '#06B6D4' }, { label: 'On Duty', value: onDuty, color: '#10B981' }, { label: 'Off Duty', value: offDuty, color: '#64748B' }, { label: 'On Leave', value: onLeave, color: '#F59E0B' }].map(s => (
<div key={s.label} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 14, padding: '18px 20px', borderLeft: `4px solid ${s.color}` }}>
<div style={{ fontSize: '1.8rem', fontWeight: 900, color: '#fff', lineHeight: 1 }}>{loading ? '—' : s.value}</div>
<div style={{ fontSize: '0.7rem', color: '#64748B', marginTop: 6, textTransform: 'uppercase', letterSpacing: '0.5px' }}>{s.label}</div>
</div>
))}
</div>
{/* Toolbar */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}>
<div style={{ display: 'flex', gap: 6, background: 'rgba(255,255,255,0.03)', padding: 4, borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)' }}>
{FILTERS.map(f => (
<button key={f} onClick={() => { setFilter(f); setPage(1); }} style={{ padding: '7px 14px', borderRadius: 8, border: 'none', cursor: 'pointer', fontSize: '0.78rem', fontWeight: filter === f ? 700 : 500, background: filter === f ? 'linear-gradient(135deg,#06B6D4,#3B82F6)' : 'transparent', color: filter === f ? '#fff' : '#64748B', transition: 'all 0.2s' }}>
{f === 'ALL' ? 'All' : f}
</button>
))}
</div>
<div style={{ display: 'flex', gap: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'rgba(255,255,255,0.03)', padding: '8px 14px', borderRadius: 10, border: '1px solid rgba(255,255,255,0.06)' }}>
<Search size={14} color="#64748B" />
<input value={search} onChange={e => { setSearch(e.target.value); setPage(1); }} placeholder="Search staff..." style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '0.85rem', outline: 'none', width: 170 }} />
</div>
<button onClick={() => { resetModal(); setShowModal(true); }} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '9px 18px', background: 'linear-gradient(135deg,#06B6D4,#3B82F6)', border: 'none', borderRadius: 10, color: '#fff', fontWeight: 700, cursor: 'pointer', fontSize: '0.82rem', whiteSpace: 'nowrap', boxShadow: '0 4px 12px rgba(6,182,212,0.3)' }}>
<UserPlus size={15} /> ADD STAFF
</button>
</div>
</div>
{loading && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: 200, gap: 12, color: '#64748B' }}><Loader2 size={24} style={{ animation: 'spin 1s linear infinite' }} /><span>Loading staff...</span></div>}
{!loading && fetchError && (
<div style={{ textAlign: 'center', padding: 40, background: 'rgba(239,68,68,0.05)', border: '1px solid rgba(239,68,68,0.15)', borderRadius: 16 }}>
<AlertCircle size={32} color="#EF4444" style={{ marginBottom: 12 }} />
<p style={{ color: '#EF4444', fontWeight: 600, marginBottom: 12 }}>{fetchError}</p>
<button onClick={fetchStaff} style={{ padding: '9px 22px', background: 'linear-gradient(135deg,#06B6D4,#3B82F6)', border: 'none', borderRadius: 8, color: '#fff', fontWeight: 700, cursor: 'pointer' }}> Retry</button>
</div>
)}
{!loading && !fetchError && (
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 16, overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
{['#', 'Staff Member', 'Role', 'Status', 'Phone', 'Specialization', 'Joined', 'Cert Expiry'].map(h => <th key={h} style={th}>{h}</th>)}
</tr>
</thead>
<tbody>
{pageData.length === 0 ? (
<tr><td colSpan={8} style={{ textAlign: 'center', padding: 48, color: '#64748B' }}>{staffList.length === 0 ? 'No staff registered yet.' : 'No results match your filter.'}</td></tr>
) : pageData.map((s, idx) => {
const rc = ROLE_COLORS[s.role] || DEF;
const sc = STATUS_CFG[s.status] || { label: s.status, color: '#94A3B8' };
const certSoon = s.certExpiry ? new Date(s.certExpiry) < new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) : false;
return (
<tr key={s.id}
onClick={() => { const raw = staffList.find(r => r.id === s.id); if (raw) setSelectedRaw(raw); }}
style={{ borderBottom: '1px solid rgba(255,255,255,0.05)', cursor: 'pointer', transition: 'background 0.15s' }}
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(255,255,255,0.04)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
<td style={{ padding: '14px 18px', color: '#475569', fontSize: '0.78rem', fontWeight: 600 }}>{(safePage - 1) * PAGE_SIZE + idx + 1}</td>
<td style={{ padding: '14px 18px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 38, height: 38, borderRadius: 10, background: rc.bg, border: `1.5px solid ${rc.color}40`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.85rem', fontWeight: 900, color: rc.color, flexShrink: 0 }}>
{s.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()}
</div>
<div>
<div style={{ fontWeight: 700, fontSize: '0.88rem', color: '#F1F5F9' }}>{s.name}</div>
</div>
</div>
</td>
<td style={{ padding: '14px 18px' }}><span style={{ padding: '4px 10px', borderRadius: 6, fontSize: '0.68rem', fontWeight: 800, background: rc.bg, color: rc.color }}>{s.role || '—'}</span></td>
<td style={{ padding: '14px 18px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 7, height: 7, borderRadius: '50%', background: sc.color, boxShadow: `0 0 6px ${sc.color}` }} />
<span style={{ fontSize: '0.75rem', fontWeight: 700, color: sc.color }}>{sc.label}</span>
</div>
</td>
<td style={{ padding: '14px 18px' }}><div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '0.78rem', color: '#94A3B8' }}><Phone size={12} color="#06B6D4" />{s.phone}</div></td>
<td style={{ padding: '14px 18px' }}><span style={{ fontSize: '0.78rem', color: '#94A3B8' }}>{s.specialization || '—'}</span></td>
<td style={{ padding: '14px 18px' }}><span style={{ fontSize: '0.78rem', color: '#94A3B8' }}>{s.joinedDate || '—'}</span></td>
<td style={{ padding: '14px 18px' }}>
{s.certExpiry ? <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}><ShieldCheck size={14} color={certSoon ? '#F59E0B' : '#10B981'} /><span style={{ fontSize: '0.75rem', fontWeight: 600, color: certSoon ? '#F59E0B' : '#94A3B8' }}>{s.certExpiry}</span></div> : <span style={{ color: '#475569' }}></span>}
</td>
</tr>
);
})}
</tbody>
</table>
{/* Pagination */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 20px', borderTop: '1px solid rgba(255,255,255,0.06)' }}>
<span style={{ fontSize: '0.78rem', color: '#64748B' }}>Showing <strong style={{ color: '#94A3B8' }}>{filtered.length === 0 ? 0 : (safePage - 1) * PAGE_SIZE + 1}{Math.min(safePage * PAGE_SIZE, filtered.length)}</strong> of <strong style={{ color: '#94A3B8' }}>{filtered.length}</strong> staff</span>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={safePage === 1} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid rgba(255,255,255,0.08)', background: 'rgba(255,255,255,0.04)', color: safePage === 1 ? '#334155' : '#94A3B8', cursor: safePage === 1 ? 'not-allowed' : 'pointer' }}><ChevronLeft size={15} /></button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => (
<button key={p} onClick={() => setPage(p)} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: 'none', background: p === safePage ? 'linear-gradient(135deg,#06B6D4,#3B82F6)' : 'rgba(255,255,255,0.04)', color: p === safePage ? '#fff' : '#64748B', fontWeight: p === safePage ? 700 : 500, cursor: 'pointer', fontSize: '0.82rem' }}>{p}</button>
))}
<button onClick={() => setPage(p => Math.min(totalPages, p + 1))} disabled={safePage === totalPages} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid rgba(255,255,255,0.08)', background: 'rgba(255,255,255,0.04)', color: safePage === totalPages ? '#334155' : '#94A3B8', cursor: safePage === totalPages ? 'not-allowed' : 'pointer' }}><ChevronRight size={15} /></button>
</div>
</div>
</div>
)}
<style>{`@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}} .spin{animation:spin 1s linear infinite}`}</style>
</div>
);
};