roadmap #23

Merged
jason merged 7 commits from roadmap into master 2026-03-07 09:30:45 -06:00
Showing only changes of commit 7ee76468c4 - Show all commits

View File

@@ -2,6 +2,8 @@ import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios'; import axios from 'axios';
import CpasBadge, { getTier } from './CpasBadge'; import CpasBadge, { getTier } from './CpasBadge';
import NegateModal from './NegateModal'; import NegateModal from './NegateModal';
import EditEmployeeModal from './EditEmployeeModal';
import AmendViolationModal from './AmendViolationModal';
const s = { const s = {
overlay: { overlay: {
@@ -18,10 +20,15 @@ const s = {
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',
}, },
headerRow: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' },
closeBtn: { closeBtn: {
float: 'right', background: 'none', border: 'none', color: 'white', float: 'right', background: 'none', border: 'none', color: 'white',
fontSize: '22px', cursor: 'pointer', lineHeight: 1, marginTop: '-2px', fontSize: '22px', cursor: 'pointer', lineHeight: 1, marginTop: '-2px',
}, },
editEmpBtn: {
background: 'none', border: '1px solid #555', color: '#ccc', borderRadius: '5px',
padding: '4px 10px', fontSize: '11px', cursor: 'pointer', marginTop: '8px', fontWeight: 600,
},
body: { padding: '24px 28px', flex: 1 }, body: { padding: '24px 28px', flex: 1 },
scoreRow: { display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '24px' }, scoreRow: { display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '24px' },
scoreCard: { scoreCard: {
@@ -62,10 +69,20 @@ const s = {
borderRadius: '4px', padding: '3px 8px', fontSize: '11px', borderRadius: '4px', padding: '3px 8px', fontSize: '11px',
cursor: 'pointer', fontWeight: 600, cursor: 'pointer', fontWeight: 600,
}, },
amendBtn: {
background: 'none', border: '1px solid #4db6ac', color: '#4db6ac',
borderRadius: '4px', padding: '3px 8px', fontSize: '11px',
cursor: 'pointer', marginRight: '4px', fontWeight: 600,
},
deleteConfirm: { deleteConfirm: {
background: '#3c1114', border: '1px solid #f5c6cb', borderRadius: '6px', background: '#3c1114', border: '1px solid #f5c6cb', borderRadius: '6px',
padding: '12px', marginTop: '8px', fontSize: '12px', color: '#ffb3b8', padding: '12px', marginTop: '8px', fontSize: '12px', color: '#ffb3b8',
}, },
amendBadge: {
display: 'inline-block', marginLeft: '4px', padding: '1px 5px', borderRadius: '8px',
fontSize: '9px', fontWeight: 700, background: '#0e2a2a', color: '#4db6ac',
border: '1px solid #1a4a4a', verticalAlign: 'middle',
},
}; };
export default function EmployeeModal({ employeeId, onClose }) { export default function EmployeeModal({ employeeId, onClose }) {
@@ -75,6 +92,8 @@ export default function EmployeeModal({ employeeId, onClose }) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [negating, setNegating] = useState(null); const [negating, setNegating] = useState(null);
const [confirmDel, setConfirmDel] = useState(null); const [confirmDel, setConfirmDel] = useState(null);
const [editingEmp, setEditingEmp] = useState(false);
const [amending, setAmending] = useState(null); // violation object
const load = useCallback(() => { const load = useCallback(() => {
setLoading(true); setLoading(true);
@@ -119,9 +138,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, details, resolved_by });
resolution_type, details, resolved_by,
});
setNegating(null); setNegating(null);
setConfirmDel(null); setConfirmDel(null);
load(); load();
@@ -131,19 +148,16 @@ 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) => { if (e.target === e.currentTarget) onClose(); };
const handleOverlayClick = (e) => {
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} onClick={(e) => e.stopPropagation()}> <div style={s.panel} onClick={(e) => e.stopPropagation()}>
{/* ── Header ── */} {/* ── Header ── */}
<div style={s.header}> <div style={s.header}>
<button style={s.closeBtn} onClick={onClose}></button> <div style={s.headerRow}>
<div>
<div style={{ fontSize: '18px', fontWeight: 700 }}> <div style={{ fontSize: '18px', fontWeight: 700 }}>
{employee ? employee.name : 'Employee'} {employee ? employee.name : 'Employee'}
</div> </div>
@@ -152,6 +166,14 @@ export default function EmployeeModal({ employeeId, onClose }) {
{employee.department} {employee.supervisor && `· Supervisor: ${employee.supervisor}`} {employee.department} {employee.supervisor && `· Supervisor: ${employee.supervisor}`}
</div> </div>
)} )}
{employee && (
<button style={s.editEmpBtn} onClick={() => setEditingEmp(true)}>
Edit Employee
</button>
)}
</div>
<button style={s.closeBtn} onClick={onClose}></button>
</div>
</div> </div>
{/* ── Body ── */} {/* ── Body ── */}
@@ -190,7 +212,7 @@ export default function EmployeeModal({ employeeId, onClose }) {
{/* ── Active Violations ── */} {/* ── Active Violations ── */}
<div style={s.sectionHd}>Active Violations</div> <div style={s.sectionHd}>Active Violations</div>
{active.length === 0 ? ( {active.length === 0 ? (
<div style={{ color: '#77798a', fontStyle: 'italic', fontSize: '12px' }}> <div style={{ color: '#777990', fontStyle: 'italic', fontSize: '12px' }}>
No active violations on record. No active violations on record.
</div> </div>
) : ( ) : (
@@ -208,17 +230,22 @@ export default function EmployeeModal({ employeeId, onClose }) {
<tr key={v.id}> <tr key={v.id}>
<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}
{v.amendment_count > 0 && (
<span style={s.amendBadge}>{v.amendment_count} edit{v.amendment_count !== 1 ? 's' : ''}</span>
)}
</div>
<div style={{ fontSize: '10px', color: '#9ca0b8' }}>{v.category}</div> <div style={{ fontSize: '10px', color: '#9ca0b8' }}>{v.category}</div>
{v.details && ( {v.details && (
<div style={{ fontSize: '10px', color: '#b5b5c0', marginTop: '2px' }}> <div style={{ fontSize: '10px', color: '#b5b5c0', marginTop: '2px' }}>{v.details}</div>
{v.details}
</div>
)} )}
</td> </td>
<td style={{ ...s.td, fontWeight: 700 }}>{v.points}</td> <td style={{ ...s.td, fontWeight: 700 }}>{v.points}</td>
<td style={s.td}> <td style={s.td}>
{/* FIX: All buttons use e.stopPropagation() to prevent overlay close */} <button style={s.amendBtn} onClick={(e) => { e.stopPropagation(); setAmending(v); }}>
Amend
</button>
<button <button
style={s.actionBtn('#ffc107')} style={s.actionBtn('#ffc107')}
onClick={(e) => { e.stopPropagation(); setNegating(v); setConfirmDel(null); }} onClick={(e) => { e.stopPropagation(); setNegating(v); setConfirmDel(null); }}
@@ -294,9 +321,7 @@ export default function EmployeeModal({ employeeId, onClose }) {
</div> </div>
)} )}
{v.resolved_by && ( {v.resolved_by && (
<div style={{ fontSize: '10px', color: '#9ca0b8' }}> <div style={{ fontSize: '10px', color: '#9ca0b8' }}>by {v.resolved_by}</div>
by {v.resolved_by}
</div>
)} )}
</td> </td>
<td style={s.td}> <td style={s.td}>
@@ -349,7 +374,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 */} {/* Modals rendered outside panel to avoid z-index nesting issues */}
{negating && ( {negating && (
<NegateModal <NegateModal
violation={negating} violation={negating}
@@ -357,6 +382,20 @@ export default function EmployeeModal({ employeeId, onClose }) {
onCancel={() => setNegating(null)} onCancel={() => setNegating(null)}
/> />
)} )}
{editingEmp && employee && (
<EditEmployeeModal
employee={employee}
onClose={() => setEditingEmp(false)}
onSaved={load}
/>
)}
{amending && (
<AmendViolationModal
violation={amending}
onClose={() => setAmending(null)}
onSaved={load}
/>
)}
</div> </div>
); );
} }