This commit is contained in:
@@ -14,7 +14,9 @@
|
||||
"Bash(npm install *)",
|
||||
"Bash(node -e \"require\\('./auth.js'\\); console.log\\('auth.js OK'\\)\")",
|
||||
"Bash(node --check auth.js)",
|
||||
"Bash(node --check db/database.js)"
|
||||
"Bash(node --check db/database.js)",
|
||||
"Bash(node -e ' *)",
|
||||
"Bash(node --check lib/rolloff.js)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,73 +62,89 @@ const s = {
|
||||
};
|
||||
|
||||
export default function ExpirationTimeline({ employeeId, currentPoints }) {
|
||||
const [items, setItems] = useState([]);
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
axios.get(`/api/employees/${employeeId}/expiration`)
|
||||
.then(r => setItems(r.data))
|
||||
.then(r => setData(r.data))
|
||||
.finally(() => setLoading(false));
|
||||
}, [employeeId]);
|
||||
|
||||
if (loading) return (
|
||||
<div style={s.wrapper}>
|
||||
<div style={s.sectionHd}>Point Expiration Timeline</div>
|
||||
<div style={s.sectionHd}>Point Roll-Off Timeline</div>
|
||||
<div style={{ ...s.empty }}>Loading…</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (items.length === 0) return (
|
||||
const schedule = data?.schedule || [];
|
||||
|
||||
if (schedule.length === 0) return (
|
||||
<div style={s.wrapper}>
|
||||
<div style={s.sectionHd}>Point Expiration Timeline</div>
|
||||
<div style={s.empty}>No active violations — nothing to expire.</div>
|
||||
<div style={s.sectionHd}>Point Roll-Off Timeline</div>
|
||||
<div style={s.empty}>No active points — nothing to roll off.</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Build running totals: after each violation expires, what's the remaining score?
|
||||
let running = currentPoints || 0;
|
||||
const projected = items.map(item => {
|
||||
const before = running;
|
||||
running = Math.max(0, running - item.points);
|
||||
// Points roll off 5 at a time (oldest first) after each 90 consecutive clean
|
||||
// days. Walk the schedule to project the score and tier after each event.
|
||||
let running = data.active_points ?? currentPoints ?? 0;
|
||||
const projected = schedule.map(ev => {
|
||||
const before = running;
|
||||
running = ev.points_after;
|
||||
const tierBefore = getTier(before);
|
||||
const tierAfter = getTier(running);
|
||||
const dropped = tierAfter.min < tierBefore.min;
|
||||
return { ...item, pointsBefore: before, pointsAfter: running, tierBefore, tierAfter, tierDropped: dropped };
|
||||
return {
|
||||
...ev,
|
||||
pointsBefore: before,
|
||||
tierBefore,
|
||||
tierAfter,
|
||||
tierDropped: tierAfter.min < tierBefore.min,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={s.wrapper}>
|
||||
<div style={s.sectionHd}>Point Expiration Timeline</div>
|
||||
<div style={s.sectionHd}>Point Roll-Off Timeline</div>
|
||||
|
||||
{projected.map((item) => {
|
||||
const color = urgencyColor(item.days_remaining);
|
||||
const pct = (item.days_remaining / 90) * 100;
|
||||
<div style={{ ...s.empty, fontStyle: 'normal', marginBottom: '10px', color: '#9ca0b8' }}>
|
||||
5 points roll off after every 90 consecutive days with no new violation.
|
||||
Any new violation resets the countdown.
|
||||
{data.last_violation_date && (
|
||||
<> Clean since <strong style={{ color: '#f8f9fa' }}>{data.last_violation_date}</strong>.</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{projected.map((ev, i) => {
|
||||
const color = urgencyColor(ev.days_remaining);
|
||||
const pct = (ev.days_remaining / 90) * 100;
|
||||
return (
|
||||
<div key={item.id} style={s.row}>
|
||||
{/* Violation name */}
|
||||
<div style={s.name} title={item.violation_name}>{item.violation_name}</div>
|
||||
<div key={ev.date} style={s.row}>
|
||||
{/* Roll-off event label */}
|
||||
<div style={s.name} title={`Roll-off #${i + 1}`}>Roll-off #{i + 1}</div>
|
||||
|
||||
{/* Points badge */}
|
||||
<div style={s.pts}>−{item.points}</div>
|
||||
{/* Points retired */}
|
||||
<div style={s.pts}>−{ev.points_off}</div>
|
||||
|
||||
{/* Progress bar: how much of the 90 days has elapsed */}
|
||||
{/* Progress bar: how much of the 90-day cycle has elapsed */}
|
||||
<div style={s.bar(pct, color)}>
|
||||
<div style={s.barFill(pct, color)} />
|
||||
</div>
|
||||
|
||||
{/* Days remaining pill */}
|
||||
<div style={s.pill(color)}>
|
||||
{item.days_remaining <= 0 ? 'Expiring today' : `${item.days_remaining}d`}
|
||||
{ev.days_remaining <= 0 ? 'Today' : `${ev.days_remaining}d`}
|
||||
</div>
|
||||
|
||||
{/* Expiry date */}
|
||||
<div style={s.date}>{item.expires_on}</div>
|
||||
{/* Roll-off date */}
|
||||
<div style={s.date}>{ev.date}</div>
|
||||
|
||||
{/* Tier drop indicator */}
|
||||
{item.tierDropped && (
|
||||
{ev.tierDropped && (
|
||||
<div style={{ fontSize: '10px', color: '#69f0ae', whiteSpace: 'nowrap' }}>
|
||||
↓ {item.tierAfter.label}
|
||||
↓ {ev.tierAfter.label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -138,16 +154,16 @@ export default function ExpirationTimeline({ employeeId, currentPoints }) {
|
||||
{/* Projection summary */}
|
||||
<div style={s.projBox}>
|
||||
<div style={{ fontWeight: 700, color: '#f8f9fa', marginBottom: '8px', fontSize: '12px' }}>
|
||||
Projected score after each expiration
|
||||
Projected score after each roll-off
|
||||
</div>
|
||||
{projected.map((item, i) => (
|
||||
<div key={item.id} style={s.projRow}>
|
||||
<span style={{ color: '#9ca0b8' }}>{item.expires_on} — {item.violation_name}</span>
|
||||
{projected.map((ev) => (
|
||||
<div key={ev.date} style={s.projRow}>
|
||||
<span style={{ color: '#9ca0b8' }}>{ev.date} — −{ev.points_off} pts</span>
|
||||
<span>
|
||||
<span style={{ color: '#f8f9fa', fontWeight: 700 }}>{item.pointsAfter} pts</span>
|
||||
{item.tierDropped && (
|
||||
<span style={{ marginLeft: '8px', color: item.tierAfter.color, fontWeight: 700 }}>
|
||||
→ {item.tierAfter.label}
|
||||
<span style={{ color: '#f8f9fa', fontWeight: 700 }}>{ev.points_after} pts</span>
|
||||
{ev.tierDropped && (
|
||||
<span style={{ marginLeft: '8px', color: ev.tierAfter.color, fontWeight: 700 }}>
|
||||
→ {ev.tierAfter.label}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -215,7 +215,7 @@ export default function ViolationForm() {
|
||||
<span style={{ fontSize: '13px', color: '#d1d3e0', fontWeight: 600 }}>Current Standing:</span>
|
||||
<CpasBadge points={intel.score.active_points} />
|
||||
<span style={{ fontSize: '12px', color: '#9ca0b8' }}>
|
||||
{intel.score.violation_count} violation{intel.score.violation_count !== 1 ? 's' : ''} in last 90 days
|
||||
{intel.score.violation_count} active violation{intel.score.violation_count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+5
-11
@@ -98,17 +98,11 @@ db.exec(`CREATE TABLE IF NOT EXISTS sessions (
|
||||
expires_at DATETIME NOT NULL
|
||||
)`);
|
||||
|
||||
// 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;`);
|
||||
// The old `active_cpas_scores` view implemented a naive per-violation 90-day
|
||||
// window. CPAS standing now follows the clean-cycle roll-off model (see
|
||||
// lib/rolloff.js), which is order-dependent and computed in JS, so the view is
|
||||
// retired here to keep a single source of truth.
|
||||
db.exec('DROP VIEW IF EXISTS active_cpas_scores;');
|
||||
|
||||
console.log('[DB] Connected:', dbPath);
|
||||
module.exports = db;
|
||||
|
||||
+3
-10
@@ -38,13 +38,6 @@ CREATE TABLE IF NOT EXISTS violation_resolutions (
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Active score: only non-negated violations in rolling 90 days
|
||||
CREATE VIEW IF NOT EXISTS 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;
|
||||
-- CPAS standing (active points + roll-off schedule) is computed in JS from the
|
||||
-- clean-cycle model in lib/rolloff.js; see db/database.js for why the old
|
||||
-- active_cpas_scores view was retired.
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// ── CPAS point roll-off model ────────────────────────────────────────────────
|
||||
// Handbook rule: points roll off only after the employee goes a full
|
||||
// ROLLOFF_DAYS (90) with NO new violations. The clock is shared across all of an
|
||||
// employee's active points and is RESET by any new non-negated violation. Each
|
||||
// completed clean cycle removes ROLLOFF_POINTS (5), oldest violation first, and
|
||||
// every successive roll-off requires another fresh clean cycle.
|
||||
//
|
||||
// This is intentionally NOT a per-violation "expires 90 days after its own
|
||||
// incident date" window — a new violation pushes back roll-off for everything.
|
||||
|
||||
const ROLLOFF_POINTS = 5;
|
||||
const ROLLOFF_DAYS = 90;
|
||||
const MS_PER_DAY = 86400000;
|
||||
|
||||
// All dates are treated as UTC calendar days to match SQLite's DATE('now').
|
||||
function dayValue(dateStr) {
|
||||
const [y, m, d] = String(dateStr).slice(0, 10).split('-').map(Number);
|
||||
return Date.UTC(y, m - 1, d);
|
||||
}
|
||||
function daysBetween(fromStr, toStr) {
|
||||
return Math.round((dayValue(toStr) - dayValue(fromStr)) / MS_PER_DAY);
|
||||
}
|
||||
function addDays(dateStr, n) {
|
||||
return new Date(dayValue(dateStr) + n * MS_PER_DAY).toISOString().slice(0, 10);
|
||||
}
|
||||
function todayStr() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
// Compute an employee's CPAS standing as of `asOf` (default today).
|
||||
//
|
||||
// `violations` is an array of rows with at least { id, points, incident_date,
|
||||
// negated }. Negated rows and rows dated after `asOf` are ignored.
|
||||
//
|
||||
// Returns:
|
||||
// activePoints — points still counting against the employee
|
||||
// totalPoints — sum of all (non-negated, on-or-before asOf) points
|
||||
// rolledOffPoints — points already retired by completed clean cycles
|
||||
// cycles — completed 90-day clean cycles since the last violation
|
||||
// violationCount — non-negated violations still retaining points
|
||||
// lastViolationDate — most recent non-negated incident date (anchors the clock)
|
||||
// nextRolloffDate — date the next 5 points retire (null if nothing left)
|
||||
// perViolation — oldest-first breakdown: each row + { rolledOff, activePoints }
|
||||
// schedule — future roll-off events: { date, points_off, points_after, days_remaining }
|
||||
function computeStanding(violations, asOf = todayStr()) {
|
||||
const asOfVal = dayValue(asOf);
|
||||
const active = violations.filter(
|
||||
v => !v.negated && dayValue(v.incident_date) <= asOfVal
|
||||
);
|
||||
|
||||
if (active.length === 0) {
|
||||
return {
|
||||
activePoints: 0, totalPoints: 0, rolledOffPoints: 0, cycles: 0,
|
||||
violationCount: 0, lastViolationDate: null, nextRolloffDate: null,
|
||||
perViolation: [], schedule: [],
|
||||
};
|
||||
}
|
||||
|
||||
const sorted = [...active].sort((a, b) => {
|
||||
const d = dayValue(a.incident_date) - dayValue(b.incident_date);
|
||||
return d !== 0 ? d : (a.id - b.id);
|
||||
});
|
||||
|
||||
const totalPoints = sorted.reduce((s, v) => s + v.points, 0);
|
||||
const lastViolationDate = String(sorted[sorted.length - 1].incident_date).slice(0, 10);
|
||||
const daysSince = daysBetween(lastViolationDate, asOf);
|
||||
const cycles = daysSince > 0 ? Math.floor(daysSince / ROLLOFF_DAYS) : 0;
|
||||
const rolledOffPoints = Math.min(totalPoints, cycles * ROLLOFF_POINTS);
|
||||
const activePoints = totalPoints - rolledOffPoints;
|
||||
|
||||
// Retire points oldest-first.
|
||||
let toRoll = rolledOffPoints;
|
||||
const perViolation = sorted.map(v => {
|
||||
const off = Math.min(v.points, toRoll);
|
||||
toRoll -= off;
|
||||
return { ...v, rolledOff: off, activePoints: v.points - off };
|
||||
});
|
||||
const violationCount = perViolation.filter(v => v.activePoints > 0).length;
|
||||
|
||||
// Project remaining roll-offs forward. Cycle N retires at lastViolation + N*90.
|
||||
const schedule = [];
|
||||
let remaining = activePoints;
|
||||
let c = cycles;
|
||||
while (remaining > 0) {
|
||||
c += 1;
|
||||
const date = addDays(lastViolationDate, c * ROLLOFF_DAYS);
|
||||
const off = Math.min(ROLLOFF_POINTS, remaining);
|
||||
remaining -= off;
|
||||
schedule.push({
|
||||
date,
|
||||
points_off: off,
|
||||
points_after: remaining,
|
||||
days_remaining: daysBetween(asOf, date),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
activePoints, totalPoints, rolledOffPoints, cycles, violationCount,
|
||||
lastViolationDate,
|
||||
nextRolloffDate: schedule.length ? schedule[0].date : null,
|
||||
perViolation, schedule,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ROLLOFF_POINTS, ROLLOFF_DAYS,
|
||||
computeStanding, daysBetween, addDays, todayStr,
|
||||
};
|
||||
@@ -4,6 +4,7 @@ const path = require('path');
|
||||
const db = require('./db/database');
|
||||
const auth = require('./auth');
|
||||
const generatePdf = require('./pdf/generator');
|
||||
const { computeStanding } = require('./lib/rolloff');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
@@ -33,6 +34,20 @@ function audit(action, entityType, entityId, performedBy, details) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── CPAS standing helpers ────────────────────────────────────────────────────
|
||||
// All point/roll-off math lives in lib/rolloff.js. These helpers just feed it
|
||||
// the employee's non-negated violations and return the computed standing.
|
||||
function loadViolations(employeeId) {
|
||||
return db.prepare(
|
||||
`SELECT id, points, incident_date, negated, violation_name, violation_type, category
|
||||
FROM violations
|
||||
WHERE employee_id = ? AND negated = 0`
|
||||
).all(employeeId);
|
||||
}
|
||||
function standingFor(employeeId, asOf) {
|
||||
return computeStanding(loadViolations(employeeId), asOf);
|
||||
}
|
||||
|
||||
// ── Version info (written by Dockerfile at build time) ───────────────────────
|
||||
// Falls back to { sha: 'dev' } when running outside a Docker build (local dev).
|
||||
let BUILD_VERSION = { sha: 'dev', shortSha: 'dev', buildTime: null };
|
||||
@@ -224,8 +239,8 @@ app.patch('/api/employees/:id/notes', (req, res) => {
|
||||
app.get('/api/employees/:id/score', (req, res) => {
|
||||
const empId = req.params.id;
|
||||
|
||||
// Active points from the 90-day rolling view
|
||||
const active = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?').get(empId);
|
||||
// Active points from the clean-cycle roll-off model
|
||||
const standing = standingFor(empId);
|
||||
|
||||
// Total violations (all time) and negated count
|
||||
const totals = db.prepare(`
|
||||
@@ -238,50 +253,57 @@ app.get('/api/employees/:id/score', (req, res) => {
|
||||
|
||||
res.json({
|
||||
employee_id: empId,
|
||||
active_points: active ? active.active_points : 0,
|
||||
violation_count: active ? active.violation_count : 0,
|
||||
active_points: standing.activePoints,
|
||||
violation_count: standing.violationCount,
|
||||
total_violations: totals ? totals.total_violations : 0,
|
||||
negated_count: totals ? totals.negated_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.
|
||||
// ── Roll-off Timeline ────────────────────────────────────────────────────────
|
||||
// GET /api/employees/:id/expiration — projected clean-cycle roll-off schedule.
|
||||
// Points only retire after 90 consecutive days with no new violation; any new
|
||||
// violation resets the clock. Returns the active total plus each future 5-point
|
||||
// roll-off event (oldest points first) until the balance reaches zero.
|
||||
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);
|
||||
const standing = standingFor(req.params.id);
|
||||
res.json({
|
||||
active_points: standing.activePoints,
|
||||
last_violation_date: standing.lastViolationDate,
|
||||
next_rolloff_date: standing.nextRolloffDate,
|
||||
schedule: standing.schedule,
|
||||
});
|
||||
});
|
||||
|
||||
// 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();
|
||||
const employees = db.prepare(
|
||||
'SELECT id, name, department, supervisor FROM employees'
|
||||
).all();
|
||||
|
||||
// Pull every non-negated violation once, bucket by employee, then run the
|
||||
// roll-off model per employee rather than issuing a query each.
|
||||
const buckets = new Map();
|
||||
for (const v of db.prepare(
|
||||
`SELECT id, employee_id, points, incident_date, negated
|
||||
FROM violations WHERE negated = 0`
|
||||
).all()) {
|
||||
if (!buckets.has(v.employee_id)) buckets.set(v.employee_id, []);
|
||||
buckets.get(v.employee_id).push(v);
|
||||
}
|
||||
|
||||
const rows = employees.map(e => {
|
||||
const standing = computeStanding(buckets.get(e.id) || []);
|
||||
return {
|
||||
...e,
|
||||
active_points: standing.activePoints,
|
||||
violation_count: standing.violationCount,
|
||||
};
|
||||
});
|
||||
|
||||
rows.sort((a, b) =>
|
||||
b.active_points - a.active_points || a.name.localeCompare(b.name)
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
@@ -309,33 +331,32 @@ app.get('/api/violations/:id/amendments', (req, res) => {
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// Helper: compute prior_active_points at time of insert
|
||||
// Helper: compute prior_active_points at time of insert — the employee's active
|
||||
// CPAS standing the instant BEFORE this incident, under the clean-cycle model.
|
||||
// Uses only violations strictly earlier than this incident, evaluated as of the
|
||||
// incident date.
|
||||
function getPriorActivePoints(employeeId, incidentDate) {
|
||||
const row = db.prepare(
|
||||
`SELECT COALESCE(SUM(points),0) AS pts
|
||||
const earlier = db.prepare(
|
||||
`SELECT id, points, incident_date, negated
|
||||
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;
|
||||
WHERE employee_id = ? AND negated = 0 AND incident_date < ?`
|
||||
).all(employeeId, incidentDate);
|
||||
return computeStanding(earlier, incidentDate).activePoints;
|
||||
}
|
||||
|
||||
// Helper: after a back-dated insert, refresh snapshots on any existing
|
||||
// violations whose 90-day prior-window now includes the new (earlier)
|
||||
// incident_date. Without this, their PDFs would still show the pre-backdate
|
||||
// "Prior Active Points" and miss the inserted earlier violation.
|
||||
// Snapshots are still immutable w.r.t. negate/restore — only timeline-
|
||||
// rewriting events (back-dated inserts) trigger a refresh.
|
||||
// Helper: after a back-dated insert, refresh snapshots on every later
|
||||
// violation. Under the clean-cycle roll-off model an earlier incident raises
|
||||
// the running total and shifts the roll-off clock for ALL subsequent
|
||||
// violations, so their "Prior Active Points" snapshots can all change — not
|
||||
// just those within 90 days. Snapshots remain immutable w.r.t. negate/restore;
|
||||
// only timeline-rewriting events (back-dated inserts) trigger a refresh.
|
||||
function recomputeSnapshotsAfter(employeeId, incidentDate) {
|
||||
const affected = db.prepare(`
|
||||
SELECT id, incident_date, prior_active_points
|
||||
FROM violations
|
||||
WHERE employee_id = ?
|
||||
AND incident_date > ?
|
||||
AND incident_date <= DATE(?, '+90 days')
|
||||
`).all(employeeId, incidentDate, incidentDate);
|
||||
`).all(employeeId, incidentDate);
|
||||
|
||||
const updateStmt = db.prepare('UPDATE violations SET prior_active_points = ? WHERE id = ?');
|
||||
const changes = [];
|
||||
@@ -732,13 +753,12 @@ app.get('/api/violations/:id/pdf', async (req, res) => {
|
||||
|
||||
if (!violation) return res.status(404).json({ error: 'Violation not found' });
|
||||
|
||||
const active = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?')
|
||||
.get(violation.employee_id) || { active_points: 0, violation_count: 0 };
|
||||
const standing = standingFor(violation.employee_id);
|
||||
|
||||
const scoreForPdf = {
|
||||
employee_id: violation.employee_id,
|
||||
active_points: violation.prior_active_points != null ? violation.prior_active_points : active.active_points,
|
||||
violation_count: active.violation_count,
|
||||
active_points: violation.prior_active_points != null ? violation.prior_active_points : standing.activePoints,
|
||||
violation_count: standing.violationCount,
|
||||
};
|
||||
|
||||
const pdfBuffer = await generatePdf(violation, scoreForPdf);
|
||||
|
||||
Reference in New Issue
Block a user