From 5bcd6d07dccf41c0afdbfa51c1e31bd7a57fa5b0 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 13:50:25 -0600 Subject: [PATCH 001/100] Upload files to "client/src/components" -- 2.52.0 From 610d72ab662a0852aa87113205e7fb2ebf50e6cc Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 13:50:42 -0600 Subject: [PATCH 002/100] Upload files to "pdf" -- 2.52.0 From 821192e879914f28e8d592e6636e13eba80f70a5 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 13:56:36 -0600 Subject: [PATCH 003/100] Update db/database.js --- db/database.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/db/database.js b/db/database.js index d2f72d7..a95c7c7 100755 --- a/db/database.js +++ b/db/database.js @@ -18,6 +18,21 @@ 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"); +// 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 db.exec(`CREATE TABLE IF NOT EXISTS violation_resolutions ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- 2.52.0 From 590aae5cca7e277f884b6cc658b3493a05ae8278 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:11:19 -0600 Subject: [PATCH 004/100] Upload files to "/" --- Dockerfile | 39 +++------------------------------------ 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/Dockerfile b/Dockerfile index e400493..c3bebec 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,61 +1,28 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Stage 1: Builder -# Installs ALL dependencies and compiles the React frontend inside Docker. -# Nothing needs to be installed on the host machine except Docker itself. -# ───────────────────────────────────────────────────────────────────────────── FROM node:20-alpine AS builder - WORKDIR /build - -# Install backend deps COPY package.json ./ RUN npm install - -# Install frontend deps and build React app COPY client/package.json ./client/ RUN cd client && npm install - COPY client/ ./client/ RUN cd client && npm run build -# ───────────────────────────────────────────────────────────────────────────── -# Stage 2: Production image -# ───────────────────────────────────────────────────────────────────────────── FROM node:20-alpine AS production - -# Chromium for Puppeteer PDF generation -RUN apk add --no-cache \ - chromium \ - nss \ - freetype \ - harfbuzz \ - ca-certificates \ - ttf-freefont - +RUN apk add --no-cache chromium nss freetype harfbuzz ca-certificates ttf-freefont ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser ENV NODE_ENV=production ENV PORT=3001 ENV DB_PATH=/data/cpas.db - WORKDIR /app - -# Copy backend node_modules and compiled frontend from builder COPY --from=builder /build/node_modules ./node_modules COPY --from=builder /build/client/dist ./client/dist - -# Copy all backend source files COPY server.js ./ COPY package.json ./ COPY db/ ./db/ COPY pdf/ ./pdf/ - -# Ensure data directory exists +COPY client/public/static ./client/dist/static RUN mkdir -p /data - EXPOSE 3001 - -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD wget -qO- http://localhost:3001/api/health || exit 1 - +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD wget -qO- http://localhost:3001/api/health || exit 1 CMD ["node", "server.js"] -- 2.52.0 From 8bbfd90f48c6ce018d506d327d787b47da8e3b2c Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:11:36 -0600 Subject: [PATCH 005/100] Upload files to "client/src" --- client/src/App.jsx | 60 +++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index 683e43d..6fdac9c 100755 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -3,38 +3,44 @@ import ViolationForm from './components/ViolationForm'; import Dashboard from './components/Dashboard'; const tabs = [ - { id: 'dashboard', label: '📊 Dashboard' }, - { id: 'violation', label: '+ New Violation' }, + { id: 'dashboard', label: '📊 Dashboard' }, + { id: 'violation', label: '+ New Violation' }, ]; const s = { - app: { minHeight: '100vh', background: '#f5f6fa', fontFamily: "'Segoe UI', Arial, sans-serif" }, - nav: { background: 'linear-gradient(135deg, #2c3e50, #34495e)', padding: '0 40px', display: 'flex', alignItems: 'center', gap: 0 }, - logo: { color: 'white', fontWeight: 800, fontSize: '18px', letterSpacing: '0.5px', marginRight: '32px', padding: '18px 0' }, - tab: (active) => ({ - padding: '18px 22px', color: active ? 'white' : 'rgba(255,255,255,0.6)', - borderBottom: active ? '3px solid #667eea' : '3px solid transparent', - cursor: 'pointer', fontWeight: active ? 700 : 400, fontSize: '14px', - background: 'none', border: 'none', borderBottom: active ? '3px solid #667eea' : '3px solid transparent', - }), - card: { maxWidth: '1100px', margin: '30px auto', background: 'white', borderRadius: '10px', boxShadow: '0 2px 12px rgba(0,0,0,0.08)' }, + app: { minHeight: '100vh', background: '#050608', fontFamily: "'Segoe UI', Arial, sans-serif", color: '#f8f9fa' }, + nav: { background: '#000000', padding: '0 40px', display: 'flex', alignItems: 'center', gap: 0, borderBottom: '1px solid #333' }, + logoWrap: { display: 'flex', alignItems: 'center', marginRight: '32px', padding: '14px 0' }, + logoImg: { height: '28px', marginRight: '10px' }, + logoText: { color: '#f8f9fa', fontWeight: 800, fontSize: '18px', letterSpacing: '0.5px' }, + tab: (active) => ({ + padding: '18px 22px', + color: active ? '#f8f9fa' : 'rgba(248,249,250,0.6)', + borderBottom: active ? '3px solid #d4af37' : '3px solid transparent', + cursor: 'pointer', fontWeight: active ? 700 : 400, fontSize: '14px', + background: 'none', border: 'none', + }), + card: { maxWidth: '1100px', margin: '30px auto', background: '#111217', borderRadius: '10px', boxShadow: '0 2px 16px rgba(0,0,0,0.6)', border: '1px solid #222' }, }; export default function App() { - const [tab, setTab] = useState('dashboard'); - return ( -
- -
- {tab === 'dashboard' ? : } -
+ const [tab, setTab] = useState('dashboard'); + return ( +
+ +
+ {tab === 'dashboard' ? : } +
+
+ ); } -- 2.52.0 From 2383e3cc9453a021c182f63f8b64a4ef5ebe06a1 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:11:46 -0600 Subject: [PATCH 006/100] Upload files to "client/src/components" --- client/src/components/Dashboard.jsx | 290 ++++++++++++++-------------- 1 file changed, 143 insertions(+), 147 deletions(-) diff --git a/client/src/components/Dashboard.jsx b/client/src/components/Dashboard.jsx index 33f6b81..6257990 100755 --- a/client/src/components/Dashboard.jsx +++ b/client/src/components/Dashboard.jsx @@ -3,171 +3,167 @@ import axios from 'axios'; import CpasBadge, { getTier } from './CpasBadge'; import EmployeeModal from './EmployeeModal'; -const AT_RISK_THRESHOLD = 2; // points within next tier boundary +const AT_RISK_THRESHOLD = 2; const TIERS = [ - { min: 0, max: 4 }, - { min: 5, max: 9 }, - { min: 10, max: 14 }, - { min: 15, max: 19 }, - { min: 20, max: 24 }, - { min: 25, max: 29 }, - { min: 30, max: 999}, + { min: 0, max: 4 }, + { min: 5, max: 9 }, + { min: 10, max: 14 }, + { min: 15, max: 19 }, + { min: 20, max: 24 }, + { min: 25, max: 29 }, + { min: 30, max: 999}, ]; function nextTierBoundary(points) { - for (const t of TIERS) { - if (points >= t.min && points <= t.max && t.max < 999) - return t.max + 1; - } - return null; + for (const t of TIERS) { + if (points >= t.min && points <= t.max && t.max < 999) return t.max + 1; + } + return null; } function isAtRisk(points) { - const boundary = nextTierBoundary(points); - return boundary !== null && (boundary - points) <= AT_RISK_THRESHOLD; + const boundary = nextTierBoundary(points); + return boundary !== null && (boundary - points) <= AT_RISK_THRESHOLD; } const s = { - wrap: { padding: '40px' }, - header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', flexWrap: 'wrap', gap: '12px' }, - title: { fontSize: '24px', fontWeight: 700, color: '#2c3e50' }, - subtitle: { fontSize: '13px', color: '#888', marginTop: '3px' }, - statsRow: { display: 'flex', gap: '16px', flexWrap: 'wrap', marginBottom: '28px' }, - statCard: { flex: '1', minWidth: '140px', background: '#f8f9fa', border: '1px solid #dee2e6', borderRadius: '8px', padding: '16px', textAlign: 'center' }, - statNum: { fontSize: '28px', fontWeight: 800, color: '#2c3e50' }, - statLbl: { fontSize: '11px', color: '#888', marginTop: '4px' }, - search: { padding: '10px 14px', border: '1px solid #ddd', borderRadius: '6px', fontSize: '14px', width: '260px' }, - table: { width: '100%', borderCollapse: 'collapse', background: 'white', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }, - th: { background: '#34495e', color: 'white', padding: '10px 14px', textAlign: 'left', fontSize: '12px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px' }, - td: { padding: '11px 14px', borderBottom: '1px solid #f0f0f0', fontSize: '13px', verticalAlign: 'middle' }, - nameBtn: { background: 'none', border: 'none', cursor: 'pointer', fontWeight: 600, color: '#667eea', fontSize: '14px', padding: 0, textDecoration: 'underline dotted' }, - atRiskBadge: { display: 'inline-block', marginLeft: '8px', padding: '2px 8px', borderRadius: '10px', fontSize: '10px', fontWeight: 700, background: '#fff3cd', color: '#856404', border: '1px solid #ffc107', verticalAlign: 'middle' }, - zeroRow: { color: '#aaa', fontStyle: 'italic', fontSize: '12px' }, - refreshBtn:{ padding: '9px 18px', background: '#667eea', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '13px' }, + wrap: { padding: '32px 40px', color: '#f8f9fa' }, + header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', flexWrap: 'wrap', gap: '12px' }, + title: { fontSize: '24px', fontWeight: 700, color: '#f8f9fa' }, + subtitle: { fontSize: '13px', color: '#b5b5c0', marginTop: '3px' }, + statsRow: { display: 'flex', gap: '16px', flexWrap: 'wrap', marginBottom: '28px' }, + statCard: { flex: '1', minWidth: '140px', background: '#181924', border: '1px solid #30313f', borderRadius: '8px', padding: '16px', textAlign: 'center' }, + statNum: { fontSize: '28px', fontWeight: 800, color: '#f8f9fa' }, + statLbl: { fontSize: '11px', color: '#b5b5c0', marginTop: '4px' }, + search: { padding: '10px 14px', border: '1px solid #333544', borderRadius: '6px', fontSize: '14px', width: '260px', background: '#050608', color: '#f8f9fa' }, + table: { width: '100%', borderCollapse: 'collapse', background: '#111217', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 1px 8px rgba(0,0,0,0.6)', border: '1px solid #222' }, + th: { background: '#000000', color: '#f8f9fa', padding: '10px 14px', textAlign: 'left', fontSize: '12px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px' }, + td: { padding: '11px 14px', borderBottom: '1px solid #1c1d29', fontSize: '13px', verticalAlign: 'middle', color: '#f8f9fa' }, + nameBtn: { background: 'none', border: 'none', cursor: 'pointer', fontWeight: 600, color: '#d4af37', fontSize: '14px', padding: 0, textDecoration: 'underline dotted' }, + atRiskBadge: { display: 'inline-block', marginLeft: '8px', padding: '2px 8px', borderRadius: '10px', fontSize: '10px', fontWeight: 700, background: '#3b2e00', color: '#ffd666', border: '1px solid #d4af37', verticalAlign: 'middle' }, + zeroRow: { color: '#77798a', fontStyle: 'italic', fontSize: '12px' }, + refreshBtn:{ padding: '9px 18px', background: '#d4af37', color: '#000', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '13px' }, }; export default function Dashboard() { - const [employees, setEmployees] = useState([]); - const [filtered, setFiltered] = useState([]); - const [search, setSearch] = useState(''); - const [selectedId, setSelectedId] = useState(null); - const [loading, setLoading] = useState(true); + const [employees, setEmployees] = useState([]); + const [filtered, setFiltered] = useState([]); + const [search, setSearch] = useState(''); + const [selectedId,setSelectedId] = useState(null); + const [loading, setLoading] = useState(true); - const load = useCallback(() => { - setLoading(true); - axios.get('/api/dashboard') - .then(r => { setEmployees(r.data); setFiltered(r.data); }) - .finally(() => setLoading(false)); - }, []); + const load = useCallback(() => { + setLoading(true); + axios.get('/api/dashboard') + .then(r => { setEmployees(r.data); setFiltered(r.data); }) + .finally(() => setLoading(false)); + }, []); - useEffect(() => { load(); }, [load]); + useEffect(() => { load(); }, [load]); - useEffect(() => { - const q = search.toLowerCase(); - setFiltered(employees.filter(e => - e.name.toLowerCase().includes(q) || - (e.department || '').toLowerCase().includes(q) || - (e.supervisor || '').toLowerCase().includes(q) - )); - }, [search, employees]); + useEffect(() => { + const q = search.toLowerCase(); + setFiltered(employees.filter(e => + e.name.toLowerCase().includes(q) || + (e.department || '').toLowerCase().includes(q) || + (e.supervisor || '').toLowerCase().includes(q) + )); + }, [search, employees]); - const atRiskCount = employees.filter(e => isAtRisk(e.active_points)).length; - const activeCount = employees.filter(e => e.active_points > 0).length; - const cleanCount = employees.filter(e => e.active_points === 0).length; - const maxPoints = employees.reduce((m, e) => Math.max(m, e.active_points), 0); + const atRiskCount = employees.filter(e => isAtRisk(e.active_points)).length; + const activeCount = employees.filter(e => e.active_points > 0).length; + const cleanCount = employees.filter(e => e.active_points === 0).length; + const maxPoints = employees.reduce((m, e) => Math.max(m, e.active_points), 0); - return ( -
-
-
-
Company Dashboard
-
Click any employee name to view their full profile
-
-
- setSearch(e.target.value)} /> - -
-
- - {/* ── Stat cards ───────────────────────────────────────── */} -
-
-
{employees.length}
-
Total Employees
-
-
-
{cleanCount}
-
Elite Standing (0 pts)
-
-
-
{activeCount}
-
With Active Points
-
-
-
{atRiskCount}
-
At Risk (≤{AT_RISK_THRESHOLD} pts to next tier)
-
-
-
{maxPoints}
-
Highest Active Score
-
-
- - {/* ── Scoreboard table ─────────────────────────────────── */} - {loading ? ( -

Loading…

- ) : ( - - - - - - - - - - - - - - {filtered.length === 0 && ( - - )} - {filtered.map((emp, i) => { - const risk = isAtRisk(emp.active_points); - const tier = getTier(emp.active_points); - const boundary = nextTierBoundary(emp.active_points); - return ( - - - - - - - - - - ); - })} - -
#EmployeeDepartmentSupervisorTier / StandingActive Points90-Day Violations
No employees found.
{i + 1} - - {risk && ( - - ⚠ {boundary - emp.active_points} pt{boundary - emp.active_points > 1 ? 's' : ''} to {getTier(boundary).label.split('—')[0].trim()} - - )} - {emp.department || '—'}{emp.supervisor || '—'}{emp.active_points}{emp.violation_count}
- )} - - {/* ── Employee profile modal ───────────────────────────── */} - {selectedId && ( - { setSelectedId(null); load(); }} - /> - )} + return ( +
+
+
+
Company Dashboard
+
Click any employee name to view their full profile
- ); +
+ setSearch(e.target.value)} /> + +
+
+ +
+
+
{employees.length}
+
Total Employees
+
+
+
{cleanCount}
+
Elite Standing (0 pts)
+
+
+
{activeCount}
+
With Active Points
+
+
+
{atRiskCount}
+
At Risk (≤{AT_RISK_THRESHOLD} pts to next tier)
+
+
+
{maxPoints}
+
Highest Active Score
+
+
+ + {loading ? ( +

Loading…

+ ) : ( + + + + + + + + + + + + + + {filtered.length === 0 && ( + + )} + {filtered.map((emp, i) => { + const risk = isAtRisk(emp.active_points); + const tier = getTier(emp.active_points); + const boundary = nextTierBoundary(emp.active_points); + return ( + + + + + + + + + + ); + })} + +
#EmployeeDepartmentSupervisorTier / StandingActive Points90-Day Violations
No employees found.
{i + 1} + + {risk && ( + + ⚠ {boundary - emp.active_points} pt{boundary - emp.active_points > 1 ? 's' : ''} to {getTier(boundary).label.split('—')[0].trim()} + + )} + {emp.department || '—'}{emp.supervisor || '—'}{emp.active_points}{emp.violation_count}
+ )} + + {selectedId && ( + { setSelectedId(null); load(); }} + /> + )} +
+ ); } -- 2.52.0 From 066f95cc8826fab72e0aa485a4b4f1f74a54809e Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:12:00 -0600 Subject: [PATCH 007/100] Upload files to "pdf" --- pdf/template.js | 405 ++++++++++++++++++++++++------------------------ 1 file changed, 199 insertions(+), 206 deletions(-) diff --git a/pdf/template.js b/pdf/template.js index 16fdc4e..76da038 100755 --- a/pdf/template.js +++ b/pdf/template.js @@ -1,281 +1,274 @@ -/** - * Builds the full HTML string for a CPAS violation PDF document. - * Matches the styling of the original HTML violation form. - */ +/** 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' }, - { min: 10, max: 14, label: 'Tier 2 — Administrative Lockdown', color: '#d9534f' }, - { min: 15, max: 19, label: 'Tier 3 — Verification', color: '#d9534f' }, - { min: 20, max: 24, label: 'Tier 4 — Risk Mitigation', color: '#c0392b' }, - { min: 25, max: 29, label: 'Tier 5 — Final Decision', color: '#c0392b' }, - { min: 30, max: 999,label: 'Tier 6 — Separation', color: '#721c24' }, + { min: 0, max: 4, label: 'Tier 0-1 — Elite Standing', color: '#28a745' }, + { min: 5, max: 9, label: 'Tier 1 — Realignment', color: '#856404' }, + { min: 10, max: 14, label: 'Tier 2 — Administrative Lockdown', color: '#d9534f' }, + { min: 15, max: 19, label: 'Tier 3 — Verification', color: '#d9534f' }, + { min: 20, max: 24, label: 'Tier 4 — Risk Mitigation', color: '#c0392b' }, + { min: 25, max: 29, label: 'Tier 5 — Final Decision', color: '#c0392b' }, + { 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]; + 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' - }); + 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' + }); } function formatDateTime(d, t) { - const date = formatDate(d); - return t ? `${date} at ${t}` : date; + const date = formatDate(d); + return t ? `${date} at ${t}` : date; } function row(label, value) { - return ` - - ${label} - ${value || '—'} - `; + return ` + + ${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 ` + return ` -
-
- Document ID: CPAS-${v.id.toString().padStart(5,'0')}
- Generated: ${generatedAt} +
+ +
+

CPAS Individual Violation Record

+

Message Point Media — Comprehensive Professional Accountability System

-

CPAS Individual Violation Record

-

Message Point Media — Confidential HR Document

+
+
+ Document ID: CPAS-${v.id.toString().padStart(5,'0')}
+ Generated: ${generatedAt} +
- ⚠ CONFIDENTIAL — For authorized HR and management use only + ⚠ CONFIDENTIAL — For authorized HR and management use only
-
-
Employee Information
- - ${row('Employee Name', `${v.employee_name}`)} - ${row('Department', v.department)} - ${row('Supervisor', v.supervisor)} - ${row('Witness / Documenting Officer', v.witness_name)} -
+
Employee Information
+ + ${row('Employee Name', `${v.employee_name}`)} + ${row('Department', v.department)} + ${row('Supervisor', v.supervisor)} + ${row('Witness / Documenting Officer', v.witness_name)} +
-
-
Violation Details
- - ${row('Violation Type', `${v.violation_name}`)} - ${row('Category', v.category)} - ${row('Policy Reference', 'Chapter 4, Section 5 — Comprehensive Professional Accountability System (CPAS)')} - ${row('Incident Date / Time', formatDateTime(v.incident_date, v.incident_time))} - ${v.location ? row('Location / Context', v.location) : ''} - ${row('Submitted By', v.submitted_by || 'System')} -
- - ${v.details ? ` +
Violation Details
+ + ${row('Violation Type', `${v.violation_name}`)} + ${row('Category', v.category)} + ${row('Policy Reference', 'Chapter 4, Section 5 — Comprehensive Professional Accountability System (CPAS)')} + ${row('Incident Date / Time', formatDateTime(v.incident_date, v.incident_time))} + ${v.location ? row('Location / Context', v.location) : ''} + ${row('Submitted By', v.submitted_by || 'System')} +
+ ${v.details ? `
- Incident Details:
- ${v.details} + Incident Details:
+ ${v.details}
` : ''}
-
-
CPAS Point Assessment
- -
-
${v.points}
-
Points Assessed — This Violation
+
CPAS Point Assessment
+
+
${v.points}
+
Points Assessed — This Violation
+
+
+
+
${activePts}
+
Active Points (Prior)
+
+ ${tier.label} +
- -
-
-
${activePts}
-
Active Points (Prior)
-
- ${tier.label} -
-
-
+
-
-
${v.points}
-
Points — This Violation
-
-
=
-
-
${newTotal}
-
New Active Total
-
- ${newTier.label} -
-
+
+
+
+
${v.points}
+
Points — This Violation
- - ${tierChange ? ` +
=
+
+
${newTotal}
+
New Active Total
+
+ ${newTier.label} +
+
+
+ ${tierChange ? `
- ⚠ Tier Escalation: This violation advances the employee from - ${tier.label} to ${newTier.label}. - Review associated tier consequences per the Employee Handbook. + ⚠ Tier Escalation: This violation advances the employee from + ${tier.label} to ${newTier.label}.
` : ''}
-
-
CPAS Tier Reference
- - - - - - ${TIERS.map(t => ` - - - - `).join('')} -
PointsTier
${t.min === 30 ? '30+' : t.min + '–' + t.max}${t.label}
+
CPAS Tier Reference
+ + + + + + ${TIERS.map(t => ` + + + + `).join('')} +
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, - Chapter 4, Section 5. This document should be reviewed with the employee and signed by all parties. + 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. -

-
-
-
-
Employee Signature
-
-
-
Date
-
-
-
-
-
Supervisor / Documenting Officer Signature
-
-
-
Date
-
-
-
+
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. +

+
+
+
Employee Signature
+
Date
+
+
+
Supervisor / Documenting Officer Signature
+
Date
+
+
-
+
`; } -- 2.52.0 From f006c015b064501f26cdd78fc1b5ed32333837d2 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 6 Mar 2026 14:14:11 -0600 Subject: [PATCH 008/100] 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 009/100] 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 010/100] 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:20:57 -0600 Subject: [PATCH 011/100] Upload files to "client/src/components" --- client/src/components/ViolationForm.jsx | 553 ++++++++++++------------ 1 file changed, 274 insertions(+), 279 deletions(-) diff --git a/client/src/components/ViolationForm.jsx b/client/src/components/ViolationForm.jsx index 165515c..f33a3ef 100755 --- a/client/src/components/ViolationForm.jsx +++ b/client/src/components/ViolationForm.jsx @@ -1,3 +1,4 @@ +\ import React, { useState, useEffect } from 'react'; import axios from 'axios'; import { violationData, violationGroups } from '../data/violations'; @@ -7,313 +8,307 @@ import TierWarning from './TierWarning'; import ViolationHistory from './ViolationHistory'; const s = { - content: { padding: '40px' }, - section: { background: '#f8f9fa', borderLeft: '4px solid #667eea', padding: '20px', marginBottom: '30px', borderRadius: '4px' }, - sectionTitle: { color: '#2c3e50', fontSize: '20px', marginBottom: '15px' }, - grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '15px', marginTop: '15px' }, - item: { display: 'flex', flexDirection: 'column' }, - label: { fontWeight: 600, color: '#555', marginBottom: '5px', fontSize: '13px' }, - input: { padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '14px', fontFamily: 'inherit' }, - fullCol: { gridColumn: '1 / -1' }, - contextBox: { background: '#f1f3f5', border: '1px solid #ced4da', borderRadius: '4px', padding: '10px', fontSize: '12px', color: '#444', marginTop: '4px' }, - repeatBadge: { display: 'inline-block', marginLeft: '8px', padding: '1px 7px', borderRadius: '10px', fontSize: '11px', fontWeight: 700, background: '#fff3cd', color: '#856404', border: '1px solid #ffc107' }, - repeatWarn: { background: '#fff3cd', border: '1px solid #ffc107', borderRadius: '4px', padding: '8px 12px', marginTop: '6px', fontSize: '12px', color: '#856404' }, - pointBox: { background: '#fff3cd', border: '2px solid #ffc107', padding: '15px', borderRadius: '6px', marginTop: '15px', textAlign: 'center' }, - pointValue: { fontSize: '24px', fontWeight: 'bold', color: '#667eea', margin: '10px 0' }, - scoreRow: { display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '14px', flexWrap: 'wrap' }, - btnRow: { display: 'flex', gap: '15px', justifyContent: 'center', marginTop: '30px', flexWrap: 'wrap' }, - btnPrimary: { padding: '15px 40px', fontSize: '16px', fontWeight: 600, border: 'none', borderRadius: '6px', cursor: 'pointer', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: 'white', textTransform: 'uppercase' }, - btnPdf: { padding: '15px 40px', fontSize: '16px', fontWeight: 600, border: 'none', borderRadius: '6px', cursor: 'pointer', background: 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)', color: 'white', textTransform: 'uppercase' }, - btnSecondary: { padding: '15px 40px', fontSize: '16px', fontWeight: 600, border: 'none', borderRadius: '6px', cursor: 'pointer', background: '#6c757d', color: 'white', textTransform: 'uppercase' }, - note: { background: '#e7f3ff', borderLeft: '4px solid #2196F3', padding: '15px', margin: '20px 0', borderRadius: '4px' }, - statusOk: { marginTop: '15px', padding: '15px', borderRadius: '6px', textAlign: 'center', fontWeight: 600, background: '#d4edda', color: '#155724', border: '1px solid #c3e6cb' }, - statusErr: { marginTop: '15px', padding: '15px', borderRadius: '6px', textAlign: 'center', fontWeight: 600, background: '#f8d7da', color: '#721c24', border: '1px solid #f5c6cb' }, + content: { padding: '32px 40px', background: '#111217', borderRadius: '10px', color: '#f8f9fa' }, + section: { background: '#181924', borderLeft: '4px solid #d4af37', padding: '20px', marginBottom: '30px', borderRadius: '4px', border: '1px solid #2a2b3a' }, + sectionTitle: { color: '#f8f9fa', fontSize: '20px', marginBottom: '15px', fontWeight: 700 }, + grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '15px', marginTop: '15px' }, + item: { display: 'flex', flexDirection: 'column' }, + label: { fontWeight: 600, color: '#e5e7f1', marginBottom: '5px', fontSize: '13px' }, + input: { padding: '10px', border: '1px solid #333544', borderRadius: '4px', fontSize: '14px', fontFamily: 'inherit', background: '#050608', color: '#f8f9fa' }, + fullCol: { gridColumn: '1 / -1' }, + contextBox: { background: '#141623', border: '1px solid #333544', borderRadius: '4px', padding: '10px', fontSize: '12px', color: '#d1d3e0', marginTop: '4px' }, + repeatBadge: { display: 'inline-block', marginLeft: '8px', padding: '1px 7px', borderRadius: '10px', fontSize: '11px', fontWeight: 700, background: '#3b2e00', color: '#ffd666', border: '1px solid #d4af37' }, + repeatWarn: { background: '#3b2e00', border: '1px solid #d4af37', borderRadius: '4px', padding: '8px 12px', marginTop: '6px', fontSize: '12px', color: '#ffdf8a' }, + pointBox: { background: '#181200', border: '2px solid #d4af37', padding: '15px', borderRadius: '6px', marginTop: '15px', textAlign: 'center' }, + pointValue: { fontSize: '24px', fontWeight: 'bold', color: '#ffd666', margin: '10px 0' }, + scoreRow: { display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '14px', flexWrap: 'wrap' }, + btnRow: { display: 'flex', gap: '15px', justifyContent: 'center', marginTop: '30px', flexWrap: 'wrap' }, + btnPrimary: { padding: '15px 40px', fontSize: '16px', fontWeight: 600, border: 'none', borderRadius: '6px', cursor: 'pointer', background: 'linear-gradient(135deg, #d4af37 0%, #ffdf8a 100%)', color: '#000', textTransform: 'uppercase' }, + btnPdf: { padding: '15px 40px', fontSize: '16px', fontWeight: 600, border: 'none', borderRadius: '6px', cursor: 'pointer', background: 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)', color: 'white', textTransform: 'uppercase' }, + btnSecondary: { padding: '15px 40px', fontSize: '16px', fontWeight: 600, border: '1px solid #333544', borderRadius: '6px', cursor: 'pointer', background: '#050608', color: '#f8f9fa', textTransform: 'uppercase' }, + note: { background: '#141623', borderLeft: '4px solid #2196F3', padding: '15px', margin: '20px 0', borderRadius: '4px', fontSize: '13px', color: '#d1d3e0' }, + statusOk: { marginTop: '15px', padding: '15px', borderRadius: '6px', textAlign: 'center', fontWeight: 600, background: '#053321', color: '#9ef7c1', border: '1px solid #0f5132' }, + statusErr: { marginTop: '15px', padding: '15px', borderRadius: '6px', textAlign: 'center', fontWeight: 600, background: '#3c1114', color: '#ffb3b8', border: '1px solid #f5c6cb' }, }; const EMPTY_FORM = { - employeeId: '', employeeName: '', department: '', supervisor: '', witnessName: '', - violationType: '', incidentDate: '', incidentTime: '', - amount: '', minutesLate: '', location: '', additionalDetails: '', points: 1, + employeeId: '', employeeName: '', department: '', supervisor: '', witnessName: '', + violationType: '', incidentDate: '', incidentTime: '', + amount: '', minutesLate: '', location: '', additionalDetails: '', points: 1, }; export default function ViolationForm() { - const [employees, setEmployees] = useState([]); - const [form, setForm] = useState(EMPTY_FORM); - const [violation, setViolation] = useState(null); - const [status, setStatus] = useState(null); - const [lastViolId, setLastViolId] = useState(null); // ID of most recently saved violation - const [pdfLoading, setPdfLoading] = useState(false); + const [employees, setEmployees] = useState([]); + const [form, setForm] = useState(EMPTY_FORM); + const [violation, setViolation] = useState(null); + const [status, setStatus] = useState(null); + const [lastViolId, setLastViolId] = useState(null); + const [pdfLoading, setPdfLoading] = useState(false); - const intel = useEmployeeIntelligence(form.employeeId || null); + const intel = useEmployeeIntelligence(form.employeeId || null); - useEffect(() => { - axios.get('/api/employees').then(r => setEmployees(r.data)).catch(() => {}); - }, []); + useEffect(() => { + axios.get('/api/employees').then(r => setEmployees(r.data)).catch(() => {}); + }, []); - useEffect(() => { - if (!violation || !form.violationType) return; - const allTime = intel.countsAllTime[form.violationType]; - if (allTime && allTime.count >= 1 && violation.minPoints !== violation.maxPoints) { - setForm(prev => ({ ...prev, points: violation.maxPoints })); - } else { - setForm(prev => ({ ...prev, points: violation.minPoints })); - } - }, [form.violationType, violation, intel.countsAllTime]); + useEffect(() => { + if (!violation || !form.violationType) return; + const allTime = intel.countsAllTime[form.violationType]; + if (allTime && allTime.count >= 1 && violation.minPoints !== violation.maxPoints) { + setForm(prev => ({ ...prev, points: violation.maxPoints })); + } else { + setForm(prev => ({ ...prev, points: violation.minPoints })); + } + }, [form.violationType, violation, intel.countsAllTime]); - const handleEmployeeSelect = e => { - const emp = employees.find(x => x.id === parseInt(e.target.value)); - if (!emp) return; - setForm(prev => ({ ...prev, employeeId: emp.id, employeeName: emp.name, department: emp.department || '', supervisor: emp.supervisor || '' })); - }; + const handleEmployeeSelect = e => { + const emp = employees.find(x => x.id === parseInt(e.target.value)); + if (!emp) return; + setForm(prev => ({ ...prev, employeeId: emp.id, employeeName: emp.name, department: emp.department || '', supervisor: emp.supervisor || '' })); + }; - const handleViolationChange = e => { - const key = e.target.value; - const v = violationData[key] || null; - setViolation(v); - setForm(prev => ({ ...prev, violationType: key, points: v ? v.minPoints : 1 })); - }; + const handleViolationChange = e => { + const key = e.target.value; + const v = violationData[key] || null; + setViolation(v); + setForm(prev => ({ ...prev, violationType: key, points: v ? v.minPoints : 1 })); + }; - const handleChange = e => setForm(prev => ({ ...prev, [e.target.name]: e.target.value })); + const handleChange = e => setForm(prev => ({ ...prev, [e.target.name]: e.target.value })); - const handleSubmit = async e => { - e.preventDefault(); - if (!form.violationType) return setStatus({ ok: false, msg: 'Please select a violation type.' }); - if (!form.employeeName) return setStatus({ ok: false, msg: 'Please enter an employee name.' }); - try { - const empRes = await axios.post('/api/employees', { name: form.employeeName, department: form.department, supervisor: form.supervisor }); - const employeeId = empRes.data.id; - const violRes = await axios.post('/api/violations', { - employee_id: employeeId, - violation_type: form.violationType, - violation_name: violation?.name || form.violationType, - category: violation?.category || 'General', - points: parseInt(form.points), - incident_date: form.incidentDate, - incident_time: form.incidentTime || null, - location: form.location || null, - details: form.additionalDetails || null, - witness_name: form.witnessName || null, - }); + const handleSubmit = async e => { + e.preventDefault(); + if (!form.violationType) return setStatus({ ok: false, msg: 'Please select a violation type.' }); + if (!form.employeeName) return setStatus({ ok: false, msg: 'Please enter an employee name.' }); + try { + const empRes = await axios.post('/api/employees', { name: form.employeeName, department: form.department, supervisor: form.supervisor }); + const employeeId = empRes.data.id; + const violRes = await axios.post('/api/violations', { + employee_id: employeeId, + violation_type: form.violationType, + violation_name: violation?.name || form.violationType, + category: violation?.category || 'General', + points: parseInt(form.points), + incident_date: form.incidentDate, + incident_time: form.incidentTime || null, + location: form.location || null, + details: form.additionalDetails || null, + witness_name: form.witnessName || null, + }); - const newId = violRes.data.id; - setLastViolId(newId); + const newId = violRes.data.id; + setLastViolId(newId); - const empList = await axios.get('/api/employees'); - setEmployees(empList.data); + const empList = await axios.get('/api/employees'); + setEmployees(empList.data); - setStatus({ ok: true, msg: `✓ Violation #${newId} recorded — click Download PDF to save the document.` }); - setForm(EMPTY_FORM); - setViolation(null); - } catch (err) { - setStatus({ ok: false, msg: '✗ Error: ' + (err.response?.data?.error || err.message) }); - } - }; + setStatus({ ok: true, msg: `✓ Violation #${newId} recorded — click Download PDF to save the document.` }); + setForm(EMPTY_FORM); + setViolation(null); + } catch (err) { + setStatus({ ok: false, msg: '✗ Error: ' + (err.response?.data?.error || err.message) }); + } + }; - const handleDownloadPdf = async () => { - if (!lastViolId) return; - setPdfLoading(true); - try { - const response = await axios.get(`/api/violations/${lastViolId}/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_Violation_${lastViolId}.pdf`; - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(url); - } catch (err) { - setStatus({ ok: false, msg: '✗ PDF generation failed: ' + err.message }); - } finally { - setPdfLoading(false); - } - }; + const handleDownloadPdf = async () => { + if (!lastViolId) return; + setPdfLoading(true); + try { + const response = await axios.get(`/api/violations/${lastViolId}/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_Violation_${lastViolId}.pdf`; + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + } catch (err) { + setStatus({ ok: false, msg: '✗ PDF generation failed: ' + err.message }); + } finally { + setPdfLoading(false); + } + }; - const showField = f => violation?.fields?.includes(f); - const priorCount90 = key => intel.counts90[key] || 0; - const isRepeat = key => (intel.countsAllTime[key]?.count || 0) >= 1; + const showField = f => violation?.fields?.includes(f); + const priorCount90 = key => intel.counts90[key] || 0; + const isRepeat = key => (intel.countsAllTime[key]?.count || 0) >= 1; - return ( -
+ return ( +
- {/* ── Employee Information ─────────────────────────────── */} -
-

