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 = {
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' },
2026-03-06 12:02:52 -06:00
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' },
2026-03-06 11:33:32 -06:00
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' },
2026-03-06 12:02:52 -06:00
scoreRow : { display : 'flex' , alignItems : 'center' , gap : '12px' , marginBottom : '14px' , flexWrap : 'wrap' },
2026-03-06 11:33:32 -06:00
btnRow : { display : 'flex' , gap : '15px' , justifyContent : 'center' , marginTop : '30px' , flexWrap : 'wrap' },
2026-03-06 12:02:52 -06:00
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' },
btnSecondary : { padding : '15px 40px' , fontSize : '16px' , fontWeight : 600 , border : 'none' , borderRadius : '6px' , cursor : 'pointer' , background : '#6c757d' , color : 'white' , textTransform : 'uppercase' },
2026-03-06 11:33:32 -06:00
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' },
};
const EMPTY_FORM = {
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 );
2026-03-06 12:02:52 -06:00
// Phase 2: pull score + history whenever employee changes
const intel = useEmployeeIntelligence ( form . employeeId || null );
2026-03-06 11:33:32 -06:00
useEffect (() => {
axios . get ( '/api/employees' ). then ( r => setEmployees ( r . data )). catch (() => {});
}, []);
2026-03-06 12:02:52 -06:00
// When violation type changes, check all-time counts and auto-suggest higher pts for recidivists
useEffect (() => {
if ( ! violation || ! form . violationType ) return ;
const allTime = intel . countsAllTime [ form . violationType ];
if ( allTime && allTime . count >= 1 && violation . minPoints !== violation . maxPoints ) {
// Suggest max points for repeat offenders
setForm ( prev => ({ ... prev , points : violation . maxPoints }));
} else {
setForm ( prev => ({ ... prev , points : violation . minPoints }));
}
}, [ form . violationType , violation , intel . countsAllTime ]);
2026-03-06 11:33:32 -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 || '' }));
};
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.' });
2026-03-06 12:02:52 -06:00
if ( ! form . employeeName ) return setStatus ({ ok : false , msg : 'Please enter an employee name.' });
2026-03-06 11:33:32 -06:00
try {
const empRes = await axios . post ( '/api/employees' , { name : form . employeeName , department : form . department , supervisor : form . supervisor });
const employeeId = empRes . data . id ;
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:02:52 -06:00
// Refresh employee list and re-run intel for updated score
const empList = await axios . get ( '/api/employees' );
setEmployees ( empList . data );
2026-03-06 11:33:32 -06:00
setStatus ({ ok : true , msg : '✓ Violation recorded successfully' });
setForm ( EMPTY_FORM );
setViolation ( null );
} catch ( err ) {
setStatus ({ ok : false , msg : '✗ Error: ' + ( err . response ? . data ? . error || err . message ) });
}
};
2026-03-06 12:02:52 -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
return (
< div style = { s . content }>
2026-03-06 12:02:52 -06:00
{ /* ── Employee Information ────────────────────────────────── */ }
2026-03-06 11:33:32 -06:00
< div style = { s . section }>
< h2 style = { s . sectionTitle }> Employee Information </ h2 >
2026-03-06 12:02:52 -06:00
{ /* CPAS score banner — shown once employee is selected */ }
{ intel . score && form . employeeId && (
< div style = { s . scoreRow }>
< span style = {{ fontSize : '13px' , color : '#555' , fontWeight : 600 }}> Current Standing : </ span >
< CpasBadge points = { intel . score . active_points } />
< span style = {{ fontSize : '12px' , color : '#888' }}>
{ intel . score . violation_count } violation { intel . score . violation_count !== 1 ? 's' : '' } in last 90 days
</ span >
</ div >
)}
2026-03-06 11:33:32 -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 11:33:32 -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 >
2026-03-06 12:02:52 -06:00
{ /* ── Violation Details ────────────────────────────────────── */ }
2026-03-06 11:33:32 -06:00
< form onSubmit = { handleSubmit }>
< div style = { s . section }>
< h2 style = { s . sectionTitle }> Violation Details </ h2 >
< div style = { s . grid }>
2026-03-06 12:02:52 -06:00
{ /* Violation type dropdown with prior-use badges */ }
2026-03-06 11:33:32 -06:00
< 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 }>
2026-03-06 12:02:52 -06:00
{ 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 >
);
})}
2026-03-06 11:33:32 -06:00
</ optgroup >
))}
</ select >
2026-03-06 12:02:52 -06:00
{ /* Handbook definition */ }
2026-03-06 11:33:32 -06:00
{ violation && (
< div style = { s . contextBox }>
2026-03-06 12:02:52 -06:00
< 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 />
2026-03-06 11:33:32 -06:00
< span style = {{ fontSize : '11px' , color : '#666' }}>{ violation . chapter }</ span >
</ div >
)}
2026-03-06 12:02:52 -06:00
{ /* Recidivist auto-suggest notice */ }
{ 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 12:02:52 -06:00
{ /* Incident date */ }
2026-03-06 11:33:32 -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 11:33:32 -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 >
)}
{ 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
{ /* Tier escalation warning */ }
{ intel . score && violation && (
< TierWarning
currentPoints = { intel . score . active_points }
addingPoints = { parseInt ( form . points ) || 0 }
/>
)}
{ /* Point slider */ }
2026-03-06 11:33:32 -06:00
{ violation && (
< div style = { s . pointBox }>
< h4 style = {{ color : '#856404' , 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 >
2026-03-06 12:02:52 -06:00
< input
style = {{ width : '100%' , marginTop : '10px' }}
type = "range" name = "points"
min = { violation . minPoints } max = { violation . maxPoints }
value = { form . points } onChange = { handleChange }
/>
2026-03-06 11:33:32 -06:00
< div style = { s . pointValue }>{ form . points } Points </ div >
< p style = {{ fontSize : '12px' , color : '#666' }}> Adjust to reflect severity and context </ p >
</ div >
)}
</ div >
2026-03-06 12:02:52 -06:00
2026-03-06 11:33:32 -06:00
< div style = { s . note }>
< strong > Note : </ strong > Submitting saves the violation to the database linked to the employee record . PDF generation available in Phase 3.
</ div >
2026-03-06 12:02:52 -06:00
2026-03-06 11:33:32 -06:00
< div style = { s . btnRow }>
< button type = "submit" style = { s . btnPrimary }> Submit Violation </ button >
2026-03-06 12:02:52 -06:00
< button type = "button" style = { s . btnSecondary } onClick = {() => { setForm ( EMPTY_FORM ); setViolation ( null ); setStatus ( null ); }}>
Clear Form
</ button >
2026-03-06 11:33:32 -06:00
</ div >
2026-03-06 12:02:52 -06:00
2026-03-06 11:33:32 -06:00
{ status && < div style = { status . ok ? s . statusOk : s . statusErr }>{ status . msg }</ div >}
</ form >
2026-03-06 12:02:52 -06:00
{ /* ── Violation History Panel ──────────────────────────────── */ }
{ form . employeeId && (
< div style = { s . section }>
< h2 style = { s . sectionTitle }> Violation History </ h2 >
< ViolationHistory history = { intel . history } loading = { intel . loading } />
</ div >
)}
2026-03-06 11:33:32 -06:00
</ div >
);
}