Files
cpas/server.js
2026-03-06 12:19:55 -06:00

170 lines
7.3 KiB
JavaScript
Executable File

const express = require('express');
const cors = require('cors');
const path = require('path');
const db = require('./db/database');
const generatePdf = require('./pdf/generator');
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'client', 'dist')));
// ── Health ─────────────────────────────────────────────────────────────────
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// ── Employees ──────────────────────────────────────────────────────────────
app.get('/api/employees', (req, res) => {
const rows = db.prepare(
'SELECT id, name, department, supervisor FROM employees ORDER BY name ASC'
).all();
res.json(rows);
});
app.post('/api/employees', (req, res) => {
const { name, department, supervisor } = req.body;
if (!name) return res.status(400).json({ error: 'name is required' });
const existing = db.prepare(
'SELECT * FROM employees WHERE LOWER(name) = LOWER(?)'
).get(name);
if (existing) {
if (department || supervisor) {
db.prepare(
'UPDATE employees SET department = COALESCE(?, department), supervisor = COALESCE(?, supervisor) WHERE 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);
res.status(201).json({ id: result.lastInsertRowid, name, department, supervisor });
});
// ── Employee CPAS Score ────────────────────────────────────────────────────
app.get('/api/employees/:employeeId/score', (req, res) => {
const row = db.prepare(
'SELECT * FROM active_cpas_scores WHERE employee_id = ?'
).get(req.params.employeeId);
res.json(row || { employee_id: req.params.employeeId, active_points: 0, violation_count: 0 });
});
// ── Violation type counts (90-day) ─────────────────────────────────────────
app.get('/api/employees/:employeeId/violation-counts', (req, res) => {
const rows = db.prepare(`
SELECT violation_type, COUNT(*) as count
FROM violations
WHERE employee_id = ?
AND incident_date >= DATE('now', '-90 days')
GROUP BY violation_type
`).all(req.params.employeeId);
const map = {};
rows.forEach(r => { map[r.violation_type] = r.count; });
res.json(map);
});
// ── Violation type counts (all-time) ───────────────────────────────────────
app.get('/api/employees/:employeeId/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 = ?
GROUP BY violation_type
`).all(req.params.employeeId);
const map = {};
rows.forEach(r => { map[r.violation_type] = { count: r.count, max_points_used: r.max_points_used }; });
res.json(map);
});
// ── Violation history ──────────────────────────────────────────────────────
app.get('/api/violations/employee/:employeeId', (req, res) => {
const limit = parseInt(req.query.limit) || 50;
const rows = db.prepare(`
SELECT * FROM violations
WHERE employee_id = ?
ORDER BY incident_date DESC, created_at DESC
LIMIT ?
`).all(req.params.employeeId, limit);
res.json(rows);
});
// ── POST new violation ─────────────────────────────────────────────────────
app.post('/api/violations', (req, res) => {
const {
employee_id, violation_type, violation_name, category,
points, incident_date, incident_time, location,
details, submitted_by, witness_name
} = req.body;
if (!employee_id || !violation_type || !points || !incident_date) {
return res.status(400).json({
error: 'Missing required fields: employee_id, violation_type, points, 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
) 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 });
});
// ── PDF Generation ─────────────────────────────────────────────────────────
// GET /api/violations/:id/pdf
// Returns a binary PDF of the violation document
app.get('/api/violations/:id/pdf', async (req, res) => {
try {
const violation = db.prepare(`
SELECT v.*, e.name as employee_name, e.department, e.supervisor
FROM violations v
JOIN employees e ON e.id = v.employee_id
WHERE v.id = ?
`).get(req.params.id);
if (!violation) return res.status(404).json({ error: 'Violation not found' });
// Pull employee 90-day score for context block in PDF
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);
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] Error:', err);
res.status(500).json({ error: 'PDF generation failed', detail: err.message });
}
});
// ── SPA fallback ───────────────────────────────────────────────────────────
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}`);
});