import React, { useState, useEffect, useCallback } from 'react'; import axios from 'axios'; import CpasBadge, { getTier } from './CpasBadge'; import NegateModal from './NegateModal'; import EditEmployeeModal from './EditEmployeeModal'; import AmendViolationModal from './AmendViolationModal'; import ExpirationTimeline from './ExpirationTimeline'; import EmployeeNotes from './EmployeeNotes'; import { useToast } from './ToastProvider'; const s = { overlay: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', zIndex: 1000, display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-end', }, panel: { background: '#111217', color: '#f8f9fa', width: '680px', maxWidth: '95vw', height: '100vh', overflowY: 'auto', boxShadow: '-4px 0 24px rgba(0,0,0,0.7)', display: 'flex', flexDirection: 'column', }, header: { background: 'linear-gradient(135deg, #000000, #151622)', color: 'white', padding: '24px 28px', position: 'sticky', top: 0, zIndex: 10, borderBottom: '1px solid #222', }, headerRow: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }, closeBtn: { float: 'right', background: 'none', border: 'none', color: 'white', fontSize: '22px', cursor: 'pointer', lineHeight: 1, marginTop: '-2px', }, editEmpBtn: { background: 'none', border: '1px solid #555', color: '#ccc', borderRadius: '5px', padding: '4px 10px', fontSize: '11px', cursor: 'pointer', marginTop: '8px', fontWeight: 600, }, body: { padding: '24px 28px', flex: 1 }, scoreRow: { display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '24px' }, scoreCard: { flex: '1', minWidth: '100px', background: '#181924', borderRadius: '8px', padding: '14px', textAlign: 'center', border: '1px solid #2a2b3a', }, scoreNum: { fontSize: '26px', fontWeight: 800 }, scoreLbl: { fontSize: '11px', color: '#b5b5c0', marginTop: '3px' }, sectionHd: { fontSize: '13px', fontWeight: 700, color: '#f8f9fa', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '10px', marginTop: '24px', }, table: { width: '100%', borderCollapse: 'collapse', fontSize: '12px', background: '#181924', borderRadius: '6px', overflow: 'hidden', border: '1px solid #2a2b3a', }, th: { background: '#050608', padding: '8px 10px', textAlign: 'left', color: '#f8f9fa', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', }, td: { padding: '9px 10px', borderBottom: '1px solid #202231', verticalAlign: 'top', color: '#f8f9fa', }, negatedRow: { background: '#151622', color: '#9ca0b8' }, actionBtn: (color) => ({ background: 'none', border: `1px solid ${color}`, color, borderRadius: '4px', padding: '3px 8px', fontSize: '11px', cursor: 'pointer', marginRight: '4px', fontWeight: 600, }), resTag: { display: 'inline-block', padding: '2px 8px', borderRadius: '10px', fontSize: '10px', fontWeight: 700, background: '#053321', color: '#9ef7c1', border: '1px solid #0f5132', }, pdfBtn: { background: 'none', border: '1px solid #d4af37', color: '#ffd666', borderRadius: '4px', padding: '3px 8px', fontSize: '11px', cursor: 'pointer', fontWeight: 600, }, amendBtn: { background: 'none', border: '1px solid #4db6ac', color: '#4db6ac', borderRadius: '4px', padding: '3px 8px', fontSize: '11px', cursor: 'pointer', marginRight: '4px', fontWeight: 600, }, deleteConfirm: { background: '#3c1114', border: '1px solid #f5c6cb', borderRadius: '6px', padding: '12px', marginTop: '8px', fontSize: '12px', color: '#ffb3b8', }, amendBadge: { display: 'inline-block', marginLeft: '4px', padding: '1px 5px', borderRadius: '8px', fontSize: '9px', fontWeight: 700, background: '#0e2a2a', color: '#4db6ac', border: '1px solid #1a4a4a', verticalAlign: 'middle', }, }; export default function EmployeeModal({ employeeId, onClose }) { const [employee, setEmployee] = useState(null); const [score, setScore] = useState(null); const [violations, setViolations] = useState([]); const [loading, setLoading] = useState(true); const [negating, setNegating] = useState(null); const [confirmDel, setConfirmDel] = useState(null); const [editingEmp, setEditingEmp] = useState(false); const [amending, setAmending] = useState(null); // violation object const toast = useToast(); const load = useCallback(() => { setLoading(true); Promise.all([ axios.get(`/api/employees/${employeeId}`), axios.get(`/api/employees/${employeeId}/score`), axios.get(`/api/violations/employee/${employeeId}?limit=100`), ]) .then(([empRes, scoreRes, violRes]) => { setEmployee(empRes.data || null); setScore(scoreRes.data); setViolations(violRes.data); }) .finally(() => setLoading(false)); }, [employeeId]); useEffect(() => { load(); }, [load]); const handleDownloadPdf = async (violId, empName, date) => { try { const response = await axios.get(`/api/violations/${violId}/pdf`, { responseType: 'blob' }); const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/pdf' })); const link = document.createElement('a'); link.href = url; link.download = `CPAS_${(empName || '').replace(/[^a-z0-9]/gi, '_')}_${date}.pdf`; document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url); toast.success('PDF downloaded.'); } catch (err) { toast.error('PDF generation failed: ' + (err.response?.data?.error || err.message)); } }; const handleHardDelete = async (id) => { try { await axios.delete(`/api/violations/${id}`); toast.success('Violation permanently deleted.'); setConfirmDel(null); load(); } catch (err) { toast.error('Delete failed: ' + (err.response?.data?.error || err.message)); } }; const handleRestore = async (id) => { try { await axios.patch(`/api/violations/${id}/restore`); toast.success('Violation restored to active.'); setConfirmDel(null); load(); } catch (err) { toast.error('Restore failed: ' + (err.response?.data?.error || err.message)); } }; const handleNegate = async ({ resolution_type, details, resolved_by }) => { try { await axios.patch(`/api/violations/${negating.id}/negate`, { resolution_type, details, resolved_by }); toast.success('Violation negated.'); setNegating(null); setConfirmDel(null); load(); } catch (err) { toast.error('Negate failed: ' + (err.response?.data?.error || err.message)); } }; const tier = score ? getTier(score.active_points) : null; const active = violations.filter((v) => !v.negated); const negated = violations.filter((v) => v.negated); const handleOverlayClick = (e) => { if (e.target === e.currentTarget) onClose(); }; return (
e.stopPropagation()}> {/* ── Header ── */}
{employee ? employee.name : 'Employee'}
{employee && (
{employee.department} {employee.supervisor && `· Supervisor: ${employee.supervisor}`}
)} {employee && ( )}
{/* ── Body ── */}
{loading ? (
Loading…
) : ( <> {/* Score Cards */} {score && (
{score.active_points}
Active Points
{score.total_violations}
Total Violations
{score.negated_count}
Negated
{tier ? tier.label : '—'}
Current Tier
)} {score && } {/* ── Employee Notes ── */} {employee && ( setEmployee(prev => ({ ...prev, notes }))} /> )} {/* ── Expiration Timeline ── */} {score && score.active_points > 0 && ( )} {/* ── Active Violations ── */}
Active Violations
{active.length === 0 ? (
No active violations on record.
) : ( {active.map((v) => ( ))}
Date Violation Pts Actions
{v.incident_date}
{v.violation_name} {v.amendment_count > 0 && ( {v.amendment_count} edit{v.amendment_count !== 1 ? 's' : ''} )}
{v.category}
{v.details && (
{v.details}
)}
{v.points} {confirmDel === v.id && (
Permanently delete? This cannot be undone.
)}
)} {/* ── Negated / Resolved Violations ── */} {negated.length > 0 && ( <>
Negated / Resolved
{negated.map((v) => ( ))}
Date Violation Pts Resolution Actions
{v.incident_date}
{v.violation_name}
{v.category}
{v.points} {v.resolution_type} {v.resolution_details && (
{v.resolution_details}
)} {v.resolved_by && (
by {v.resolved_by}
)}
{confirmDel === v.id && (
Permanently delete? This cannot be undone.
)}
)} )}
{/* Modals rendered outside panel to avoid z-index nesting issues */} {negating && ( setNegating(null)} /> )} {editingEmp && employee && ( setEditingEmp(false)} onSaved={() => { toast.success('Employee updated.'); load(); }} /> )} {amending && ( setAmending(null)} onSaved={() => { toast.success('Violation amended.'); load(); }} /> )}
); }