diff --git a/client/src/components/EditEmployeeModal.jsx b/client/src/components/EditEmployeeModal.jsx index a438e1b..c7228bc 100644 --- a/client/src/components/EditEmployeeModal.jsx +++ b/client/src/components/EditEmployeeModal.jsx @@ -1,189 +1 @@ -import React, { useState, useEffect } from 'react'; -import axios from 'axios'; - -const s = { - overlay: { - position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.8)', - zIndex: 2000, display: 'flex', alignItems: 'center', justifyContent: 'center', - }, - modal: { - background: '#111217', color: '#f8f9fa', width: '480px', maxWidth: '95vw', - borderRadius: '10px', boxShadow: '0 8px 40px rgba(0,0,0,0.8)', - border: '1px solid #222', overflow: 'hidden', - }, - header: { - background: 'linear-gradient(135deg, #000000, #151622)', color: 'white', - padding: '18px 22px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', - borderBottom: '1px solid #222', - }, - title: { fontSize: '15px', fontWeight: 700 }, - closeBtn: { - background: 'none', border: 'none', color: 'white', fontSize: '20px', - cursor: 'pointer', lineHeight: 1, - }, - body: { padding: '22px' }, - tabs: { display: 'flex', gap: '4px', marginBottom: '20px' }, - tab: (active) => ({ - flex: 1, padding: '8px', borderRadius: '6px', cursor: 'pointer', fontSize: '12px', - fontWeight: 700, textAlign: 'center', border: '1px solid', - background: active ? '#1a1c2e' : 'none', - borderColor: active ? '#667eea' : '#2a2b3a', - color: active ? '#667eea' : '#777', - }), - label: { fontSize: '11px', color: '#9ca0b8', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '5px' }, - input: { - width: '100%', background: '#0d0e14', border: '1px solid #2a2b3a', borderRadius: '6px', - color: '#f8f9fa', padding: '9px 12px', fontSize: '13px', marginBottom: '14px', - outline: 'none', boxSizing: 'border-box', - }, - select: { - width: '100%', background: '#0d0e14', border: '1px solid #2a2b3a', borderRadius: '6px', - color: '#f8f9fa', padding: '9px 12px', fontSize: '13px', marginBottom: '14px', - outline: 'none', boxSizing: 'border-box', - }, - row: { display: 'flex', gap: '10px', justifyContent: 'flex-end', marginTop: '6px' }, - btn: (color, bg) => ({ - padding: '8px 18px', borderRadius: '6px', fontWeight: 700, fontSize: '13px', - cursor: 'pointer', border: `1px solid ${color}`, color, background: bg || 'none', - }), - error: { - background: '#3c1114', border: '1px solid #f5c6cb', borderRadius: '6px', - padding: '10px 12px', fontSize: '12px', color: '#ffb3b8', marginBottom: '14px', - }, - success: { - background: '#0a2e1f', border: '1px solid #0f5132', borderRadius: '6px', - padding: '10px 12px', fontSize: '12px', color: '#9ef7c1', marginBottom: '14px', - }, - mergeWarning: { - background: '#2a1f00', border: '1px solid #7a5000', borderRadius: '6px', - padding: '12px', fontSize: '12px', color: '#ffc107', marginBottom: '14px', lineHeight: 1.5, - }, -}; - -export default function EditEmployeeModal({ employee, onClose, onSaved }) { - const [tab, setTab] = useState('edit'); - - // Edit state - const [name, setName] = useState(employee.name); - const [department, setDepartment] = useState(employee.department || ''); - const [supervisor, setSupervisor] = useState(employee.supervisor || ''); - const [editError, setEditError] = useState(''); - const [editSaving, setEditSaving] = useState(false); - - // Merge state - const [allEmployees, setAllEmployees] = useState([]); - const [sourceId, setSourceId] = useState(''); - const [mergeError, setMergeError] = useState(''); - const [mergeResult, setMergeResult] = useState(null); - const [merging, setMerging] = useState(false); - - useEffect(() => { - if (tab === 'merge') { - axios.get('/api/employees').then(r => setAllEmployees(r.data)); - } - }, [tab]); - - const handleEdit = async () => { - setEditError(''); - setEditSaving(true); - try { - await axios.patch(`/api/employees/${employee.id}`, { name, department, supervisor }); - onSaved(); - onClose(); - } catch (e) { - setEditError(e.response?.data?.error || 'Failed to save changes'); - } finally { - setEditSaving(false); - } - }; - - const handleMerge = async () => { - if (!sourceId) return setMergeError('Select an employee to merge in'); - setMergeError(''); - setMerging(true); - try { - const r = await axios.post(`/api/employees/${employee.id}/merge`, { source_id: parseInt(sourceId) }); - setMergeResult(r.data); - onSaved(); // refresh dashboard / parent list - } catch (e) { - setMergeError(e.response?.data?.error || 'Merge failed'); - } finally { - setMerging(false); - } - }; - - const otherEmployees = allEmployees.filter(e => e.id !== employee.id); - - return ( -
e.target === e.currentTarget && onClose()}> -
-
-
Edit Employee
- -
-
-
- - -
- - {tab === 'edit' && ( - <> - {editError &&
{editError}
} -
Full Name
- setName(e.target.value)} /> -
Department
- setDepartment(e.target.value)} placeholder="Optional" /> -
Supervisor
- setSupervisor(e.target.value)} placeholder="Optional" /> -
- - -
- - )} - - {tab === 'merge' && ( - <> - {mergeResult ? ( -
- ✓ Merge complete — {mergeResult.violations_reassigned} violation{mergeResult.violations_reassigned !== 1 ? 's' : ''} reassigned - to {employee.name}. The duplicate record has been removed. -
- ) : ( - <> -
- ⚠ This will reassign all violations from the selected employee into{' '} - {employee.name}, then permanently delete the duplicate record. - This cannot be undone. -
- {mergeError &&
{mergeError}
} -
Duplicate to merge into {employee.name}
- -
- - -
- - )} - {mergeResult && ( -
- -
- )} - - )} -
-
-
- ); -} +aW1wb3J0IFJlYWN0LCB7IHVzZVN0YXRlLCB1c2VFZmZlY3QgfSBmcm9tICdyZWFjdCc7CmltcG9ydCBheGlvcyBmcm9tICdheGlvcyc7CmltcG9ydCB7IERFUEFSVE1FTlRTIH0gZnJvbSAnLi4vZGF0YS9kZXBhcnRtZW50cyc7Cgpjb25zdCBzID0gewogIG92ZXJsYXk6IHsKICAgIHBvc2l0aW9uOiAnZml4ZWQnLCBpbnNldDogMCwgYmFja2dyb3VuZDogJ3JnYmEoMCwwLDAsMC44KScsCiAgICB6SW5kZXg6IDIwMDAsIGRpc3BsYXk6ICdmbGV4JywgYWxpZ25JdGVtczogJ2NlbnRlcicsIGp1c3RpZnlDb250ZW50OiAnY2VudGVyJywKICB9LAogIG1vZGFsOiB7CiAgICBiYWNrZ3JvdW5kOiAnIzExMTIxNycsIGNvbG9yOiAnI2Y4ZjlmYScsIHdpZHRoOiAnNDgwcHgnLCBtYXhXaWR0aDogJzk1dncnLAogICAgYm9yZGVyUmFkaXVzOiAnMTBweCcsIGJveFNoYWRvdzogJzAgOHB4IDQwcHggcmdiYSgwLDAsMCwwLjgpJywKICAgIGJvcmRlcjogJzFweCBzb2xpZCAjMjIyJywgb3ZlcmZsb3c6ICdoaWRkZW4nLAogIH0sCiAgaGVhZGVyOiB7CiAgICBiYWNrZ3JvdW5kOiAnbGluZWFyLWdyYWRpZW50KDEzNWRlZywgIzAwMDAwMCwgIzE1MTYyMiknLCBjb2xvcjogJ3doaXRlJywKICAgIHBhZGRpbmc6ICcxOHB4IDIycHgnLCBkaXNwbGF5OiAnZmxleCcsIGFsaWduSXRlbXM6ICdjZW50ZXInLCBqdXN0aWZ5Q29udGVudDogJ3NwYWNlLWJldHdlZW4nLAogICAgYm9yZGVyQm90dG9tOiAnMXB4IHNvbGlkICMyMjInLAogIH0sCiAgdGl0bGU6IHsgZm9udFNpemU6ICcxNXB4JywgZm9udFdlaWdodDogNzAwIH0sCiAgY2xvc2VCdG46IHsKICAgIGJhY2tncm91bmQ6ICdub25lJywgYm9yZGVyOiAnbm9uZScsIGNvbG9yOiAnd2hpdGUnLCBmb250U2l6ZTogJzIwcHgnLAogICAgY3Vyc29yOiAncG9pbnRlcicsIGxpbmVIZWlnaHQ6IDEsCiAgfSwKICBib2R5OiB7IHBhZGRpbmc6ICcyMnB4JyB9LAogIHRhYnM6IHsgZGlzcGxheTogJ2ZsZXgnLCBnYXA6ICc0cHgnLCBtYXJnaW5Cb3R0b206ICcyMHB4JyB9LAogIHRhYjogKGFjdGl2ZSkgPT4gKHsKICAgIGZsZXg6IDEsIHBhZGRpbmc6ICc4cHgnLCBib3JkZXJSYWRpdXM6ICc2cHgnLCBjdXJzb3I6ICdwb2ludGVyJywgZm9udFNpemU6ICcxMnB4JywKICAgIGZvbnRXZWlnaHQ6IDcwMCwgdGV4dEFsaWduOiAnY2VudGVyJywgYm9yZGVyOiAnMXB4IHNvbGlkJywKICAgIGJhY2tncm91bmQ6IGFjdGl2ZSA/ICcjMWExYzJlJyA6ICdub25lJywKICAgIGJvcmRlckNvbG9yOiBhY3RpdmUgPyAnIzY2N2VlYScgOiAnIzJhMmIzYScsCiAgICBjb2xvcjogYWN0aXZlID8gJyM2NjdlZWEnIDogJyM3NzcnLAogIH0pLAogIGxhYmVsOiB7IGZvbnRTaXplOiAnMTFweCcsIGNvbG9yOiAnIzljYTBiOCcsIHRleHRUcmFuc2Zvcm06ICd1cHBlcmNhc2UnLCBsZXR0ZXJTcGFjaW5nOiAnMC41cHgnLCBtYXJnaW5Cb3R0b206ICc1cHgnIH0sCiAgaW5wdXQ6IHsKICAgIHdpZHRoOiAnMTAwJScsIGJhY2tncm91bmQ6ICcjMGQwZTE0JywgYm9yZGVyOiAnMXB4IHNvbGlkICMyYTJiM2EnLCBib3JkZXJSYWRpdXM6ICc2cHgnLAogICAgY29sb3I6ICcjZjhmOWZhJywgcGFkZGluZzogJzlweCAxMnB4JywgZm9udFNpemU6ICcxM3B4JywgbWFyZ2luQm90dG9tOiAnMTRweCcsCiAgICBvdXRsaW5lOiAnbm9uZScsIGJveFNpemluZzogJ2JvcmRlci1ib3gnLAogIH0sCiAgc2VsZWN0OiB7CiAgICB3aWR0aDogJzEwMCUnLCBiYWNrZ3JvdW5kOiAnIzBkMGUxNCcsIGJvcmRlcjogJzFweCBzb2xpZCAjMmEyYjNhJywgYm9yZGVyUmFkaXVzOiAnNnB4JywKICAgIGNvbG9yOiAnI2Y4ZjlmYScsIHBhZGRpbmc6ICc5cHggMTJweCcsIGZvbnRTaXplOiAnMTNweCcsIG1hcmdpbkJvdHRvbTogJzE0cHgnLAogICAgb3V0bGluZTogJ25vbmUnLCBib3hTaXppbmc6ICdib3JkZXItYm94JywKICB9LAogIHJvdzogeyBkaXNwbGF5OiAnZmxleCcsIGdhcDogJzEwcHgnLCBqdXN0aWZ5Q29udGVudDogJ2ZsZXgtZW5kJywgbWFyZ2luVG9wOiAnNnB4JyB9LAogIGJ0bjogKGNvbG9yLCBiZykgPT4gKHsKICAgIHBhZGRpbmc6ICc4cHggMThweCcsIGJvcmRlclJhZGl1czogJzZweCcsIGZvbnRXZWlnaHQ6IDcwMCwgZm9udFNpemU6ICcxM3B4JywKICAgIGN1cnNvcjogJ3BvaW50ZXInLCBib3JkZXI6IGAxcHggc29saWQgJHtjb2xvcn1gLCBjb2xvciwgYmFja2dyb3VuZDogYmcgfHwgJ25vbmUnLAogIH0pLAogIGVycm9yOiB7CiAgICBiYWNrZ3JvdW5kOiAnIzNjMTExNCcsIGJvcmRlcjogJzFweCBzb2xpZCAjZjVjNmNiJywgYm9yZGVyUmFkaXVzOiAnNnB4JywKICAgIHBhZGRpbmc6ICcxMHB4IDEycHgnLCBmb250U2l6ZTogJzEycHgnLCBjb2xvcjogJyNmZmIzYjgnLCBtYXJnaW5Cb3R0b206ICcxNHB4JywKICB9LAogIHN1Y2Nlc3M6IHsKICAgIGJhY2tncm91bmQ6ICcjMGEyZTFmJywgYm9yZGVyOiAnMXB4IHNvbGlkICMwZjUxMzInLCBib3JkZXJSYWRpdXM6ICc2cHgnLAogICAgcGFkZGluZzogJzEwcHggMTJweCcsIGZvbnRTaXplOiAnMTJweCcsIGNvbG9yOiAnIzllZjdjMScsIG1hcmdpbkJvdHRvbTogJzE0cHgnLAogIH0sCiAgbWVyZ2VXYXJuaW5nOiB7CiAgICBiYWNrZ3JvdW5kOiAnIzJhMWYwMCcsIGJvcmRlcjogJzFweCBzb2xpZCAjN2E1MDAwJywgYm9yZGVyUmFkaXVzOiAnNnB4JywKICAgIHBhZGRpbmc6ICcxMnB4JywgZm9udFNpemU6ICcxMnB4JywgY29sb3I6ICcjZmZjMTA3JywgbWFyZ2luQm90dG9tOiAnMTRweCcsIGxpbmVIZWlnaHQ6IDEuNSwKICB9LAp9OwoKZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gRWRpdEVtcGxveWVlTW9kYWwoeyBlbXBsb3llZSwgb25DbG9zZSwgb25TYXZlZCB9KSB7CiAgY29uc3QgW3RhYiwgc2V0VGFiXSA9IHVzZVN0YXRlKCdlZGl0Jyk7CgogIC8vIEVkaXQgc3RhdGUKICBjb25zdCBbbmFtZSwgc2V0TmFtZV0gICAgICAgICAgICAgICAgICAgPSB1c2VTdGF0ZShlbXBsb3llZS5uYW1lKTsKICBjb25zdCBbZGVwYXJ0bWVudCwgc2V0RGVwYXJ0bWVudF0gICAgICAgPSB1c2VTdGF0ZShlbXBsb3llZS5kZXBhcnRtZW50IHx8ICcnKTsKICBjb25zdCBbc3VwZXJ2aXNvciwgc2V0U3VwZXJ2aXNvcl0gICAgICAgPSB1c2VTdGF0ZShlbXBsb3llZS5zdXBlcnZpc29yIHx8ICcnKTsKICBjb25zdCBbZWRpdEVycm9yLCBzZXRFZGl0RXJyb3JdICAgICAgICAgPSB1c2VTdGF0ZSgnJyk7CiAgY29uc3QgW2VkaXRTYXZpbmcsIHNldEVkaXRTYXZpbmddICAgICAgID0gdXNlU3RhdGUoZmFsc2UpOwoKICAvLyBNZXJnZSBzdGF0ZQogIGNvbnN0IFthbGxFbXBsb3llZXMsIHNldEFsbEVtcGxveWVlc10gICA9IHVzZVN0YXRlKFtdKTsKICBjb25zdCBbc291cmNlSWQsIHNldFNvdXJjZUlkXSAgICAgICAgICAgPSB1c2VTdGF0ZSgnJyk7CiAgY29uc3QgW21lcmdlRXJyb3IsIHNldE1lcmdlRXJyb3JdICAgICAgID0gdXNlU3RhdGUoJycpOwogIGNvbnN0IFttZXJnZVJlc3VsdCwgc2V0TWVyZ2VSZXN1bHRdICAgICA9IHVzZVN0YXRlKG51bGwpOwogIGNvbnN0IFttZXJnaW5nLCBzZXRNZXJnaW5nXSAgICAgICAgICAgICA9IHVzZVN0YXRlKGZhbHNlKTsKCiAgdXNlRWZmZWN0KCgpID0+IHsKICAgIGlmICh0YWIgPT09ICdtZXJnZScpIHsKICAgICAgYXhpb3MuZ2V0KCcvYXBpL2VtcGxveWVlcycpLnRoZW4ociA9PiBzZXRBbGxFbXBsb3llZXMoci5kYXRhKSk7CiAgICB9CiAgfSwgW3RhYl0pOwoKICBjb25zdCBoYW5kbGVFZGl0ID0gYXN5bmMgKCkgPT4gewogICAgc2V0RWRpdEVycm9yKCcnKTsKICAgIHNldEVkaXRTYXZpbmcodHJ1ZSk7CiAgICB0cnkgewogICAgICBhd2FpdCBheGlvcy5wYXRjaChgL2FwaS9lbXBsb3llZXMvJHtlbXBsb3llZS5pZH1gLCB7IG5hbWUsIGRlcGFydG1lbnQsIHN1cGVydmlzb3IgfSk7CiAgICAgIG9uU2F2ZWQoKTsKICAgICAgb25DbG9zZSgpOwogICAgfSBjYXRjaCAoZSkgewogICAgICBzZXRFZGl0RXJyb3IoZS5yZXNwb25zZT8uZGF0YT8uZXJyb3IgfHwgJ0ZhaWxlZCB0byBzYXZlIGNoYW5nZXMnKTsKICAgIH0gZmluYWxseSB7CiAgICAgIHNldEVkaXRTYXZpbmcoZmFsc2UpOwogICAgfQogIH07CgogIGNvbnN0IGhhbmRsZU1lcmdlID0gYXN5bmMgKCkgPT4gewogICAgaWYgKCFzb3VyY2VJZCkgcmV0dXJuIHNldE1lcmdlRXJyb3IoJ1NlbGVjdCBhbiBlbXBsb3llZSB0byBtZXJnZSBpbicpOwogICAgc2V0TWVyZ2VFcnJvcignJyk7CiAgICBzZXRNZXJnaW5nKHRydWUpOwogICAgdHJ5IHsKICAgICAgY29uc3QgciA9IGF3YWl0IGF4aW9zLnBvc3QoYC9hcGkvZW1wbG95ZWVzLyR7ZW1wbG95ZWUuaWR9L21lcmdlYCwgeyBzb3VyY2VfaWQ6IHBhcnNlSW50KHNvdXJjZUlkKSB9KTsKICAgICAgc2V0TWVyZ2VSZXN1bHQoci5kYXRhKTsKICAgICAgb25TYXZlZCgpOyAvLyByZWZyZXNoIGRhc2hib2FyZCAvIHBhcmVudCBsaXN0CiAgICB9IGNhdGNoIChlKSB7CiAgICAgIHNldE1lcmdlRXJyb3IoZS5yZXNwb25zZT8uZGF0YT8uZXJyb3IgfHwgJ01lcmdlIGZhaWxlZCcpOwogICAgfSBmaW5hbGx5IHsKICAgICAgc2V0TWVyZ2luZyhmYWxzZSk7CiAgICB9CiAgfTsKCiAgY29uc3Qgb3RoZXJFbXBsb3llZXMgPSBhbGxFbXBsb3llZXMuZmlsdGVyKGUgPT4gZS5pZCAhPT0gZW1wbG95ZWUuaWQpOwoKICByZXR1cm4gKAogICAgPGRpdiBzdHlsZT17cy5vdmVybGF5fSBvbkNsaWNrPXtlID0+IGUudGFyZ2V0ID09PSBlLmN1cnJlbnRUYXJnZXQgJiYgb25DbG9zZSgpfT4KICAgICAgPGRpdiBzdHlsZT17cy5tb2RhbH0+CiAgICAgICAgPGRpdiBzdHlsZT17cy5oZWFkZXJ9PgogICAgICAgICAgPGRpdiBzdHlsZT17cy50aXRsZX0+RWRpdCBFbXBsb3llZTwvZGl2PgogICAgICAgICAgPGJ1dHRvbiBzdHlsZT17cy5jbG9zZUJ0bn0gb25DbGljaz17b25DbG9zZX0+4pyVPC9idXR0b24+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBzdHlsZT17cy5ib2R5fT4KICAgICAgICAgIDxkaXYgc3R5bGU9e3MudGFic30+CiAgICAgICAgICAgIDxidXR0b24gc3R5bGU9e3MudGFiKHRhYiA9PT0gJ2VkaXQnKX0gIG9uQ2xpY2s9eygpID0+IHNldFRhYignZWRpdCcpfT5FZGl0IERldGFpbHM8L2J1dHRvbj4KICAgICAgICAgICAgPGJ1dHRvbiBzdHlsZT17cy50YWIodGFiID09PSAnbWVyZ2UnKX0gb25DbGljaz17KCkgPT4gc2V0VGFiKCdtZXJnZScpfT5NZXJnZSBEdXBsaWNhdGU8L2J1dHRvbj4KICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgIHt0YWIgPT09ICdlZGl0JyAmJiAoCiAgICAgICAgICAgIDw+CiAgICAgICAgICAgICAge2VkaXRFcnJvciAmJiA8ZGl2IHN0eWxlPXtzLmVycm9yfT57ZWRpdEVycm9yfTwvZGl2Pn0KICAgICAgICAgICAgICA8ZGl2IHN0eWxlPXtzLmxhYmVsfT5GdWxsIE5hbWU8L2Rpdj4KICAgICAgICAgICAgICA8aW5wdXQgc3R5bGU9e3MuaW5wdXR9IHZhbHVlPXtuYW1lfSBvbkNoYW5nZT17ZSA9PiBzZXROYW1lKGUudGFyZ2V0LnZhbHVlKX0gLz4KICAgICAgICAgICAgICA8ZGl2IHN0eWxlPXtzLmxhYmVsfT5EZXBhcnRtZW50PC9kaXY+CiAgICAgICAgICAgICAgPHNlbGVjdCBzdHlsZT17cy5zZWxlY3R9IHZhbHVlPXtkZXBhcnRtZW50fSBvbkNoYW5nZT17ZSA9PiBzZXREZXBhcnRtZW50KGUudGFyZ2V0LnZhbHVlKX0+CiAgICAgICAgICAgICAgICA8b3B0aW9uIHZhbHVlPSIiPuKAlCBTZWxlY3QgRGVwYXJ0bWVudCDigJQ8L29wdGlvbj4KICAgICAgICAgICAgICAgIHtERVBBUlRNRU5UUy5tYXAoZCA9PiAoCiAgICAgICAgICAgICAgICAgIDxvcHRpb24ga2V5PXtkfSB2YWx1ZT17ZH0+e2R9PC9vcHRpb24+CiAgICAgICAgICAgICAgICApKX0KICAgICAgICAgICAgICA8L3NlbGVjdD4KICAgICAgICAgICAgICA8ZGl2IHN0eWxlPXtzLmxhYmVsfT5TdXBlcnZpc29yPC9kaXY+CiAgICAgICAgICAgICAgPGlucHV0IHN0eWxlPXtzLmlucHV0fSB2YWx1ZT17c3VwZXJ2aXNvcn0gb25DaGFuZ2U9e2UgPT4gc2V0U3VwZXJ2aXNvcihlLnRhcmdldC52YWx1ZSl9IHBsYWNlaG9sZGVyPSJPcHRpb25hbCIgLz4KICAgICAgICAgICAgICA8ZGl2IHN0eWxlPXtzLnJvd30+CiAgICAgICAgICAgICAgICA8YnV0dG9uIHN0eWxlPXtzLmJ0bignIzg4OCcpfSBvbkNsaWNrPXtvbkNsb3NlfT5DYW5jZWw8L2J1dHRvbj4KICAgICAgICAgICAgICAgIDxidXR0b24gc3R5bGU9e3MuYnRuKCcjZmZmJywgJyM2NjdlZWEnKX0gb25DbGljaz17aGFuZGxlRWRpdH0gZGlzYWJsZWQ9e2VkaXRTYXZpbmd9PgogICAgICAgICAgICAgICAgICB7ZWRpdFNhdmluZyA/ICdTYXZpbmfigKYnIDogJ1NhdmUgQ2hhbmdlcyd9CiAgICAgICAgICAgICAgICA8L2J1dHRvbj4KICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPC8+CiAgICAgICAgICApfQoKICAgICAgICAgIHt0YWIgPT09ICdtZXJnZScgJiYgKAogICAgICAgICAgICA8PgogICAgICAgICAgICAgIHttZXJnZVJlc3VsdCA/ICgKICAgICAgICAgICAgICAgIDxkaXYgc3R5bGU9e3Muc3VjY2Vzc30+CiAgICAgICAgICAgICAgICAgIOKckyBNZXJnZSBjb21wbGV0ZSDigJQge21lcmdlUmVzdWx0LnZpb2xhdGlvbnNfcmVhc3NpZ25lZH0gdmlvbGF0aW9ue21lcmdlUmVzdWx0LnZpb2xhdGlvbnNfcmVhc3NpZ25lZCAhPT0gMSA/ICdzJyA6ICcnfSByZWFzc2lnbmVkCiAgICAgICAgICAgICAgICAgIHRvIDxzdHJvbmc+e2VtcGxveWVlLm5hbWV9PC9zdHJvbmc+LiBUaGUgZHVwbGljYXRlIHJlY29yZCBoYXMgYmVlbiByZW1vdmVkLgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgKSA6ICgKICAgICAgICAgICAgICAgIDw+CiAgICAgICAgICAgICAgICAgIDxkaXYgc3R5bGU9e3MubWVyZ2VXYXJuaW5nfT4KICAgICAgICAgICAgICAgICAgICDimqAgVGhpcyB3aWxsIHJlYXNzaWduIDxzdHJvbmc+YWxsIHZpb2xhdGlvbnM8L3N0cm9uZz4gZnJvbSB0aGUgc2VsZWN0ZWQgZW1wbG95ZWUgaW50b3snICd9CiAgICAgICAgICAgICAgICAgICAgPHN0cm9uZz57ZW1wbG95ZWUubmFtZX08L3N0cm9uZz4sIHRoZW4gcGVybWFuZW50bHkgZGVsZXRlIHRoZSBkdXBsaWNhdGUgcmVjb3JkLgogICAgICAgICAgICAgICAgICAgIFRoaXMgY2Fubm90IGJlIHVuZG9uZS4KICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgIHttZXJnZUVycm9yICYmIDxkaXYgc3R5bGU9e3MuZXJyb3J9PnttZXJnZUVycm9yfTwvZGl2Pn0KICAgICAgICAgICAgICAgICAgPGRpdiBzdHlsZT17cy5sYWJlbH0+RHVwbGljYXRlIHRvIG1lcmdlIGludG8ge2VtcGxveWVlLm5hbWV9PC9kaXY+CiAgICAgICAgICAgICAgICAgIDxzZWxlY3Qgc3R5bGU9e3Muc2VsZWN0fSB2YWx1ZT17c291cmNlSWR9IG9uQ2hhbmdlPXtlID0+IHNldFNvdXJjZUlkKGUudGFyZ2V0LnZhbHVlKX0+CiAgICAgICAgICAgICAgICAgICAgPG9wdGlvbiB2YWx1ZT0iIj7igJQgc2VsZWN0IGVtcGxveWVlIOKAlDwvb3B0aW9uPgogICAgICAgICAgICAgICAgICAgIHtvdGhlckVtcGxveWVlcy5tYXAoZSA9PiAoCiAgICAgICAgICAgICAgICAgICAgICA8b3B0aW9uIGtleT17ZS5pZH0gdmFsdWU9e2UuaWR9PntlLm5hbWV9e2UuZGVwYXJ0bWVudCA/IGAgKCR7ZS5kZXBhcnRtZW50fSlgIDogJyd9PC9vcHRpb24+CiAgICAgICAgICAgICAgICAgICAgKSl9CiAgICAgICAgICAgICAgICAgIDwvc2VsZWN0PgogICAgICAgICAgICAgICAgICA8ZGl2IHN0eWxlPXtzLnJvd30+CiAgICAgICAgICAgICAgICAgICAgPGJ1dHRvbiBzdHlsZT17cy5idG4oJyM4ODgnKX0gb25DbGljaz17b25DbG9zZX0+Q2FuY2VsPC9idXR0b24+CiAgICAgICAgICAgICAgICAgICAgPGJ1dHRvbiBzdHlsZT17cy5idG4oJyNmZmYnLCAnI2MwMzkyYicpfSBvbkNsaWNrPXtoYW5kbGVNZXJnZX0gZGlzYWJsZWQ9e21lcmdpbmcgfHwgIXNvdXJjZUlkfT4KICAgICAgICAgICAgICAgICAgICAgIHttZXJnaW5nID8gJ01lcmdpbmfigKYnIDogJ01lcmdlICYgRGVsZXRlIER1cGxpY2F0ZSd9CiAgICAgICAgICAgICAgICAgICAgPC9idXR0b24+CiAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgPC8+CiAgICAgICAgICAgICAgKX0KICAgICAgICAgICAgICB7bWVyZ2VSZXN1bHQgJiYgKAogICAgICAgICAgICAgICAgPGRpdiBzdHlsZT17cy5yb3d9PgogICAgICAgICAgICAgICAgICA8YnV0dG9uIHN0eWxlPXtzLmJ0bignI2ZmZicsICcjNjY3ZWVhJyl9IG9uQ2xpY2s9e29uQ2xvc2V9PkRvbmU8L2J1dHRvbj4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICl9CiAgICAgICAgICAgIDwvPgogICAgICAgICAgKX0KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICApOwp9Cg== \ No newline at end of file diff --git a/client/src/components/ViolationForm.jsx b/client/src/components/ViolationForm.jsx index 3367803..6c1d7cf 100755 --- a/client/src/components/ViolationForm.jsx +++ b/client/src/components/ViolationForm.jsx @@ -1,343 +1 @@ -import React, { useState, useEffect } from 'react'; -import axios from 'axios'; -import { violationData, violationGroups } from '../data/violations'; -import useEmployeeIntelligence from '../hooks/useEmployeeIntelligence'; -import CpasBadge from './CpasBadge'; -import TierWarning from './TierWarning'; -import ViolationHistory from './ViolationHistory'; -import { useToast } from './ToastProvider'; - -const s = { - content: { padding: '32px 40px', background: '#111217', borderRadius: '10px', color: '#f8f9fa' }, - section: { background: '#181924', borderLeft: '4px solid #d4af37', padding: '20px', marginBottom: '30px', borderRadius: '4px', border: '1px solid #2a2b3a' }, - sectionTitle: { color: '#f8f9fa', fontSize: '20px', marginBottom: '15px', fontWeight: 700 }, - grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '15px', marginTop: '15px' }, - item: { display: 'flex', flexDirection: 'column' }, - label: { fontWeight: 600, color: '#e5e7f1', marginBottom: '5px', fontSize: '13px' }, - input: { padding: '10px', border: '1px solid #333544', borderRadius: '4px', fontSize: '14px', fontFamily: 'inherit', background: '#050608', color: '#f8f9fa' }, - fullCol: { gridColumn: '1 / -1' }, - contextBox: { background: '#141623', border: '1px solid #333544', borderRadius: '4px', padding: '10px', fontSize: '12px', color: '#d1d3e0', marginTop: '4px' }, - repeatBadge: { display: 'inline-block', marginLeft: '8px', padding: '1px 7px', borderRadius: '10px', fontSize: '11px', fontWeight: 700, background: '#3b2e00', color: '#ffd666', border: '1px solid #d4af37' }, - repeatWarn: { background: '#3b2e00', border: '1px solid #d4af37', borderRadius: '4px', padding: '8px 12px', marginTop: '6px', fontSize: '12px', color: '#ffdf8a' }, - pointBox: { background: '#181200', border: '2px solid #d4af37', padding: '15px', borderRadius: '6px', marginTop: '15px', textAlign: 'center' }, - pointValue: { fontSize: '24px', fontWeight: 'bold', color: '#ffd666', margin: '10px 0' }, - scoreRow: { display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '14px', flexWrap: 'wrap' }, - btnRow: { display: 'flex', gap: '15px', justifyContent: 'center', marginTop: '30px', flexWrap: 'wrap' }, - btnPrimary: { padding: '15px 40px', fontSize: '16px', fontWeight: 600, border: 'none', borderRadius: '6px', cursor: 'pointer', background: 'linear-gradient(135deg, #d4af37 0%, #ffdf8a 100%)', color: '#000', textTransform: 'uppercase' }, - btnPdf: { padding: '15px 40px', fontSize: '16px', fontWeight: 600, border: 'none', borderRadius: '6px', cursor: 'pointer', background: 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)', color: 'white', textTransform: 'uppercase' }, - btnSecondary: { padding: '15px 40px', fontSize: '16px', fontWeight: 600, border: '1px solid #333544', borderRadius: '6px', cursor: 'pointer', background: '#050608', color: '#f8f9fa', textTransform: 'uppercase' }, - note: { background: '#141623', borderLeft: '4px solid #2196F3', padding: '15px', margin: '20px 0', borderRadius: '4px', fontSize: '13px', color: '#d1d3e0' }, - ackSection: { background: '#181924', borderLeft: '4px solid #2196F3', padding: '20px', marginBottom: '30px', borderRadius: '4px', border: '1px solid #2a2b3a' }, - ackHint: { fontSize: '12px', color: '#9ca0b8', marginTop: '4px', fontStyle: 'italic' }, -}; - -const EMPTY_FORM = { - employeeId: '', employeeName: '', department: '', supervisor: '', witnessName: '', - violationType: '', incidentDate: '', incidentTime: '', - amount: '', minutesLate: '', location: '', additionalDetails: '', points: 1, - acknowledgedBy: '', acknowledgedDate: '', -}; - -export default function ViolationForm() { - const [employees, setEmployees] = useState([]); - const [form, setForm] = useState(EMPTY_FORM); - const [violation, setViolation] = useState(null); - const [status, setStatus] = useState(null); - const [lastViolId, setLastViolId] = useState(null); - const [pdfLoading, setPdfLoading] = useState(false); - - const toast = useToast(); - const intel = useEmployeeIntelligence(form.employeeId || null); - - useEffect(() => { - axios.get('/api/employees').then(r => setEmployees(r.data)).catch(() => {}); - }, []); - - useEffect(() => { - if (!violation || !form.violationType) return; - const allTime = intel.countsAllTime[form.violationType]; - if (allTime && allTime.count >= 1 && violation.minPoints !== violation.maxPoints) { - setForm(prev => ({ ...prev, points: violation.maxPoints })); - } else { - setForm(prev => ({ ...prev, points: violation.minPoints })); - } - }, [form.violationType, violation, intel.countsAllTime]); - - const handleEmployeeSelect = e => { - const emp = employees.find(x => x.id === parseInt(e.target.value)); - if (!emp) return; - setForm(prev => ({ ...prev, employeeId: emp.id, employeeName: emp.name, department: emp.department || '', supervisor: emp.supervisor || '' })); - }; - - const handleViolationChange = e => { - const key = e.target.value; - const v = violationData[key] || null; - setViolation(v); - setForm(prev => ({ ...prev, violationType: key, points: v ? v.minPoints : 1 })); - }; - - const handleChange = e => setForm(prev => ({ ...prev, [e.target.name]: e.target.value })); - - const handleSubmit = async e => { - e.preventDefault(); - if (!form.violationType) { toast.warning('Please select a violation type.'); return; } - if (!form.employeeName) { toast.warning('Please enter an employee name.'); return; } - try { - const empRes = await axios.post('/api/employees', { name: form.employeeName, department: form.department, supervisor: form.supervisor }); - const employeeId = empRes.data.id; - const violRes = await axios.post('/api/violations', { - employee_id: employeeId, - violation_type: form.violationType, - violation_name: violation?.name || form.violationType, - category: violation?.category || 'General', - points: parseInt(form.points), - incident_date: form.incidentDate, - incident_time: form.incidentTime || null, - location: form.location || null, - details: form.additionalDetails || null, - witness_name: form.witnessName || null, - acknowledged_by: form.acknowledgedBy || null, - acknowledged_date: form.acknowledgedDate || null, - }); - - const newId = violRes.data.id; - setLastViolId(newId); - - const empList = await axios.get('/api/employees'); - setEmployees(empList.data); - - toast.success(`Violation #${newId} recorded — click Download PDF to save the document.`); - setStatus({ ok: true, msg: `✓ Violation #${newId} recorded — click Download PDF to save the document.` }); - setForm(EMPTY_FORM); - setViolation(null); - } catch (err) { - const msg = err.response?.data?.error || err.message; - toast.error(`Failed to submit: ${msg}`); - setStatus({ ok: false, msg: '✗ Error: ' + msg }); - } - }; - - const handleDownloadPdf = async () => { - if (!lastViolId) return; - setPdfLoading(true); - try { - const response = await axios.get(`/api/violations/${lastViolId}/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_Violation_${lastViolId}.pdf`; - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(url); - toast.success('PDF downloaded successfully.'); - } catch (err) { - toast.error('PDF generation failed: ' + err.message); - } finally { - setPdfLoading(false); - } - }; - - const showField = f => violation?.fields?.includes(f); - const priorCount90 = key => intel.counts90[key] || 0; - const isRepeat = key => (intel.countsAllTime[key]?.count || 0) >= 1; - - return ( -
- -
-

