Phase 4
This commit is contained in:
197
server.js
197
server.js
@@ -1,8 +1,8 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const db = require('./db/database');
|
||||
const generatePdf = require('./pdf/generator');
|
||||
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;
|
||||
@@ -11,142 +11,157 @@ 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() });
|
||||
});
|
||||
// ── Health ──────────────────────────────────────────────────────────────────
|
||||
app.get('/api/health', (req, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() }));
|
||||
|
||||
// ── Employees ──────────────────────────────────────────────────────────────
|
||||
// ── Employees ───────────────────────────────────────────────────────────────
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
|
||||
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 });
|
||||
// ── Employee CPAS Score ─────────────────────────────────────────────────────
|
||||
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 });
|
||||
});
|
||||
|
||||
// ── Violation type counts (90-day) ─────────────────────────────────────────
|
||||
app.get('/api/employees/:employeeId/violation-counts', (req, res) => {
|
||||
// ── Dashboard — all employees with scores ───────────────────────────────────
|
||||
app.get('/api/dashboard', (req, res) => {
|
||||
const rows = db.prepare(`
|
||||
SELECT violation_type, COUNT(*) as count
|
||||
FROM violations
|
||||
WHERE employee_id = ?
|
||||
AND incident_date >= DATE('now', '-90 days')
|
||||
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 counts (90-day) ───────────────────────────────────────────────
|
||||
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.employeeId);
|
||||
`).all(req.params.id);
|
||||
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) => {
|
||||
// ── 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 = ?
|
||||
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.employeeId);
|
||||
`).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 ──────────────────────────────────────────────────────
|
||||
app.get('/api/violations/employee/:employeeId', (req, res) => {
|
||||
// ── Violation history (per employee) ───────────────────────────────────────
|
||||
app.get('/api/violations/employee/:id', (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
|
||||
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.employeeId, limit);
|
||||
`).all(req.params.id, limit);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// ── POST new violation ─────────────────────────────────────────────────────
|
||||
// ── 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'
|
||||
});
|
||||
}
|
||||
if (!employee_id || !violation_type || !points || !incident_date)
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
|
||||
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
|
||||
);
|
||||
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
|
||||
// ── PATCH — Soft Negate (add resolution) ───────────────────────────────────
|
||||
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);
|
||||
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) => {
|
||||
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
|
||||
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 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, '_');
|
||||
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"`,
|
||||
@@ -154,16 +169,12 @@ app.get('/api/violations/:id/pdf', async (req, res) => {
|
||||
});
|
||||
res.end(pdfBuffer);
|
||||
} catch (err) {
|
||||
console.error('[PDF] Error:', 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'));
|
||||
});
|
||||
// ── 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}`);
|
||||
});
|
||||
app.listen(PORT, '0.0.0.0', () => console.log(`[CPAS] Server running on port ${PORT}`));
|
||||
|
||||
Reference in New Issue
Block a user