From f006c015b064501f26cdd78fc1b5ed32333837d2 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:14:11 -0600 Subject: [PATCH 01/92] Add client/public --- client/public | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 client/public diff --git a/client/public b/client/public new file mode 100644 index 0000000..e69de29 -- 2.52.0 From 6305cedb14b1e164dcfda0aced54c693c9c7dfa4 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:14:27 -0600 Subject: [PATCH 02/92] Delete client/public --- client/public | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 client/public diff --git a/client/public b/client/public deleted file mode 100644 index e69de29..0000000 -- 2.52.0 From eb07832cc7a05fc8568121a6d6dc48232e23ae2c Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:15:05 -0600 Subject: [PATCH 03/92] Upload files to "client/public/static" --- client/public/static/mpm-logo.png | Bin 0 -> 4188 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 client/public/static/mpm-logo.png diff --git a/client/public/static/mpm-logo.png b/client/public/static/mpm-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..02e50cc2a2713bdd2a2ade7301d92179d5b25707 GIT binary patch literal 4188 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 Date: Fri, 6 Mar 2026 14:23:19 -0600 Subject: [PATCH 04/92] Update client/src/components/ViolationForm.jsx --- client/src/components/ViolationForm.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/ViolationForm.jsx b/client/src/components/ViolationForm.jsx index f33a3ef..ad6d329 100755 --- a/client/src/components/ViolationForm.jsx +++ b/client/src/components/ViolationForm.jsx @@ -1,4 +1,3 @@ -\ import React, { useState, useEffect } from 'react'; import axios from 'axios'; import { violationData, violationGroups } from '../data/violations'; -- 2.52.0 From 1a09efbfe5a40904c003944d5e95199af7fbc510 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:38:29 -0600 Subject: [PATCH 05/92] 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)} + /> + )} +
+ ); } -- 2.52.0 From 60e9da488c0766f971bf11c3d3aaf4fe8e781007 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:38:43 -0600 Subject: [PATCH 06/92] 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) {
- +
-- 2.52.0 From fdfa0bcf2f4c3a04eeeb5a24af777cb806f30abb Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:42:12 -0600 Subject: [PATCH 07/92] Upload files to "/" --- server.js | 256 +++++++++++++++++++++++++----------------------------- 1 file changed, 119 insertions(+), 137 deletions(-) diff --git a/server.js b/server.js index ac3e336..667afec 100755 --- a/server.js +++ b/server.js @@ -11,170 +11,152 @@ app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'client', 'dist'))); -// ── Health ────────────────────────────────────────────────────────────────── +// Health app.get('/api/health', (req, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() })); -// ── Employees ─────────────────────────────────────────────────────────────── +// Employees app.get('/api/employees', (req, res) => { - const rows = db.prepare('SELECT id, name, department, supervisor FROM employees ORDER BY name ASC').all(); - res.json(rows); + const rows = db.prepare('SELECT id, name, department, supervisor FROM employees ORDER BY name ASC').all(); + res.json(rows); }); app.post('/api/employees', (req, res) => { - const { name, department, supervisor } = req.body; - if (!name) return res.status(400).json({ error: 'name is required' }); - const existing = db.prepare('SELECT * FROM employees WHERE LOWER(name) = LOWER(?)').get(name); - if (existing) { - if (department || supervisor) - db.prepare('UPDATE employees SET department = COALESCE(?, department), supervisor = COALESCE(?, supervisor) WHERE id = ?') - .run(department || null, supervisor || null, existing.id); - return res.json({ ...existing, department, supervisor }); + const { name, department, supervisor } = req.body; + if (!name) return res.status(400).json({ error: 'name is required' }); + const existing = db.prepare('SELECT * FROM employees WHERE LOWER(name) = LOWER(?)').get(name); + if (existing) { + if (department || supervisor) { + db.prepare('UPDATE employees SET department = COALESCE(?, department), supervisor = COALESCE(?, supervisor) WHERE id = ?') + .run(department || null, supervisor || null, existing.id); } - const result = db.prepare('INSERT INTO employees (name, department, supervisor) VALUES (?, ?, ?)').run(name, department || null, supervisor || null); - res.status(201).json({ id: result.lastInsertRowid, name, department, supervisor }); + return res.json({ ...existing, department, supervisor }); + } + const result = db.prepare('INSERT INTO employees (name, department, supervisor) VALUES (?, ?, ?)') + .run(name, department || null, supervisor || null); + res.status(201).json({ id: result.lastInsertRowid, name, department, supervisor }); }); -// ── Employee CPAS Score ───────────────────────────────────────────────────── +// Employee score (current snapshot) app.get('/api/employees/:id/score', (req, res) => { - const row = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?').get(req.params.id); - res.json(row || { employee_id: req.params.id, active_points: 0, violation_count: 0 }); + const row = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?').get(req.params.id); + res.json(row || { employee_id: req.params.id, active_points: 0, violation_count: 0 }); }); -// ── Dashboard — all employees with scores ─────────────────────────────────── +// Dashboard app.get('/api/dashboard', (req, res) => { - const rows = db.prepare(` - SELECT - e.id, e.name, e.department, e.supervisor, - COALESCE(s.active_points, 0) AS active_points, - COALESCE(s.violation_count,0) AS violation_count - FROM employees e - LEFT JOIN active_cpas_scores s ON s.employee_id = e.id - ORDER BY active_points DESC, e.name ASC - `).all(); - res.json(rows); + const rows = db.prepare(` + SELECT e.id, e.name, e.department, e.supervisor, + COALESCE(s.active_points, 0) AS active_points, + COALESCE(s.violation_count,0) AS violation_count + FROM employees e + LEFT JOIN active_cpas_scores s ON s.employee_id = e.id + ORDER BY active_points DESC, e.name ASC + `).all(); + res.json(rows); }); -// ── Violation counts (90-day) ─────────────────────────────────────────────── -app.get('/api/employees/:id/violation-counts', (req, res) => { - const rows = db.prepare(` - SELECT violation_type, COUNT(*) as count FROM violations - WHERE employee_id = ? AND negated = 0 AND incident_date >= DATE('now', '-90 days') - GROUP BY violation_type - `).all(req.params.id); - const map = {}; - rows.forEach(r => { map[r.violation_type] = r.count; }); - res.json(map); -}); - -// ── Violation counts (all-time) ───────────────────────────────────────────── -app.get('/api/employees/:id/violation-counts/alltime', (req, res) => { - const rows = db.prepare(` - SELECT violation_type, COUNT(*) as count, MAX(points) as max_points_used FROM violations - WHERE employee_id = ? AND negated = 0 - GROUP BY violation_type - `).all(req.params.id); - const map = {}; - rows.forEach(r => { map[r.violation_type] = { count: r.count, max_points_used: r.max_points_used }; }); - res.json(map); -}); - -// ── Violation history (per employee) ─────────────────────────────────────── +// Violation history (per employee) with resolutions app.get('/api/violations/employee/:id', (req, res) => { - const limit = parseInt(req.query.limit) || 50; - const rows = db.prepare(` - SELECT v.*, r.resolution_type, r.details AS resolution_details, - r.resolved_by, r.created_at AS resolved_at - FROM violations v - LEFT JOIN violation_resolutions r ON r.violation_id = v.id - WHERE v.employee_id = ? - ORDER BY v.incident_date DESC, v.created_at DESC - LIMIT ? - `).all(req.params.id, limit); - res.json(rows); + const limit = parseInt(req.query.limit) || 50; + const rows = db.prepare(` + SELECT v.*, r.resolution_type, r.details AS resolution_details, + r.resolved_by, r.created_at AS resolved_at + FROM violations v + LEFT JOIN violation_resolutions r ON r.violation_id = v.id + WHERE v.employee_id = ? + ORDER BY v.incident_date DESC, v.created_at DESC + LIMIT ? + `).all(req.params.id, limit); + res.json(rows); }); -// ── POST new violation ────────────────────────────────────────────────────── +// NEW helper: compute prior_active_points at time of insert (excluding this violation) +function getPriorActivePoints(employeeId, incidentDate) { + const row = db.prepare( + `SELECT COALESCE(SUM(points),0) AS pts + FROM violations + WHERE employee_id = ? + AND negated = 0 + AND incident_date >= DATE(?, '-90 days') + AND incident_date < ?` + ).get(employeeId, incidentDate, incidentDate); + return row ? row.pts : 0; +} + +// POST new violation app.post('/api/violations', (req, res) => { - const { - employee_id, violation_type, violation_name, category, - points, incident_date, incident_time, location, - details, submitted_by, witness_name - } = req.body; - if (!employee_id || !violation_type || !points || !incident_date) - return res.status(400).json({ error: 'Missing required fields' }); + const { + employee_id, violation_type, violation_name, category, + points, incident_date, incident_time, location, + details, submitted_by, witness_name + } = req.body; - const result = db.prepare(` - INSERT INTO violations (employee_id, violation_type, violation_name, category, - points, incident_date, incident_time, location, details, submitted_by, witness_name) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(employee_id, violation_type, violation_name || violation_type, - category || 'General', points, incident_date, - incident_time || null, location || null, - details || null, submitted_by || null, witness_name || null); + if (!employee_id || !violation_type || !points || !incident_date) { + return res.status(400).json({ error: 'Missing required fields' }); + } - res.status(201).json({ id: result.lastInsertRowid }); + const ptsInt = parseInt(points); + const priorPts = getPriorActivePoints(employee_id, incident_date); + + const result = db.prepare(` + INSERT INTO violations ( + employee_id, violation_type, violation_name, category, + points, incident_date, incident_time, location, + details, submitted_by, witness_name, + prior_active_points + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + employee_id, violation_type, violation_name || violation_type, + category || 'General', ptsInt, incident_date, + incident_time || null, location || null, + details || null, submitted_by || null, witness_name || null, + priorPts + ); + + res.status(201).json({ id: result.lastInsertRowid }); }); -// ── PATCH — Soft Negate (add resolution) ─────────────────────────────────── -app.patch('/api/violations/:id/negate', (req, res) => { - const { resolution_type, details, resolved_by } = req.body; - if (!resolution_type) return res.status(400).json({ error: 'resolution_type is required' }); +// Negate / restore / delete endpoints unchanged ... - const violation = db.prepare('SELECT * FROM violations WHERE id = ?').get(req.params.id); - if (!violation) return res.status(404).json({ error: 'Violation not found' }); - - db.prepare('UPDATE violations SET negated = 1, negated_at = CURRENT_TIMESTAMP WHERE id = ?').run(req.params.id); - db.prepare(` - INSERT INTO violation_resolutions (violation_id, resolution_type, details, resolved_by) - VALUES (?, ?, ?, ?) - `).run(req.params.id, resolution_type, details || null, resolved_by || null); - - res.json({ success: true }); -}); - -// ── PATCH — Restore negated violation ────────────────────────────────────── -app.patch('/api/violations/:id/restore', (req, res) => { - const violation = db.prepare('SELECT * FROM violations WHERE id = ?').get(req.params.id); - if (!violation) return res.status(404).json({ error: 'Violation not found' }); - db.prepare('UPDATE violations SET negated = 0, negated_at = NULL WHERE id = ?').run(req.params.id); - db.prepare('DELETE FROM violation_resolutions WHERE violation_id = ?').run(req.params.id); - res.json({ success: true }); -}); - -// ── DELETE — Hard Delete ──────────────────────────────────────────────────── -app.delete('/api/violations/:id', (req, res) => { - const violation = db.prepare('SELECT * FROM violations WHERE id = ?').get(req.params.id); - if (!violation) return res.status(404).json({ error: 'Violation not found' }); - db.prepare('DELETE FROM violation_resolutions WHERE violation_id = ?').run(req.params.id); - db.prepare('DELETE FROM violations WHERE id = ?').run(req.params.id); - res.json({ success: true }); -}); - -// ── PDF ───────────────────────────────────────────────────────────────────── +// PDF endpoint — use stored prior_active_points snapshot app.get('/api/violations/:id/pdf', async (req, res) => { - try { - const violation = db.prepare(` - SELECT v.*, e.name as employee_name, e.department, e.supervisor - FROM violations v JOIN employees e ON e.id = v.employee_id - WHERE v.id = ? - `).get(req.params.id); - if (!violation) return res.status(404).json({ error: 'Violation not found' }); - const score = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?').get(violation.employee_id) || { active_points: 0, violation_count: 0 }; - const pdfBuffer = await generatePdf(violation, score); - const safeName = violation.employee_name.replace(/[^a-z0-9]/gi, '_'); - res.set({ - 'Content-Type': 'application/pdf', - 'Content-Disposition': `attachment; filename="CPAS_${safeName}_${violation.incident_date}.pdf"`, - 'Content-Length': pdfBuffer.length, - }); - res.end(pdfBuffer); - } catch (err) { - console.error('[PDF]', err); - res.status(500).json({ error: 'PDF generation failed', detail: err.message }); - } + try { + const violation = db.prepare(` + SELECT v.*, e.name as employee_name, e.department, e.supervisor + FROM violations v + JOIN employees e ON e.id = v.employee_id + WHERE v.id = ? + `).get(req.params.id); + + if (!violation) return res.status(404).json({ error: 'Violation not found' }); + + // For PDF, compute score row but pass stored prior_active_points so math is stable + const active = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?') + .get(violation.employee_id) || { active_points: 0, violation_count: 0 }; + + const scoreForPdf = { + employee_id: violation.employee_id, + // snapshot at time of violation (if present); fall back to current + active_points: violation.prior_active_points != null ? violation.prior_active_points : active.active_points, + violation_count: active.violation_count, + }; + + const pdfBuffer = await generatePdf(violation, scoreForPdf); + const safeName = violation.employee_name.replace(/[^a-z0-9]/gi, '_'); + + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="CPAS_${safeName}_${violation.incident_date}.pdf"`, + 'Content-Length': pdfBuffer.length, + }); + res.end(pdfBuffer); + } catch (err) { + console.error('[PDF]', err); + res.status(500).json({ error: 'PDF generation failed', detail: err.message }); + } }); -// ── SPA fallback ──────────────────────────────────────────────────────────── +// SPA fallback app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'client', 'dist', 'index.html'))); app.listen(PORT, '0.0.0.0', () => console.log(`[CPAS] Server running on port ${PORT}`)); -- 2.52.0 From accb24a286677f03012aad72fce1dd062860bd40 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:59:55 -0600 Subject: [PATCH 08/92] Upload files to "db" --- db/database.js | 39 +++++++++++++++++++-------------------- db/schema.sql | 32 +++++++++++++++++--------------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/db/database.js b/db/database.js index a95c7c7..a29416f 100755 --- a/db/database.js +++ b/db/database.js @@ -13,27 +13,14 @@ db.pragma('foreign_keys = ON'); const schema = fs.readFileSync(path.join(__dirname, 'schema.sql'), 'utf8'); db.exec(schema); -// Migrate: add negated columns if upgrading from Phase 1-3 -const cols = db.prepare("PRAGMA table_info(violations)").all().map(c => c.name); -if (!cols.includes('negated')) db.exec("ALTER TABLE violations ADD COLUMN negated INTEGER NOT NULL DEFAULT 0"); -if (!cols.includes('negated_at')) db.exec("ALTER TABLE violations ADD COLUMN negated_at DATETIME"); +// ── Migrations for existing DBs ───────────────────────────────────────────── +const cols = db.prepare('PRAGMA table_info(violations)').all().map(c => c.name); +if (!cols.includes('negated')) db.exec("ALTER TABLE violations ADD COLUMN negated INTEGER NOT NULL DEFAULT 0"); +if (!cols.includes('negated_at')) db.exec("ALTER TABLE violations ADD COLUMN negated_at DATETIME"); +if (!cols.includes('prior_active_points')) db.exec("ALTER TABLE violations ADD COLUMN prior_active_points INTEGER"); +if (!cols.includes('prior_tier_label')) db.exec("ALTER TABLE violations ADD COLUMN prior_tier_label TEXT"); -// After adding columns + resolutions table, ensure view is correct -db.exec(` - DROP VIEW IF EXISTS active_cpas_scores; - CREATE VIEW active_cpas_scores AS - SELECT - employee_id, - SUM(points) AS active_points, - COUNT(*) AS violation_count - FROM violations - WHERE negated = 0 - AND incident_date >= DATE('now', '-90 days') - GROUP BY employee_id; -`); - - -// Ensure resolutions table exists on upgrade +// Ensure resolutions table exists db.exec(`CREATE TABLE IF NOT EXISTS violation_resolutions ( id INTEGER PRIMARY KEY AUTOINCREMENT, violation_id INTEGER NOT NULL REFERENCES violations(id) ON DELETE CASCADE, @@ -43,5 +30,17 @@ db.exec(`CREATE TABLE IF NOT EXISTS violation_resolutions ( created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); +// Recreate view so it always filters negated rows +db.exec(`DROP VIEW IF EXISTS active_cpas_scores; +CREATE VIEW active_cpas_scores AS +SELECT + employee_id, + SUM(points) AS active_points, + COUNT(*) AS violation_count +FROM violations +WHERE negated = 0 + AND incident_date >= DATE('now', '-90 days') +GROUP BY employee_id;`); + console.log('[DB] Connected:', dbPath); module.exports = db; diff --git a/db/schema.sql b/db/schema.sql index 1a45e0e..3bcc928 100755 --- a/db/schema.sql +++ b/db/schema.sql @@ -7,21 +7,23 @@ CREATE TABLE IF NOT EXISTS employees ( ); CREATE TABLE IF NOT EXISTS violations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - employee_id INTEGER NOT NULL REFERENCES employees(id), - violation_type TEXT NOT NULL, - violation_name TEXT NOT NULL, - category TEXT NOT NULL DEFAULT 'General', - points INTEGER NOT NULL, - incident_date TEXT NOT NULL, - incident_time TEXT, - location TEXT, - details TEXT, - submitted_by TEXT, - witness_name TEXT, - negated INTEGER NOT NULL DEFAULT 0, - negated_at DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP + id INTEGER PRIMARY KEY AUTOINCREMENT, + employee_id INTEGER NOT NULL REFERENCES employees(id), + violation_type TEXT NOT NULL, + violation_name TEXT NOT NULL, + category TEXT NOT NULL DEFAULT 'General', + points INTEGER NOT NULL, + incident_date TEXT NOT NULL, + incident_time TEXT, + location TEXT, + details TEXT, + submitted_by TEXT, + witness_name TEXT, + negated INTEGER NOT NULL DEFAULT 0, + negated_at DATETIME, + prior_active_points INTEGER, -- snapshot at time of logging + prior_tier_label TEXT, -- optional human-readable tier + created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS violation_resolutions ( -- 2.52.0 From cb8c56adfaa482c729f5b8d20a0872e6bd2d4ffc Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 15:00:27 -0600 Subject: [PATCH 09/92] Upload files to "pdf" --- pdf/template.js | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/pdf/template.js b/pdf/template.js index 7034b80..67fc5cc 100755 --- a/pdf/template.js +++ b/pdf/template.js @@ -16,10 +16,7 @@ function formatDate(d) { return dt.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'America/Chicago' }); } -function formatDateTime(d, t) { - const date = formatDate(d); - return t ? `${date} at ${t}` : date; -} +function formatDateTime(d, t) { const date = formatDate(d); return t ? `${date} at ${t}` : date; } function row(label, value) { return ` @@ -30,11 +27,11 @@ 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 priorPts = score.active_points || 0; // snapshot at time of logging + const priorTier= getTier(priorPts); + const newTotal = priorPts + v.points; // math always based on stored snapshot + const newTier = getTier(newTotal); + const tierChange = priorTier.label !== newTier.label; const generatedAt = new Date().toLocaleString('en-US', { timeZone: 'America/Chicago', dateStyle: 'full', timeStyle: 'short' }); @@ -84,10 +81,7 @@ function buildHtml(v, score) {

