Compare commits

...

2 Commits

2 changed files with 182 additions and 58 deletions

View File

@@ -68,12 +68,18 @@ export default function EmployeeModal({ employeeId, onClose }) {
const handleRestore = async (id) => { const handleRestore = async (id) => {
await axios.patch(`/api/violations/${id}/restore`); await axios.patch(`/api/violations/${id}/restore`);
setConfirmDel(null);
load(); load();
}; };
const handleNegate = async ({ resolution_type, details, resolved_by }) => { const handleNegate = async ({ resolution_type, details, resolved_by }) => {
await axios.patch(`/api/violations/${negating.id}/negate`, { resolution_type, details, resolved_by }); await axios.patch(`/api/violations/${negating.id}/negate`, {
resolution_type,
details,
resolved_by,
});
setNegating(null); setNegating(null);
setConfirmDel(null);
load(); load();
}; };

View File

@@ -1,68 +1,186 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
const RESOLUTION_TYPES = [
'Corrective Training Completed',
'Management Discretion',
'Data Entry Error',
'Successfully Appealed',
];
const s = { const s = {
overlay: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.65)', zIndex: 2000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, overlay: {
box: { background: 'white', borderRadius: '10px', padding: '28px', width: '440px', maxWidth: '95vw', boxShadow: '0 8px 32px rgba(0,0,0,0.22)' }, position: 'fixed',
title: { fontSize: '17px', fontWeight: 700, color: '#2c3e50', marginBottom: '6px' }, inset: 0,
sub: { fontSize: '12px', color: '#888', marginBottom: '20px' }, background: 'rgba(0,0,0,0.75)',
label: { fontWeight: 600, color: '#555', fontSize: '12px', marginBottom: '5px', display: 'block' }, display: 'flex',
input: { width: '100%', padding: '9px 12px', border: '1px solid #ddd', borderRadius: '5px', fontSize: '13px', fontFamily: 'inherit', marginBottom: '14px' }, alignItems: 'center',
btnRow: { display: 'flex', gap: '10px', justifyContent: 'flex-end', marginTop: '8px' }, justifyContent: 'center',
btnOk: { padding: '10px 22px', background: '#856404', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 700, fontSize: '13px' }, zIndex: 1100,
btnCancel:{ padding: '10px 22px', background: '#f1f3f5', color: '#555', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '13px' }, },
violBox: { background: '#fff3cd', border: '1px solid #ffc107', borderRadius: '6px', padding: '10px 14px', marginBottom: '18px', fontSize: '13px' }, modal: {
width: '480px',
maxWidth: '95vw',
background: '#111217',
borderRadius: '12px',
boxShadow: '0 16px 40px rgba(0,0,0,0.8)',
color: '#f8f9fa',
overflow: 'hidden',
border: '1px solid #2a2b3a',
},
header: {
padding: '18px 24px',
borderBottom: '1px solid #222',
background: 'linear-gradient(135deg, #000000, #151622)',
},
title: {
fontSize: '18px',
fontWeight: 700,
},
subtitle: {
fontSize: '12px',
color: '#c0c2d6',
marginTop: '4px',
},
body: {
padding: '18px 24px 8px 24px',
},
pill: {
background: '#3b2e00',
borderRadius: '6px',
padding: '8px 10px',
fontSize: '12px',
color: '#ffd666',
border: '1px solid #d4af37',
marginBottom: '14px',
},
label: {
fontSize: '13px',
fontWeight: 600,
marginBottom: '4px',
color: '#e5e7f1',
},
input: {
width: '100%',
padding: '9px 10px',
borderRadius: '6px',
border: '1px solid #333544',
background: '#050608',
color: '#f8f9fa',
fontSize: '13px',
fontFamily: 'inherit',
marginBottom: '14px',
},
textarea: {
width: '100%',
minHeight: '80px',
resize: 'vertical',
padding: '9px 10px',
borderRadius: '6px',
border: '1px solid #333544',
background: '#050608',
color: '#f8f9fa',
fontSize: '13px',
fontFamily: 'inherit',
marginBottom: '14px',
},
footer: {
display: 'flex',
justifyContent: 'flex-end',
gap: '10px',
padding: '16px 24px 20px 24px',
background: '#0c0d14',
borderTop: '1px solid #222',
},
btnCancel: {
padding: '10px 20px',
borderRadius: '6px',
border: '1px solid #333544',
background: '#050608',
color: '#f8f9fa',
fontWeight: 600,
fontSize: '13px',
cursor: 'pointer',
},
btnConfirm: {
padding: '10px 22px',
borderRadius: '6px',
border: 'none',
background: 'linear-gradient(135deg, #d4af37 0%, #ffdf8a 100%)',
color: '#000',
fontWeight: 700,
fontSize: '13px',
cursor: 'pointer',
textTransform: 'uppercase',
},
}; };
export default function NegateModal({ violation, onConfirm, onCancel }) { export default function NegateModal({ violation, onConfirm, onCancel }) {
const [resType, setResType] = useState(''); const [resolutionType, setResolutionType] = useState('Corrective Training Completed');
const [details, setDetails] = useState(''); const [details, setDetails] = useState('');
const [resolvedBy, setResolvedBy] = useState(''); const [resolvedBy, setResolvedBy] = useState('');
const [error, setError] = useState('');
const handleSubmit = () => { if (!violation) return null;
if (!resType) { setError('Please select a resolution type.'); return; }
onConfirm({ resolution_type: resType, details, resolved_by: resolvedBy });
};
return ( const handleConfirm = () => {
<div style={s.overlay}> onConfirm({
<div style={s.box}> resolution_type: resolutionType,
<div style={s.title}> Negate Violation Points</div> details,
<div style={s.sub}>This will zero out the points from this incident. The record remains in the audit log.</div> resolved_by: resolvedBy,
});
};
<div style={s.violBox}> return (
<strong>{violation.violation_name}</strong> &nbsp;·&nbsp; {violation.points} pts &nbsp;·&nbsp; {violation.incident_date} <div style={s.overlay} onClick={e => { if (e.target === e.currentTarget) onCancel(); }}>
</div> <div style={s.modal}>
<div style={s.header}>
<label style={s.label}>Resolution Type *</label> <div style={s.title}> Negate Violation Points</div>
<select style={s.input} value={resType} onChange={e => { setResType(e.target.value); setError(''); }}> <div style={s.subtitle}>
<option value="">-- Select Resolution --</option> This will zero out the points from this incident. The record remains in the audit log.
{RESOLUTION_TYPES.map(r => <option key={r} value={r}>{r}</option>)} </div>
</select>
<label style={s.label}>Additional Details</label>
<textarea style={{ ...s.input, resize: 'vertical', minHeight: '70px' }}
placeholder="Training course completed, specific context, approving manager notes…"
value={details} onChange={e => setDetails(e.target.value)} />
<label style={s.label}>Resolved By</label>
<input style={s.input} type="text" placeholder="Officer / Manager name"
value={resolvedBy} onChange={e => setResolvedBy(e.target.value)} />
{error && <div style={{ color: '#c0392b', fontSize: '12px', marginBottom: '10px' }}>{error}</div>}
<div style={s.btnRow}>
<button style={s.btnCancel} onClick={onCancel}>Cancel</button>
<button style={s.btnOk} onClick={handleSubmit}>Confirm Negation</button>
</div>
</div>
</div> </div>
);
<div style={s.body}>
<div style={s.pill}>
{violation.violation_name} · {violation.points} pts · {violation.incident_date}
</div>
<div>
<div style={s.label}>Resolution Type *</div>
<select
style={s.input}
value={resolutionType}
onChange={e => setResolutionType(e.target.value)}
>
<option>Corrective Training Completed</option>
<option>Documentation Error</option>
<option>Policy Clarification / Exception</option>
<option>Management Discretion</option>
</select>
</div>
<div>
<div style={s.label}>Additional Details</div>
<textarea
style={s.textarea}
value={details}
onChange={e => setDetails(e.target.value)}
placeholder="Briefly describe why points are being negated..."
/>
</div>
<div>
<div style={s.label}>Resolved By</div>
<input
style={s.input}
value={resolvedBy}
onChange={e => setResolvedBy(e.target.value)}
placeholder="Supervisor or HR"
/>
</div>
</div>
<div style={s.footer}>
<button type="button" style={s.btnCancel} onClick={onCancel}>
Cancel
</button>
<button type="button" style={s.btnConfirm} onClick={handleConfirm}>
Confirm Negation
</button>
</div>
</div>
</div>
);
} }