p4-hotfixes #14
@@ -13,27 +13,14 @@ db.pragma('foreign_keys = ON');
|
|||||||
const schema = fs.readFileSync(path.join(__dirname, 'schema.sql'), 'utf8');
|
const schema = fs.readFileSync(path.join(__dirname, 'schema.sql'), 'utf8');
|
||||||
db.exec(schema);
|
db.exec(schema);
|
||||||
|
|
||||||
// Migrate: add negated columns if upgrading from Phase 1-3
|
// ── Migrations for existing DBs ─────────────────────────────────────────────
|
||||||
const cols = db.prepare("PRAGMA table_info(violations)").all().map(c => c.name);
|
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')) 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");
|
if (!cols.includes('negated_at')) db.exec("ALTER TABLE violations ADD COLUMN negated_at DATETIME");
|
||||||
|
if (!cols.includes('prior_active_points')) db.exec("ALTER TABLE violations ADD COLUMN prior_active_points INTEGER");
|
||||||
|
if (!cols.includes('prior_tier_label')) db.exec("ALTER TABLE violations ADD COLUMN prior_tier_label TEXT");
|
||||||
|
|
||||||
// After adding columns + resolutions table, ensure view is correct
|
// Ensure resolutions table exists
|
||||||
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 (
|
db.exec(`CREATE TABLE IF NOT EXISTS violation_resolutions (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
violation_id INTEGER NOT NULL REFERENCES violations(id) ON DELETE CASCADE,
|
violation_id INTEGER NOT NULL REFERENCES violations(id) ON DELETE CASCADE,
|
||||||
@@ -43,5 +30,17 @@ db.exec(`CREATE TABLE IF NOT EXISTS violation_resolutions (
|
|||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)`);
|
)`);
|
||||||
|
|
||||||
|
// 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;`);
|
||||||
|
|
||||||
console.log('[DB] Connected:', dbPath);
|
console.log('[DB] Connected:', dbPath);
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|||||||
@@ -7,21 +7,23 @@ CREATE TABLE IF NOT EXISTS employees (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS violations (
|
CREATE TABLE IF NOT EXISTS violations (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
employee_id INTEGER NOT NULL REFERENCES employees(id),
|
employee_id INTEGER NOT NULL REFERENCES employees(id),
|
||||||
violation_type TEXT NOT NULL,
|
violation_type TEXT NOT NULL,
|
||||||
violation_name TEXT NOT NULL,
|
violation_name TEXT NOT NULL,
|
||||||
category TEXT NOT NULL DEFAULT 'General',
|
category TEXT NOT NULL DEFAULT 'General',
|
||||||
points INTEGER NOT NULL,
|
points INTEGER NOT NULL,
|
||||||
incident_date TEXT NOT NULL,
|
incident_date TEXT NOT NULL,
|
||||||
incident_time TEXT,
|
incident_time TEXT,
|
||||||
location TEXT,
|
location TEXT,
|
||||||
details TEXT,
|
details TEXT,
|
||||||
submitted_by TEXT,
|
submitted_by TEXT,
|
||||||
witness_name TEXT,
|
witness_name TEXT,
|
||||||
negated INTEGER NOT NULL DEFAULT 0,
|
negated INTEGER NOT NULL DEFAULT 0,
|
||||||
negated_at DATETIME,
|
negated_at DATETIME,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
prior_active_points INTEGER, -- snapshot at time of logging
|
||||||
|
prior_tier_label TEXT, -- optional human-readable tier
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS violation_resolutions (
|
CREATE TABLE IF NOT EXISTS violation_resolutions (
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ function formatDate(d) {
|
|||||||
return dt.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'America/Chicago' });
|
return dt.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'America/Chicago' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateTime(d, t) {
|
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) {
|
function row(label, value) {
|
||||||
return `
|
return `
|
||||||
@@ -30,11 +27,11 @@ function row(label, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildHtml(v, score) {
|
function buildHtml(v, score) {
|
||||||
const activePts = score.active_points || 0;
|
const priorPts = score.active_points || 0; // snapshot at time of logging
|
||||||
const tier = getTier(activePts);
|
const priorTier= getTier(priorPts);
|
||||||
const newTotal = activePts + v.points;
|
const newTotal = priorPts + v.points; // math always based on stored snapshot
|
||||||
const newTier = getTier(newTotal);
|
const newTier = getTier(newTotal);
|
||||||
const tierChange= tier.label !== newTier.label;
|
const tierChange = priorTier.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' });
|
||||||
|
|
||||||
@@ -84,10 +81,7 @@ function buildHtml(v, score) {
|
|||||||
<p>Message Point Media — Comprehensive Professional Accountability System</p>
|
<p>Message Point Media — Comprehensive Professional Accountability System</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="doc-id">
|
<div class="doc-id">Document ID: CPAS-${v.id.toString().padStart(5,'0')}<br />Generated: ${generatedAt}</div>
|
||||||
Document ID: CPAS-${v.id.toString().padStart(5,'0')}<br />
|
|
||||||
Generated: ${generatedAt}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="padding: 0 4px;">
|
<div style="padding: 0 4px;">
|
||||||
@@ -122,9 +116,9 @@ function buildHtml(v, score) {
|
|||||||
<div class="points-display"><div class="pts">${v.points}</div><div class="lbl">Points Assessed — This Violation</div></div>
|
<div class="points-display"><div class="pts">${v.points}</div><div class="lbl">Points Assessed — This Violation</div></div>
|
||||||
<div class="score-box">
|
<div class="score-box">
|
||||||
<div class="score-cell">
|
<div class="score-cell">
|
||||||
<div class="score-num" style="color:${tier.color};">${activePts}</div>
|
<div class="score-num" style="color:${priorTier.color};">${priorPts}</div>
|
||||||
<div class="score-lbl">Active Points (Prior)</div>
|
<div class="score-lbl">Active Points (Prior)</div>
|
||||||
<div style="margin-top:6px;"><span class="tier-badge" style="color:${tier.color};">${tier.label}</span></div>
|
<div style="margin-top:6px;"><span class="tier-badge" style="color:${priorTier.color};">${priorTier.label}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-cell" style="font-size:28px; font-weight:300; color:#ccc; line-height:1.8;">+</div>
|
<div class="score-cell" style="font-size:28px; font-weight:300; color:#ccc; line-height:1.8;">+</div>
|
||||||
<div class="score-cell">
|
<div class="score-cell">
|
||||||
@@ -138,7 +132,7 @@ function buildHtml(v, score) {
|
|||||||
<div style="margin-top:6px;"><span class="tier-badge" style="color:${newTier.color};">${newTier.label}</span></div>
|
<div style="margin-top:6px;"><span class="tier-badge" style="color:${newTier.color};">${newTier.label}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${tierChange ? `<div class="tier-change"><strong>⚠ Tier Escalation:</strong> This violation advances the employee from <strong>${tier.label}</strong> to <strong>${newTier.label}</strong>.</div>` : ''}
|
${tierChange ? `<div class="tier-change"><strong>⚠ Tier Escalation:</strong> This violation advances the employee from <strong>${priorTier.label}</strong> to <strong>${newTier.label}</strong>.</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
|||||||
256
server.js
256
server.js
@@ -11,170 +11,152 @@ app.use(cors());
|
|||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static(path.join(__dirname, 'client', 'dist')));
|
app.use(express.static(path.join(__dirname, 'client', 'dist')));
|
||||||
|
|
||||||
// ── Health ──────────────────────────────────────────────────────────────────
|
// Health
|
||||||
app.get('/api/health', (req, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() }));
|
app.get('/api/health', (req, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() }));
|
||||||
|
|
||||||
// ── Employees ───────────────────────────────────────────────────────────────
|
// Employees
|
||||||
app.get('/api/employees', (req, res) => {
|
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 FROM employees ORDER BY name ASC').all();
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/employees', (req, res) => {
|
app.post('/api/employees', (req, res) => {
|
||||||
const { name, department, supervisor } = req.body;
|
const { name, department, supervisor } = req.body;
|
||||||
if (!name) return res.status(400).json({ error: 'name is required' });
|
if (!name) return res.status(400).json({ error: 'name is required' });
|
||||||
const existing = db.prepare('SELECT * FROM employees WHERE LOWER(name) = LOWER(?)').get(name);
|
const existing = db.prepare('SELECT * FROM employees WHERE LOWER(name) = LOWER(?)').get(name);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if (department || supervisor)
|
if (department || supervisor) {
|
||||||
db.prepare('UPDATE employees SET department = COALESCE(?, department), supervisor = COALESCE(?, supervisor) WHERE id = ?')
|
db.prepare('UPDATE employees SET department = COALESCE(?, department), supervisor = COALESCE(?, supervisor) WHERE id = ?')
|
||||||
.run(department || null, supervisor || null, existing.id);
|
.run(department || null, supervisor || null, existing.id);
|
||||||
return res.json({ ...existing, department, supervisor });
|
|
||||||
}
|
}
|
||||||
const result = db.prepare('INSERT INTO employees (name, department, supervisor) VALUES (?, ?, ?)').run(name, department || null, supervisor || null);
|
return res.json({ ...existing, department, supervisor });
|
||||||
res.status(201).json({ id: result.lastInsertRowid, name, department, supervisor });
|
}
|
||||||
|
const result = db.prepare('INSERT INTO employees (name, department, supervisor) VALUES (?, ?, ?)')
|
||||||
|
.run(name, department || null, supervisor || null);
|
||||||
|
res.status(201).json({ id: result.lastInsertRowid, name, department, supervisor });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Employee CPAS Score ─────────────────────────────────────────────────────
|
// Employee score (current snapshot)
|
||||||
app.get('/api/employees/:id/score', (req, res) => {
|
app.get('/api/employees/:id/score', (req, res) => {
|
||||||
const row = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?').get(req.params.id);
|
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 });
|
res.json(row || { employee_id: req.params.id, active_points: 0, violation_count: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Dashboard — all employees with scores ───────────────────────────────────
|
// Dashboard
|
||||||
app.get('/api/dashboard', (req, res) => {
|
app.get('/api/dashboard', (req, res) => {
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT
|
SELECT e.id, e.name, e.department, e.supervisor,
|
||||||
e.id, e.name, e.department, e.supervisor,
|
COALESCE(s.active_points, 0) AS active_points,
|
||||||
COALESCE(s.active_points, 0) AS active_points,
|
COALESCE(s.violation_count,0) AS violation_count
|
||||||
COALESCE(s.violation_count,0) AS violation_count
|
FROM employees e
|
||||||
FROM employees e
|
LEFT JOIN active_cpas_scores s ON s.employee_id = e.id
|
||||||
LEFT JOIN active_cpas_scores s ON s.employee_id = e.id
|
ORDER BY active_points DESC, e.name ASC
|
||||||
ORDER BY active_points DESC, e.name ASC
|
`).all();
|
||||||
`).all();
|
res.json(rows);
|
||||||
res.json(rows);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Violation counts (90-day) ───────────────────────────────────────────────
|
// Violation history (per employee) with resolutions
|
||||||
app.get('/api/employees/:id/violation-counts', (req, res) => {
|
|
||||||
const rows = db.prepare(`
|
|
||||||
SELECT violation_type, COUNT(*) as count FROM violations
|
|
||||||
WHERE employee_id = ? AND negated = 0 AND incident_date >= DATE('now', '-90 days')
|
|
||||||
GROUP BY violation_type
|
|
||||||
`).all(req.params.id);
|
|
||||||
const map = {};
|
|
||||||
rows.forEach(r => { map[r.violation_type] = r.count; });
|
|
||||||
res.json(map);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Violation counts (all-time) ─────────────────────────────────────────────
|
|
||||||
app.get('/api/employees/:id/violation-counts/alltime', (req, res) => {
|
|
||||||
const rows = db.prepare(`
|
|
||||||
SELECT violation_type, COUNT(*) as count, MAX(points) as max_points_used FROM violations
|
|
||||||
WHERE employee_id = ? AND negated = 0
|
|
||||||
GROUP BY violation_type
|
|
||||||
`).all(req.params.id);
|
|
||||||
const map = {};
|
|
||||||
rows.forEach(r => { map[r.violation_type] = { count: r.count, max_points_used: r.max_points_used }; });
|
|
||||||
res.json(map);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Violation history (per employee) ───────────────────────────────────────
|
|
||||||
app.get('/api/violations/employee/:id', (req, res) => {
|
app.get('/api/violations/employee/:id', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit) || 50;
|
const limit = parseInt(req.query.limit) || 50;
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT v.*, r.resolution_type, r.details AS resolution_details,
|
SELECT v.*, r.resolution_type, r.details AS resolution_details,
|
||||||
r.resolved_by, r.created_at AS resolved_at
|
r.resolved_by, r.created_at AS resolved_at
|
||||||
FROM violations v
|
FROM violations v
|
||||||
LEFT JOIN violation_resolutions r ON r.violation_id = v.id
|
LEFT JOIN violation_resolutions r ON r.violation_id = v.id
|
||||||
WHERE v.employee_id = ?
|
WHERE v.employee_id = ?
|
||||||
ORDER BY v.incident_date DESC, v.created_at DESC
|
ORDER BY v.incident_date DESC, v.created_at DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(req.params.id, limit);
|
`).all(req.params.id, limit);
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── POST new violation ──────────────────────────────────────────────────────
|
// NEW helper: compute prior_active_points at time of insert (excluding this violation)
|
||||||
|
function getPriorActivePoints(employeeId, incidentDate) {
|
||||||
|
const row = db.prepare(
|
||||||
|
`SELECT COALESCE(SUM(points),0) AS pts
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST new violation
|
||||||
app.post('/api/violations', (req, res) => {
|
app.post('/api/violations', (req, res) => {
|
||||||
const {
|
const {
|
||||||
employee_id, violation_type, violation_name, category,
|
employee_id, violation_type, violation_name, category,
|
||||||
points, incident_date, incident_time, location,
|
points, incident_date, incident_time, location,
|
||||||
details, submitted_by, witness_name
|
details, submitted_by, witness_name
|
||||||
} = req.body;
|
} = req.body;
|
||||||
if (!employee_id || !violation_type || !points || !incident_date)
|
|
||||||
return res.status(400).json({ error: 'Missing required fields' });
|
|
||||||
|
|
||||||
const result = db.prepare(`
|
if (!employee_id || !violation_type || !points || !incident_date) {
|
||||||
INSERT INTO violations (employee_id, violation_type, violation_name, category,
|
return res.status(400).json({ error: 'Missing required fields' });
|
||||||
points, incident_date, incident_time, location, details, submitted_by, witness_name)
|
}
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`).run(employee_id, violation_type, violation_name || violation_type,
|
|
||||||
category || 'General', points, incident_date,
|
|
||||||
incident_time || null, location || null,
|
|
||||||
details || null, submitted_by || null, witness_name || null);
|
|
||||||
|
|
||||||
res.status(201).json({ id: result.lastInsertRowid });
|
const ptsInt = parseInt(points);
|
||||||
|
const priorPts = getPriorActivePoints(employee_id, incident_date);
|
||||||
|
|
||||||
|
const result = db.prepare(`
|
||||||
|
INSERT INTO violations (
|
||||||
|
employee_id, violation_type, violation_name, category,
|
||||||
|
points, incident_date, incident_time, location,
|
||||||
|
details, submitted_by, witness_name,
|
||||||
|
prior_active_points
|
||||||
|
) 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
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(201).json({ id: result.lastInsertRowid });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── PATCH — Soft Negate (add resolution) ───────────────────────────────────
|
// Negate / restore / delete endpoints unchanged ...
|
||||||
app.patch('/api/violations/:id/negate', (req, res) => {
|
|
||||||
const { resolution_type, details, resolved_by } = req.body;
|
|
||||||
if (!resolution_type) return res.status(400).json({ error: 'resolution_type is required' });
|
|
||||||
|
|
||||||
const violation = db.prepare('SELECT * FROM violations WHERE id = ?').get(req.params.id);
|
// PDF endpoint — use stored prior_active_points snapshot
|
||||||
if (!violation) return res.status(404).json({ error: 'Violation not found' });
|
|
||||||
|
|
||||||
db.prepare('UPDATE violations SET negated = 1, negated_at = CURRENT_TIMESTAMP WHERE id = ?').run(req.params.id);
|
|
||||||
db.prepare(`
|
|
||||||
INSERT INTO violation_resolutions (violation_id, resolution_type, details, resolved_by)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
`).run(req.params.id, resolution_type, details || null, resolved_by || null);
|
|
||||||
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── PATCH — Restore negated violation ──────────────────────────────────────
|
|
||||||
app.patch('/api/violations/:id/restore', (req, res) => {
|
|
||||||
const violation = db.prepare('SELECT * FROM violations WHERE id = ?').get(req.params.id);
|
|
||||||
if (!violation) return res.status(404).json({ error: 'Violation not found' });
|
|
||||||
db.prepare('UPDATE violations SET negated = 0, negated_at = NULL WHERE id = ?').run(req.params.id);
|
|
||||||
db.prepare('DELETE FROM violation_resolutions WHERE violation_id = ?').run(req.params.id);
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── DELETE — Hard Delete ────────────────────────────────────────────────────
|
|
||||||
app.delete('/api/violations/:id', (req, res) => {
|
|
||||||
const violation = db.prepare('SELECT * FROM violations WHERE id = ?').get(req.params.id);
|
|
||||||
if (!violation) return res.status(404).json({ error: 'Violation not found' });
|
|
||||||
db.prepare('DELETE FROM violation_resolutions WHERE violation_id = ?').run(req.params.id);
|
|
||||||
db.prepare('DELETE FROM violations WHERE id = ?').run(req.params.id);
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── PDF ─────────────────────────────────────────────────────────────────────
|
|
||||||
app.get('/api/violations/:id/pdf', async (req, res) => {
|
app.get('/api/violations/:id/pdf', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const violation = db.prepare(`
|
const violation = db.prepare(`
|
||||||
SELECT v.*, e.name as employee_name, e.department, e.supervisor
|
SELECT v.*, e.name as employee_name, e.department, e.supervisor
|
||||||
FROM violations v JOIN employees e ON e.id = v.employee_id
|
FROM violations v
|
||||||
WHERE v.id = ?
|
JOIN employees e ON e.id = v.employee_id
|
||||||
`).get(req.params.id);
|
WHERE v.id = ?
|
||||||
if (!violation) return res.status(404).json({ error: 'Violation not found' });
|
`).get(req.params.id);
|
||||||
const score = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?').get(violation.employee_id) || { active_points: 0, violation_count: 0 };
|
|
||||||
const pdfBuffer = await generatePdf(violation, score);
|
if (!violation) return res.status(404).json({ error: 'Violation not found' });
|
||||||
const safeName = violation.employee_name.replace(/[^a-z0-9]/gi, '_');
|
|
||||||
res.set({
|
// For PDF, compute score row but pass stored prior_active_points so math is stable
|
||||||
'Content-Type': 'application/pdf',
|
const active = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?')
|
||||||
'Content-Disposition': `attachment; filename="CPAS_${safeName}_${violation.incident_date}.pdf"`,
|
.get(violation.employee_id) || { active_points: 0, violation_count: 0 };
|
||||||
'Content-Length': pdfBuffer.length,
|
|
||||||
});
|
const scoreForPdf = {
|
||||||
res.end(pdfBuffer);
|
employee_id: violation.employee_id,
|
||||||
} catch (err) {
|
// snapshot at time of violation (if present); fall back to current
|
||||||
console.error('[PDF]', err);
|
active_points: violation.prior_active_points != null ? violation.prior_active_points : active.active_points,
|
||||||
res.status(500).json({ error: 'PDF generation failed', detail: err.message });
|
violation_count: active.violation_count,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const pdfBuffer = await generatePdf(violation, scoreForPdf);
|
||||||
|
const safeName = violation.employee_name.replace(/[^a-z0-9]/gi, '_');
|
||||||
|
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Disposition': `attachment; filename="CPAS_${safeName}_${violation.incident_date}.pdf"`,
|
||||||
|
'Content-Length': pdfBuffer.length,
|
||||||
|
});
|
||||||
|
res.end(pdfBuffer);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PDF]', err);
|
||||||
|
res.status(500).json({ error: 'PDF generation failed', detail: err.message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── SPA fallback ────────────────────────────────────────────────────────────
|
// SPA fallback
|
||||||
app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'client', 'dist', 'index.html')));
|
app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'client', 'dist', 'index.html')));
|
||||||
|
|
||||||
app.listen(PORT, '0.0.0.0', () => console.log(`[CPAS] Server running on port ${PORT}`));
|
app.listen(PORT, '0.0.0.0', () => console.log(`[CPAS] Server running on port ${PORT}`));
|
||||||
|
|||||||
Reference in New Issue
Block a user