first commit.
This commit is contained in:
@@ -1,205 +1,433 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
UserPlus,
|
||||
Search,
|
||||
Filter,
|
||||
Users,
|
||||
Medal,
|
||||
Clock,
|
||||
ShieldCheck,
|
||||
AlertTriangle,
|
||||
Mail,
|
||||
Phone,
|
||||
Calendar,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
MoreVertical
|
||||
} from 'lucide-react';
|
||||
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';
|
||||
import { Card } from '../../components/Common';
|
||||
|
||||
interface Staff {
|
||||
id: string;
|
||||
name: string;
|
||||
role: 'DRIVER' | 'EMT' | 'DOCTOR' | 'PARAMEDIC';
|
||||
status: 'ON_DUTY' | 'OFF_DUTY' | 'ON_LEAVE';
|
||||
specialization?: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
joinedDate: string;
|
||||
tripsCompleted: number;
|
||||
rating: number;
|
||||
certExpiry: string;
|
||||
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 MOCK_STAFF: Staff[] = [
|
||||
{ id: 'S-101', name: 'Vikram Singh', role: 'DRIVER', status: 'ON_DUTY', phone: '+91 98765 43210', email: 'v.singh@teleems.com', joinedDate: '2023-01-15', tripsCompleted: 452, rating: 4.8, certExpiry: '2026-12-01' },
|
||||
{ id: 'S-102', name: 'Dr. Ananya Iyer', role: 'DOCTOR', specialization: 'Critical Care', status: 'ON_DUTY', phone: '+91 98765 43211', email: 'a.iyer@teleems.com', joinedDate: '2023-06-20', tripsCompleted: 128, rating: 4.9, certExpiry: '2026-05-15' },
|
||||
{ id: 'S-103', name: 'Rahul Verma', role: 'EMT', status: 'ON_LEAVE', phone: '+91 98765 43212', email: 'r.verma@teleems.com', joinedDate: '2024-02-10', tripsCompleted: 215, rating: 4.7, certExpiry: '2026-06-01' },
|
||||
{ id: 'S-104', name: 'Suresh Kumar', role: 'DRIVER', status: 'OFF_DUTY', phone: '+91 98765 43213', email: 's.kumar@teleems.com', joinedDate: '2022-11-05', tripsCompleted: 890, rating: 4.6, certExpiry: '2026-08-20' },
|
||||
];
|
||||
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 [activeTab, setActiveTab] = useState<'ALL' | 'DRIVER' | 'EMT' | 'DOCTOR'>('ALL');
|
||||
const [selectedStaff, setSelectedStaff] = useState<Staff | null>(null);
|
||||
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 filteredStaff = MOCK_STAFF.filter(s => activeTab === 'ALL' || s.role === activeTab);
|
||||
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]);
|
||||
|
||||
return (
|
||||
<div className="fleet-personnel animate-in fade-in duration-500">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
{['ALL', 'DRIVER', 'EMT', 'DOCTOR'].map(t => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setActiveTab(t as any)}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
background: activeTab === t ? 'var(--accent-cyan)' : 'rgba(255,255,255,0.05)',
|
||||
color: activeTab === t ? '#000' : 'var(--text-secondary)',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
>
|
||||
{t}S
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button className="btn-primary" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<UserPlus size={18} /> REGISTER PERSONNEL
|
||||
</button>
|
||||
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>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: '24px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<Card style={{ padding: '0', overflow: 'hidden' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.1)', textAlign: 'left', background: 'rgba(255,255,255,0.02)' }}>
|
||||
<th style={{ padding: '16px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Personnel</th>
|
||||
<th style={{ padding: '16px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Role / Specialization</th>
|
||||
<th style={{ padding: '16px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Status</th>
|
||||
<th style={{ padding: '16px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Trips</th>
|
||||
<th style={{ padding: '16px', fontSize: '0.75rem', textTransform: 'uppercase', opacity: 0.5 }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredStaff.map(s => (
|
||||
<tr
|
||||
key={s.id}
|
||||
onClick={() => setSelectedStaff(s)}
|
||||
style={{
|
||||
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||
cursor: 'pointer',
|
||||
background: selectedStaff?.id === s.id ? 'rgba(59, 130, 246, 0.05)' : 'transparent'
|
||||
}}
|
||||
className="hover-glow"
|
||||
>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div style={{ width: '36px', height: '36px', borderRadius: '50%', background: 'rgba(59, 130, 246, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--accent-cyan)', fontWeight: 700, fontSize: '0.875rem', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
|
||||
{s.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700 }}>{s.name}</div>
|
||||
<div style={{ fontSize: '0.65rem', opacity: 0.5 }}>ID: {s.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<div style={{ fontSize: '0.8125rem', fontWeight: 600 }}>{s.role}</div>
|
||||
{s.specialization && <div style={{ fontSize: '0.65rem', opacity: 0.5 }}>{s.specialization}</div>}
|
||||
</td>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div style={{ width: '6px', height: '6px', borderRadius: '50%', background: s.status === 'ON_DUTY' ? '#22C55E' : s.status === 'ON_LEAVE' ? '#EF4444' : '#94A3B8' }}></div>
|
||||
<span style={{ fontSize: '0.7rem', fontWeight: 700, opacity: 0.8 }}>{s.status.replace('_', ' ')}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<div style={{ fontWeight: 800 }}>{s.tripsCompleted}</div>
|
||||
</td>
|
||||
<td style={{ padding: '16px' }}>
|
||||
<button className="btn-ghost-sm"><MoreVertical size={14} /></button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
{/* 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>
|
||||
|
||||
<div style={{ position: 'sticky', top: '0' }}>
|
||||
{selectedStaff ? (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={selectedStaff.id}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
>
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
|
||||
<div style={{ width: '80px', height: '80px', borderRadius: '50%', background: 'rgba(59, 130, 246, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--accent-cyan)', fontSize: '1.5rem', fontWeight: 900, border: '2px solid var(--accent-cyan)', margin: '0 auto 16px', boxShadow: '0 0 20px rgba(59, 130, 246, 0.2)' }}>
|
||||
{selectedStaff.name.charAt(0)}
|
||||
{/* 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>
|
||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 800 }}>{selectedStaff.name}</h2>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--accent-cyan)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em' }}>{selectedStaff.role}</div>
|
||||
<div style={{ fontSize: '0.95rem', fontWeight: 800, color: cw ? '#F59E0B' : '#10B981' }}>{s.certExpiry}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '24px' }}>
|
||||
<div style={{ padding: '12px', borderRadius: '12px', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<div style={{ fontSize: '0.65rem', opacity: 0.5, textTransform: 'uppercase', marginBottom: '4px' }}>Trips Rate</div>
|
||||
<div style={{ fontSize: '1.125rem', fontWeight: 800, color: 'var(--accent-green)' }}>{selectedStaff.rating}/5.0</div>
|
||||
</div>
|
||||
<div style={{ padding: '12px', borderRadius: '12px', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<div style={{ fontSize: '0.65rem', opacity: 0.5, textTransform: 'uppercase', marginBottom: '4px' }}>SLA Compliance</div>
|
||||
<div style={{ fontSize: '1.125rem', fontWeight: 800 }}>98.4%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<div>
|
||||
<h4 style={{ fontSize: '0.7rem', fontWeight: 800, textTransform: 'uppercase', opacity: 0.5, marginBottom: '8px' }}>Contact Information</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', fontSize: '0.8125rem' }}>
|
||||
<Phone size={14} style={{ opacity: 0.5 }} /> {selectedStaff.phone}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', fontSize: '0.8125rem' }}>
|
||||
<Mail size={14} style={{ opacity: 0.5 }} /> {selectedStaff.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style={{ fontSize: '0.7rem', fontWeight: 800, textTransform: 'uppercase', opacity: 0.5, marginBottom: '8px' }}>Certifications</h4>
|
||||
<div style={{ padding: '12px', borderRadius: '12px', background: 'rgba(245, 158, 11, 0.05)', border: '1px solid rgba(245, 158, 11, 0.1)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', fontWeight: 700 }}>Professional License</div>
|
||||
<div style={{ fontSize: '0.65rem', opacity: 0.6 }}>Expiry: {selectedStaff.certExpiry}</div>
|
||||
</div>
|
||||
<AlertTriangle size={16} color="#F59E0B" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="btn-primary" style={{ width: '100%', marginTop: '24px' }}>MANAGE SHIFT SCHEDULE</button>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
) : (
|
||||
<div className="glass" style={{ padding: '40px', borderRadius: '16px', textAlign: 'center', border: '1px dashed rgba(255,255,255,0.1)' }}>
|
||||
<Users size={32} style={{ opacity: 0.2, margin: '0 auto 16px' }} />
|
||||
<h3 style={{ fontSize: '1rem', fontWeight: 700, marginBottom: '8px' }}>Select Personnel</h3>
|
||||
<p style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}>View detailed performance metrics, licensing status, and shift history for your fleet crew.</p>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user