163 lines
6.0 KiB
JavaScript
Executable File
163 lines
6.0 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 score (current snapshot)
|
|
app.get('/api/employees/:id/score', (req, res) => {
|
|
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 });
|
|
});
|
|
|
|
// 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();
|
|
res.json(rows);
|
|
});
|
|
|
|
// Violation history (per employee) with resolutions
|
|
app.get('/api/violations/employee/:id', (req, res) => {
|
|
const limit = parseInt(req.query.limit) || 50;
|
|
const rows = db.prepare(`
|
|
SELECT v.*, r.resolution_type, r.details AS resolution_details,
|
|
r.resolved_by, r.created_at AS resolved_at
|
|
FROM violations v
|
|
LEFT JOIN violation_resolutions r ON r.violation_id = v.id
|
|
WHERE v.employee_id = ?
|
|
ORDER BY v.incident_date DESC, v.created_at DESC
|
|
LIMIT ?
|
|
`).all(req.params.id, limit);
|
|
res.json(rows);
|
|
});
|
|
|
|
// 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) => {
|
|
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' });
|
|
}
|
|
|
|
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 });
|
|
});
|
|
|
|
// Negate / restore / delete endpoints unchanged ...
|
|
|
|
// PDF endpoint — use stored prior_active_points snapshot
|
|
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' });
|
|
|
|
// For PDF, compute score row but pass stored prior_active_points so math is stable
|
|
const active = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?')
|
|
.get(violation.employee_id) || { active_points: 0, violation_count: 0 };
|
|
|
|
const scoreForPdf = {
|
|
employee_id: violation.employee_id,
|
|
// snapshot at time of violation (if present); fall back to current
|
|
active_points: violation.prior_active_points != null ? violation.prior_active_points : active.active_points,
|
|
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
|
|
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}`));
|