Employee Information

+
+

Employee Information

- {intel.score && form.employeeId && ( -
- Current Standing: - - - {intel.score.violation_count} violation{intel.score.violation_count !== 1 ? 's' : ''} in last 90 days - -
- )} + {intel.score && form.employeeId && ( +
+ Current Standing: + + + {intel.score.violation_count} violation{intel.score.violation_count !== 1 ? 's' : ''} in last 90 days + +
+ )} - {employees.length > 0 && ( -
- - -
- )} + {employees.length > 0 && ( +
+ + +
+ )} -
- {[['employeeName','Employee Name','text','John Doe'],['department','Department','text','Engineering'],['supervisor','Supervisor Name','text','Jane Smith'],['witnessName','Witness Name (Officer)','text','Officer Name']].map(([name,label,type,ph]) => ( -
- - -
- ))} +
+ {[['employeeName','Employee Name','text','John Doe'],['department','Department','text','Engineering'],['supervisor','Supervisor Name','text','Jane Smith'],['witnessName','Witness Name (Officer)','text','Officer Name']].map(([name,label,type,ph]) => ( +
+ + +
+ ))} +
+
+ +
+
+

Violation Details

+
+ +
+ + + + {violation && ( +
+ {violation.name} + {isRepeat(form.violationType) && form.employeeId && ( + + ★ Repeat — {intel.countsAllTime[form.violationType]?.count}x prior + + )} +
{violation.description}
+ {violation.chapter}
+ )} + + {violation && isRepeat(form.violationType) && form.employeeId && violation.minPoints !== violation.maxPoints && ( +
+ Repeat offense detected. Point slider set to maximum ({violation.maxPoints} pts) per recidivist policy. Adjust if needed. +
+ )}
- {/* ── Violation Details ────────────────────────────────── */} - -
-

Violation Details

-
+
+ + +
-
- - - - {violation && ( -
- {violation.name} - {isRepeat(form.violationType) && form.employeeId && ( - - ★ Repeat — {intel.countsAllTime[form.violationType]?.count}x prior - - )} -
{violation.description}
- {violation.chapter} -
- )} - - {violation && isRepeat(form.violationType) && form.employeeId && violation.minPoints !== violation.maxPoints && ( -
- Repeat offense detected. Point slider set to maximum ({violation.maxPoints} pts) per recidivist policy. Adjust if needed. -
- )} -
- -
- - -
- - {showField('time') && ( -
- - -
- )} - {showField('minutes') && ( -
- - -
- )} - {showField('amount') && ( -
- - -
- )} - {showField('location') && ( -
- - -
- )} - {showField('description') && ( -
- -