feat: accept acknowledged_by/acknowledged_date in violation creation and amendment

- POST /api/violations now accepts acknowledged_by and acknowledged_date
- Both fields added to AMENDABLE_FIELDS whitelist for post-submission edits
- Acknowledgment data persisted to DB and passed through to PDF generation
This commit is contained in:
2026-03-07 21:32:15 -06:00
parent 8944cc80e0
commit b4edcdc945

View File

@@ -27,7 +27,7 @@ function audit(action, entityType, entityId, performedBy, details) {
// 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, notes FROM employees ORDER BY name ASC').all();
res.json(rows);
@@ -50,7 +50,7 @@ app.post('/api/employees', (req, res) => {
res.status(201).json({ id: result.lastInsertRowid, name, department, supervisor });
});
// ── Employee Edit ────────────────────────────────────────────────────────────
// ── Employee Edit ────────────────────────────────────────────────────────────
// PATCH /api/employees/:id — update name, department, supervisor, or notes
app.patch('/api/employees/:id', (req, res) => {
const id = parseInt(req.params.id);
@@ -81,7 +81,7 @@ app.patch('/api/employees/:id', (req, res) => {
res.json({ id, name: newName, department: newDept, supervisor: newSupervisor, notes: newNotes });
});
// ── Employee Merge ───────────────────────────────────────────────────────────
// ── Employee Merge ───────────────────────────────────────────────────────────
// POST /api/employees/:id/merge — reassign all violations from sourceId → id, then delete source
app.post('/api/employees/:id/merge', (req, res) => {
const targetId = parseInt(req.params.id);
@@ -134,7 +134,7 @@ app.get('/api/employees/:id/score', (req, res) => {
res.json(row || { employee_id: req.params.id, active_points: 0, violation_count: 0 });
});
// ── Expiration Timeline ──────────────────────────────────────────────────────
// ── Expiration Timeline ──────────────────────────────────────────────────────
// GET /api/employees/:id/expiration — active violations sorted by roll-off date
// Returns each active violation with days_remaining until it exits the 90-day window.
app.get('/api/employees/:id/expiration', (req, res) => {
@@ -151,7 +151,7 @@ app.get('/api/employees/:id/expiration', (req, res) => {
JULIANDAY(DATE(v.incident_date, '+90 days')) -
JULIANDAY(DATE('now'))
AS INTEGER
) AS days_remaining
) AS days_remaining
FROM violations v
WHERE v.employee_id = ?
AND v.negated = 0
@@ -190,7 +190,7 @@ app.get('/api/violations/employee/:id', (req, res) => {
res.json(rows);
});
// ── Violation amendment history ──────────────────────────────────────────────
// ── Violation amendment history ──────────────────────────────────────────────
app.get('/api/violations/:id/amendments', (req, res) => {
const rows = db.prepare(`
SELECT * FROM violation_amendments WHERE violation_id = ? ORDER BY created_at DESC
@@ -216,7 +216,8 @@ 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
details, submitted_by, witness_name,
acknowledged_by, acknowledged_date
} = req.body;
if (!employee_id || !violation_type || !points || !incident_date) {
@@ -231,14 +232,16 @@ app.post('/api/violations', (req, res) => {
employee_id, violation_type, violation_name, category,
points, incident_date, incident_time, location,
details, submitted_by, witness_name,
prior_active_points
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
prior_active_points,
acknowledged_by, acknowledged_date
) 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
priorPts,
acknowledged_by || null, acknowledged_date || null
);
audit('violation_created', 'violation', result.lastInsertRowid, submitted_by, {
@@ -248,9 +251,9 @@ app.post('/api/violations', (req, res) => {
res.status(201).json({ id: result.lastInsertRowid });
});
// ── Violation Amendment (edit) ───────────────────────────────────────────────
// ── Violation Amendment (edit) ───────────────────────────────────────────────
// PATCH /api/violations/:id/amend — edit mutable fields; logs a diff per changed field
const AMENDABLE_FIELDS = ['incident_time', 'location', 'details', 'submitted_by', 'witness_name'];
const AMENDABLE_FIELDS = ['incident_time', 'location', 'details', 'submitted_by', 'witness_name', 'acknowledged_by', 'acknowledged_date'];
app.patch('/api/violations/:id/amend', (req, res) => {
const id = parseInt(req.params.id);
@@ -295,7 +298,7 @@ app.patch('/api/violations/:id/amend', (req, res) => {
res.json(updated);
});
// ── Negate a violation ───────────────────────────────────────────────────────
// ── Negate a violation ───────────────────────────────────────────────────────
app.patch('/api/violations/:id/negate', (req, res) => {
const { resolution_type, details, resolved_by } = req.body;
const id = req.params.id;
@@ -323,7 +326,7 @@ app.patch('/api/violations/:id/negate', (req, res) => {
res.json({ success: true });
});
// ── Restore a negated violation ──────────────────────────────────────────────
// ── Restore a negated violation ──────────────────────────────────────────────
app.patch('/api/violations/:id/restore', (req, res) => {
const id = req.params.id;
@@ -337,7 +340,7 @@ app.patch('/api/violations/:id/restore', (req, res) => {
res.json({ success: true });
});
// ── Hard delete a violation ──────────────────────────────────────────────────
// ── Hard delete a violation ──────────────────────────────────────────────────
app.delete('/api/violations/:id', (req, res) => {
const id = req.params.id;
@@ -353,7 +356,7 @@ app.delete('/api/violations/:id', (req, res) => {
res.json({ success: true });
});
// ── Audit log ────────────────────────────────────────────────────────────────
// ── Audit log ────────────────────────────────────────────────────────────────
app.get('/api/audit', (req, res) => {
const limit = Math.min(parseInt(req.query.limit) || 100, 500);
const offset = parseInt(req.query.offset) || 0;
@@ -372,7 +375,7 @@ app.get('/api/audit', (req, res) => {
res.json(db.prepare(sql).all(...args));
});
// ── PDF endpoint ─────────────────────────────────────────────────────────────
// ── PDF endpoint ─────────────────────────────────────────────────────────────
app.get('/api/violations/:id/pdf', async (req, res) => {
try {
const violation = db.prepare(`
@@ -399,7 +402,7 @@ app.get('/api/violations/:id/pdf', async (req, res) => {
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="CPAS_${safeName}_${violation.incident_date}.pdf"`,
'Content-Length': pdfBuffer.length,
'Content-Length': pdfBuffer.length,
});
res.end(pdfBuffer);
} catch (err) {