diff --git a/client/src/components/ViolationForm.jsx b/client/src/components/ViolationForm.jsx index 165515c..f33a3ef 100755 --- a/client/src/components/ViolationForm.jsx +++ b/client/src/components/ViolationForm.jsx @@ -1,3 +1,4 @@ +\ import React, { useState, useEffect } from 'react'; import axios from 'axios'; import { violationData, violationGroups } from '../data/violations'; @@ -7,313 +8,307 @@ import TierWarning from './TierWarning'; import ViolationHistory from './ViolationHistory'; const s = { - content: { padding: '40px' }, - section: { background: '#f8f9fa', borderLeft: '4px solid #667eea', padding: '20px', marginBottom: '30px', borderRadius: '4px' }, - sectionTitle: { color: '#2c3e50', fontSize: '20px', marginBottom: '15px' }, - grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '15px', marginTop: '15px' }, - item: { display: 'flex', flexDirection: 'column' }, - label: { fontWeight: 600, color: '#555', marginBottom: '5px', fontSize: '13px' }, - input: { padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '14px', fontFamily: 'inherit' }, - fullCol: { gridColumn: '1 / -1' }, - contextBox: { background: '#f1f3f5', border: '1px solid #ced4da', borderRadius: '4px', padding: '10px', fontSize: '12px', color: '#444', marginTop: '4px' }, - repeatBadge: { display: 'inline-block', marginLeft: '8px', padding: '1px 7px', borderRadius: '10px', fontSize: '11px', fontWeight: 700, background: '#fff3cd', color: '#856404', border: '1px solid #ffc107' }, - repeatWarn: { background: '#fff3cd', border: '1px solid #ffc107', borderRadius: '4px', padding: '8px 12px', marginTop: '6px', fontSize: '12px', color: '#856404' }, - pointBox: { background: '#fff3cd', border: '2px solid #ffc107', padding: '15px', borderRadius: '6px', marginTop: '15px', textAlign: 'center' }, - pointValue: { fontSize: '24px', fontWeight: 'bold', color: '#667eea', 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, #667eea 0%, #764ba2 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: 'none', borderRadius: '6px', cursor: 'pointer', background: '#6c757d', color: 'white', textTransform: 'uppercase' }, - note: { background: '#e7f3ff', borderLeft: '4px solid #2196F3', padding: '15px', margin: '20px 0', borderRadius: '4px' }, - statusOk: { marginTop: '15px', padding: '15px', borderRadius: '6px', textAlign: 'center', fontWeight: 600, background: '#d4edda', color: '#155724', border: '1px solid #c3e6cb' }, - statusErr: { marginTop: '15px', padding: '15px', borderRadius: '6px', textAlign: 'center', fontWeight: 600, background: '#f8d7da', color: '#721c24', border: '1px solid #f5c6cb' }, + 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' }, + 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' }, }; const EMPTY_FORM = { - employeeId: '', employeeName: '', department: '', supervisor: '', witnessName: '', - violationType: '', incidentDate: '', incidentTime: '', - amount: '', minutesLate: '', location: '', additionalDetails: '', points: 1, + employeeId: '', employeeName: '', department: '', supervisor: '', witnessName: '', + violationType: '', incidentDate: '', incidentTime: '', + amount: '', minutesLate: '', location: '', additionalDetails: '', points: 1, }; 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); // ID of most recently saved violation - const [pdfLoading, setPdfLoading] = useState(false); + 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 intel = useEmployeeIntelligence(form.employeeId || null); + const intel = useEmployeeIntelligence(form.employeeId || null); - useEffect(() => { - axios.get('/api/employees').then(r => setEmployees(r.data)).catch(() => {}); - }, []); + 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]); + 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 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 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 handleChange = e => setForm(prev => ({ ...prev, [e.target.name]: e.target.value })); - 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.' }); - 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, - }); + 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.' }); + 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, + }); - const newId = violRes.data.id; - setLastViolId(newId); + const newId = violRes.data.id; + setLastViolId(newId); - const empList = await axios.get('/api/employees'); - setEmployees(empList.data); + const empList = await axios.get('/api/employees'); + setEmployees(empList.data); - 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) }); - } - }; + 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 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); - } catch (err) { - setStatus({ ok: false, msg: '✗ PDF generation failed: ' + err.message }); - } finally { - setPdfLoading(false); - } - }; + 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); + } catch (err) { + setStatus({ ok: false, msg: '✗ 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; + const showField = f => violation?.fields?.includes(f); + const priorCount90 = key => intel.counts90[key] || 0; + const isRepeat = key => (intel.countsAllTime[key]?.count || 0) >= 1; - return ( -