From 725dfa29638d61c6e0b90a6a5e4a3982106abdcf Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 21:30:29 -0600 Subject: [PATCH] feat: add acknowledgment signature fields + toast notifications to ViolationForm - New "Employee Acknowledgment" section with acknowledged_by name and date - Replaces blank signature line on PDF with recorded acknowledgment - Toast notifications for submit success/error, PDF download, and validation warnings - Inline status messages retained as fallback --- client/src/components/ViolationForm.jsx | 44 +++++++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/client/src/components/ViolationForm.jsx b/client/src/components/ViolationForm.jsx index ad6d329..3367803 100755 --- a/client/src/components/ViolationForm.jsx +++ b/client/src/components/ViolationForm.jsx @@ -5,6 +5,7 @@ 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' }, @@ -26,14 +27,15 @@ const s = { 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' }, - statusOk: { marginTop: '15px', padding: '15px', borderRadius: '6px', textAlign: 'center', fontWeight: 600, background: '#053321', color: '#9ef7c1', border: '1px solid #0f5132' }, - statusErr: { marginTop: '15px', padding: '15px', borderRadius: '6px', textAlign: 'center', fontWeight: 600, background: '#3c1114', color: '#ffb3b8', border: '1px solid #f5c6cb' }, + 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() { @@ -44,6 +46,7 @@ export default function ViolationForm() { const [lastViolId, setLastViolId] = useState(null); const [pdfLoading, setPdfLoading] = useState(false); + const toast = useToast(); const intel = useEmployeeIntelligence(form.employeeId || null); useEffect(() => { @@ -77,8 +80,8 @@ export default function ViolationForm() { const handleSubmit = async e => { e.preventDefault(); - if (!form.violationType) return setStatus({ ok: false, msg: 'Please select a violation type.' }); - if (!form.employeeName) return setStatus({ ok: false, msg: 'Please enter an employee name.' }); + 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; @@ -93,6 +96,8 @@ export default function ViolationForm() { 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; @@ -101,11 +106,14 @@ export default function ViolationForm() { 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) { - setStatus({ ok: false, msg: '✗ Error: ' + (err.response?.data?.error || err.message) }); + const msg = err.response?.data?.error || err.message; + toast.error(`Failed to submit: ${msg}`); + setStatus({ ok: false, msg: '✗ Error: ' + msg }); } }; @@ -122,8 +130,9 @@ export default function ViolationForm() { link.click(); link.remove(); window.URL.revokeObjectURL(url); + toast.success('PDF downloaded successfully.'); } catch (err) { - setStatus({ ok: false, msg: '✗ PDF generation failed: ' + err.message }); + toast.error('PDF generation failed: ' + err.message); } finally { setPdfLoading(false); } @@ -275,6 +284,27 @@ export default function ViolationForm() { )} + {/* Acknowledgment Signature Section */} +
+

Employee Acknowledgment

+

+ If the employee is present and acknowledges receipt of this violation, enter their name and the date below. + This replaces the blank signature line on the PDF with a recorded acknowledgment. +

+
+
+ + +
Leave blank if employee is not present or declines to sign
+
+
+ + +
Date the employee received and acknowledged this document
+
+
+
+
)} - {status &&
{status.msg}
} + {status &&
{status.msg}
} {form.employeeId && (