Files
cpas/client/src/components/EmployeeModal.jsx

103 lines
5.2 KiB
React
Raw Normal View History

2026-03-06 12:53:40 -06:00
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
import CpasBadge, { getTier } from './CpasBadge';
import NegateModal from './NegateModal';
const s = {
overlay: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', zIndex: 1000, display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-end' },
panel: { background: '#111217', color: '#f8f9fa', width: '680px', maxWidth: '95vw', height: '100vh', overflowY: 'auto', boxShadow: '-4px 0 24px rgba(0,0,0,0.7)', display: 'flex', flexDirection: 'column' },
header: { background: 'linear-gradient(135deg, #000000, #151622)', color: 'white', padding: '24px 28px', position: 'sticky', top: 0, zIndex: 10, borderBottom: '1px solid #222' },
closeBtn: { float: 'right', background: 'none', 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' },
scoreCard: { flex: '1', minWidth: '100px', background: '#181924', borderRadius: '8px', padding: '14px', textAlign: 'center', border: '1px solid #2a2b3a' },
scoreNum: { fontSize: '26px', fontWeight: 800 },
scoreLbl: { fontSize: '11px', color: '#b5b5c0', marginTop: '3px' },
sectionHd: { fontSize: '13px', fontWeight: 700, color: '#f8f9fa', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '10px', marginTop: '24px' },
table: { width: '100%', borderCollapse: 'collapse', fontSize: '12px', background: '#181924', borderRadius: '6px', overflow: 'hidden', border: '1px solid #2a2b3a' },
th: { background: '#050608', padding: '8px 10px', textAlign: 'left', color: '#f8f9fa', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase' },
td: { padding: '9px 10px', borderBottom: '1px solid #202231', verticalAlign: 'top', color: '#f8f9fa' },
negatedRow: { background: '#151622', color: '#9ca0b8' },
actionBtn: (color) => ({ background: 'none', border: `1px solid ${color}`, color, borderRadius: '4px', padding: '3px 8px', fontSize: '11px', cursor: 'pointer', marginRight: '4px', fontWeight: 600 }),
resTag: { display: 'inline-block', padding: '2px 8px', borderRadius: '10px', fontSize: '10px', fontWeight: 700, background: '#053321', color: '#9ef7c1', border: '1px solid #0f5132' },
pdfBtn: { background: 'none', border: '1px solid #d4af37', color: '#ffd666', borderRadius: '4px', padding: '3px 8px', fontSize: '11px', cursor: 'pointer', fontWeight: 600 },
deleteConfirm: { background: '#3c1114', border: '1px solid #f5c6cb', borderRadius: '6px', padding: '12px', marginTop: '8px', fontSize: '12px', color: '#ffb3b8' },
2026-03-06 12:53:40 -06:00
};
export default function EmployeeModal({ employeeId, onClose }) {
const [employee, setEmployee] = useState(null);
const [score, setScore] = useState(null);
const [violations, setViolations] = useState([]);
const [loading, setLoading] = useState(true);
const [negating, setNegating] = useState(null);
const [confirmDel, setConfirmDel] = useState(null);
const load = useCallback(() => {
setLoading(true);
Promise.all([
axios.get('/api/employees'),
axios.get(`/api/employees/${employeeId}/score`),
axios.get(`/api/violations/employee/${employeeId}?limit=100`),
]).then(([empRes, scoreRes, violRes]) => {
const emp = empRes.data.find(e => e.id === employeeId);
setEmployee(emp || null);
setScore(scoreRes.data);
setViolations(violRes.data);
}).finally(() => setLoading(false));
}, [employeeId]);
useEffect(() => { load(); }, [load]);
const handleDownloadPdf = async (violId, empName, date) => {
const response = await axios.get(`/api/violations/${violId}/pdf`, { responseType: 'blob' });
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/pdf' }));
const link = document.createElement('a');
link.href = url;
link.download = `CPAS_${(empName||'').replace(/[^a-z0-9]/gi,'_')}_${date}.pdf`;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
};
const handleHardDelete = async (id) => {
await axios.delete(`/api/violations/${id}`);
setConfirmDel(null);
load();
};
const handleRestore = async (id) => {
await axios.patch(`/api/violations/${id}/restore`);
setConfirmDel(null);
load();
};
const handleNegate = async ({ resolution_type, details, resolved_by }) => {
await axios.patch(`/api/violations/${negating.id}/negate`, {
resolution_type,
details,
resolved_by,
});
setNegating(null);
setConfirmDel(null);
load();
};
const tier = score ? getTier(score.active_points) : null;
const active = violations.filter(v => !v.negated);
const negated = violations.filter(v => v.negated);
return (
<div style={s.overlay} onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
{/* panel and tables unchanged; omitted here for brevity */}
{negating && (
<NegateModal
violation={negating}
onConfirm={handleNegate}
onCancel={() => setNegating(null)}
/>
)}
</div>
);
2026-03-06 12:53:40 -06:00
}