import express from 'express'; import { Parser } from 'json2csv'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { v4 as uuidv4 } from 'uuid'; 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; next(); }); } else { res.status(401).json({ message: 'Unauthorized' }); } }; router.use(authenticateJWT); // --- START: Date Management Routes --- router.get('/enabled-dates', 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', 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', 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', 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', 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', authenticateJWT, 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', 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 cr.worker_id, w.username, w.full_name, cr.event_type, cr.timestamp FROM clock_records cr JOIN workers w ON cr.worker_id = w.id WHERE cr.timestamp BETWEEN ? AND ? ${workerIdClause} ORDER BY cr.worker_id, cr.timestamp ASC `; const [rows] = await db.execute(query, params); const workHoursByWorkerAndDay = {}; rows.forEach(row => { const day = new Date(row.timestamp).toISOString().split('T')[0]; if (!workHoursByWorkerAndDay[row.worker_id]) { workHoursByWorkerAndDay[row.worker_id] = { username: row.username, full_name: row.full_name, days: {} }; } if (!workHoursByWorkerAndDay[row.worker_id].days[day]) { workHoursByWorkerAndDay[row.worker_id].days[day] = []; } workHoursByWorkerAndDay[row.worker_id].days[day].push({ type: row.event_type, time: new Date(row.timestamp) }); }); const csvData = []; for (const workerId in workHoursByWorkerAndDay) { const workerData = workHoursByWorkerAndDay[workerId]; for (const day in workerData.days) { const events = workerData.days[day]; let dailyTotalSeconds = 0; let lastClockIn = null; events.forEach(event => { if (event.type === 'clock_in') { lastClockIn = event.time; } else if (event.type === 'clock_out' && lastClockIn) { dailyTotalSeconds += (event.time - lastClockIn) / 1000; lastClockIn = null; } }); csvData.push({ username: workerData.username, full_name: workerData.full_name, date: day, work_hours: (dailyTotalSeconds / 3600).toFixed(2) }); } } const json2csvParser = new Parser({ fields: ['username', 'full_name', 'date', 'work_hours'] }); 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', 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 all workers with filtering and pagination router.get('/workers', 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 FROM workers w `; let countQuery = `SELECT COUNT(w.id) as totalCount FROM workers w`; const params = []; const countParams = []; let whereClauses = ["w.role = 'worker'"]; 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 }); } }); // POST (add) a new worker router.post('/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) VALUES (?, ?, ?, ?, ?, ?)', [username, hashedPassword, fullName, role, department, position] ); res.status(201).json({ id: result.insertId, username, fullName, role, department, position }); } 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 }); } }); // DELETE a worker router.delete('/workers/:id', async (req, res) => { try { const { id } = req.params; const [result] = await db.execute("DELETE FROM workers WHERE id = ? AND role = 'worker'", [id]); if (result.affectedRows === 0) { return res.status(404).json({ message: 'Worker not found.' }); } res.status(204).send(); } catch (error) { console.error('Delete worker error:', error); res.status(500).json({ message: 'Database error deleting worker.', details: error.message }); } }); // PUT (update) a worker's password router.put('/workers/:workerId/password', 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 (clear) a worker's device UUID router.put('/workers/:workerId/reset-device', async (req, res) => { try { const { workerId } = req.params; const [result] = await db.execute("UPDATE workers SET device_uuid = NULL WHERE id = ?", [workerId]); if (result.affectedRows === 0) { return res.status(404).json({ message: 'Worker not found.' }); } res.status(200).json({ message: 'Device registration cleared.' }); } catch (error) { console.error('Reset device error:', error); res.status(500).json({ message: 'Database error resetting device.', details: error.message }); } }); // Geofence Management Routes router.get('/geofences', 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', 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', 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', 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', authenticateJWT, 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', authenticateJWT, 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', authenticateJWT, 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', authenticateJWT, 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; }