Upload files to "client/src/components"
This commit is contained in:
@@ -4,24 +4,24 @@ import CpasBadge, { getTier } from './CpasBadge';
|
|||||||
import NegateModal from './NegateModal';
|
import NegateModal from './NegateModal';
|
||||||
|
|
||||||
const s = {
|
const s = {
|
||||||
overlay: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', zIndex: 1000, display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-end' },
|
overlay: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', zIndex: 1000, display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-end' },
|
||||||
panel: { background: 'white', width: '680px', maxWidth: '95vw', height: '100vh', overflowY: 'auto', boxShadow: '-4px 0 24px rgba(0,0,0,0.18)', display: 'flex', flexDirection: 'column' },
|
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, #2c3e50, #34495e)', color: 'white', padding: '24px 28px', position: 'sticky', top: 0, zIndex: 10 },
|
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' },
|
closeBtn: { float: 'right', background: 'none', border: 'none', color: 'white', fontSize: '22px', cursor: 'pointer', lineHeight: 1, marginTop: '-2px' },
|
||||||
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: { flex: '1', minWidth: '100px', background: '#f8f9fa', borderRadius: '8px', padding: '14px', textAlign: 'center', border: '1px solid #dee2e6' },
|
scoreCard: { flex: '1', minWidth: '100px', background: '#181924', borderRadius: '8px', padding: '14px', textAlign: 'center', border: '1px solid #2a2b3a' },
|
||||||
scoreNum: { fontSize: '26px', fontWeight: 800 },
|
scoreNum: { fontSize: '26px', fontWeight: 800 },
|
||||||
scoreLbl: { fontSize: '11px', color: '#888', marginTop: '3px' },
|
scoreLbl: { fontSize: '11px', color: '#b5b5c0', marginTop: '3px' },
|
||||||
sectionHd: { fontSize: '13px', fontWeight: 700, color: '#34495e', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '10px', marginTop: '24px' },
|
sectionHd: { fontSize: '13px', fontWeight: 700, color: '#f8f9fa', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '10px', marginTop: '24px' },
|
||||||
table: { width: '100%', borderCollapse: 'collapse', fontSize: '12px' },
|
table: { width: '100%', borderCollapse: 'collapse', fontSize: '12px', background: '#181924', borderRadius: '6px', overflow: 'hidden', border: '1px solid #2a2b3a' },
|
||||||
th: { background: '#f1f3f5', padding: '8px 10px', textAlign: 'left', color: '#555', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase' },
|
th: { background: '#050608', padding: '8px 10px', textAlign: 'left', color: '#f8f9fa', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase' },
|
||||||
td: { padding: '9px 10px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'top' },
|
td: { padding: '9px 10px', borderBottom: '1px solid #202231', verticalAlign: 'top', color: '#f8f9fa' },
|
||||||
negatedRow: { background: '#f8f8f8', color: '#aaa' },
|
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 }),
|
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: '#d4edda', color: '#155724', border: '1px solid #c3e6cb' },
|
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 #667eea', color: '#667eea', borderRadius: '4px', padding: '3px 8px', fontSize: '11px', cursor: 'pointer', fontWeight: 600 },
|
pdfBtn: { background: 'none', border: '1px solid #d4af37', color: '#ffd666', borderRadius: '4px', padding: '3px 8px', fontSize: '11px', cursor: 'pointer', fontWeight: 600 },
|
||||||
deleteConfirm: { background: '#f8d7da', border: '1px solid #f5c6cb', borderRadius: '6px', padding: '12px', marginTop: '8px', fontSize: '12px' },
|
deleteConfirm: { background: '#3c1114', border: '1px solid #f5c6cb', borderRadius: '6px', padding: '12px', marginTop: '8px', fontSize: '12px', color: '#ffb3b8' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EmployeeModal({ employeeId, onClose }) {
|
export default function EmployeeModal({ employeeId, onClose }) {
|
||||||
@@ -63,18 +63,18 @@ export default function EmployeeModal({ employeeId, onClose }) {
|
|||||||
const handleHardDelete = async (id) => {
|
const handleHardDelete = async (id) => {
|
||||||
await axios.delete(`/api/violations/${id}`);
|
await axios.delete(`/api/violations/${id}`);
|
||||||
setConfirmDel(null);
|
setConfirmDel(null);
|
||||||
load(); // ← refetch employee list, score, and violations
|
load();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRestore = async (id) => {
|
const handleRestore = async (id) => {
|
||||||
await axios.patch(`/api/violations/${id}/restore`);
|
await axios.patch(`/api/violations/${id}/restore`);
|
||||||
load(); // ← refetch employee list, score, and violations
|
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);
|
||||||
load(); // ← CRITICAL FIX: refetch score immediately after negation
|
load();
|
||||||
};
|
};
|
||||||
|
|
||||||
const tier = score ? getTier(score.active_points) : null;
|
const tier = score ? getTier(score.active_points) : null;
|
||||||
@@ -84,15 +84,13 @@ export default function EmployeeModal({ employeeId, onClose }) {
|
|||||||
return (
|
return (
|
||||||
<div style={s.overlay} onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
|
<div style={s.overlay} onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
|
||||||
<div style={s.panel}>
|
<div style={s.panel}>
|
||||||
|
|
||||||
{/* ── Header ──────────────────────────────────── */}
|
|
||||||
<div style={s.header}>
|
<div style={s.header}>
|
||||||
<button style={s.closeBtn} onClick={onClose}>✕</button>
|
<button style={s.closeBtn} onClick={onClose}>✕</button>
|
||||||
<div style={{ fontSize: '20px', fontWeight: 700 }}>
|
<div style={{ fontSize: '20px', fontWeight: 700 }}>
|
||||||
{loading ? 'Loading…' : (employee?.name || 'Employee Profile')}
|
{loading ? 'Loading…' : (employee?.name || 'Employee Profile')}
|
||||||
</div>
|
</div>
|
||||||
{employee && (
|
{employee && (
|
||||||
<div style={{ fontSize: '12px', opacity: 0.75, marginTop: '4px' }}>
|
<div style={{ fontSize: '12px', opacity: 0.8, marginTop: '4px' }}>
|
||||||
{[employee.department, employee.supervisor ? `Supervisor: ${employee.supervisor}` : null].filter(Boolean).join(' · ')}
|
{[employee.department, employee.supervisor ? `Supervisor: ${employee.supervisor}` : null].filter(Boolean).join(' · ')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -100,10 +98,9 @@ export default function EmployeeModal({ employeeId, onClose }) {
|
|||||||
|
|
||||||
<div style={s.body}>
|
<div style={s.body}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p style={{ color: '#aaa', textAlign: 'center', paddingTop: '40px' }}>Loading…</p>
|
<p style={{ color: '#77798a', textAlign: 'center', paddingTop: '40px' }}>Loading…</p>
|
||||||
) : (<>
|
) : (<>
|
||||||
|
|
||||||
{/* ── Score cards ───────────────────────── */}
|
|
||||||
<div style={s.scoreRow}>
|
<div style={s.scoreRow}>
|
||||||
<div style={{ ...s.scoreCard, borderTop: `3px solid ${tier?.color}` }}>
|
<div style={{ ...s.scoreCard, borderTop: `3px solid ${tier?.color}` }}>
|
||||||
<div style={{ ...s.scoreNum, color: tier?.color }}>{score?.active_points ?? 0}</div>
|
<div style={{ ...s.scoreNum, color: tier?.color }}>{score?.active_points ?? 0}</div>
|
||||||
@@ -118,22 +115,21 @@ export default function EmployeeModal({ employeeId, onClose }) {
|
|||||||
<div style={s.scoreLbl}>Total On Record</div>
|
<div style={s.scoreLbl}>Total On Record</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={s.scoreCard}>
|
<div style={s.scoreCard}>
|
||||||
<div style={{ ...s.scoreNum, color: '#888' }}>{negated.length}</div>
|
<div style={{ ...s.scoreNum, color: '#ffd666' }}>{negated.length}</div>
|
||||||
<div style={s.scoreLbl}>Negated</div>
|
<div style={s.scoreLbl}>Negated</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tier && (
|
{tier && (
|
||||||
<div style={{ background: '#f8f9fa', borderRadius: '6px', padding: '10px 14px', marginBottom: '16px', fontSize: '13px', border: `1px solid ${tier.color}33` }}>
|
<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>
|
<strong style={{ color: tier.color }}>{tier.label}</strong>
|
||||||
<span style={{ color: '#888', marginLeft: '10px', fontSize: '12px' }}>Rolling 90-day window · Points expire automatically</span>
|
<span style={{ color: '#b5b5c0', marginLeft: '10px', fontSize: '12px' }}>Rolling 90-day window · Points expire automatically</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Active violations ─────────────────── */}
|
|
||||||
<div style={s.sectionHd}>Active Violations</div>
|
<div style={s.sectionHd}>Active Violations</div>
|
||||||
{active.length === 0 ? (
|
{active.length === 0 ? (
|
||||||
<p style={{ color: '#aaa', fontSize: '13px', fontStyle: 'italic' }}>No active violations on record.</p>
|
<p style={{ color: '#77798a', fontSize: '13px', fontStyle: 'italic' }}>No active violations on record.</p>
|
||||||
) : (
|
) : (
|
||||||
<table style={s.table}>
|
<table style={s.table}>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -150,20 +146,20 @@ 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 style={{ color: '#888', fontSize: '11px' }}>{v.category}</div>
|
<div style={{ color: '#b5b5c0', fontSize: '11px' }}>{v.category}</div>
|
||||||
{v.details && <div style={{ color: '#666', fontSize: '11px', marginTop: '3px', fontStyle: 'italic' }}>{v.details}</div>}
|
{v.details && <div style={{ color: '#d1d3e0', fontSize: '11px', marginTop: '3px', fontStyle: 'italic' }}>{v.details}</div>}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ ...s.td, fontWeight: 700, color: '#c0392b' }}>{v.points}</td>
|
<td style={{ ...s.td, fontWeight: 700, color: '#ff8a80' }}>{v.points}</td>
|
||||||
<td style={s.td}>
|
<td style={s.td}>
|
||||||
<button style={s.actionBtn('#856404')} onClick={() => setNegating(v)}>⊘ Negate</button>
|
<button style={s.actionBtn('#ffd666')} onClick={() => setNegating(v)}>⊘ Negate</button>
|
||||||
<button style={s.pdfBtn} onClick={() => handleDownloadPdf(v.id, employee?.name, v.incident_date)}>PDF</button>
|
<button style={s.pdfBtn} onClick={() => handleDownloadPdf(v.id, employee?.name, v.incident_date)}>PDF</button>
|
||||||
<br />
|
<br />
|
||||||
{confirmDel === v.id ? (
|
{confirmDel === v.id ? (
|
||||||
<div style={s.deleteConfirm}>
|
<div style={s.deleteConfirm}>
|
||||||
<strong>Permanently delete?</strong> This cannot be undone.
|
<strong>Permanently delete?</strong> This cannot be undone.
|
||||||
<div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>
|
<div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>
|
||||||
<button style={s.actionBtn('#c0392b')} onClick={() => handleHardDelete(v.id)}>Confirm Delete</button>
|
<button style={s.actionBtn('#ffb3b8')} onClick={() => handleHardDelete(v.id)}>Confirm Delete</button>
|
||||||
<button style={s.actionBtn('#666')} onClick={() => setConfirmDel(null)}>Cancel</button>
|
<button style={s.actionBtn('#9ca0b8')} onClick={() => setConfirmDel(null)}>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -176,7 +172,6 @@ export default function EmployeeModal({ employeeId, onClose }) {
|
|||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Negated violations ────────────────── */}
|
|
||||||
{negated.length > 0 && (<>
|
{negated.length > 0 && (<>
|
||||||
<div style={s.sectionHd}>Negated / Resolved Violations</div>
|
<div style={s.sectionHd}>Negated / Resolved Violations</div>
|
||||||
<table style={s.table}>
|
<table style={s.table}>
|
||||||
@@ -195,22 +190,22 @@ 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={{ textDecoration: 'line-through' }}>{v.violation_name}</div>
|
<div style={{ textDecoration: 'line-through' }}>{v.violation_name}</div>
|
||||||
<div style={{ fontSize: '11px', color: '#aaa' }}>{v.category}</div>
|
<div style={{ fontSize: '11px', color: '#9ca0b8' }}>{v.category}</div>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ ...s.td, textDecoration: 'line-through', color: '#aaa' }}>{v.points}</td>
|
<td style={{ ...s.td, textDecoration: 'line-through', color: '#9ca0b8' }}>{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 && <div style={{ fontSize: '11px', marginTop: '3px', color: '#666' }}>{v.resolution_details}</div>}
|
{v.resolution_details && <div style={{ fontSize: '11px', marginTop: '3px', color: '#d1d3e0' }}>{v.resolution_details}</div>}
|
||||||
{v.resolved_by && <div style={{ fontSize: '10px', color: '#aaa' }}>by {v.resolved_by}</div>}
|
{v.resolved_by && <div style={{ fontSize: '10px', color: '#9ca0b8' }}>by {v.resolved_by}</div>}
|
||||||
</td>
|
</td>
|
||||||
<td style={s.td}>
|
<td style={s.td}>
|
||||||
<button style={s.actionBtn('#28a745')} onClick={() => handleRestore(v.id)}>↩ Restore</button>
|
<button style={s.actionBtn('#9ef7c1')} onClick={() => handleRestore(v.id)}>↩ Restore</button>
|
||||||
{confirmDel === v.id ? (
|
{confirmDel === v.id ? (
|
||||||
<div style={s.deleteConfirm}>
|
<div style={s.deleteConfirm}>
|
||||||
<strong>Permanently delete?</strong>
|
<strong>Permanently delete?</strong>
|
||||||
<div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>
|
<div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>
|
||||||
<button style={s.actionBtn('#c0392b')} onClick={() => handleHardDelete(v.id)}>Confirm</button>
|
<button style={s.actionBtn('#ffb3b8')} onClick={() => handleHardDelete(v.id)}>Confirm</button>
|
||||||
<button style={s.actionBtn('#666')} onClick={() => setConfirmDel(null)}>Cancel</button>
|
<button style={s.actionBtn('#9ca0b8')} onClick={() => setConfirmDel(null)}>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -227,7 +222,6 @@ export default function EmployeeModal({ employeeId, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Negate sub-modal ────────────────────────────────── */}
|
|
||||||
{negating && (
|
{negating && (
|
||||||
<NegateModal
|
<NegateModal
|
||||||
violation={negating}
|
violation={negating}
|
||||||
|
|||||||
Reference in New Issue
Block a user