This commit is contained in:
@@ -5,7 +5,11 @@
|
|||||||
"Bash(env)",
|
"Bash(env)",
|
||||||
"Bash(findstr /I OBSIDIAN)",
|
"Bash(findstr /I OBSIDIAN)",
|
||||||
"Bash(set)",
|
"Bash(set)",
|
||||||
"Bash(xargs grep -l -i \"obsidian\")"
|
"Bash(xargs grep -l -i \"obsidian\")",
|
||||||
|
"Bash(xargs -I {} git log --oneline -1 {})",
|
||||||
|
"Bash(ls -la data)",
|
||||||
|
"Bash(ls *.db)",
|
||||||
|
"Bash(find . -name \"cpas.db\" -not -path \"*/node_modules/*\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,13 @@ Violations are **never hard-deleted** in normal workflow. Use the `negated` flag
|
|||||||
|
|
||||||
Every `INSERT` into `violations` must compute and store `prior_active_points` (the employee's current active score before this violation is added). This snapshot ensures PDFs always reflect the accurate historical tier state regardless of subsequent negate/restore actions.
|
Every `INSERT` into `violations` must compute and store `prior_active_points` (the employee's current active score before this violation is added). This snapshot ensures PDFs always reflect the accurate historical tier state regardless of subsequent negate/restore actions.
|
||||||
|
|
||||||
**Back-dated inserts are the one exception to snapshot immutability.** If a new violation's `incident_date` precedes existing violations within the 90-day window, those existing violations' snapshots are recomputed via `recomputeSnapshotsAfter()` inside the same transaction as the insert, and a `violation_snapshots_recomputed` audit entry is written. A back-dated insert is a *timeline rewrite* — the prior violations genuinely had an earlier event in their 90-day window — so their PDFs must reflect that. Negate/restore are NOT timeline rewrites and must never recompute snapshots.
|
**There are exactly two sanctioned paths that may modify `prior_active_points` after insert; both are audit-logged as `violation_snapshots_recomputed`:**
|
||||||
|
|
||||||
|
1. **Back-dated insert (automatic).** If a new violation's `incident_date` precedes existing violations within the 90-day window, those existing violations' snapshots are recomputed via `recomputeSnapshotsAfter()` inside the same transaction as the insert. Logged with `reason: "backdated_insert"`. A back-dated insert is a *timeline rewrite* — the prior violations genuinely had an earlier event in their 90-day window — so their PDFs must reflect that.
|
||||||
|
|
||||||
|
2. **Manual backfill (admin-triggered).** `POST /api/employees/:id/recompute-snapshots` calls `recomputeAllSnapshotsForEmployee()` and rewrites every row for that employee from current data. Logged with `reason: "manual_backfill"`. This exists to repair drift from back-dated inserts that happened under older code (before path #1 existed) or any other case where the snapshot diverged from current truth. It is exposed in the UI as the **↻ Backfill Snapshots** button in the Employee Profile Modal next to the Active Violations header. Treat it as a targeted repair tool, not a routine maintenance step.
|
||||||
|
|
||||||
|
Negate/restore/amend/hard-delete are NOT timeline rewrites and must never recompute snapshots — PDFs remain stable through those operations by design.
|
||||||
|
|
||||||
### Audit Log
|
### Audit Log
|
||||||
|
|
||||||
@@ -249,7 +255,7 @@ docker run -d --name cpas -p 3001:3001 -v cpas-data:/data cpas
|
|||||||
### What NOT to Do
|
### What NOT to Do
|
||||||
|
|
||||||
- Do not compute active CPAS scores in JavaScript by summing violations client-side. Always fetch from the `active_cpas_scores` view.
|
- Do not compute active CPAS scores in JavaScript by summing violations client-side. Always fetch from the `active_cpas_scores` view.
|
||||||
- Do not modify `prior_active_points` after a violation is inserted, EXCEPT when a back-dated insert retroactively places a new earlier event into another violation's 90-day prior window. That path is handled by `recomputeSnapshotsAfter()` in `server.js` and is audit-logged. Never recompute snapshots on negate, restore, amend, or hard delete.
|
- Do not modify `prior_active_points` after a violation is inserted, EXCEPT via one of the two sanctioned paths: automatic recompute on a back-dated insert (`recomputeSnapshotsAfter()`), or manual admin backfill (`recomputeAllSnapshotsForEmployee()` behind `POST /api/employees/:id/recompute-snapshots` and the **↻ Backfill Snapshots** UI button). Both are audit-logged as `violation_snapshots_recomputed`. Never recompute snapshots on negate, restore, amend, or hard delete.
|
||||||
- Do not add columns to `audit_log`. It is append-only with a fixed schema.
|
- Do not add columns to `audit_log`. It is append-only with a fixed schema.
|
||||||
- Do not add a framework or ORM. Raw SQL with prepared statements is intentional — it keeps the query behavior explicit and the dependency surface small.
|
- Do not add a framework or ORM. Raw SQL with prepared statements is intentional — it keeps the query behavior explicit and the dependency surface small.
|
||||||
- Do not add a build step beyond `vite build`. The backend is plain CommonJS `require()`; do not transpile it.
|
- Do not add a build step beyond `vite build`. The backend is plain CommonJS `require()`; do not transpile it.
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ It manages **employee violations against the CPAS rubric** on a rolling 90-day w
|
|||||||
|
|
||||||
- **Company Dashboard** — every employee sorted by active CPAS points (highest risk first), with summary stat cards, tier badges, an "at-risk" flag (within 2 pts of next tier), and search + department filter.
|
- **Company Dashboard** — every employee sorted by active CPAS points (highest risk first), with summary stat cards, tier badges, an "at-risk" flag (within 2 pts of next tier), and search + department filter.
|
||||||
- **Violation Form** — pick an employee, pick a violation type, see prior 90-day count inline; recidivist auto-escalation; pre-submit tier-crossing warning; context-aware fields; one-click PDF download on submit; optional employee acknowledgment block.
|
- **Violation Form** — pick an employee, pick a violation type, see prior 90-day count inline; recidivist auto-escalation; pre-submit tier-crossing warning; context-aware fields; one-click PDF download on submit; optional employee acknowledgment block.
|
||||||
- **Employee Profile Modal** — full violation history, amendment count, edit employee, merge duplicate, negate/restore, hard delete, per-violation PDF, free-text notes/flags ("on PIP", "union member"), and a per-violation 90-day **point expiration timeline** with projected tier drops.
|
- **Employee Profile Modal** — full violation history, amendment count, edit employee, merge duplicate, negate/restore, hard delete, per-violation PDF, free-text notes/flags ("on PIP", "union member"), a per-violation 90-day **point expiration timeline** with projected tier drops, and a **↻ Backfill Snapshots** repair button that rebuilds the `prior_active_points` snapshot on every violation for that employee (use after a back-date if older PDFs show stale prior-point totals; audit-logged with reason `manual_backfill`).
|
||||||
- **Violation Amendment** — point value / type / incident date are immutable; non-scoring fields (location, witness, narrative, acknowledgment) are amendable with a field-level diff trail.
|
- **Violation Amendment** — point value / type / incident date are immutable; non-scoring fields (location, witness, narrative, acknowledgment) are amendable with a field-level diff trail.
|
||||||
- **Audit Log** — append-only record of every write action (employee CRUD, violation logged/amended/negated/restored/deleted); filterable, paginated panel from the dashboard.
|
- **Audit Log** — append-only record of every write action (employee CRUD, violation logged/amended/negated/restored/deleted); filterable, paginated panel from the dashboard.
|
||||||
- **Toast notification system** — global success/error/warning/info, auto-dismiss with progress bar.
|
- **Toast notification system** — global success/error/warning/info, auto-dismiss with progress bar.
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ Useful for showing the app to stakeholders without exposing live employee data.
|
|||||||
- PDF download for any historical violation record
|
- PDF download for any historical violation record
|
||||||
- **Notes & Flags** — free-text notes (e.g. "on PIP", "union member") with quick-add tag buttons; visible in the profile modal without affecting scoring
|
- **Notes & Flags** — free-text notes (e.g. "on PIP", "union member") with quick-add tag buttons; visible in the profile modal without affecting scoring
|
||||||
- **Point Expiration Timeline** — shows when each active violation rolls off the 90-day window, with a progress bar, days-remaining countdown, and projected tier-drop indicators
|
- **Point Expiration Timeline** — shows when each active violation rolls off the 90-day window, with a progress bar, days-remaining countdown, and projected tier-drop indicators
|
||||||
|
- **↻ Backfill Snapshots** button (next to the Active Violations header) — manually rebuilds the `prior_active_points` snapshot on every violation for this employee. Use after back-dating a violation under older code, or any time a regenerated PDF shows stale prior-point totals. Audit-logged as `violation_snapshots_recomputed` with reason `manual_backfill`. See [Backfilling Prior-Points Snapshots](#backfilling-prior-points-snapshots) below.
|
||||||
- **Toast notifications** for all actions: negate, restore, delete, amend, PDF download, employee edit
|
- **Toast notifications** for all actions: negate, restore, delete, amend, PDF download, employee edit
|
||||||
|
|
||||||
### Audit Log
|
### Audit Log
|
||||||
@@ -208,6 +209,45 @@ Scores are computed over a **rolling 90-day window** (negated violations exclude
|
|||||||
- Filename: `CPAS_<EmployeeName>_<IncidentDate>.pdf`
|
- Filename: `CPAS_<EmployeeName>_<IncidentDate>.pdf`
|
||||||
- PDF captures prior active points **at the time of the incident** (snapshot stored on insert)
|
- PDF captures prior active points **at the time of the incident** (snapshot stored on insert)
|
||||||
- **Acknowledgment rendering**: if the violation has an `acknowledged_by` value, the employee signature block on the PDF shows the recorded name and date with an "Acknowledged" badge; otherwise, blank signature lines are rendered for wet-ink signing
|
- **Acknowledgment rendering**: if the violation has an `acknowledged_by` value, the employee signature block on the PDF shows the recorded name and date with an "Acknowledged" badge; otherwise, blank signature lines are rendered for wet-ink signing
|
||||||
|
- **Back-dated inserts** auto-refresh the snapshot on downstream violations whose 90-day prior window now includes the new earlier event (handled inside the insert transaction by `recomputeSnapshotsAfter()`). If a back-date happened under older code that lacked this auto-refresh, use the **↻ Backfill Snapshots** button in the Employee Profile Modal — see [Backfilling Prior-Points Snapshots](#backfilling-prior-points-snapshots).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backfilling Prior-Points Snapshots
|
||||||
|
|
||||||
|
Each violation stores a `prior_active_points` snapshot at insert time so its PDF always reflects the score *as it was on the incident date* (and stays stable through later negate/restore actions). Normally you never touch this column.
|
||||||
|
|
||||||
|
There is one situation where the snapshot can drift from current truth: a violation was back-dated *before* `recomputeSnapshotsAfter()` shipped (commit `e2c352d`), so the auto-refresh never ran on the violations that now sit inside its 90-day window. Symptom: re-downloading the PDF for the newer violation shows "Prior Active Points: 0" even though an earlier active violation clearly exists in the timeline.
|
||||||
|
|
||||||
|
**To fix:**
|
||||||
|
|
||||||
|
1. Open the affected employee's profile modal.
|
||||||
|
2. Click **↻ Backfill Snapshots** next to the **Active Violations** header.
|
||||||
|
3. Confirm the prompt. A toast reports `Updated X of Y snapshot(s)` or `Snapshots already up to date`.
|
||||||
|
4. Re-download the PDFs — they now reflect the corrected prior totals.
|
||||||
|
|
||||||
|
**What it does, exactly:**
|
||||||
|
|
||||||
|
- Iterates every violation belonging to that employee (active *and* negated).
|
||||||
|
- Recomputes each row's `prior_active_points` using the current set of non-negated violations in the 90 days before its `incident_date`.
|
||||||
|
- Writes only the rows that actually changed and reports the diff.
|
||||||
|
- Runs inside a single transaction.
|
||||||
|
- Writes one `violation_snapshots_recomputed` entry to the audit log with `reason: "manual_backfill"` and the per-row before/after values.
|
||||||
|
|
||||||
|
**When *not* to use it:**
|
||||||
|
|
||||||
|
- After a negate, restore, amend, or hard delete in normal workflow. The auto-managed snapshot is correct in those cases by design (PDFs are intentionally stable through negate/restore).
|
||||||
|
- As a routine maintenance step. It's a targeted repair tool, not a recurring task. If you find yourself reaching for it after normal back-dated inserts, file a bug — the auto-recompute should already be handling those.
|
||||||
|
|
||||||
|
**API endpoint:** `POST /api/employees/:id/recompute-snapshots`
|
||||||
|
|
||||||
|
Response shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "success": true, "scanned": 2, "updated": 1, "changes": [
|
||||||
|
{ "id": 47, "incident_date": "2026-04-02", "old": 0, "new": 3 }
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -222,6 +262,7 @@ Scores are computed over a **rolling 90-day window** (negated violations exclude
|
|||||||
| PATCH | `/api/employees/:id` | Edit name, department, supervisor, or notes |
|
| PATCH | `/api/employees/:id` | Edit name, department, supervisor, or notes |
|
||||||
| PATCH | `/api/employees/:id/notes` | Save employee notes only (shorthand) |
|
| PATCH | `/api/employees/:id/notes` | Save employee notes only (shorthand) |
|
||||||
| POST | `/api/employees/:id/merge` | Merge duplicate employee; reassigns all violations |
|
| POST | `/api/employees/:id/merge` | Merge duplicate employee; reassigns all violations |
|
||||||
|
| POST | `/api/employees/:id/recompute-snapshots` | Manual backfill — rebuild `prior_active_points` on every violation for this employee. See [Backfilling Prior-Points Snapshots](#backfilling-prior-points-snapshots) |
|
||||||
| GET | `/api/employees/:id/score` | Get active CPAS score for employee |
|
| GET | `/api/employees/:id/score` | Get active CPAS score for employee |
|
||||||
| GET | `/api/employees/:id/expiration` | Active violation roll-off timeline with days remaining |
|
| GET | `/api/employees/:id/expiration` | Active violation roll-off timeline with days remaining |
|
||||||
| GET | `/api/employees/:id/violation-counts` | 90-day non-negated counts grouped by violation type |
|
| GET | `/api/employees/:id/violation-counts` | 90-day non-negated counts grouped by violation type |
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ const s = {
|
|||||||
fontSize: '9px', fontWeight: 700, background: '#0e2a2a', color: '#4db6ac',
|
fontSize: '9px', fontWeight: 700, background: '#0e2a2a', color: '#4db6ac',
|
||||||
border: '1px solid #1a4a4a', verticalAlign: 'middle',
|
border: '1px solid #1a4a4a', verticalAlign: 'middle',
|
||||||
},
|
},
|
||||||
|
backfillBtn: {
|
||||||
|
background: 'none', border: '1px solid #d4af37', color: '#ffd666',
|
||||||
|
borderRadius: '4px', padding: '4px 10px', fontSize: '11px',
|
||||||
|
cursor: 'pointer', fontWeight: 600,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EmployeeModal({ employeeId, onClose }) {
|
export default function EmployeeModal({ employeeId, onClose }) {
|
||||||
@@ -156,6 +161,26 @@ export default function EmployeeModal({ employeeId, onClose }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRecomputeSnapshots = async () => {
|
||||||
|
if (!window.confirm(
|
||||||
|
'Rebuild the "Prior Active Points" snapshot on every violation for this employee?\n\n' +
|
||||||
|
'Use this after back-dating a violation if older PDFs no longer reflect the correct prior-points total. ' +
|
||||||
|
'Existing PDFs will regenerate with up-to-date numbers.'
|
||||||
|
)) return;
|
||||||
|
try {
|
||||||
|
const r = await axios.post(`/api/employees/${employeeId}/recompute-snapshots`);
|
||||||
|
const { scanned, updated } = r.data;
|
||||||
|
if (updated === 0) {
|
||||||
|
toast.success(`Snapshots already up to date (${scanned} checked).`);
|
||||||
|
} else {
|
||||||
|
toast.success(`Updated ${updated} of ${scanned} snapshot${updated === 1 ? '' : 's'}.`);
|
||||||
|
}
|
||||||
|
load();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Backfill failed: ' + (err.response?.data?.error || err.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleNegate = async ({ resolution_type, details, resolved_by }) => {
|
const handleNegate = async ({ resolution_type, details, resolved_by }) => {
|
||||||
try {
|
try {
|
||||||
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 });
|
||||||
@@ -251,7 +276,18 @@ export default function EmployeeModal({ employeeId, onClose }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Active Violations ── */}
|
{/* ── Active Violations ── */}
|
||||||
<div style={s.sectionHd}>Active Violations</div>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '24px', marginBottom: '10px' }}>
|
||||||
|
<div style={{ ...s.sectionHd, marginTop: 0, marginBottom: 0 }}>Active Violations</div>
|
||||||
|
{violations.length > 0 && (
|
||||||
|
<button
|
||||||
|
style={s.backfillBtn}
|
||||||
|
onClick={handleRecomputeSnapshots}
|
||||||
|
title="Rebuild prior-points snapshot on each violation. Use after a back-dated insert if older PDFs show the wrong Prior Active Points."
|
||||||
|
>
|
||||||
|
↻ Backfill Snapshots
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{active.length === 0 ? (
|
{active.length === 0 ? (
|
||||||
<div style={{ color: '#777990', fontStyle: 'italic', fontSize: '12px' }}>
|
<div style={{ color: '#777990', fontStyle: 'italic', fontSize: '12px' }}>
|
||||||
No active violations on record.
|
No active violations on record.
|
||||||
|
|||||||
@@ -164,6 +164,31 @@ Update name, department, or supervisor. Changes are logged to the audit trail.
|
|||||||
#### Merge Duplicate
|
#### Merge Duplicate
|
||||||
If the same employee exists under two names, use Merge to reassign all violations from the duplicate to the canonical record. The duplicate is then deleted. This cannot be undone.
|
If the same employee exists under two names, use Merge to reassign all violations from the duplicate to the canonical record. The duplicate is then deleted. This cannot be undone.
|
||||||
|
|
||||||
|
#### Backfill Snapshots (repair tool)
|
||||||
|
|
||||||
|
Each violation stores a **prior-points snapshot** at submission time so its PDF always shows the score *as it was on the incident date*. Normally you never touch this — it's set on insert, refreshed automatically when a back-dated violation lands inside another violation's 90-day window, and otherwise locked. PDFs stay stable through negate/restore by design.
|
||||||
|
|
||||||
|
The **↻ Backfill Snapshots** button sits next to the **Active Violations** header in the profile modal. It rebuilds the snapshot on every violation for that employee using current data.
|
||||||
|
|
||||||
|
**When to use it:**
|
||||||
|
|
||||||
|
- After back-dating a violation, an older PDF still shows "Prior Active Points: 0" even though an earlier violation now clearly exists in the timeline.
|
||||||
|
- More generally: any time a regenerated PDF disagrees with what you see in the Point Expiration Timeline (the timeline is computed live; PDFs use the snapshot).
|
||||||
|
|
||||||
|
**When *not* to use it:**
|
||||||
|
|
||||||
|
- After a negate, restore, amend, or hard delete in normal workflow. The system intentionally keeps existing PDFs stable through those operations.
|
||||||
|
- As a routine maintenance step. If you keep needing it after ordinary back-dated inserts, that's a bug worth reporting — the auto-refresh should already be covering you.
|
||||||
|
|
||||||
|
**What clicking it does:**
|
||||||
|
|
||||||
|
1. Iterates every violation belonging to the employee (active and negated).
|
||||||
|
2. Recomputes each row's prior-points snapshot from the current set of non-negated violations in the 90 days before its incident date.
|
||||||
|
3. Writes only the rows that actually changed.
|
||||||
|
4. Records one entry in the audit log (action: \`violation_snapshots_recomputed\`, reason: \`manual_backfill\`) with the per-row before/after values.
|
||||||
|
|
||||||
|
A toast confirms the outcome — either *"Updated X of Y snapshots"* or *"Snapshots already up to date"*. Re-download any affected PDFs after running it; the new totals will appear immediately.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Audit Log
|
### Audit Log
|
||||||
@@ -217,6 +242,7 @@ Toasts auto-dismiss after a few seconds (errors persist longer). Each toast has
|
|||||||
| Edit employee name / dept / supervisor | Yes | Logged to audit trail |
|
| Edit employee name / dept / supervisor | Yes | Logged to audit trail |
|
||||||
| Merge duplicate employees | Yes | Irreversible |
|
| Merge duplicate employees | Yes | Irreversible |
|
||||||
| Add / edit employee notes | Yes | Does not affect score |
|
| Add / edit employee notes | Yes | Does not affect score |
|
||||||
|
| Recompute prior-points snapshot | Yes | Two paths only: auto (back-dated insert) or **↻ Backfill Snapshots** button. Never touched by negate, restore, amend, or hard delete |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -237,6 +263,7 @@ Toasts auto-dismiss after a few seconds (errors persist longer). Each toast has
|
|||||||
- In-app admin guide (this panel)
|
- In-app admin guide (this panel)
|
||||||
- Acknowledgment signature field — employee name + date on form and PDF
|
- Acknowledgment signature field — employee name + date on form and PDF
|
||||||
- Toast notification system — global feedback for all user actions
|
- Toast notification system — global feedback for all user actions
|
||||||
|
- Backfill Snapshots — per-employee repair tool that rebuilds the prior-points snapshot on every violation when older PDFs drift from current data
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -287,6 +287,58 @@ function recomputeSnapshotsAfter(employeeId, incidentDate) {
|
|||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: rebuild prior_active_points for every violation belonging to an
|
||||||
|
// employee, regardless of negate state. Used by the manual backfill button
|
||||||
|
// to repair snapshots after a back-dated insert that happened under older
|
||||||
|
// code (before recomputeSnapshotsAfter existed) or any case where the
|
||||||
|
// per-row snapshot has drifted from current data.
|
||||||
|
function recomputeAllSnapshotsForEmployee(employeeId) {
|
||||||
|
const rows = db.prepare(`
|
||||||
|
SELECT id, incident_date, prior_active_points
|
||||||
|
FROM violations
|
||||||
|
WHERE employee_id = ?
|
||||||
|
ORDER BY incident_date ASC
|
||||||
|
`).all(employeeId);
|
||||||
|
|
||||||
|
const updateStmt = db.prepare('UPDATE violations SET prior_active_points = ? WHERE id = ?');
|
||||||
|
const changes = [];
|
||||||
|
for (const v of rows) {
|
||||||
|
const newPrior = getPriorActivePoints(employeeId, v.incident_date);
|
||||||
|
if (newPrior !== v.prior_active_points) {
|
||||||
|
updateStmt.run(newPrior, v.id);
|
||||||
|
changes.push({ id: v.id, incident_date: v.incident_date, old: v.prior_active_points, new: newPrior });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { scanned: rows.length, changes };
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/employees/:id/recompute-snapshots
|
||||||
|
// Manual backfill — rebuild prior_active_points for every violation on this
|
||||||
|
// employee. Use after a back-dated insert under older code left downstream
|
||||||
|
// PDFs showing stale "Prior Active Points".
|
||||||
|
app.post('/api/employees/:id/recompute-snapshots', (req, res) => {
|
||||||
|
const empId = parseInt(req.params.id);
|
||||||
|
const emp = db.prepare('SELECT id, name FROM employees WHERE id = ?').get(empId);
|
||||||
|
if (!emp) return res.status(404).json({ error: 'Employee not found' });
|
||||||
|
|
||||||
|
const result = db.transaction(() => recomputeAllSnapshotsForEmployee(empId))();
|
||||||
|
|
||||||
|
if (result.changes.length > 0) {
|
||||||
|
audit('violation_snapshots_recomputed', 'employee', empId, req.body?.performed_by, {
|
||||||
|
reason: 'manual_backfill',
|
||||||
|
scanned: result.scanned,
|
||||||
|
affected: result.changes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
scanned: result.scanned,
|
||||||
|
updated: result.changes.length,
|
||||||
|
changes: result.changes,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// POST new violation
|
// POST new violation
|
||||||
app.post('/api/violations', (req, res) => {
|
app.post('/api/violations', (req, res) => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
Reference in New Issue
Block a user