2026-03-06 14:20:57 -06:00
\
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 ) ;
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 ) 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 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 ) } ) ;
}
} ;
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 ;
return (
< div style = { s . content } >
< div style = { s . section } >
< h2 style = { s . sectionTitle } > Employee Information < / h2 >
{ 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 >
) }
{ 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 >
) }
< 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 } / >
2026-03-06 11:33:32 -06:00
< / div >
2026-03-06 14:20:57 -06:00
) ) }
< / 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
) }
2026-03-06 12:02:52 -06:00
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 .
2026-03-06 11:33:32 -06:00
< / div >
2026-03-06 14:20:57 -06:00
) }
< / div >
2026-03-06 12:02:52 -06:00
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 >
{ 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 >
) }
{ 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 >
2026-03-06 12:02:52 -06:00
) }
2026-03-06 14:20:57 -06:00
{ 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 >
{ 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 >
) }
< / 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 ) ; } } >
Clear Form
< / button >
< / div >
2026-03-06 12:02:52 -06:00
2026-03-06 14:20:57 -06:00
{ 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 } / >
2026-03-06 11:33:32 -06:00
< / div >
2026-03-06 14:20:57 -06:00
) }
< / div >
) ;
2026-03-06 11:33:32 -06:00
}