2026-05-06 17:09:54 +05:30
import React , { useState , useEffect , useCallback } from 'react' ;
import { UserPlus , Search , ShieldCheck , ChevronLeft , ChevronRight , Loader2 , AlertCircle , Phone , Mail , X , Eye , EyeOff } from 'lucide-react' ;
2026-05-04 15:27:35 +05:30
import { motion , AnimatePresence } from 'framer-motion' ;
interface Staff {
2026-05-06 17:09:54 +05:30
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 ; };
2026-05-04 15:27:35 +05:30
}
2026-05-06 17:09:54 +05:30
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 ,
};
};
2026-05-04 15:27:35 +05:30
export const FleetPersonnel : React.FC = () => {
2026-05-06 17:09:54 +05:30
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' ) || '' ;
2026-05-04 15:27:35 +05:30
2026-05-06 17:09:54 +05:30
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 ]);
2026-05-04 15:27:35 +05:30
2026-05-06 17:09:54 +05:30
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 >
2026-05-04 15:27:35 +05:30
</ div >
2026-05-06 17:09:54 +05:30
);
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 >
2026-05-04 15:27:35 +05:30
2026-05-06 17:09:54 +05:30
{ /* 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 >
2026-05-04 15:27:35 +05:30
</ div >
2026-05-06 17:09:54 +05:30
{ /* 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
2026-05-04 15:27:35 +05:30
</ div >
2026-05-06 17:09:54 +05:30
< div style = {{ fontSize : '0.95rem' , fontWeight : 800 , color : cw ? '#F59E0B' : '#10B981' }}>{ s . certExpiry }</ div >
2026-05-04 15:27:35 +05:30
</ div >
2026-05-06 17:09:54 +05:30
)}
</ div >
2026-05-04 15:27:35 +05:30
</ div >
)}
</ div >
2026-05-06 17:09:54 +05:30
< 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 >
))}
2026-05-04 15:27:35 +05:30
</ div >
2026-05-06 17:09:54 +05:30
{ /* 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 >
2026-05-04 15:27:35 +05:30
</ div >
);
};