Merge pull request 'Upload files to "client/src/components"' (#17) from p4-hotfixes into master

Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
2026-03-06 17:20:42 -06:00
2 changed files with 201 additions and 465 deletions

View File

@@ -5,148 +5,66 @@ import NegateModal from './NegateModal';
const s = { const s = {
overlay: { overlay: {
position: 'fixed', position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)',
inset: 0, zIndex: 1000, display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-end',
background: 'rgba(0,0,0,0.75)',
zIndex: 1000,
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'flex-end',
}, },
panel: { panel: {
background: '#111217', background: '#111217', color: '#f8f9fa', width: '680px', maxWidth: '95vw',
color: '#f8f9fa', height: '100vh', overflowY: 'auto', boxShadow: '-4px 0 24px rgba(0,0,0,0.7)',
width: '680px', display: 'flex', flexDirection: 'column',
maxWidth: '95vw',
height: '100vh',
overflowY: 'auto',
boxShadow: '-4px 0 24px rgba(0,0,0,0.7)',
display: 'flex',
flexDirection: 'column',
}, },
header: { header: {
background: 'linear-gradient(135deg, #000000, #151622)', background: 'linear-gradient(135deg, #000000, #151622)', color: 'white',
color: 'white', padding: '24px 28px', position: 'sticky', top: 0, zIndex: 10,
padding: '24px 28px',
position: 'sticky',
top: 0,
zIndex: 10,
borderBottom: '1px solid #222', borderBottom: '1px solid #222',
}, },
closeBtn: { closeBtn: {
float: 'right', float: 'right', background: 'none', border: 'none', color: 'white',
background: 'none', fontSize: '22px', cursor: 'pointer', lineHeight: 1, marginTop: '-2px',
border: 'none',
color: 'white',
fontSize: '22px',
cursor: 'pointer',
lineHeight: 1,
marginTop: '-2px',
},
body: {
padding: '24px 28px',
flex: 1,
},
scoreRow: {
display: 'flex',
gap: '12px',
flexWrap: 'wrap',
marginBottom: '24px',
}, },
body: { padding: '24px 28px', flex: 1 },
scoreRow: { display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '24px' },
scoreCard: { scoreCard: {
flex: '1', flex: '1', minWidth: '100px', background: '#181924', borderRadius: '8px',
minWidth: '100px', padding: '14px', textAlign: 'center', border: '1px solid #2a2b3a',
background: '#181924',
borderRadius: '8px',
padding: '14px',
textAlign: 'center',
border: '1px solid #2a2b3a',
},
scoreNum: {
fontSize: '26px',
fontWeight: 800,
},
scoreLbl: {
fontSize: '11px',
color: '#b5b5c0',
marginTop: '3px',
}, },
scoreNum: { fontSize: '26px', fontWeight: 800 },
scoreLbl: { fontSize: '11px', color: '#b5b5c0', marginTop: '3px' },
sectionHd: { sectionHd: {
fontSize: '13px', fontSize: '13px', fontWeight: 700, color: '#f8f9fa', textTransform: 'uppercase',
fontWeight: 700, letterSpacing: '0.5px', marginBottom: '10px', marginTop: '24px',
color: '#f8f9fa',
textTransform: 'uppercase',
letterSpacing: '0.5px',
marginBottom: '10px',
marginTop: '24px',
}, },
table: { table: {
width: '100%', width: '100%', borderCollapse: 'collapse', fontSize: '12px', background: '#181924',
borderCollapse: 'collapse', borderRadius: '6px', overflow: 'hidden', border: '1px solid #2a2b3a',
fontSize: '12px',
background: '#181924',
borderRadius: '6px',
overflow: 'hidden',
border: '1px solid #2a2b3a',
}, },
th: { th: {
background: '#050608', background: '#050608', padding: '8px 10px', textAlign: 'left', color: '#f8f9fa',
padding: '8px 10px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase',
textAlign: 'left',
color: '#f8f9fa',
fontWeight: 600,
fontSize: '11px',
textTransform: 'uppercase',
}, },
td: { td: {
padding: '9px 10px', padding: '9px 10px', borderBottom: '1px solid #202231',
borderBottom: '1px solid #202231', verticalAlign: 'top', color: '#f8f9fa',
verticalAlign: 'top',
color: '#f8f9fa',
},
negatedRow: {
background: '#151622',
color: '#9ca0b8',
}, },
negatedRow: { background: '#151622', color: '#9ca0b8' },
actionBtn: (color) => ({ actionBtn: (color) => ({
background: 'none', background: 'none', border: `1px solid ${color}`, color,
border: `1px solid ${color}`, borderRadius: '4px', padding: '3px 8px', fontSize: '11px',
color, cursor: 'pointer', marginRight: '4px', fontWeight: 600,
borderRadius: '4px',
padding: '3px 8px',
fontSize: '11px',
cursor: 'pointer',
marginRight: '4px',
fontWeight: 600,
}), }),
resTag: { resTag: {
display: 'inline-block', display: 'inline-block', padding: '2px 8px', borderRadius: '10px',
padding: '2px 8px', fontSize: '10px', fontWeight: 700, background: '#053321',
borderRadius: '10px', color: '#9ef7c1', border: '1px solid #0f5132',
fontSize: '10px',
fontWeight: 700,
background: '#053321',
color: '#9ef7c1',
border: '1px solid #0f5132',
}, },
pdfBtn: { pdfBtn: {
background: 'none', background: 'none', border: '1px solid #d4af37', color: '#ffd666',
border: '1px solid #d4af37', borderRadius: '4px', padding: '3px 8px', fontSize: '11px',
color: '#ffd666', cursor: 'pointer', fontWeight: 600,
borderRadius: '4px',
padding: '3px 8px',
fontSize: '11px',
cursor: 'pointer',
fontWeight: 600,
}, },
deleteConfirm: { deleteConfirm: {
background: '#3c1114', background: '#3c1114', border: '1px solid #f5c6cb', borderRadius: '6px',
border: '1px solid #f5c6cb', padding: '12px', marginTop: '8px', fontSize: '12px', color: '#ffb3b8',
borderRadius: '6px',
padding: '12px',
marginTop: '8px',
fontSize: '12px',
color: '#ffb3b8',
}, },
}; };
@@ -174,17 +92,11 @@ export default function EmployeeModal({ employeeId, onClose }) {
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [employeeId]); }, [employeeId]);
useEffect(() => { useEffect(() => { load(); }, [load]);
load();
}, [load]);
const handleDownloadPdf = async (violId, empName, date) => { const handleDownloadPdf = async (violId, empName, date) => {
const response = await axios.get(`/api/violations/${violId}/pdf`, { const response = await axios.get(`/api/violations/${violId}/pdf`, { responseType: 'blob' });
responseType: 'blob', const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/pdf' }));
});
const url = window.URL.createObjectURL(
new Blob([response.data], { type: 'application/pdf' }),
);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.download = `CPAS_${(empName || '').replace(/[^a-z0-9]/gi, '_')}_${date}.pdf`; link.download = `CPAS_${(empName || '').replace(/[^a-z0-9]/gi, '_')}_${date}.pdf`;
@@ -208,9 +120,7 @@ export default function EmployeeModal({ employeeId, onClose }) {
const handleNegate = async ({ resolution_type, details, resolved_by }) => { const handleNegate = async ({ resolution_type, details, resolved_by }) => {
await axios.patch(`/api/violations/${negating.id}/negate`, { await axios.patch(`/api/violations/${negating.id}/negate`, {
resolution_type, resolution_type, details, resolved_by,
details,
resolved_by,
}); });
setNegating(null); setNegating(null);
setConfirmDel(null); setConfirmDel(null);
@@ -221,115 +131,68 @@ export default function EmployeeModal({ employeeId, onClose }) {
const active = violations.filter((v) => !v.negated); const active = violations.filter((v) => !v.negated);
const negated = violations.filter((v) => v.negated); const negated = violations.filter((v) => v.negated);
// FIX: overlay click only closes if clicking the backdrop itself, NOT children
const handleOverlayClick = (e) => { const handleOverlayClick = (e) => {
if (e.target === e.currentTarget) onClose(); if (e.target === e.currentTarget) onClose();
}; };
return ( return (
// FIX: panel uses onClick stopPropagation to prevent bubbling to overlay
<div style={s.overlay} onClick={handleOverlayClick}> <div style={s.overlay} onClick={handleOverlayClick}>
<div style={s.panel}> <div style={s.panel} onClick={(e) => e.stopPropagation()}>
{/* ── Header ── */}
<div style={s.header}> <div style={s.header}>
<button style={s.closeBtn} onClick={onClose}> <button style={s.closeBtn} onClick={onClose}></button>
<div style={{ fontSize: '18px', fontWeight: 700 }}>
</button> {employee ? employee.name : 'Employee'}
<div style={{ fontSize: '20px', fontWeight: 700 }}>
{loading ? 'Loading…' : employee?.name || 'Employee Profile'}
</div> </div>
{employee && ( {employee && (
<div style={{ fontSize: '12px', opacity: 0.8, marginTop: '4px' }}> <div style={{ fontSize: '12px', color: '#b5b5c0', marginTop: '4px' }}>
{[employee.department, employee.supervisor ? `Supervisor: ${employee.supervisor}` : null] {employee.department} {employee.supervisor && `· Supervisor: ${employee.supervisor}`}
.filter(Boolean)
.join(' · ')}
</div> </div>
)} )}
</div> </div>
{/* ── Body ── */}
<div style={s.body}> <div style={s.body}>
{loading ? ( {loading ? (
<p <div style={{ padding: '40px', textAlign: 'center', color: '#b5b5c0' }}>Loading</div>
style={{
color: '#77798a',
textAlign: 'center',
paddingTop: '40px',
}}
>
Loading
</p>
) : ( ) : (
<> <>
<div style={s.scoreRow}> {/* Score Cards */}
<div {score && (
style={{ <div style={s.scoreRow}>
...s.scoreCard, <div style={s.scoreCard}>
borderTop: `3px solid ${tier?.color}`, <div style={{ ...s.scoreNum, color: tier?.color || '#f8f9fa' }}>
}} {score.active_points}
> </div>
<div <div style={s.scoreLbl}>Active Points</div>
style={{
...s.scoreNum,
color: tier?.color,
}}
>
{score?.active_points ?? 0}
</div> </div>
<div style={s.scoreLbl}>Active Points</div> <div style={s.scoreCard}>
</div> <div style={s.scoreNum}>{score.total_violations}</div>
<div style={s.scoreCard}> <div style={s.scoreLbl}>Total Violations</div>
<div style={s.scoreNum}>{score?.violation_count ?? 0}</div> </div>
<div style={s.scoreLbl}>90-Day Violations</div> <div style={s.scoreCard}>
</div> <div style={s.scoreNum}>{score.negated_count}</div>
<div style={s.scoreCard}> <div style={s.scoreLbl}>Negated</div>
<div style={s.scoreNum}>{active.length}</div> </div>
<div style={s.scoreLbl}>Total On Record</div> <div style={{ ...s.scoreCard, minWidth: '140px' }}>
</div> <div style={{ fontSize: '13px', fontWeight: 700, color: tier?.color || '#f8f9fa' }}>
<div style={s.scoreCard}> {tier ? tier.label : '—'}
<div </div>
style={{ <div style={s.scoreLbl}>Current Tier</div>
...s.scoreNum,
color: '#ffd666',
}}
>
{negated.length}
</div> </div>
<div style={s.scoreLbl}>Negated</div>
</div>
</div>
{tier && (
<div
style={{
background: '#181924',
borderRadius: '6px',
padding: '10px 14px',
marginBottom: '16px',
fontSize: '13px',
border: `1px solid ${tier.color}33`,
}}
>
<strong style={{ color: tier.color }}>{tier.label}</strong>
<span
style={{
color: '#b5b5c0',
marginLeft: '10px',
fontSize: '12px',
}}
>
Rolling 90-day window · Points expire automatically
</span>
</div> </div>
)} )}
{score && <CpasBadge points={score.active_points} style={{ marginBottom: '20px' }} />}
{/* ── Active Violations ── */}
<div style={s.sectionHd}>Active Violations</div> <div style={s.sectionHd}>Active Violations</div>
{active.length === 0 ? ( {active.length === 0 ? (
<p <div style={{ color: '#77798a', fontStyle: 'italic', fontSize: '12px' }}>
style={{
color: '#77798a',
fontSize: '13px',
fontStyle: 'italic',
}}
>
No active violations on record. No active violations on record.
</p> </div>
) : ( ) : (
<table style={s.table}> <table style={s.table}>
<thead> <thead>
@@ -346,91 +209,52 @@ export default function EmployeeModal({ employeeId, onClose }) {
<td style={s.td}>{v.incident_date}</td> <td style={s.td}>{v.incident_date}</td>
<td style={s.td}> <td style={s.td}>
<div style={{ fontWeight: 600 }}>{v.violation_name}</div> <div style={{ fontWeight: 600 }}>{v.violation_name}</div>
<div <div style={{ fontSize: '10px', color: '#9ca0b8' }}>{v.category}</div>
style={{
color: '#b5b5c0',
fontSize: '11px',
}}
>
{v.category}
</div>
{v.details && ( {v.details && (
<div <div style={{ fontSize: '10px', color: '#b5b5c0', marginTop: '2px' }}>
style={{
color: '#d1d3e0',
fontSize: '11px',
marginTop: '3px',
fontStyle: 'italic',
}}
>
{v.details} {v.details}
</div> </div>
)} )}
</td> </td>
<td <td style={{ ...s.td, fontWeight: 700 }}>{v.points}</td>
style={{
...s.td,
fontWeight: 700,
color: '#ff8a80',
}}
>
{v.points}
</td>
<td style={s.td}> <td style={s.td}>
{/* FIX: All buttons use e.stopPropagation() to prevent overlay close */}
<button <button
style={s.actionBtn('#ffd666')} style={s.actionBtn('#ffc107')}
onClick={() => setNegating(v)} onClick={(e) => { e.stopPropagation(); setNegating(v); setConfirmDel(null); }}
> >
Negate Negate
</button>
<button
style={s.actionBtn('#ff4d4f')}
onClick={(e) => { e.stopPropagation(); setConfirmDel(confirmDel === v.id ? null : v.id); }}
>
{confirmDel === v.id ? 'Cancel' : 'Delete'}
</button> </button>
<button <button
style={s.pdfBtn} style={s.pdfBtn}
onClick={() => onClick={(e) => { e.stopPropagation(); handleDownloadPdf(v.id, employee?.name, v.incident_date); }}
handleDownloadPdf(
v.id,
employee?.name,
v.incident_date,
)
}
> >
PDF PDF
</button> </button>
<br /> {confirmDel === v.id && (
{confirmDel === v.id ? (
<div style={s.deleteConfirm}> <div style={s.deleteConfirm}>
<strong>Permanently delete?</strong> This cannot be Permanently delete? This cannot be undone.
undone. <div style={{ marginTop: '8px' }}>
<div
style={{
marginTop: '8px',
display: 'flex',
gap: '8px',
}}
>
<button <button
style={s.actionBtn('#ffb3b8')} style={s.actionBtn('#ff4d4f')}
onClick={() => handleHardDelete(v.id)} onClick={(e) => { e.stopPropagation(); handleHardDelete(v.id); }}
> >
Confirm Delete Confirm Delete
</button> </button>
<button <button
style={s.actionBtn('#9ca0b8')} style={s.actionBtn('#888')}
onClick={() => setConfirmDel(null)} onClick={(e) => { e.stopPropagation(); setConfirmDel(null); }}
> >
Cancel Cancel
</button> </button>
</div> </div>
</div> </div>
) : (
<button
style={{
...s.actionBtn('#c0392b'),
marginTop: '4px',
}}
onClick={() => setConfirmDel(v.id)}
>
Delete
</button>
)} )}
</td> </td>
</tr> </tr>
@@ -439,9 +263,10 @@ export default function EmployeeModal({ employeeId, onClose }) {
</table> </table>
)} )}
{/* ── Negated / Resolved Violations ── */}
{negated.length > 0 && ( {negated.length > 0 && (
<> <>
<div style={s.sectionHd}>Negated / Resolved Violations</div> <div style={s.sectionHd}>Negated / Resolved</div>
<table style={s.table}> <table style={s.table}>
<thead> <thead>
<tr> <tr>
@@ -457,89 +282,60 @@ export default function EmployeeModal({ employeeId, onClose }) {
<tr key={v.id} style={s.negatedRow}> <tr key={v.id} style={s.negatedRow}>
<td style={s.td}>{v.incident_date}</td> <td style={s.td}>{v.incident_date}</td>
<td style={s.td}> <td style={s.td}>
<div style={{ textDecoration: 'line-through' }}> <div style={{ fontWeight: 600 }}>{v.violation_name}</div>
{v.violation_name} <div style={{ fontSize: '10px', color: '#9ca0b8' }}>{v.category}</div>
</div>
<div
style={{
fontSize: '11px',
color: '#9ca0b8',
}}
>
{v.category}
</div>
</td>
<td
style={{
...s.td,
textDecoration: 'line-through',
color: '#9ca0b8',
}}
>
{v.points}
</td> </td>
<td style={s.td}>{v.points}</td>
<td style={s.td}> <td style={s.td}>
<span style={s.resTag}>{v.resolution_type}</span> <span style={s.resTag}>{v.resolution_type}</span>
{v.resolution_details && ( {v.resolution_details && (
<div <div style={{ fontSize: '10px', color: '#b5b5c0', marginTop: '2px' }}>
style={{
fontSize: '11px',
marginTop: '3px',
color: '#d1d3e0',
}}
>
{v.resolution_details} {v.resolution_details}
</div> </div>
)} )}
{v.resolved_by && ( {v.resolved_by && (
<div <div style={{ fontSize: '10px', color: '#9ca0b8' }}>
style={{
fontSize: '10px',
color: '#9ca0b8',
}}
>
by {v.resolved_by} by {v.resolved_by}
</div> </div>
)} )}
</td> </td>
<td style={s.td}> <td style={s.td}>
<button <button
style={s.actionBtn('#9ef7c1')} style={s.actionBtn('#4db6ac')}
onClick={() => handleRestore(v.id)} onClick={(e) => { e.stopPropagation(); handleRestore(v.id); }}
> >
Restore Restore
</button> </button>
{confirmDel === v.id ? ( <button
style={s.actionBtn('#ff4d4f')}
onClick={(e) => { e.stopPropagation(); setConfirmDel(confirmDel === v.id ? null : v.id); }}
>
{confirmDel === v.id ? 'Cancel' : 'Delete'}
</button>
<button
style={s.pdfBtn}
onClick={(e) => { e.stopPropagation(); handleDownloadPdf(v.id, employee?.name, v.incident_date); }}
>
PDF
</button>
{confirmDel === v.id && (
<div style={s.deleteConfirm}> <div style={s.deleteConfirm}>
<strong>Permanently delete?</strong> Permanently delete? This cannot be undone.
<div <div style={{ marginTop: '8px' }}>
style={{
marginTop: '8px',
display: 'flex',
gap: '8px',
}}
>
<button <button
style={s.actionBtn('#ffb3b8')} style={s.actionBtn('#ff4d4f')}
onClick={() => handleHardDelete(v.id)} onClick={(e) => { e.stopPropagation(); handleHardDelete(v.id); }}
> >
Confirm Confirm Delete
</button> </button>
<button <button
style={s.actionBtn('#9ca0b8')} style={s.actionBtn('#888')}
onClick={() => setConfirmDel(null)} onClick={(e) => { e.stopPropagation(); setConfirmDel(null); }}
> >
Cancel Cancel
</button> </button>
</div> </div>
</div> </div>
) : (
<button
style={s.actionBtn('#c0392b')}
onClick={() => setConfirmDel(v.id)}
>
Delete
</button>
)} )}
</td> </td>
</tr> </tr>
@@ -553,6 +349,7 @@ export default function EmployeeModal({ employeeId, onClose }) {
</div> </div>
</div> </div>
{/* FIX: NegateModal rendered OUTSIDE the panel so it sits at root z-index:2000 */}
{negating && ( {negating && (
<NegateModal <NegateModal
violation={negating} violation={negating}

View File

@@ -2,115 +2,70 @@ import React, { useState } from 'react';
const s = { const s = {
overlay: { overlay: {
position: 'fixed', position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)',
inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(0,0,0,0.75)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 2000, zIndex: 2000,
}, },
modal: { modal: {
width: '480px', width: '480px', maxWidth: '95vw', background: '#111217', borderRadius: '12px',
maxWidth: '95vw', boxShadow: '0 16px 40px rgba(0,0,0,0.8)', color: '#f8f9fa',
background: '#111217', overflow: 'hidden', border: '1px solid #2a2b3a',
borderRadius: '12px',
boxShadow: '0 16px 40px rgba(0,0,0,0.8)',
color: '#f8f9fa',
overflow: 'hidden',
border: '1px solid #2a2b3a',
}, },
header: { header: {
padding: '18px 24px', padding: '18px 24px', borderBottom: '1px solid #222',
borderBottom: '1px solid #222',
background: 'linear-gradient(135deg, #000000, #151622)', background: 'linear-gradient(135deg, #000000, #151622)',
}, },
title: { title: { fontSize: '18px', fontWeight: 700 },
fontSize: '18px', subtitle: { fontSize: '12px', color: '#c0c2d6', marginTop: '4px' },
fontWeight: 700, body: { padding: '18px 24px 8px 24px' },
},
subtitle: {
fontSize: '12px',
color: '#c0c2d6',
marginTop: '4px',
},
body: {
padding: '18px 24px 8px 24px',
},
pill: { pill: {
background: '#3b2e00', background: '#3b2e00', borderRadius: '6px', padding: '8px 10px',
borderRadius: '6px', fontSize: '12px', color: '#ffd666', border: '1px solid #d4af37', marginBottom: '14px',
padding: '8px 10px',
fontSize: '12px',
color: '#ffd666',
border: '1px solid #d4af37',
marginBottom: '14px',
},
label: {
fontSize: '13px',
fontWeight: 600,
marginBottom: '4px',
color: '#e5e7f1',
}, },
label: { fontSize: '13px', fontWeight: 600, marginBottom: '4px', color: '#e5e7f1' },
input: { input: {
width: '100%', width: '100%', padding: '9px 10px', borderRadius: '6px',
padding: '9px 10px', border: '1px solid #333544', background: '#050608', color: '#f8f9fa',
borderRadius: '6px', fontSize: '13px', fontFamily: 'inherit', marginBottom: '14px',
border: '1px solid #333544', boxSizing: 'border-box',
background: '#050608',
color: '#f8f9fa',
fontSize: '13px',
fontFamily: 'inherit',
marginBottom: '14px',
}, },
textarea: { textarea: {
width: '100%', width: '100%', minHeight: '80px', resize: 'vertical',
minHeight: '80px', padding: '9px 10px', borderRadius: '6px', border: '1px solid #333544',
resize: 'vertical', background: '#050608', color: '#f8f9fa', fontSize: '13px',
padding: '9px 10px', fontFamily: 'inherit', marginBottom: '14px', boxSizing: 'border-box',
borderRadius: '6px',
border: '1px solid #333544',
background: '#050608',
color: '#f8f9fa',
fontSize: '13px',
fontFamily: 'inherit',
marginBottom: '14px',
}, },
footer: { footer: {
display: 'flex', display: 'flex', justifyContent: 'flex-end', gap: '10px',
justifyContent: 'flex-end', padding: '16px 24px 20px 24px', background: '#0c0d14', borderTop: '1px solid #222',
gap: '10px',
padding: '16px 24px 20px 24px',
background: '#0c0d14',
borderTop: '1px solid #222',
}, },
btnCancel: { btnCancel: {
padding: '10px 20px', padding: '10px 20px', borderRadius: '6px', border: '1px solid #333544',
borderRadius: '6px', background: '#050608', color: '#f8f9fa', fontWeight: 600,
border: '1px solid #333544', fontSize: '13px', cursor: 'pointer',
background: '#050608',
color: '#f8f9fa',
fontWeight: 600,
fontSize: '13px',
cursor: 'pointer',
}, },
btnConfirm: { btnConfirm: {
padding: '10px 22px', padding: '10px 22px', borderRadius: '6px', border: 'none',
borderRadius: '6px',
border: 'none',
background: 'linear-gradient(135deg, #d4af37 0%, #ffdf8a 100%)', background: 'linear-gradient(135deg, #d4af37 0%, #ffdf8a 100%)',
color: '#000', color: '#000', fontWeight: 700, fontSize: '13px',
fontWeight: 700, cursor: 'pointer', textTransform: 'uppercase',
fontSize: '13px',
cursor: 'pointer',
textTransform: 'uppercase',
}, },
}; };
const RESOLUTION_OPTIONS = [
'Corrective Training Completed',
'Verbal Warning Issued',
'Written Warning Issued',
'Management Review',
'Policy Exception Approved',
'Data Entry Error',
'Other',
];
export default function NegateModal({ violation, onConfirm, onCancel }) { export default function NegateModal({ violation, onConfirm, onCancel }) {
const [resolutionType, setResolutionType] = useState('Corrective Training Completed'); const [resolutionType, setResolutionType] = useState('Corrective Training Completed');
const [details, setDetails] = useState(''); const [details, setDetails] = useState('');
const [resolvedBy, setResolvedBy] = useState(''); const [resolvedBy, setResolvedBy] = useState('');
if (!violation) return null; if (!violation) return null;
@@ -123,75 +78,59 @@ export default function NegateModal({ violation, onConfirm, onCancel }) {
}); });
}; };
// FIX: overlay click only closes on backdrop, NOT modal children
const handleOverlayClick = (e) => { const handleOverlayClick = (e) => {
if (e.target === e.currentTarget && onCancel) onCancel(); if (e.target === e.currentTarget && onCancel) onCancel();
}; };
return ( return (
<div style={s.overlay} onClick={handleOverlayClick}> <div style={s.overlay} onClick={handleOverlayClick}>
<div style={s.modal}> {/* FIX: stopPropagation prevents modal clicks from bubbling to overlay */}
<div style={s.modal} onClick={(e) => e.stopPropagation()}>
<div style={s.header}> <div style={s.header}>
<div style={s.title}> Negate Violation Points</div> <div style={s.title}>Negate Violation</div>
<div style={s.subtitle}> <div style={s.subtitle}>
This will zero out the points from this incident. The record remains in the audit log. Record resolution for: <strong>{violation.violation_name}</strong>
</div> </div>
</div> </div>
<div style={s.body}> <div style={s.body}>
<div style={s.pill}> <div style={s.pill}>
{violation.violation_name} · {violation.points} pts · {violation.incident_date} {violation.points} pt{violation.points !== 1 ? 's' : ''} · {violation.incident_date} · {violation.category}
</div> </div>
<div> <div style={s.label}>Resolution Type</div>
<div style={s.label}>Resolution Type *</div> <select
<select style={s.input}
style={s.input} value={resolutionType}
value={resolutionType} onChange={(e) => setResolutionType(e.target.value)}
onChange={e => setResolutionType(e.target.value)} >
> {RESOLUTION_OPTIONS.map((opt) => (
<option>Corrective Training Completed</option> <option key={opt} value={opt}>{opt}</option>
<option>Documentation Error</option> ))}
<option>Policy Clarification / Exception</option> </select>
<option>Management Discretion</option>
</select>
</div>
<div> <div style={s.label}>Details / Notes</div>
<div style={s.label}>Additional Details</div> <textarea
<textarea style={s.textarea}
style={s.textarea} placeholder="Describe the resolution or context…"
value={details} value={details}
onChange={e => setDetails(e.target.value)} onChange={(e) => setDetails(e.target.value)}
placeholder="Briefly describe why points are being negated..." />
/>
</div>
<div> <div style={s.label}>Resolved By</div>
<div style={s.label}>Resolved By</div> <input
<input style={s.input}
style={s.input} placeholder="Manager or HR name…"
value={resolvedBy} value={resolvedBy}
onChange={e => setResolvedBy(e.target.value)} onChange={(e) => setResolvedBy(e.target.value)}
placeholder="Supervisor or HR" />
/>
</div>
</div> </div>
<div style={s.footer}> <div style={s.footer}>
<button <button style={s.btnCancel} onClick={onCancel}>Cancel</button>
type="button" <button style={s.btnConfirm} onClick={handleConfirm}>Confirm Negation</button>
style={s.btnCancel}
onClick={() => onCancel && onCancel()}
>
Cancel
</button>
<button
type="button"
style={s.btnConfirm}
onClick={handleConfirm}
>
Confirm Negation
</button>
</div> </div>
</div> </div>
</div> </div>