Upload files to "client/src/components" #17
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user