Message Point Media — Comprehensive Professional Accountability System

-
- Document ID: CPAS-${v.id.toString().padStart(5,'0')}
- Generated: ${generatedAt} -
+
Document ID: CPAS-${v.id.toString().padStart(5,'0')}
Generated: ${generatedAt}
@@ -122,9 +116,9 @@ function buildHtml(v, score) {
${v.points}
Points Assessed — This Violation
-
${activePts}
+
${priorPts}
Active Points (Prior)
-
${tier.label}
+
${priorTier.label}
+
@@ -138,7 +132,7 @@ function buildHtml(v, score) {
${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 ${priorTier.label} to ${newTier.label}.
` : ''}
-- 2.52.0 From 35b4ded10c4e902a17c0e10d32faf19f736d8fd1 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 15:25:48 -0600 Subject: [PATCH 10/92] Upload files to "client/src/components" --- client/src/components/EmployeeModal.jsx | 8 +- client/src/components/NegateModal.jsx | 232 ++++++++++++++++++------ 2 files changed, 182 insertions(+), 58 deletions(-) diff --git a/client/src/components/EmployeeModal.jsx b/client/src/components/EmployeeModal.jsx index 500ea7d..bd3c017 100755 --- a/client/src/components/EmployeeModal.jsx +++ b/client/src/components/EmployeeModal.jsx @@ -68,12 +68,18 @@ export default function EmployeeModal({ employeeId, onClose }) { 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 }); + await axios.patch(`/api/violations/${negating.id}/negate`, { + resolution_type, + details, + resolved_by, + }); setNegating(null); + setConfirmDel(null); load(); }; diff --git a/client/src/components/NegateModal.jsx b/client/src/components/NegateModal.jsx index 1ea38a1..e52614b 100755 --- a/client/src/components/NegateModal.jsx +++ b/client/src/components/NegateModal.jsx @@ -1,68 +1,186 @@ import React, { useState } from 'react'; -const RESOLUTION_TYPES = [ - 'Corrective Training Completed', - 'Management Discretion', - 'Data Entry Error', - 'Successfully Appealed', -]; - const s = { - overlay: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.65)', zIndex: 2000, display: 'flex', alignItems: 'center', justifyContent: 'center' }, - box: { background: 'white', borderRadius: '10px', padding: '28px', width: '440px', maxWidth: '95vw', boxShadow: '0 8px 32px rgba(0,0,0,0.22)' }, - title: { fontSize: '17px', fontWeight: 700, color: '#2c3e50', marginBottom: '6px' }, - sub: { fontSize: '12px', color: '#888', marginBottom: '20px' }, - label: { fontWeight: 600, color: '#555', fontSize: '12px', marginBottom: '5px', display: 'block' }, - input: { width: '100%', padding: '9px 12px', border: '1px solid #ddd', borderRadius: '5px', fontSize: '13px', fontFamily: 'inherit', marginBottom: '14px' }, - btnRow: { display: 'flex', gap: '10px', justifyContent: 'flex-end', marginTop: '8px' }, - btnOk: { padding: '10px 22px', background: '#856404', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 700, fontSize: '13px' }, - btnCancel:{ padding: '10px 22px', background: '#f1f3f5', color: '#555', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '13px' }, - violBox: { background: '#fff3cd', border: '1px solid #ffc107', borderRadius: '6px', padding: '10px 14px', marginBottom: '18px', fontSize: '13px' }, + overlay: { + position: 'fixed', + inset: 0, + background: 'rgba(0,0,0,0.75)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: 1100, + }, + modal: { + width: '480px', + maxWidth: '95vw', + background: '#111217', + borderRadius: '12px', + boxShadow: '0 16px 40px rgba(0,0,0,0.8)', + color: '#f8f9fa', + overflow: 'hidden', + border: '1px solid #2a2b3a', + }, + header: { + padding: '18px 24px', + borderBottom: '1px solid #222', + background: 'linear-gradient(135deg, #000000, #151622)', + }, + title: { + fontSize: '18px', + fontWeight: 700, + }, + subtitle: { + fontSize: '12px', + color: '#c0c2d6', + marginTop: '4px', + }, + body: { + padding: '18px 24px 8px 24px', + }, + pill: { + background: '#3b2e00', + borderRadius: '6px', + padding: '8px 10px', + fontSize: '12px', + color: '#ffd666', + border: '1px solid #d4af37', + marginBottom: '14px', + }, + label: { + fontSize: '13px', + fontWeight: 600, + marginBottom: '4px', + color: '#e5e7f1', + }, + input: { + width: '100%', + padding: '9px 10px', + borderRadius: '6px', + border: '1px solid #333544', + background: '#050608', + color: '#f8f9fa', + fontSize: '13px', + fontFamily: 'inherit', + marginBottom: '14px', + }, + textarea: { + width: '100%', + minHeight: '80px', + resize: 'vertical', + padding: '9px 10px', + borderRadius: '6px', + border: '1px solid #333544', + background: '#050608', + color: '#f8f9fa', + fontSize: '13px', + fontFamily: 'inherit', + marginBottom: '14px', + }, + footer: { + display: 'flex', + justifyContent: 'flex-end', + gap: '10px', + padding: '16px 24px 20px 24px', + background: '#0c0d14', + borderTop: '1px solid #222', + }, + btnCancel: { + padding: '10px 20px', + borderRadius: '6px', + border: '1px solid #333544', + background: '#050608', + color: '#f8f9fa', + fontWeight: 600, + fontSize: '13px', + cursor: 'pointer', + }, + btnConfirm: { + padding: '10px 22px', + borderRadius: '6px', + border: 'none', + background: 'linear-gradient(135deg, #d4af37 0%, #ffdf8a 100%)', + color: '#000', + fontWeight: 700, + fontSize: '13px', + cursor: 'pointer', + textTransform: 'uppercase', + }, }; export default function NegateModal({ violation, onConfirm, onCancel }) { - const [resType, setResType] = useState(''); - const [details, setDetails] = useState(''); - const [resolvedBy, setResolvedBy] = useState(''); - const [error, setError] = useState(''); + const [resolutionType, setResolutionType] = useState('Corrective Training Completed'); + const [details, setDetails] = useState(''); + const [resolvedBy, setResolvedBy] = useState(''); - const handleSubmit = () => { - if (!resType) { setError('Please select a resolution type.'); return; } - onConfirm({ resolution_type: resType, details, resolved_by: resolvedBy }); - }; + if (!violation) return null; - return ( -
-
-
⊘ Negate Violation Points
-
This will zero out the points from this incident. The record remains in the audit log.
+ const handleConfirm = () => { + onConfirm({ + resolution_type: resolutionType, + details, + resolved_by: resolvedBy, + }); + }; -
- {violation.violation_name}  ·  {violation.points} pts  ·  {violation.incident_date} -
- - - - - -