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') && ( -
- -