diff --git a/client/src/components/EmployeeModal.jsx b/client/src/components/EmployeeModal.jsx index f49da38..500ea7d 100755 --- a/client/src/components/EmployeeModal.jsx +++ b/client/src/components/EmployeeModal.jsx @@ -4,237 +4,231 @@ import CpasBadge, { getTier } from './CpasBadge'; import NegateModal from './NegateModal'; const s = { - overlay: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', zIndex: 1000, display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-end' }, - panel: { background: 'white', width: '680px', maxWidth: '95vw', height: '100vh', overflowY: 'auto', boxShadow: '-4px 0 24px rgba(0,0,0,0.18)', display: 'flex', flexDirection: 'column' }, - header: { background: 'linear-gradient(135deg, #2c3e50, #34495e)', color: 'white', padding: '24px 28px', position: 'sticky', top: 0, zIndex: 10 }, - 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: '#f8f9fa', borderRadius: '8px', padding: '14px', textAlign: 'center', border: '1px solid #dee2e6' }, - scoreNum: { fontSize: '26px', fontWeight: 800 }, - scoreLbl: { fontSize: '11px', color: '#888', marginTop: '3px' }, - sectionHd: { fontSize: '13px', fontWeight: 700, color: '#34495e', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '10px', marginTop: '24px' }, - table: { width: '100%', borderCollapse: 'collapse', fontSize: '12px' }, - th: { background: '#f1f3f5', padding: '8px 10px', textAlign: 'left', color: '#555', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase' }, - td: { padding: '9px 10px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'top' }, - negatedRow: { background: '#f8f8f8', color: '#aaa' }, - 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: '#d4edda', color: '#155724', border: '1px solid #c3e6cb' }, - pdfBtn: { background: 'none', border: '1px solid #667eea', color: '#667eea', borderRadius: '4px', padding: '3px 8px', fontSize: '11px', cursor: 'pointer', fontWeight: 600 }, - deleteConfirm: { background: '#f8d7da', border: '1px solid #f5c6cb', borderRadius: '6px', padding: '12px', marginTop: '8px', fontSize: '12px' }, + 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 [violations, setViolations] = useState([]); - const [loading, setLoading] = useState(true); - const [negating, setNegating] = useState(null); - const [confirmDel, setConfirmDel] = 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 [confirmDel, setConfirmDel] = useState(null); - const load = useCallback(() => { - setLoading(true); - Promise.all([ - 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)); - }, [employeeId]); + const load = useCallback(() => { + setLoading(true); + Promise.all([ + 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)); + }, [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 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); - }; + 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 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); + }; - const handleHardDelete = async (id) => { - await axios.delete(`/api/violations/${id}`); - setConfirmDel(null); - load(); // ← refetch employee list, score, and violations - }; + const handleHardDelete = async (id) => { + await axios.delete(`/api/violations/${id}`); + setConfirmDel(null); + load(); + }; - const handleRestore = async (id) => { - await axios.patch(`/api/violations/${id}/restore`); - load(); // ← refetch employee list, score, and violations - }; + const handleRestore = async (id) => { + await axios.patch(`/api/violations/${id}/restore`); + load(); + }; - const handleNegate = async ({ resolution_type, details, resolved_by }) => { - await axios.patch(`/api/violations/${negating.id}/negate`, { resolution_type, details, resolved_by }); - setNegating(null); - load(); // ← CRITICAL FIX: refetch score immediately after negation - }; + const handleNegate = async ({ resolution_type, details, resolved_by }) => { + await axios.patch(`/api/violations/${negating.id}/negate`, { resolution_type, details, resolved_by }); + setNegating(null); + 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); - return ( -
Loading…
+ ) : (<> -Loading…
- ) : (<> - - {/* ── Score cards ───────────────────────── */} -No active violations on record.
- ) : ( -| Date | -Violation | -Pts | -Actions | -
|---|---|---|---|
| {v.incident_date} | -
- {v.violation_name}
- {v.category}
- {v.details && {v.details} }
- |
- {v.points} | -
-
-
- - {confirmDel === v.id ? ( -
- Permanently delete? This cannot be undone.
-
- ) : (
-
- )}
-
-
-
-
- |
-
| 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?
-
- ) : (
-
- )}
-
-
-
-
- |
-
No active violations on record.
+ ) : ( +| Date | +Violation | +Pts | +Actions | +
|---|---|---|---|
| {v.incident_date} | +
+ {v.violation_name}
+ {v.category}
+ {v.details && {v.details} }
+ |
+ {v.points} | +
+
+
+ + {confirmDel === v.id ? ( +
+ Permanently delete? This cannot be undone.
+
+ ) : (
+
+ )}
+
+
+
+
+ |
+
| 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?
+
+ ) : (
+
+ )}
+
+
+
+
+ |
+
| Points | -Tier | -
|---|---|
| ${t.min === 30 ? '30+' : t.min + '–' + t.max} | -${t.label} | -
| Points | Tier |
| ${t.min === 30 ? '30+' : t.min + '–' + t.max} | ${t.label} |
- By signing below, the employee acknowledges receipt of this violation record. - Acknowledgement does not imply agreement. The employee may submit a written - response within 5 business days. -
+By signing below, the employee acknowledges receipt of this violation record. Acknowledgement does not imply agreement. The employee may submit a written response within 5 business days.