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:
@@ -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() {
|
||||
)}
|
||||
</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}>
|
||||
<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); }}>
|
||||
@@ -298,7 +328,7 @@ export default function ViolationForm() {
|
||||
</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.employeeId && (
|
||||
|
||||
Reference in New Issue
Block a user