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
This commit is contained in:
2026-03-07 21:30:29 -06:00
parent c4dd658aa7
commit 725dfa2963

View File

@@ -5,6 +5,7 @@ import useEmployeeIntelligence from '../hooks/useEmployeeIntelligence';
import CpasBadge from './CpasBadge'; import CpasBadge from './CpasBadge';
import TierWarning from './TierWarning'; import TierWarning from './TierWarning';
import ViolationHistory from './ViolationHistory'; import ViolationHistory from './ViolationHistory';
import { useToast } from './ToastProvider';
const s = { const s = {
content: { padding: '32px 40px', background: '#111217', borderRadius: '10px', color: '#f8f9fa' }, 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' }, 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' }, 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' }, 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' }, ackSection: { background: '#181924', borderLeft: '4px solid #2196F3', padding: '20px', marginBottom: '30px', borderRadius: '4px', border: '1px solid #2a2b3a' },
statusErr: { marginTop: '15px', padding: '15px', borderRadius: '6px', textAlign: 'center', fontWeight: 600, background: '#3c1114', color: '#ffb3b8', border: '1px solid #f5c6cb' }, ackHint: { fontSize: '12px', color: '#9ca0b8', marginTop: '4px', fontStyle: 'italic' },
}; };
const EMPTY_FORM = { const EMPTY_FORM = {
employeeId: '', employeeName: '', department: '', supervisor: '', witnessName: '', employeeId: '', employeeName: '', department: '', supervisor: '', witnessName: '',
violationType: '', incidentDate: '', incidentTime: '', violationType: '', incidentDate: '', incidentTime: '',
amount: '', minutesLate: '', location: '', additionalDetails: '', points: 1, amount: '', minutesLate: '', location: '', additionalDetails: '', points: 1,
acknowledgedBy: '', acknowledgedDate: '',
}; };
export default function ViolationForm() { export default function ViolationForm() {
@@ -44,6 +46,7 @@ export default function ViolationForm() {
const [lastViolId, setLastViolId] = useState(null); const [lastViolId, setLastViolId] = useState(null);
const [pdfLoading, setPdfLoading] = useState(false); const [pdfLoading, setPdfLoading] = useState(false);
const toast = useToast();
const intel = useEmployeeIntelligence(form.employeeId || null); const intel = useEmployeeIntelligence(form.employeeId || null);
useEffect(() => { useEffect(() => {
@@ -77,8 +80,8 @@ export default function ViolationForm() {
const handleSubmit = async e => { const handleSubmit = async e => {
e.preventDefault(); e.preventDefault();
if (!form.violationType) return setStatus({ ok: false, msg: 'Please select a violation type.' }); if (!form.violationType) { toast.warning('Please select a violation type.'); return; }
if (!form.employeeName) return setStatus({ ok: false, msg: 'Please enter an employee name.' }); if (!form.employeeName) { toast.warning('Please enter an employee name.'); return; }
try { try {
const empRes = await axios.post('/api/employees', { name: form.employeeName, department: form.department, supervisor: form.supervisor }); const empRes = await axios.post('/api/employees', { name: form.employeeName, department: form.department, supervisor: form.supervisor });
const employeeId = empRes.data.id; const employeeId = empRes.data.id;
@@ -93,6 +96,8 @@ export default function ViolationForm() {
location: form.location || null, location: form.location || null,
details: form.additionalDetails || null, details: form.additionalDetails || null,
witness_name: form.witnessName || null, witness_name: form.witnessName || null,
acknowledged_by: form.acknowledgedBy || null,
acknowledged_date: form.acknowledgedDate || null,
}); });
const newId = violRes.data.id; const newId = violRes.data.id;
@@ -101,11 +106,14 @@ export default function ViolationForm() {
const empList = await axios.get('/api/employees'); const empList = await axios.get('/api/employees');
setEmployees(empList.data); 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.` }); setStatus({ ok: true, msg: `✓ Violation #${newId} recorded — click Download PDF to save the document.` });
setForm(EMPTY_FORM); setForm(EMPTY_FORM);
setViolation(null); setViolation(null);
} catch (err) { } 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.click();
link.remove(); link.remove();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
toast.success('PDF downloaded successfully.');
} catch (err) { } catch (err) {
setStatus({ ok: false, msg: '✗ PDF generation failed: ' + err.message }); toast.error('PDF generation failed: ' + err.message);
} finally { } finally {
setPdfLoading(false); setPdfLoading(false);
} }
@@ -275,6 +284,27 @@ export default function ViolationForm() {
)} )}
</div> </div>
{/* Acknowledgment Signature Section */}
<div style={s.ackSection}>
<h2 style={{ ...s.sectionTitle, fontSize: '17px' }}>Employee Acknowledgment</h2>
<p style={{ fontSize: '12px', color: '#9ca0b8', marginBottom: '14px', lineHeight: 1.6 }}>
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.
</p>
<div style={s.grid}>
<div style={s.item}>
<label style={s.label}>Acknowledged By (Employee Name):</label>
<input style={s.input} type="text" name="acknowledgedBy" value={form.acknowledgedBy} onChange={handleChange} placeholder="Employee's printed name" />
<div style={s.ackHint}>Leave blank if employee is not present or declines to sign</div>
</div>
<div style={s.item}>
<label style={s.label}>Acknowledgment Date:</label>
<input style={s.input} type="date" name="acknowledgedDate" value={form.acknowledgedDate} onChange={handleChange} />
<div style={s.ackHint}>Date the employee received and acknowledged this document</div>
</div>
</div>
</div>
<div style={s.btnRow}> <div style={s.btnRow}>
<button type="submit" style={s.btnPrimary}>Submit Violation</button> <button type="submit" style={s.btnPrimary}>Submit Violation</button>
<button type="button" style={s.btnSecondary} onClick={() => { setForm(EMPTY_FORM); setViolation(null); setStatus(null); setLastViolId(null); }}> <button type="button" style={s.btnSecondary} onClick={() => { setForm(EMPTY_FORM); setViolation(null); setStatus(null); setLastViolId(null); }}>
@@ -298,7 +328,7 @@ export default function ViolationForm() {
</div> </div>
)} )}
{status && <div style={status.ok ? s.statusOk : s.statusErr}>{status.msg}</div>} {status && <div style={status.ok ? { marginTop: '15px', padding: '15px', borderRadius: '6px', textAlign: 'center', fontWeight: 600, background: '#053321', color: '#9ef7c1', border: '1px solid #0f5132' } : { marginTop: '15px', padding: '15px', borderRadius: '6px', textAlign: 'center', fontWeight: 600, background: '#3c1114', color: '#ffb3b8', border: '1px solid #f5c6cb' }}>{status.msg}</div>}
</form> </form>
{form.employeeId && ( {form.employeeId && (