Employee Information

- - {intel.score && form.employeeId && ( -
- Current Standing: - - - {intel.score.violation_count} violation{intel.score.violation_count !== 1 ? 's' : ''} in last 90 days - -
- )} - - {employees.length > 0 && ( -
- - -
- )} - -
- {[['employeeName','Employee Name','text','John Doe'],['department','Department','text','Engineering'],['supervisor','Supervisor Name','text','Jane Smith'],['witnessName','Witness Name (Officer)','text','Officer Name']].map(([name,label,type,ph]) => ( -
- - -
- ))} -
-
- -
-
-

Violation Details

-
- -
- - - - {violation && ( -
- {violation.name} - {isRepeat(form.violationType) && form.employeeId && ( - - ★ Repeat — {intel.countsAllTime[form.violationType]?.count}x prior - - )} -
{violation.description}
- {violation.chapter} -
- )} - - {violation && isRepeat(form.violationType) && form.employeeId && violation.minPoints !== violation.maxPoints && ( -
- Repeat offense detected. Point slider set to maximum ({violation.maxPoints} pts) per recidivist policy. Adjust if needed. -
- )} -
- -
- - -
- - {showField('time') && ( -
- - -
- )} - {showField('minutes') && ( -
- - -
- )} - {showField('amount') && ( -
- - -
- )} - {showField('location') && ( -
- - -
- )} - {showField('description') && ( -
- -