2026-03-06 11:33:32 -06:00
import React , { useState , useEffect } from 'react' ;
import axios from 'axios' ;
import { violationData , violationGroups } from '../data/violations' ;
2026-03-06 12:02:52 -06:00
import useEmployeeIntelligence from '../hooks/useEmployeeIntelligence' ;
import CpasBadge from './CpasBadge' ;
import TierWarning from './TierWarning' ;
import ViolationHistory from './ViolationHistory' ;
2026-03-06 11:33:32 -06:00
const s = {
2026-03-06 14:20:57 -06:00
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' },
2026-03-06 11:33:32 -06:00
};
const EMPTY_FORM = {
2026-03-06 14:20:57 -06:00
employeeId : '' , employeeName : '' , department : '' , supervisor : '' , witnessName : '' ,
violationType : '' , incidentDate : '' , incidentTime : '' ,
amount : '' , minutesLate : '' , location : '' , additionalDetails : '' , points : 1 ,
2026-03-06 11:33:32 -06:00
};
export default function ViolationForm () {
2026-03-06 14:20:57 -06:00
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 );
2026-03-06 11:33:32 -06:00
2026-03-06 14:20:57 -06:00
const intel = useEmployeeIntelligence ( form . employeeId || null );
2026-03-06 12:02:52 -06:00
2026-03-06 14:20:57 -06:00
useEffect (() => {
axios . get ( '/api/employees' ). then ( r => setEmployees ( r . data )). catch (() => {});
}, []);
2026-03-06 11:33:32 -06:00
2026-03-06 14:20:57 -06:00
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 ]);
2026-03-06 12:02:52 -06:00
2026-03-06 14:20:57 -06:00
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 || '' }));
};
2026-03-06 11:33:32 -06:00
2026-03-06 14:20:57 -06:00
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 }));
};
2026-03-06 11:33:32 -06:00
2026-03-06 14:20:57 -06:00
const handleChange = e => setForm ( prev => ({ ... prev , [ e . target . name ] : e . target . value }));
2026-03-06 11:33:32 -06:00
2026-03-06 14:20:57 -06:00
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 ,
});
2026-03-06 12:19:55 -06:00
2026-03-06 14:20:57 -06:00
const newId = violRes . data . id ;
setLastViolId ( newId );
2026-03-06 12:19:55 -06:00
2026-03-06 14:20:57 -06:00
const empList = await axios . get ( '/api/employees' );
setEmployees ( empList . data );
2026-03-06 12:19:55 -06:00
2026-03-06 14:20:57 -06:00
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 ) });
}
};
2026-03-06 11:33:32 -06:00
2026-03-06 14:20:57 -06:00
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 );
}
};
2026-03-06 12:19:55 -06:00
2026-03-06 14:20:57 -06:00
const showField = f => violation ? . fields ? . includes ( f );
const priorCount90 = key => intel . counts90 [ key ] || 0 ;
const isRepeat = key => ( intel . countsAllTime [ key ] ? . count || 0 ) >= 1 ;
2026-03-06 11:33:32 -06:00
2026-03-06 14:20:57 -06:00
return (
< div style = { s . content }>
2026-03-06 12:02:52 -06:00
2026-03-06 14:20:57 -06:00
< div style = { s . section }>
< h2 style = { s . sectionTitle }> Employee Information </ h2 >
2026-03-06 12:02:52 -06:00
2026-03-06 14:20:57 -06:00
{ intel . score && form . employeeId && (
< div style = { s . scoreRow }>
< span style = {{ fontSize : '13px' , color : '#d1d3e0' , fontWeight : 600 }}> Current Standing : </ span >
< CpasBadge points = { intel . score . active_points } />
< span style = {{ fontSize : '12px' , color : '#9ca0b8' }}>
{ intel . score . violation_count } violation { intel . score . violation_count !== 1 ? 's' : '' } in last 90 days
</ span >
</ div >
)}
2026-03-06 12:02:52 -06:00
2026-03-06 14:20:57 -06:00
{ employees . length > 0 && (
< div style = {{ marginBottom : '12px' }}>
< label style = { s . label }> Quick - Select Existing Employee : </ label >
< select style = { s . input } onChange = { handleEmployeeSelect } value = { form . employeeId || '' }>
< option value = "" > -- Select existing or enter new below -- </ option >
{ employees . map ( e => (
< option key = { e . id } value = { e . id }>{ e . name }{ e . department ? ` — ${ e . department } ` : '' }</ option >
))}
</ select >
</ div >
)}
2026-03-06 12:02:52 -06:00
2026-03-06 14:20:57 -06:00
< div style = { s . grid }>
{[[ '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 ]) => (
< div key = { name } style = { s . item }>
< label style = { s . label }>{ label } : </ label >
< input style = { s . input } type = { type } name = { name } value = { form [ name ]} onChange = { handleChange } placeholder = { ph } />
</ div >
))}
</ div >
</ div >
< form onSubmit = { handleSubmit }>
< div style = { s . section }>
< h2 style = { s . sectionTitle }> Violation Details </ h2 >
< div style = { s . grid }>
< div style = {{ ... s . item , ... s . fullCol }}>
< label style = { s . label }> Violation Type : </ label >
< select style = { s . input } value = { form . violationType } onChange = { handleViolationChange } required >
< option value = "" > -- Select Violation Type -- </ option >
{ Object . entries ( violationGroups ). map (([ group , items ]) => (
< optgroup key = { group } label = { group }>
{ items . map ( v => {
const prior = priorCount90 ( v . key );
return (
< option key = { v . key } value = { v . key }>
{ v . name }{ prior > 0 ? ` ★ ${ prior } x in 90 days` : '' }
</ option >
);
})}
</ optgroup >
))}
</ select >
{ violation && (
< div style = { s . contextBox }>
< strong >{ violation . name }</ strong >
{ isRepeat ( form . violationType ) && form . employeeId && (
< span style = { s . repeatBadge }>
★ Repeat — { intel . countsAllTime [ form . violationType ] ? . count } x prior
</ span >
)}
< br />{ violation . description }< br />
< span style = {{ fontSize : '11px' , color : '#a0a3ba' }}>{ violation . chapter }</ span >
2026-03-06 11:33:32 -06:00
</ div >
2026-03-06 14:20:57 -06:00
)}
{ violation && isRepeat ( form . violationType ) && form . employeeId && violation . minPoints !== violation . maxPoints && (
< div style = { s . repeatWarn }>
< strong > Repeat offense detected .</ strong > Point slider set to maximum ({ violation . maxPoints } pts ) per recidivist policy . Adjust if needed .
</ div >
)}
2026-03-06 11:33:32 -06:00
</ div >
2026-03-06 14:20:57 -06:00
< div style = { s . item }>
< label style = { s . label }> Incident Date : </ label >
< input style = { s . input } type = "date" name = "incidentDate" value = { form . incidentDate } onChange = { handleChange } required />
</ div >
2026-03-06 12:02:52 -06:00
2026-03-06 14:20:57 -06:00
{ showField ( 'time' ) && (
< div style = { s . item }>
< label style = { s . label }> Incident Time : </ label >
< input style = { s . input } type = "time" name = "incidentTime" value = { form . incidentTime } onChange = { handleChange } />
</ div >
2026-03-06 12:02:52 -06:00
)}
2026-03-06 14:20:57 -06:00
{ showField ( 'minutes' ) && (
< div style = { s . item }>
< label style = { s . label }> Minutes Late : </ label >
< input style = { s . input } type = "number" name = "minutesLate" value = { form . minutesLate } onChange = { handleChange } placeholder = "15" />
</ div >
)}
{ showField ( 'amount' ) && (
< div style = { s . item }>
< label style = { s . label }> Amount / Value : </ label >
< input style = { s . input } type = "text" name = "amount" value = { form . amount } onChange = { handleChange } placeholder = "$150.00" />
</ div >
)}
{ showField ( 'location' ) && (
< div style = {{ ... s . item , ... s . fullCol }}>
< label style = { s . label }> Location / Context : </ label >
< input style = { s . input } type = "text" name = "location" value = { form . location } onChange = { handleChange } placeholder = "Office, vehicle, facility area, etc." />
</ div >
)}
{ showField ( 'description' ) && (
< div style = {{ ... s . item , ... s . fullCol }}>
< label style = { s . label }> Additional Details : </ label >
< textarea style = {{ ... s . input , resize : 'vertical' , minHeight : '80px' }} name = "additionalDetails" value = { form . additionalDetails } onChange = { handleChange } placeholder = "Provide specific context, observations, or details..." />
</ div >
)}
</ div >
2026-03-06 12:02:52 -06:00
2026-03-06 14:20:57 -06:00
{ intel . score && violation && (
< TierWarning
currentPoints = { intel . score . active_points }
addingPoints = { parseInt ( form . points ) || 0 }
/>
)}
{ violation && (
< div style = { s . pointBox }>
< h4 style = {{ color : '#ffdf8a' , marginBottom : '10px' }}> CPAS Point Assessment </ h4 >
< p style = {{ margin : 0 }}>
{ violation . name } : { violation . minPoints === violation . maxPoints
? ` ${ violation . minPoints } Points (Fixed)`
: ` ${ violation . minPoints } – ${ violation . maxPoints } Points` }
</ p >
< input style = {{ width : '100%' , marginTop : '10px' }} type = "range" name = "points"
min = { violation . minPoints } max = { violation . maxPoints }
value = { form . points } onChange = { handleChange } />
< div style = { s . pointValue }>{ form . points } Points </ div >
< p style = {{ fontSize : '12px' , color : '#d1d3e0' }}> Adjust to reflect severity and context </ p >
</ div >
)}
2026-03-06 11:33:32 -06:00
</ div >
2026-03-06 14:20:57 -06:00
< 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 ); }}>
Clear Form
</ button >
</ div >
{ lastViolId && status ? . ok && (
< div style = {{ textAlign : 'center' , marginTop : '16px' }}>
< button
type = "button"
style = {{ ... s . btnPdf , opacity : pdfLoading ? 0.7 : 1 }}
onClick = { handleDownloadPdf }
disabled = { pdfLoading }
>
{ pdfLoading ? '⏳ Generating PDF...' : '⬇ Download PDF' }
</ button >
< p style = {{ fontSize : '11px' , color : '#9ca0b8' , marginTop : '6px' }}>
Violation # { lastViolId } — click to download the signed violation document
</ p >
</ div >
)}
{ status && < div style = { status . ok ? s . statusOk : s . statusErr }>{ status . msg }</ div >}
</ form >
{ form . employeeId && (
< div style = { s . section }>
< h2 style = { s . sectionTitle }> Violation History </ h2 >
< ViolationHistory history = { intel . history } loading = { intel . loading } />
</ div >
)}
</ div >
);
2026-03-06 11:33:32 -06:00
}