From 98fe9d4a79892f583348a23804e090a2e76849e7 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 15:44:15 -0600 Subject: [PATCH] Upload files to "client/src/components" --- client/src/components/EmployeeModal.jsx | 539 ++++++++++++++++++++++-- client/src/components/NegateModal.jsx | 19 +- 2 files changed, 516 insertions(+), 42 deletions(-) diff --git a/client/src/components/EmployeeModal.jsx b/client/src/components/EmployeeModal.jsx index 41938aa..2f66c0b 100755 --- a/client/src/components/EmployeeModal.jsx +++ b/client/src/components/EmployeeModal.jsx @@ -4,32 +4,158 @@ import CpasBadge, { getTier } from './CpasBadge'; import NegateModal from './NegateModal'; 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' }, - closeBtn: { float: 'right', background: 'none', border: 'none', color: 'white', fontSize: '22px', cursor: 'pointer', lineHeight: 1, marginTop: '-2px' }, - 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 }, - deleteConfirm: { background: '#3c1114', border: '1px solid #f5c6cb', borderRadius: '6px', padding: '12px', marginTop: '8px', fontSize: '12px', color: '#ffb3b8' }, + 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', + }, + closeBtn: { + float: 'right', + background: 'none', + border: 'none', + color: 'white', + fontSize: '22px', + cursor: 'pointer', + lineHeight: 1, + marginTop: '-2px', + }, + 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, + }, + deleteConfirm: { + background: '#3c1114', + border: '1px solid #f5c6cb', + borderRadius: '6px', + padding: '12px', + marginTop: '8px', + fontSize: '12px', + color: '#ffb3b8', + }, }; export default function EmployeeModal({ employeeId, onClose }) { - const [employee, setEmployee] = useState(null); - const [score, setScore] = useState(null); + 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 [loading, setLoading] = useState(true); + const [negating, setNegating] = useState(null); const [confirmDel, setConfirmDel] = useState(null); const load = useCallback(() => { @@ -38,22 +164,30 @@ export default function EmployeeModal({ employeeId, onClose }) { axios.get('/api/employees'), axios.get(`/api/employees/${employeeId}/score`), axios.get(`/api/violations/employee/${employeeId}?limit=100`), - ]).then(([empRes, scoreRes, violRes]) => { - const emp = empRes.data.find(e => e.id === employeeId); - setEmployee(emp || null); - setScore(scoreRes.data); - setViolations(violRes.data); - }).finally(() => setLoading(false)); + ]) + .then(([empRes, scoreRes, violRes]) => { + const emp = empRes.data.find((e) => e.id === employeeId); + setEmployee(emp || null); + setScore(scoreRes.data); + setViolations(violRes.data); + }) + .finally(() => setLoading(false)); }, [employeeId]); - useEffect(() => { load(); }, [load]); + useEffect(() => { + load(); + }, [load]); const handleDownloadPdf = async (violId, empName, date) => { - const response = await axios.get(`/api/violations/${violId}/pdf`, { responseType: 'blob' }); - const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/pdf' })); + 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`; + link.href = url; + link.download = `CPAS_${(empName || '').replace(/[^a-z0-9]/gi, '_')}_${date}.pdf`; document.body.appendChild(link); link.click(); link.remove(); @@ -83,13 +217,342 @@ export default function EmployeeModal({ employeeId, onClose }) { load(); }; - const tier = score ? getTier(score.active_points) : null; - const active = violations.filter(v => !v.negated); - const negated = violations.filter(v => v.negated); + 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 ( -
{ if (e.target === e.currentTarget) onClose(); }}> - {/* panel and tables unchanged; omitted here for brevity */} +
+
+
+ +
+ {loading ? 'Loading…' : employee?.name || 'Employee Profile'} +
+ {employee && ( +
+ {[employee.department, employee.supervisor ? `Supervisor: ${employee.supervisor}` : null] + .filter(Boolean) + .join(' · ')} +
+ )} +
+ +
+ {loading ? ( +

+ Loading… +

+ ) : ( + <> +
+
+
+ {score?.active_points ?? 0} +
+
Active Points
+
+
+
{score?.violation_count ?? 0}
+
90-Day Violations
+
+
+
{active.length}
+
Total On Record
+
+
+
+ {negated.length} +
+
Negated
+
+
+ + {tier && ( +
+ {tier.label} + + Rolling 90-day window · Points expire automatically + +
+ )} + +
Active Violations
+ {active.length === 0 ? ( +

+ No active violations on record. +

+ ) : ( + + + + + + + + + + + {active.map((v) => ( + + + + + + + ))} + +
DateViolationPtsActions
{v.incident_date} +
{v.violation_name}
+
+ {v.category} +
+ {v.details && ( +
+ {v.details} +
+ )} +
+ {v.points} + + + +
+ {confirmDel === v.id ? ( +
+ Permanently delete? This cannot be + undone. +
+ + +
+
+ ) : ( + + )} +
+ )} + + {negated.length > 0 && ( + <> +
Negated / Resolved Violations
+ + + + + + + + + + + + {negated.map((v) => ( + + + + + + + + ))} + +
DateViolationPtsResolutionActions
{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? +
+ + +
+
+ ) : ( + + )} +
+ + )} + + )} +
+
+ {negating && ( { + if (e.target === e.currentTarget && onCancel) onCancel(); + }; + return ( -
{ if (e.target === e.currentTarget) onCancel && onCancel(); }}> +
⊘ Negate Violation Points
@@ -149,7 +153,6 @@ export default function NegateModal({ violation, onConfirm, onCancel }) { -
@@ -175,10 +178,18 @@ export default function NegateModal({ violation, onConfirm, onCancel }) {
- -