Upload files to "/"

This commit is contained in:
2026-03-06 14:42:12 -06:00
parent 60e9da488c
commit fdfa0bcf2f

256
server.js
View File

@@ -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}`));