diff --git a/server.js b/server.js index 0a33626..01216e2 100755 --- a/server.js +++ b/server.js @@ -29,7 +29,7 @@ app.get('/api/health', (req, res) => res.json({ status: 'ok', timestamp: new Dat // ── Employees ───────────────────────────────────────────────────────────────── app.get('/api/employees', (req, res) => { - const rows = db.prepare('SELECT id, name, department, supervisor FROM employees ORDER BY name ASC').all(); + const rows = db.prepare('SELECT id, name, department, supervisor, notes FROM employees ORDER BY name ASC').all(); res.json(rows); }); @@ -51,13 +51,13 @@ app.post('/api/employees', (req, res) => { }); // ── Employee Edit ───────────────────────────────────────────────────────────── -// PATCH /api/employees/:id — update name, department, or supervisor +// PATCH /api/employees/:id — update name, department, supervisor, or notes app.patch('/api/employees/:id', (req, res) => { const id = parseInt(req.params.id); const emp = db.prepare('SELECT * FROM employees WHERE id = ?').get(id); if (!emp) return res.status(404).json({ error: 'Employee not found' }); - const { name, department, supervisor, performed_by } = req.body; + const { name, department, supervisor, notes, performed_by } = req.body; // Prevent name collision with a different employee if (name && name.trim() !== emp.name) { @@ -68,20 +68,21 @@ app.patch('/api/employees/:id', (req, res) => { const newName = (name || emp.name).trim(); const newDept = department !== undefined ? (department || null) : emp.department; const newSupervisor = supervisor !== undefined ? (supervisor || null) : emp.supervisor; + const newNotes = notes !== undefined ? (notes || null) : emp.notes; - db.prepare('UPDATE employees SET name = ?, department = ?, supervisor = ? WHERE id = ?') - .run(newName, newDept, newSupervisor, id); + db.prepare('UPDATE employees SET name = ?, department = ?, supervisor = ?, notes = ? WHERE id = ?') + .run(newName, newDept, newSupervisor, newNotes, id); audit('employee_edited', 'employee', id, performed_by, { - before: { name: emp.name, department: emp.department, supervisor: emp.supervisor }, - after: { name: newName, department: newDept, supervisor: newSupervisor }, + before: { name: emp.name, department: emp.department, supervisor: emp.supervisor, notes: emp.notes }, + after: { name: newName, department: newDept, supervisor: newSupervisor, notes: newNotes }, }); - res.json({ id, name: newName, department: newDept, supervisor: newSupervisor }); + res.json({ id, name: newName, department: newDept, supervisor: newSupervisor, notes: newNotes }); }); // ── Employee Merge ──────────────────────────────────────────────────────────── -// POST /api/employees/:id/merge — reassign all violations from sourceId → id, then delete source +// 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); const { source_id, performed_by } = req.body; @@ -112,12 +113,54 @@ app.post('/api/employees/:id/merge', (req, res) => { res.json({ success: true, violations_reassigned: violationsMoved }); }); +// ── Employee notes (PATCH shorthand) ───────────────────────────────────────── +// PATCH /api/employees/:id/notes — save free-text notes only +app.patch('/api/employees/:id/notes', (req, res) => { + const id = parseInt(req.params.id); + const emp = db.prepare('SELECT * FROM employees WHERE id = ?').get(id); + if (!emp) return res.status(404).json({ error: 'Employee not found' }); + + const { notes, performed_by } = req.body; + const newNotes = notes !== undefined ? (notes || null) : emp.notes; + + db.prepare('UPDATE employees SET notes = ? WHERE id = ?').run(newNotes, id); + audit('employee_notes_updated', 'employee', id, performed_by, { notes: newNotes }); + res.json({ id, notes: newNotes }); +}); + // 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 }); }); +// ── 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) => { + const rows = db.prepare(` + SELECT + v.id, + v.violation_name, + v.violation_type, + v.category, + v.points, + v.incident_date, + DATE(v.incident_date, '+90 days') AS expires_on, + CAST( + JULIANDAY(DATE(v.incident_date, '+90 days')) - + JULIANDAY(DATE('now')) + AS INTEGER + ) AS days_remaining + FROM violations v + WHERE v.employee_id = ? + AND v.negated = 0 + AND v.incident_date >= DATE('now', '-90 days') + ORDER BY v.incident_date ASC + `).all(req.params.id); + res.json(rows); +}); + // Dashboard app.get('/api/dashboard', (req, res) => { const rows = db.prepare(`