import express from 'express'; import { Parser } from 'json2csv'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { v4 as uuidv4 } from 'uuid'; import ExcelJS from 'exceljs'; export default function(db) { const router = express.Router(); // Middleware to authenticate and authorize managers const authenticateJWT = (req, res, next) => { const authHeader = req.headers.authorization; if (authHeader) { const token = authHeader.split(' ')[1]; jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err || user.role !== 'manager') { return res.status(403).json({ message: 'Forbidden' }); } req.user = { ...user, id: user.userId }; // Correctly map userId to id next(); }); } else { res.status(401).json({ message: 'Unauthorized' }); } }; // Middleware to check for specific permissions const checkPermission = (requiredPermission) => { return async (req, res, next) => { try { const managerId = req.user.id; const [rows] = await db.execute( 'SELECT * FROM manager_permissions WHERE manager_id = ?', [managerId] ); if (rows.length === 0 || !rows[0][requiredPermission]) { return res.status(403).json({ message: 'Forbidden: Insufficient permissions.' }); } next(); } catch (error) { console.error('Permission check error:', error); res.status(500).json({ message: 'Database error during permission check.' }); } }; }; router.use(authenticateJWT); // --- START: Date Management Routes --- router.get('/enabled-dates', checkPermission('view_all'), async (req, res) => { try { const [rows] = await db.execute('SELECT YEAR(enabled_date) as year, MONTH(enabled_date) as month, DAY(enabled_date) as day FROM enabled_dates'); // Format date safely using components from the database to avoid timezone shifts const dates = rows.map(r => `${r.year}-${String(r.month).padStart(2, '0')}-${String(r.day).padStart(2, '0')}`); res.json(dates); } catch (error) { console.error('Error fetching enabled dates:', error); res.status(500).json({ message: 'Database error fetching enabled dates.' }); } }); // Definitive version using a dedicated database connection router.post('/enabled-dates/update', checkPermission('manage_resources'), async (req, res) => { let connection; // Define connection here to ensure it's accessible in the 'finally' block try { const { datesToEnable, datesToDisable } = req.body; if (!Array.isArray(datesToEnable) || !Array.isArray(datesToDisable)) { return res.status(400).json({ message: 'Invalid input format.' }); } // 1. Get a single, dedicated connection from the pool connection = await db.getConnection(); // 2. Process all deletions sequentially on the dedicated connection for (const date of datesToDisable) { await connection.execute('DELETE FROM enabled_dates WHERE enabled_date = ?', [date]); } // 3. Process all insertions sequentially on the dedicated connection for (const date of datesToEnable) { await connection.execute('INSERT IGNORE INTO enabled_dates (enabled_date) VALUES (?)', [date]); } res.status(200).json({ message: 'Work schedule updated successfully.' }); } catch (error) { console.error('Error updating work schedule:', error); res.status(500).json({ message: 'Database error during schedule update.' }); } finally { // 4. Ensure the dedicated connection is always released back to the pool if (connection) { connection.release(); } } }); // --- END: Date Management Routes --- // --- ATTENDANCE & REPORTING --- router.get('/failed-records', checkPermission('view_all'), async (req, res) => { try { const { search = '', startDate, endDate } = req.query; if (!startDate || !endDate) { return res.status(400).json({ message: 'Start date and end date are required.' }); } const searchTerm = `%${search}%`; const params = [startDate, `${endDate} 23:59:59`]; let searchQuery = ''; if (search) { searchQuery = `AND (w.full_name LIKE ? OR w.department LIKE ?)`; params.push(searchTerm, searchTerm); } const query = ` SELECT cr.worker_id, w.full_name, COUNT(*) as count FROM clock_records cr JOIN workers w ON cr.worker_id = w.id WHERE cr.event_type = 'failed' AND cr.timestamp BETWEEN ? AND ? ${searchQuery} GROUP BY cr.worker_id, w.full_name ORDER BY count DESC `; const [rows] = await db.execute(query, params); res.json(rows); } catch (error) { console.error('Failed records summary error:', error); res.status(500).json({ message: 'Database error fetching failed records summary.', details: error.message }); } }); router.get('/failed-records/details', checkPermission('view_all'), async (req, res) => { try { const { workerId, startDate, endDate } = req.query; if (!workerId || !startDate || !endDate) { return res.status(400).json({ message: 'Worker ID, start date, and end date are required.' }); } const query = ` SELECT cr.id, cr.timestamp, cr.event_type, COALESCE(qc.name, 'N/A') as qrCodeUsedName, cr.notes FROM clock_records cr LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id WHERE cr.worker_id = ? AND cr.event_type = 'failed' AND cr.timestamp BETWEEN ? AND ? ORDER BY cr.timestamp DESC `; const params = [workerId, startDate, `${endDate} 23:59:59`]; const [rows] = await db.execute(query, params); res.json(rows); } catch (error) { console.error('Failed records details error:', error); res.status(500).json({ message: 'Database error fetching failed records details.', details: error.message }); } }); // GET attendance records with a modified query to avoid the MySQL 5.7 bug router.get('/attendance-records/export-raw', checkPermission('view_all'), async (req, res) => { try { const { workerIds, startDate, endDate } = req.query; if (!startDate || !endDate) { return res.status(400).json({ message: 'Start date and end date are required.' }); } let workerIdClause = ''; const params = [startDate, `${endDate} 23:59:59`]; if (workerIds) { const idsArray = workerIds.split(',').map(Number).filter(id => !isNaN(id)); if (idsArray.length > 0) { workerIdClause = `AND cr.worker_id IN (${idsArray.join(',')})`; } } const query = ` SELECT w.username, w.full_name, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qr_code_name, cr.notes FROM clock_records cr JOIN workers w ON cr.worker_id = w.id LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id WHERE cr.timestamp BETWEEN ? AND ? ${workerIdClause} ORDER BY cr.timestamp DESC `; const [rows] = await db.execute(query, params); const json2csvParser = new Parser({ fields: ['username', 'full_name', 'event_type', 'timestamp', 'qr_code_name', 'notes'] }); const csv = json2csvParser.parse(rows); res.header('Content-Type', 'text/csv').attachment(`raw_attendance_${startDate}_to_${endDate}.csv`).send(csv); } catch (error) { console.error('Raw attendance export error:', error); res.status(500).json({ message: 'Database error exporting raw attendance.', details: error.message }); } }); router.post('/add-record', checkPermission('edit_workers'), async (req, res) => { try { const { workerId, eventType, timestamp, notes } = req.body if (!workerId || !eventType || !timestamp) { return res .status(400) .json({ message: 'Worker ID, event type, and timestamp are required.' }) } // Check last event to prevent adding a duplicate event type const [lastEventRows] = await db.execute( 'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1', [workerId], ) if (lastEventRows.length > 0 && lastEventRows[0].event_type === eventType) { const status = eventType === 'clock_in' ? 'in' : 'out' return res.status(409).json({ message: `Worker is already clocked ${status}.` }) } // --- THIS IS THE FIX --- const sanitizedTimestamp = timestamp.replace('T', ' ') await db.execute( 'INSERT INTO clock_records (worker_id, event_type, timestamp, notes, qr_code_id, latitude, longitude) VALUES (?, ?, ?, ?, NULL, NULL, NULL)', [workerId, eventType, sanitizedTimestamp, notes], ) res.status(201).json({ message: 'Manual record added successfully.' }) } catch (error) { console.error('Add manual record error:', error) res.status(500).json({ message: 'Database error adding manual record.' }) } }) router.get('/attendance-records/export', checkPermission('view_all'), async (req, res) => { try { const { workerIds, startDate, endDate } = req.query; if (!startDate || !endDate) { return res.status(400).json({ message: 'Start date and end date are required.' }); } const wantXlsx = String(req.query.format || 'csv').toLowerCase() === 'xlsx'; let workerIdClause = ''; const params = [startDate, `${endDate} 23:59:59`]; if (workerIds) { const idsArray = workerIds.split(',').map(Number).filter(id => !isNaN(id)); if (idsArray.length > 0) { workerIdClause = `AND cr.worker_id IN (${idsArray.join(',')})`; } } const query = ` SELECT cr.worker_id, w.username, w.full_name, w.department, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') AS qr_code_name FROM clock_records cr JOIN workers w ON cr.worker_id = w.id LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id WHERE cr.timestamp BETWEEN ? AND ? ${workerIdClause} ORDER BY cr.worker_id, cr.timestamp ASC `; const [rows] = await db.execute(query, params); // ---- Group events by worker/day ---- const workByDay = {}; rows.forEach(row => { const day = new Date(row.timestamp).toISOString().split('T')[0]; if (!workByDay[row.worker_id]) { workByDay[row.worker_id] = { username: row.username, full_name: row.full_name, department: row.department || '', days: {} }; } if (!workByDay[row.worker_id].days[day]) { workByDay[row.worker_id].days[day] = []; } workByDay[row.worker_id].days[day].push({ type: row.event_type, time: new Date(row.timestamp), qr_code_name: row.qr_code_name }); }); // ---- Helpers for formatting ---- const fmtHM = (d) => { const p = (n) => String(n).padStart(2, '0'); return `${p(d.getHours())}:${p(d.getMinutes())}`; }; const dayName = (yyyyMmDd) => { const [y, m, dd] = yyyyMmDd.split('-').map(Number); const d = new Date(y, m - 1, dd); return ['SUN','MON','TUE','WED','THU','FRI','SAT'][d.getDay()]; }; // ---- Build ONE row per worker/day (no double rows) ---- const csvData = []; const byWorkerForXlsx = new Map(); // key = "username||full_name||department" → daily rows for (const workerId in workByDay) { const w = workByDay[workerId]; const perWorkerRows = []; for (const day of Object.keys(w.days).sort()) { const events = w.days[day].slice().sort((a, b) => a.time - b.time); // total hours across all in/out pairs let totalSec = 0; let open = null; for (const e of events) { if (e.type === 'clock_in' && open == null) open = e.time; else if (e.type === 'clock_out' && open != null) { totalSec += (e.time - open) / 1000; open = null; } } const firstIn = events.find(e => e.type === 'clock_in') || null; const lastOut = [...events].reverse().find(e => e.type === 'clock_out') || null; const dailyRow = { username: w.username, full_name: w.full_name, date: day, day: dayName(day), clock_in: firstIn ? fmtHM(firstIn.time) : '', clock_out: lastOut ? fmtHM(lastOut.time) : '', work_hours: (firstIn && lastOut) ? (totalSec / 3600).toFixed(2) : '', qr_code_name: firstIn ? (firstIn.qr_code_name || 'Manual Entry') : '' }; csvData.push(dailyRow); perWorkerRows.push(dailyRow); } byWorkerForXlsx.set(`${w.username}||${w.full_name}||${w.department}`, perWorkerRows); } // ===== XLSX branch: grouped header + per-day summary columns ===== if (wantXlsx) { const wb = new ExcelJS.Workbook(); const ws = wb.addWorksheet('Attendance'); ws.columns = [ { header: 'Date', key: 'date', width: 12 }, { header: 'Day', key: 'day', width: 8 }, { header: 'Clock In', key: 'clock_in', width: 10 }, { header: 'Clock Out', key: 'clock_out', width: 10 }, { header: 'Work Hours', key: 'work_hours', width: 12 }, { header: 'QR Code', key: 'qr_code_name', width: 24 }, ]; for (const [key, rowsForWorker] of byWorkerForXlsx.entries()) { const [username, full_name, dept] = key.split('||'); if (ws.lastRow) ws.addRow([]); // Bold merged group header: "username full_name Dept: X" const titleRowIdx = (ws.lastRow ? ws.lastRow.number : 0) + 1; ws.mergeCells(`A${titleRowIdx}:F${titleRowIdx}`); const titleCell = ws.getCell(`A${titleRowIdx}`); titleCell.value = dept ? `${username} ${full_name} Dept: ${dept}` : `${username} ${full_name}`; titleCell.font = { bold: true, size: 12 }; titleCell.alignment = { horizontal: 'left', vertical: 'middle' }; // Header row under the group const hdr = ws.addRow({ date: 'Date', day: 'Day', clock_in: 'Clock In', clock_out: 'Clock Out', work_hours: 'Work Hours', qr_code_name: 'QR Code', }); hdr.font = { bold: true }; // Detail rows (one per day) for (const r of rowsForWorker) { ws.addRow(r); } } ws.eachRow(row => { row.alignment = { vertical: 'middle' }; }); const buf = await wb.xlsx.writeBuffer(); res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', `attachment; filename="work_hours_${startDate}_to_${endDate}.xlsx"`); return res.send(Buffer.from(buf)); } // ===== CSV fallback: one row per day; include identity columns ===== const json2csvParser = new Parser({ fields: ['username','full_name','date','day','clock_in','clock_out','work_hours','qr_code_name'] }); const csv = json2csvParser.parse(csvData); res .header('Content-Type', 'text/csv') .attachment(`work_hours_${startDate}_to_${endDate}.csv`) .send(csv); } catch (error) { console.error('Work hours export error:', error); res.status(500).json({ message: 'Database error exporting work hours.', details: error.message }); } }); router.get('/attendance-records', checkPermission('view_all'), async (req, res) => { try { const { workerIds, startDate, endDate, format } = req.query; if (!workerIds) { return res.status(400).json({ message: 'Worker IDs are required.' }); } // Ensure all IDs are numbers to prevent SQL injection. const idsArray = workerIds.split(',').map(Number).filter(id => !isNaN(id)); if (idsArray.length === 0) { return res.json([]); } // --- MODIFICATION START --- // Instead of using a '?' placeholder for the IN clause, we build it directly. // This is safe because we have already sanitized idsArray to be only numbers. // This change is intended to bypass the specific bug in your MySQL version. const inClause = idsArray.join(','); let query = ` SELECT cr.id, w.full_name, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName, cr.latitude, cr.longitude, cr.notes FROM clock_records cr JOIN workers w ON cr.worker_id = w.id LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id WHERE cr.worker_id IN (${inClause})`; // Placeholder is replaced here const params = []; // --- MODIFICATION END --- if (startDate && endDate) { query += ' AND cr.timestamp BETWEEN ? AND ?'; const endOfDay = new Date(endDate); endOfDay.setHours(23, 59, 59, 999); params.push(startDate, endOfDay); } query += ' ORDER BY w.full_name, cr.timestamp DESC'; const [rows] = await db.execute(query, params); if (format === 'csv') { const json2csvParser = new Parser({ fields: ['full_name', 'event_type', 'timestamp', 'qrCodeUsedName', 'notes'] }); const csv = json2csvParser.parse(rows); res.header('Content-Type', 'text/csv').attachment('attendance.csv').send(csv); } else { res.json(rows); } } catch (error) { console.error('Attendance records error:', error); res.status(500).json({ message: 'Database error fetching attendance records.', details: error.message }); } }); // --- All other manager routes remain the same --- // GET a specific manager's permissions router.get('/permissions/:id', async (req, res) => { try { const requesterId = req.user.id; const targetId = parseInt(req.params.id, 10); // Check if the user is trying to access their own permissions if (requesterId !== targetId) { // If not, check if they have permission to manage permissions const [permissionRows] = await db.execute( 'SELECT can_manage_permissions FROM manager_permissions WHERE manager_id = ?', [requesterId] ); if (permissionRows.length === 0 || !permissionRows[0].can_manage_permissions) { return res.status(403).json({ message: 'Forbidden: Insufficient permissions to view others\' permissions.' }); } } // If they are accessing their own, or have permission, fetch the target's permissions const [rows] = await db.execute( 'SELECT * FROM manager_permissions WHERE manager_id = ?', [targetId] ); if (rows.length === 0) { // If no permissions are set, return a default set of all false const [fields] = await db.execute('DESCRIBE manager_permissions'); const defaultPermissions = fields.reduce((acc, field) => { if (field.Field !== 'manager_id') { acc[field.Field] = 0; // Use 0 for false } return acc; }, {}); return res.json(defaultPermissions); } // Convert buffer values to booleans const permissions = Object.entries(rows[0]).reduce((acc, [key, value]) => { if (key !== 'manager_id') { acc[key] = Boolean(value); } return acc; }, {}); res.json(permissions); } catch (error) { console.error('Get manager permissions error:', error); res.status(500).json({ message: 'Database error fetching manager permissions.', details: error.message }); } }); // PUT (update) a manager's permissions router.put('/permissions/:id', checkPermission('manager_permissions'), async (req, res) => { try { const { id } = req.params; const permissions = req.body; const fields = [ 'view_all', 'edit_workers', 'manage_resources', 'manager_permissions' ]; const values = fields.map(field => permissions[field] || false); // Convert to new simplified permissions schema const query = ` INSERT INTO manager_permissions (manager_id, view_all, edit_workers, manage_resources, manager_permissions) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE view_all = VALUES(view_all), edit_workers = VALUES(edit_workers), manage_resources = VALUES(manage_resources), manager_permissions = VALUES(manager_permissions) `; const queryParams = [id, ...values]; await db.execute(query, queryParams); res.status(200).json({ message: 'Permissions updated successfully.' }); } catch (error) { console.error('Update manager permissions error:', error); res.status(500).json({ message: 'Database error updating manager permissions.', details: error.message }); } }); // GET all workers with filtering and pagination router.get('/workers', checkPermission('view_all'), async (req, res) => { try { const { search = '', page = 1, limit = 20 } = req.query; const offset = (parseInt(page) - 1) * parseInt(limit); const searchTerm = `%${search}%`; let baseQuery = ` SELECT w.id, w.username, w.full_name, w.department, w.position, w.created_at, w.status FROM workers w `; let countQuery = `SELECT COUNT(w.id) as totalCount FROM workers w`; const params = []; const countParams = []; let whereClauses = ["w.role = 'worker'", "w.status != 'deleted'"]; // Filter out soft-deleted workers if (search) { whereClauses.push(`(w.full_name LIKE ? OR w.department LIKE ?)`); params.push(searchTerm, searchTerm); countParams.push(searchTerm, searchTerm); } if (whereClauses.length > 0) { const whereString = ` WHERE ${whereClauses.join(' AND ')}`; baseQuery += whereString; countQuery += whereString; } baseQuery += ` ORDER BY w.created_at DESC LIMIT ? OFFSET ?`; params.push(parseInt(limit), offset); const [workers] = await db.execute(baseQuery, params); const [[{ totalCount }]] = await db.execute(countQuery, countParams); res.json({ workers, totalCount }); } catch (error) { console.error('Get workers error:', error); res.status(500).json({ message: 'Database error fetching workers.', details: error.message }); } }); // GET all managers with their permissions router.get('/managers', checkPermission('manager_permissions'), async (req, res) => { try { const { search = '', page = 1, limit = 20 } = req.query; const offset = (parseInt(page) - 1) * parseInt(limit); const searchTerm = `%${search}%`; let baseQuery = ` SELECT w.id, w.username, w.full_name, w.department, w.position, w.created_at, w.status, mp.* FROM workers w LEFT JOIN manager_permissions mp ON w.id = mp.manager_id `; let countQuery = `SELECT COUNT(w.id) as totalCount FROM workers w`; const params = []; const countParams = []; let whereClauses = ["w.role = 'manager'", "w.status != 'deleted'"]; if (search) { whereClauses.push(`(w.full_name LIKE ? OR w.department LIKE ?)`); params.push(searchTerm, searchTerm); countParams.push(searchTerm, searchTerm); } if (whereClauses.length > 0) { const whereString = ` WHERE ${whereClauses.join(' AND ')}`; baseQuery += whereString; countQuery += whereString; } baseQuery += ` ORDER BY w.created_at DESC LIMIT ? OFFSET ?`; params.push(parseInt(limit), offset); const [managers] = await db.execute(baseQuery, params); const [[{ totalCount }]] = await db.execute(countQuery, countParams); res.json({ managers, totalCount }); } catch (error) { console.error('Get managers error:', error); res.status(500).json({ message: 'Database error fetching managers.', details: error.message }); } }); // POST (add) a new manager router.post('/managers', checkPermission('manager_permissions'), async (req, res) => { try { const { username, password, fullName, department, position } = req.body; if (!username || !password || !fullName) { return res.status(400).json({ message: 'Username, password, and full name are required.' }); } const hashedPassword = await bcrypt.hash(password, 10); const [result] = await db.execute( 'INSERT INTO workers (username, password_hash, full_name, role, department, position, status) VALUES (?, ?, ?, ?, ?, ?, ?)', [username, hashedPassword, fullName, 'manager', department, position, 'active'] ); // Set default view_all permission await db.execute( 'INSERT INTO manager_permissions (manager_id, view_all) VALUES (?, ?)', [result.insertId, true] ); res.status(201).json({ id: result.insertId, username, fullName, role: 'manager', department, position, status: 'active', view_all: true }); } catch (error) { console.error('Add manager error:', error); if (error.code === 'ER_DUP_ENTRY') { return res.status(409).json({ message: 'Username already exists.' }); } res.status(500).json({ message: 'Database error adding manager.', details: error.message }); } }); // POST (add) a new worker router.post('/workers', checkPermission('edit_workers'), async (req, res) => { try { const { username, password, fullName, department, position, role = 'worker' } = req.body; if (!username || !password || !fullName) { return res.status(400).json({ message: 'Username, password, and full name are required.' }); } const hashedPassword = await bcrypt.hash(password, 10); const [result] = await db.execute( 'INSERT INTO workers (username, password_hash, full_name, role, department, position, status) VALUES (?, ?, ?, ?, ?, ?, ?)', [username, hashedPassword, fullName, role, department, position, 'active'] // Default status to 'active' ); res.status(201).json({ id: result.insertId, username, fullName, role, department, position, status: 'active' }); } catch (error) { console.error('Add worker error:', error); if (error.code === 'ER_DUP_ENTRY') { return res.status(409).json({ message: 'Username already exists.' }); } res.status(500).json({ message: 'Database error adding worker.', details: error.message }); } }); // Soft DELETE a worker (update status to 'deleted') router.delete('/workers/:id', checkPermission('edit_workers'), async (req, res) => { try { const { id } = req.params; const [result] = await db.execute("UPDATE workers SET status = 'deleted' WHERE id = ? AND role = 'worker'", [id]); if (result.affectedRows === 0) { return res.status(404).json({ message: 'Worker not found or already deleted.' }); } res.status(204).send(); // Maintain existing response for client compatibility } catch (error) { console.error('Soft delete worker error:', error); res.status(500).json({ message: 'Database error soft deleting worker.', details: error.message }); } }); // Soft DELETE a manager (update status to 'deleted') router.delete('/managers/:id', checkPermission('manager_permissions'), async (req, res) => { try { const { id } = req.params; const [result] = await db.execute("UPDATE workers SET status = 'deleted' WHERE id = ? AND role = 'manager'", [id]); if (result.affectedRows === 0) { return res.status(404).json({ message: 'Manager not found or already deleted.' }); } res.status(204).send(); } catch (error) { console.error('Soft delete manager error:', error); res.status(500).json({ message: 'Database error soft deleting manager.', details: error.message }); } }); // PUT (update) a worker's details (department, position, status) router.put('/workers/:id', checkPermission('edit_workers'), async (req, res) => { try { const { id } = req.params; const { department, position, status } = req.body; // Basic validation if (!department && !position && !status) { return res.status(400).json({ message: 'No update information provided.' }); } if (status && !['active', 'inactive'].includes(status)) { return res.status(400).json({ message: 'Invalid status value.' }); } let updateQuery = 'UPDATE workers SET'; const params = []; const fieldsToUpdate = []; if (department) { fieldsToUpdate.push('department = ?'); params.push(department); } if (position) { fieldsToUpdate.push('position = ?'); params.push(position); } if (status) { fieldsToUpdate.push('status = ?'); params.push(status); } updateQuery += ` ${fieldsToUpdate.join(', ')} WHERE id = ? AND role = 'worker'`; params.push(id); const [result] = await db.execute(updateQuery, params); if (result.affectedRows === 0) { return res.status(404).json({ message: 'Worker not found.' }); } res.status(200).json({ message: 'Worker details updated successfully.' }); } catch (error) { console.error('Update worker details error:', error); res.status(500).json({ message: 'Database error updating worker details.', details: error.message }); } }); // PUT (update) a manager's details (department, position, status) router.put('/managers/:id', checkPermission('manager_permissions'), async (req, res) => { try { const { id } = req.params; const { department, position, status } = req.body; // Basic validation if (!department && !position && !status) { return res.status(400).json({ message: 'No update information provided.' }); } if (status && !['active', 'inactive'].includes(status)) { return res.status(400).json({ message: 'Invalid status value.' }); } let updateQuery = 'UPDATE workers SET'; const params = []; const fieldsToUpdate = []; if (department) { fieldsToUpdate.push('department = ?'); params.push(department); } if (position) { fieldsToUpdate.push('position = ?'); params.push(position); } if (status) { fieldsToUpdate.push('status = ?'); params.push(status); } updateQuery += ` ${fieldsToUpdate.join(', ')} WHERE id = ? AND role = 'manager'`; params.push(id); const [result] = await db.execute(updateQuery, params); if (result.affectedRows === 0) { return res.status(404).json({ message: 'Manager not found.' }); } res.status(200).json({ message: 'Manager details updated successfully.' }); } catch (error) { console.error('Update manager details error:', error); res.status(500).json({ message: 'Database error updating manager details.', details: error.message }); } }); // PUT (update) a worker's password router.put('/workers/:workerId/password', checkPermission('edit_workers'), async (req, res) => { try { const { workerId } = req.params; const { newPassword } = req.body; if (!newPassword || newPassword.length < 6) { return res.status(400).json({ message: 'Password must be at least 6 characters long.' }); } const hashedPassword = await bcrypt.hash(newPassword, 10); const [result] = await db.execute("UPDATE workers SET password_hash = ? WHERE id = ? AND role = 'worker'", [hashedPassword, workerId]); if (result.affectedRows === 0) { return res.status(404).json({ message: 'Worker not found.' }); } res.status(200).json({ message: 'Password updated successfully.' }); } catch (error) { console.error('Update password error:', error); res.status(500).json({ message: 'Database error updating password.', details: error.message }); } }); // PUT (update) a manager's password router.put('/managers/:managerId/password', checkPermission('manager_permissions'), async (req, res) => { try { const { managerId } = req.params; const { newPassword } = req.body; if (!newPassword || newPassword.length < 6) { return res.status(400).json({ message: 'Password must be at least 6 characters long.' }); } const hashedPassword = await bcrypt.hash(newPassword, 10); const [result] = await db.execute("UPDATE workers SET password_hash = ? WHERE id = ? AND role = 'manager'", [hashedPassword, managerId]); if (result.affectedRows === 0) { return res.status(404).json({ message: 'Manager not found.' }); } res.status(200).json({ message: 'Password updated successfully.' }); } catch (error) { console.error('Update manager password error:', error); res.status(500).json({ message: 'Database error updating manager password.', details: error.message }); } }); // PUT (clear) a worker's device UUID and/or update status router.put('/workers/:workerId/reset-device', checkPermission('edit_workers'), async (req, res) => { try { const { workerId } = req.params; const { status } = req.body; // Optional status field let updateQuery = "UPDATE workers SET device_uuid = NULL"; const params = [workerId]; if (status && ['active', 'inactive', 'deleted'].includes(status)) { updateQuery += ", status = ?"; params.unshift(status); // Add status to the beginning of params for correct order } updateQuery += " WHERE id = ?"; const [result] = await db.execute(updateQuery, params); if (result.affectedRows === 0) { return res.status(404).json({ message: 'Worker not found.' }); } res.status(200).json({ message: 'Device registration cleared and/or status updated.' }); } catch (error) { console.error('Reset device/update status error:', error); res.status(500).json({ message: 'Database error resetting device or updating status.', details: error.message }); } }); // Geofence Management Routes router.get('/geofences', checkPermission('view_all'), async (req, res) => { try { const [rows] = await db.execute( 'SELECT id, name, coordinates, is_active, created_at FROM geofences ORDER BY created_at DESC' ); const geofences = rows.map(row => ({ ...row, coordinates: JSON.parse(row.coordinates || '[]') })); res.json(geofences); } catch (error) { console.error('Get geofences error:', error); res.status(500).json({ message: 'Database error fetching geofences.', details: error.message }); } }); router.post('/geofences', checkPermission('manage_resources'), async (req, res) => { try { const { name, coordinates } = req.body; if (!name || !coordinates) { return res.status(400).json({ message: 'Geofence name and coordinates are required.' }); } const [result] = await db.execute( 'INSERT INTO geofences (name, coordinates, is_active) VALUES (?, ?, ?)', [name, JSON.stringify(coordinates), true] ); const newGeofence = { id: result.insertId, name, coordinates, is_active: true, }; res.status(201).json(newGeofence); } catch (error) { console.error('Add geofence error:', error); res.status(500).json({ message: 'Database error adding geofence.', details: error.message }); } }); router.put('/geofences/:id', checkPermission('manage_resources'), async (req, res) => { try { const { id } = req.params; const { is_active } = req.body; if (typeof is_active !== 'boolean') { return res.status(400).json({ message: 'is_active must be a boolean.' }); } const [result] = await db.execute( 'UPDATE geofences SET is_active = ? WHERE id = ?', [is_active, id] ); if (result.affectedRows === 0) { return res.status(404).json({ message: 'Geofence not found.' }); } res.json({ id, is_active }); } catch (error) { console.error('Update geofence error:', error); res.status(500).json({ message: 'Database error updating geofence.', details: error.message }); } }); router.delete('/geofences/:id', checkPermission('manage_resources'), async (req, res) => { try { const { id } = req.params; const [result] = await db.execute('DELETE FROM geofences WHERE id = ?', [id]); if (result.affectedRows === 0) { return res.status(404).json({ message: 'Geofence not found.' }); } res.status(204).send(); } catch (error) { console.error('Delete geofence error:', error); res.status(500).json({ message: 'Database error deleting geofence.', details: error.message }); } }); // QR Code Management Routes router.get('/qr-codes', checkPermission('view_all'), async (req, res) => { try { const [rows] = await db.execute( 'SELECT id, name, is_active, created_at FROM qr_codes ORDER BY created_at DESC' ); res.json(rows); } catch (error) { console.error('Get QR codes error:', error); res.status(500).json({ message: 'Database error fetching QR codes.' }); } }); router.post('/qr-codes', checkPermission('manage_resources'), async (req, res) => { try { const { name } = req.body; if (!name) return res.status(400).json({ message: 'QR Code name is required.' }); const newQrCode = { id: uuidv4(), name, is_active: true }; await db.execute( 'INSERT INTO qr_codes (id, name, is_active) VALUES (?, ?, ?)', [newQrCode.id, newQrCode.name, newQrCode.is_active] ); res.status(201).json(newQrCode); } catch (error) { console.error('Add QR code error:', error); res.status(500).json({ message: 'Database error adding QR code.' }); } }); router.put('/qr-codes/:id', checkPermission('manage_resources'), async (req, res) => { try { const { id } = req.params; // Handle both isActive (camelCase) and is_active (snake_case) const is_active = req.body.is_active ?? req.body.isActive; if (typeof is_active !== 'boolean') { return res.status(400).json({ message: 'Status must be a boolean value.' }); } const [result] = await db.execute( 'UPDATE qr_codes SET is_active = ? WHERE id = ?', [is_active, id] ); if (result.affectedRows === 0) { return res.status(404).json({ message: 'QR Code not found.' }); } res.json({ id, is_active }); } catch (error) { console.error('Update QR code error:', error); res.status(500).json({ message: 'Database error updating QR code.' }); } }); router.delete('/qr-codes/:id', checkPermission('manage_resources'), async (req, res) => { try { const { id } = req.params; const [result] = await db.execute( 'DELETE FROM qr_codes WHERE id = ?', [id] ); if (result.affectedRows === 0) { return res.status(404).json({ message: 'QR Code not found.' }); } res.status(204).send(); } catch (error) { console.error('Delete QR code error:', error); res.status(500).json({ message: 'Database error deleting QR code.' }); } }); return router; }