From 1a09efbfe5a40904c003944d5e95199af7fbc510 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:38:29 -0600 Subject: [PATCH 1/2] Upload files to "client/src/components" --- client/src/components/EmployeeModal.jsx | 426 ++++++++++++------------ 1 file changed, 210 insertions(+), 216 deletions(-) diff --git a/client/src/components/EmployeeModal.jsx b/client/src/components/EmployeeModal.jsx index f49da38..500ea7d 100755 --- a/client/src/components/EmployeeModal.jsx +++ b/client/src/components/EmployeeModal.jsx @@ -4,237 +4,231 @@ import CpasBadge, { getTier } from './CpasBadge'; import NegateModal from './NegateModal'; const s = { - overlay: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', 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' }, - header: { background: 'linear-gradient(135deg, #2c3e50, #34495e)', color: 'white', padding: '24px 28px', position: 'sticky', top: 0, zIndex: 10 }, - 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: '#f8f9fa', borderRadius: '8px', padding: '14px', textAlign: 'center', border: '1px solid #dee2e6' }, - scoreNum: { fontSize: '26px', fontWeight: 800 }, - scoreLbl: { fontSize: '11px', color: '#888', marginTop: '3px' }, - sectionHd: { fontSize: '13px', fontWeight: 700, color: '#34495e', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '10px', marginTop: '24px' }, - table: { width: '100%', borderCollapse: 'collapse', fontSize: '12px' }, - th: { background: '#f1f3f5', padding: '8px 10px', textAlign: 'left', color: '#555', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase' }, - td: { padding: '9px 10px', borderBottom: '1px solid #f0f0f0', verticalAlign: 'top' }, - negatedRow: { background: '#f8f8f8', color: '#aaa' }, - 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' }, - pdfBtn: { background: 'none', border: '1px solid #667eea', color: '#667eea', 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' }, + 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' }, }; 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 [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]); + 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]); + 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 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(); // ← refetch employee list, score, and violations - }; + const handleHardDelete = async (id) => { + await axios.delete(`/api/violations/${id}`); + setConfirmDel(null); + load(); + }; - const handleRestore = async (id) => { - await axios.patch(`/api/violations/${id}/restore`); - load(); // ← refetch employee list, score, and violations - }; + const handleRestore = async (id) => { + await axios.patch(`/api/violations/${id}/restore`); + load(); + }; - const handleNegate = async ({ resolution_type, details, resolved_by }) => { - await axios.patch(`/api/violations/${negating.id}/negate`, { resolution_type, details, resolved_by }); - setNegating(null); - load(); // ← CRITICAL FIX: refetch score immediately after negation - }; + const handleNegate = async ({ resolution_type, details, resolved_by }) => { + await axios.patch(`/api/violations/${negating.id}/negate`, { resolution_type, details, resolved_by }); + setNegating(null); + load(); + }; - const tier = score ? getTier(score.active_points) : null; - const active = violations.filter(v => !v.negated); - const negated = violations.filter(v => v.negated); + const tier = score ? getTier(score.active_points) : null; + const active = violations.filter(v => !v.negated); + const negated = violations.filter(v => v.negated); - return ( -
{ if (e.target === e.currentTarget) onClose(); }}> -
+ return ( +
{ if (e.target === e.currentTarget) onClose(); }}> +
+
+ +
+ {loading ? 'Loading…' : (employee?.name || 'Employee Profile')} +
+ {employee && ( +
+ {[employee.department, employee.supervisor ? `Supervisor: ${employee.supervisor}` : null].filter(Boolean).join(' · ')} +
+ )} +
- {/* ── Header ──────────────────────────────────── */} -
- -
- {loading ? 'Loading…' : (employee?.name || 'Employee Profile')} -
- {employee && ( -
- {[employee.department, employee.supervisor ? `Supervisor: ${employee.supervisor}` : null].filter(Boolean).join(' · ')} -
- )} -
+
+ {loading ? ( +

Loading…

+ ) : (<> -
- {loading ? ( -

Loading…

- ) : (<> - - {/* ── Score cards ───────────────────────── */} -
-
-
{score?.active_points ?? 0}
-
Active Points
-
-
-
{score?.violation_count ?? 0}
-
90-Day Violations
-
-
-
{active.length}
-
Total On Record
-
-
-
{negated.length}
-
Negated
-
-
- - {tier && ( -
- {tier.label} - Rolling 90-day window · Points expire automatically -
- )} - - {/* ── Active violations ─────────────────── */} -
Active Violations
- {active.length === 0 ? ( -

No active violations on record.

- ) : ( - - - - - - - - - - - {active.map(v => ( - - - - - - - ))} - -
DateViolationPtsActions
{v.incident_date} -
{v.violation_name}
-
{v.category}
- {v.details &&
{v.details}
} -
{v.points} - - -
- {confirmDel === v.id ? ( -
- Permanently delete? This cannot be undone. -
- - -
-
- ) : ( - - )} -
- )} - - {/* ── Negated violations ────────────────── */} - {negated.length > 0 && (<> -
Negated / Resolved Violations
- - - - - - - - - - - - {negated.map(v => ( - - - - - - - - ))} - -
DateViolationPtsResolutionActions
{v.incident_date} -
{v.violation_name}
-
{v.category}
-
{v.points} - {v.resolution_type} - {v.resolution_details &&
{v.resolution_details}
} - {v.resolved_by &&
by {v.resolved_by}
} -
- - {confirmDel === v.id ? ( -
- Permanently delete? -
- - -
-
- ) : ( - - )} -
- )} - - )} -
+
+
+
{score?.active_points ?? 0}
+
Active Points
+
+
+
{score?.violation_count ?? 0}
+
90-Day Violations
+
+
+
{active.length}
+
Total On Record
+
+
+
{negated.length}
+
Negated
+
- {/* ── Negate sub-modal ────────────────────────────────── */} - {negating && ( - setNegating(null)} - /> + {tier && ( +
+ {tier.label} + Rolling 90-day window · Points expire automatically +
)} + +
Active Violations
+ {active.length === 0 ? ( +

No active violations on record.

+ ) : ( + + + + + + + + + + + {active.map(v => ( + + + + + + + ))} + +
DateViolationPtsActions
{v.incident_date} +
{v.violation_name}
+
{v.category}
+ {v.details &&
{v.details}
} +
{v.points} + + +
+ {confirmDel === v.id ? ( +
+ Permanently delete? This cannot be undone. +
+ + +
+
+ ) : ( + + )} +
+ )} + + {negated.length > 0 && (<> +
Negated / Resolved Violations
+ + + + + + + + + + + + {negated.map(v => ( + + + + + + + + ))} + +
DateViolationPtsResolutionActions
{v.incident_date} +
{v.violation_name}
+
{v.category}
+
{v.points} + {v.resolution_type} + {v.resolution_details &&
{v.resolution_details}
} + {v.resolved_by &&
by {v.resolved_by}
} +
+ + {confirmDel === v.id ? ( +
+ Permanently delete? +
+ + +
+
+ ) : ( + + )} +
+ )} + + )}
- ); +
+ + {negating && ( + setNegating(null)} + /> + )} +
+ ); } From 60e9da488c0766f971bf11c3d3aaf4fe8e781007 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:38:43 -0600 Subject: [PATCH 2/2] Upload files to "pdf" --- pdf/template.js | 172 +++++++++++------------------------------------- 1 file changed, 37 insertions(+), 135 deletions(-) diff --git a/pdf/template.js b/pdf/template.js index 76da038..7034b80 100755 --- a/pdf/template.js +++ b/pdf/template.js @@ -1,5 +1,3 @@ -/** PDF template with MPM logo from /static/mpm-logo.png */ - const TIERS = [ { min: 0, max: 4, label: 'Tier 0-1 — Elite Standing', color: '#28a745' }, { min: 5, max: 9, label: 'Tier 1 — Realignment', color: '#856404' }, @@ -10,17 +8,12 @@ const TIERS = [ { min: 30, max: 999,label: 'Tier 6 — Separation', color: '#721c24' }, ]; -function getTier(points) { - return TIERS.find(t => points >= t.min && points <= t.max) || TIERS[0]; -} +function getTier(points) { return TIERS.find(t => points >= t.min && points <= t.max) || TIERS[0]; } function formatDate(d) { if (!d) return '—'; const dt = new Date(d + 'T12:00:00'); - return dt.toLocaleDateString('en-US', { - weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', - timeZone: 'America/Chicago' - }); + return dt.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'America/Chicago' }); } function formatDateTime(d, t) { @@ -37,16 +30,13 @@ function row(label, value) { } function buildHtml(v, score) { - const activePts = score.active_points || 0; - const tier = getTier(activePts); - const newTotal = activePts + v.points; - const newTier = getTier(newTotal); - const tierChange = tier.label !== newTier.label; + const activePts = score.active_points || 0; + const tier = getTier(activePts); + const newTotal = activePts + v.points; + const newTier = getTier(newTotal); + const tierChange= tier.label !== newTier.label; - const generatedAt = new Date().toLocaleString('en-US', { - timeZone: 'America/Chicago', - dateStyle: 'full', timeStyle: 'short' - }); + const generatedAt = new Date().toLocaleString('en-US', { timeZone: 'America/Chicago', dateStyle: 'full', timeStyle: 'short' }); return ` @@ -55,86 +45,33 @@ function buildHtml(v, score) { @@ -155,9 +92,7 @@ function buildHtml(v, score) {
-
- ⚠ CONFIDENTIAL — For authorized HR and management use only -
+
⚠ CONFIDENTIAL — For authorized HR and management use only
Employee Information
@@ -179,26 +114,17 @@ function buildHtml(v, score) { ${v.location ? row('Location / Context', v.location) : ''} ${row('Submitted By', v.submitted_by || 'System')} - ${v.details ? ` -
- Incident Details:
- ${v.details} -
` : ''} + ${v.details ? `
Incident Details:
${v.details}
` : ''}
CPAS Point Assessment
-
-
${v.points}
-
Points Assessed — This Violation
-
+
${v.points}
Points Assessed — This Violation
${activePts}
Active Points (Prior)
-
- ${tier.label} -
+
${tier.label}
+
@@ -209,46 +135,26 @@ function buildHtml(v, score) {
${newTotal}
New Active Total
-
- ${newTier.label} -
+
${newTier.label}
- ${tierChange ? ` -
- ⚠ Tier Escalation: This violation advances the employee from - ${tier.label} to ${newTier.label}. -
` : ''} + ${tierChange ? `
⚠ Tier Escalation: This violation advances the employee from ${tier.label} to ${newTier.label}.
` : ''}
CPAS Tier Reference
- - - - - ${TIERS.map(t => ` - - - - `).join('')} + + ${TIERS.map(t => ``).join('')}
PointsTier
${t.min === 30 ? '30+' : t.min + '–' + t.max}${t.label}
PointsTier
${t.min === 30 ? '30+' : t.min + '–' + t.max}${t.label}
-
- Employee Notice: CPAS points remain active for a rolling 90-day period from the date of each incident. - Accumulation of points may result in tier escalation and associated consequences as outlined in the Employee Handbook. -
+
Employee Notice: CPAS points remain active for a rolling 90-day period from the date of each incident. Accumulation of points may result in tier escalation and associated consequences as outlined in the Employee Handbook.
Acknowledgement & Signatures
-
-

- By signing below, the employee acknowledges receipt of this violation record. - Acknowledgement does not imply agreement. The employee may submit a written - response within 5 business days. -

+
+

By signing below, the employee acknowledges receipt of this violation record. Acknowledgement does not imply agreement. The employee may submit a written response within 5 business days.

Employee Signature
@@ -262,11 +168,7 @@ function buildHtml(v, score) {
- +