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