diff --git a/backend/managerRoutes.js b/backend/managerRoutes.js index ffd0490..c4acd20 100644 --- a/backend/managerRoutes.js +++ b/backend/managerRoutes.js @@ -1,154 +1,184 @@ -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'; -import { APP_TIMEZONE } from './config/db.js'; -import { getConnection } from './pool.js'; -const db = await getConnection(); -export default function() { - const router = express.Router(); +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' +import { APP_TIMEZONE } from './config/db.js' +import { getConnection } from './pool.js' + +export default function () { + const router = express.Router() router.use((req, _res, next) => { - req.tz = APP_TIMEZONE; - next(); - }); + req.tz = APP_TIMEZONE + next() + }) /* === TZ helpers (no deps) === */ - const _partsToObj = (parts) => parts.reduce((a, p) => (a[p.type] = p.value, a), {}); + const _partsToObj = (parts) => parts.reduce((a, p) => ((a[p.type] = p.value), a), {}) const _tzOffsetMinutes = (zone, dUtc) => { const fmt = new Intl.DateTimeFormat('en-US', { - timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit', - hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false - }); - const p = _partsToObj(fmt.formatToParts(dUtc)); - const asUTC = Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second); - return (asUTC - dUtc.getTime()) / 60000; // e.g. +480 for +08:00 - }; + timeZone: zone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }) + const p = _partsToObj(fmt.formatToParts(dUtc)) + const asUTC = Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second) + return (asUTC - dUtc.getTime()) / 60000 // e.g. +480 for +08:00 + } const parseNaiveAsTZ = (s, zone) => { - if (s instanceof Date) return s; - if (typeof s === 'number') return new Date(s); - if (typeof s === 'string' && s.includes('T')) return new Date(s); // ISO with Z/offset - const [d, t = '00:00:00'] = String(s).split(' '); - const [Y, M, D] = d.split('-').map(Number); - const [h, m, sec = 0] = t.split(':').map(Number); - const guessUtc = new Date(Date.UTC(Y, M - 1, D, h, m, sec)); - const off = _tzOffsetMinutes(zone, guessUtc); - return new Date(guessUtc.getTime() - off * 60000); - }; + if (s instanceof Date) return s + if (typeof s === 'number') return new Date(s) + if (typeof s === 'string' && s.includes('T')) return new Date(s) // ISO with Z/offset + const [d, t = '00:00:00'] = String(s).split(' ') + const [Y, M, D] = d.split('-').map(Number) + const [h, m, sec = 0] = t.split(':').map(Number) + const guessUtc = new Date(Date.UTC(Y, M - 1, D, h, m, sec)) + const off = _tzOffsetMinutes(zone, guessUtc) + return new Date(guessUtc.getTime() - off * 60000) + } const ymdInTZ = (date, zone) => - new Intl.DateTimeFormat('en-CA', { timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(date); + new Intl.DateTimeFormat('en-CA', { + timeZone: zone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + }).format(date) const hmInTZ = (date, zone) => - new Intl.DateTimeFormat('en-GB', { timeZone: zone, hour: '2-digit', minute: '2-digit', hour12: false }).format(date); - const hmsInTZ = (date, zone) => - new Intl.DateTimeFormat('en-GB', { timeZone: zone, hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(date); - const ymdHmsInTZ = (date, zone) => `${ymdInTZ(date, zone)} ${hmsInTZ(date, zone)}`; + new Intl.DateTimeFormat('en-GB', { + timeZone: zone, + hour: '2-digit', + minute: '2-digit', + hour12: false, + }).format(date) + // const hmsInTZ = (date, zone) => + // new Intl.DateTimeFormat('en-GB', { + // timeZone: zone, + // hour: '2-digit', + // minute: '2-digit', + // second: '2-digit', + // hour12: false, + // }).format(date) + //const ymdHmsInTZ = (date, zone) => `${ymdInTZ(date, zone)} ${hmsInTZ(date, zone)}` const dayNameFromYMD = (yyyyMmDd) => { - const [y, m, dd] = yyyyMmDd.split('-').map(Number); - const d = new Date(y, m - 1, dd, 12, 0, 0, 0); // noon avoids DST edges - return ['SUN','MON','TUE','WED','THU','FRI','SAT'][d.getDay()]; - }; - + const [y, m, dd] = yyyyMmDd.split('-').map(Number) + const d = new Date(y, m - 1, dd, 12, 0, 0, 0) // noon avoids DST edges + return ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'][d.getDay()] + } // Middleware to authenticate and authorize managers const authenticateJWT = (req, res, next) => { - const authHeader = req.headers.authorization; + const authHeader = req.headers.authorization if (authHeader) { - const token = authHeader.split(' ')[1]; + 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' }); + return res.status(403).json({ message: 'Forbidden' }) } - req.user = { ...user, id: user.userId }; // Correctly map userId to id - next(); - }); + req.user = { ...user, id: user.userId } // Correctly map userId to id + next() + }) } else { - res.status(401).json({ message: 'Unauthorized' }); + res.status(401).json({ message: 'Unauthorized' }) } - }; + } // Middleware to check for specific permissions const checkPermission = (requiredPermission) => { return async (req, res, next) => { + const db = await getConnection() try { - const managerId = req.user.id; - const [rows] = await db.execute( - 'SELECT * FROM manager_permissions WHERE manager_id = ?', - [managerId] - ); + 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.' }); + return res.status(403).json({ message: 'Forbidden: Insufficient permissions.' }) } - next(); + next() } catch (error) { - console.error('Permission check error:', error); - res.status(500).json({ message: 'Database error during permission check.' }); + console.error('Permission check error:', error) + res.status(500).json({ message: 'Database error during permission check.' }) + } finally { + db.release() } - }; - }; + } + } - router.use(authenticateJWT); + router.use(authenticateJWT) // --- START: Date Management Routes --- router.get('/enabled-dates', checkPermission('view_all'), async (req, res) => { + const db = await getConnection() 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'); + 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); + 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.' }); + console.error('Error fetching enabled dates:', error) + res.status(500).json({ message: 'Database error fetching enabled dates.' }) + } finally { + db.release() } - }); + }) -// Definitive version using a dedicated database connection router.post('/enabled-dates/update', checkPermission('manage_resources'), async (req, res) => { + const db = await getConnection() try { - const { datesToEnable, datesToDisable } = req.body; + const { datesToEnable, datesToDisable } = req.body if (!Array.isArray(datesToEnable) || !Array.isArray(datesToDisable)) { - return res.status(400).json({ message: 'Invalid input format.' }); + return res.status(400).json({ message: 'Invalid input format.' }) } - // 2. Process all deletions sequentially on the dedicated connection + // Process all deletions sequentially for (const date of datesToDisable) { - await db.execute('DELETE FROM enabled_dates WHERE enabled_date = ?', [date]); + await db.execute('DELETE FROM enabled_dates WHERE enabled_date = ?', [date]) } - // 3. Process all insertions sequentially on the dedicated connection + // Process all insertions sequentially for (const date of datesToEnable) { - await db.execute('INSERT IGNORE INTO enabled_dates (enabled_date) VALUES (?)', [date]); + await db.execute('INSERT IGNORE INTO enabled_dates (enabled_date) VALUES (?)', [date]) } - res.status(200).json({ message: 'Work schedule updated successfully.' }); - + 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.' }); + console.error('Error updating work schedule:', error) + res.status(500).json({ message: 'Database error during schedule update.' }) } finally { - ; + db.release() } - }); + }) // --- END: Date Management Routes --- // --- ATTENDANCE & REPORTING --- router.get('/failed-records', checkPermission('view_all'), async (req, res) => { + const db = await getConnection() try { - const { search = '', startDate, endDate } = req.query; + const { search = '', startDate, endDate } = req.query if (!startDate || !endDate) { - return res.status(400).json({ message: 'Start date and end date are required.' }); + return res.status(400).json({ message: 'Start date and end date are required.' }) } - const searchTerm = `%${search}%`; - const params = [startDate, `${endDate} 23:59:59`]; + const searchTerm = `%${search}%` + const params = [startDate, `${endDate} 23:59:59`] - let searchQuery = ''; + let searchQuery = '' if (search) { - searchQuery = `AND (w.full_name LIKE ? OR w.department LIKE ?)`; - params.push(searchTerm, searchTerm); + searchQuery = `AND (w.full_name LIKE ? OR w.department LIKE ?)` + params.push(searchTerm, searchTerm) } const query = ` @@ -160,21 +190,31 @@ export default function() { ${searchQuery} GROUP BY cr.worker_id, w.full_name ORDER BY count DESC - `; + ` - const [rows] = await db.execute(query, params); - res.json(rows); + 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 }); + console.error('Failed records summary error:', error) + res + .status(500) + .json({ + message: 'Database error fetching failed records summary.', + details: error.message, + }) + } finally { + db.release() } - }); + }) router.get('/failed-records/details', checkPermission('view_all'), async (req, res) => { + const db = await getConnection() try { - const { workerId, startDate, endDate } = req.query; + 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.' }); + return res + .status(400) + .json({ message: 'Worker ID, start date, and end date are required.' }) } const query = ` @@ -185,34 +225,44 @@ export default function() { 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); + 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 }); + console.error('Failed records details error:', error) + res + .status(500) + .json({ + message: 'Database error fetching failed records details.', + details: error.message, + }) + } finally { + db.release() } - }); + }) - // 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) => { + const db = await getConnection() try { - const { workerIds, startDate, endDate } = req.query; - const TZ = req.tz; + const { workerIds, startDate, endDate } = req.query + const TZ = req.tz if (!startDate || !endDate) { - return res.status(400).json({ message: 'Start date and end date are required.' }); + return res.status(400).json({ message: 'Start date and end date are required.' }) } - let workerIdClause = ''; - const params = [`${startDate} 00:00:00`, `${endDate} 23:59:59`]; + let workerIdClause = '' + const params = [`${startDate} 00:00:00`, `${endDate} 23:59:59`] if (workerIds) { - const idsArray = workerIds.split(',').map(Number).filter(id => !isNaN(id)); + const idsArray = workerIds + .split(',') + .map(Number) + .filter((id) => !isNaN(id)) if (idsArray.length > 0) { - workerIdClause = `AND cr.worker_id IN (${idsArray.join(',')})`; + workerIdClause = `AND cr.worker_id IN (${idsArray.join(',')})` } } @@ -224,34 +274,40 @@ export default function() { 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 shaped = rows.map(r => ({ + const [rows] = await db.execute(query, params) + const shaped = rows.map((r) => ({ username: r.username, full_name: r.full_name, event_type: r.event_type, timestamp: r.timestamp, qr_code_name: r.qr_code_name, - notes: r.notes - })); + notes: r.notes, + })) const json2csvParser = new Parser({ - fields: ['username', 'full_name', 'event_type', 'timestamp', 'qr_code_name', 'notes'] - }); - const csv = json2csvParser.parse(shaped); + fields: ['username', 'full_name', 'event_type', 'timestamp', 'qr_code_name', 'notes'], + }) + const csv = json2csvParser.parse(shaped) - res.set('X-Export-TZ', TZ); - res.header('Content-Type', 'text/csv') + res.set('X-Export-TZ', TZ) + res + .header('Content-Type', 'text/csv') .attachment(`raw_attendance_${startDate}_to_${endDate}.csv`) - .send(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 }); + console.error('Raw attendance export error:', error) + res + .status(500) + .json({ message: 'Database error exporting raw attendance.', details: error.message }) + } finally { + db.release() } - }); + }) router.post('/add-record', checkPermission('edit_workers'), async (req, res) => { + const db = await getConnection() try { const { workerId, eventType, timestamp, notes } = req.body @@ -271,40 +327,46 @@ export default function() { const status = eventType === 'clock_in' ? 'in' : 'out' return res.status(409).json({ message: `Worker is already clocked ${status}.` }) } - // --- THIS IS THE FIX --- - const dt = parseNaiveAsTZ(timestamp, req.tz); // Date at the correct instant - const utcSql = dt.toISOString().slice(0, 19).replace('T', ' '); + + const dt = parseNaiveAsTZ(timestamp, req.tz) + const utcSql = dt.toISOString().slice(0, 19).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, utcSql, 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.' }) + } finally { + db.release() } }) router.get('/attendance-records/export', checkPermission('view_all'), async (req, res) => { + const db = await getConnection() try { - const { workerIds, startDate, endDate } = req.query; - const TZ = req.tz; + const { workerIds, startDate, endDate } = req.query + const TZ = req.tz if (!startDate || !endDate) { - return res.status(400).json({ message: 'Start date and end date are required.' }); + return res.status(400).json({ message: 'Start date and end date are required.' }) } - const wantXlsx = String(req.query.format || 'csv').toLowerCase() === 'xlsx'; + const wantXlsx = String(req.query.format || 'csv').toLowerCase() === 'xlsx' - let workerIdClause = ''; - const params = [`${startDate} 00:00:00`, `${endDate} 23:59:59`]; + let workerIdClause = '' + const params = [`${startDate} 00:00:00`, `${endDate} 23:59:59`] if (workerIds) { - const idsArray = workerIds.split(',').map(Number).filter(id => !isNaN(id)); + const idsArray = workerIds + .split(',') + .map(Number) + .filter((id) => !isNaN(id)) if (idsArray.length > 0) { - workerIdClause = `AND cr.worker_id IN (${idsArray.join(',')})`; + workerIdClause = `AND cr.worker_id IN (${idsArray.join(',')})` } } @@ -323,55 +385,55 @@ export default function() { WHERE cr.timestamp BETWEEN ? AND ? ${workerIdClause} AND cr.event_type IN ('clock_in','clock_out') ORDER BY cr.worker_id, cr.timestamp ASC - `; + ` - const [rows] = await db.execute(query, params); + const [rows] = await db.execute(query, params) -// ---- Group events by worker/day ---- - const workByDay = {}; - rows.forEach(row => { - const ts = parseNaiveAsTZ(row.timestamp, TZ); - const day = ymdInTZ(ts, TZ); + // ---- Group events by worker/day ---- + const workByDay = {} + rows.forEach((row) => { + const ts = parseNaiveAsTZ(row.timestamp, TZ) + const day = ymdInTZ(ts, TZ) if (!workByDay[row.worker_id]) { workByDay[row.worker_id] = { username: row.username, full_name: row.full_name, department: row.department || '', - days: {} - }; + days: {}, + } } if (!workByDay[row.worker_id].days[day]) { - workByDay[row.worker_id].days[day] = []; + workByDay[row.worker_id].days[day] = [] } workByDay[row.worker_id].days[day].push({ type: row.event_type, time: ts, - qr_code_name: row.qr_code_name - }); - }); + qr_code_name: row.qr_code_name, + }) + }) // ---- Build rows: one per successful [clock_in, clock_out] session ---- - const csvData = []; - const byWorkerForXlsx = new Map(); // key = "username||full_name||department" → daily 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 = []; + const w = workByDay[workerId] + const perWorkerRows = [] for (const day of Object.keys(w.days).sort()) { // events for this day in ascending time - const events = w.days[day].slice().sort((a, b) => a.time - b.time); + const events = w.days[day].slice().sort((a, b) => a.time - b.time) - let open = null; - let openQr = 'Manual Entry'; + let open = null + let openQr = 'Manual Entry' for (const e of events) { if (e.type === 'clock_in' && open == null) { - open = e.time; - openQr = e.qr_code_name || 'Manual Entry'; + open = e.time + openQr = e.qr_code_name || 'Manual Entry' } else if (e.type === 'clock_out' && open != null) { - const start = open; - const end = e.time; + const start = open + const end = e.time const dailyRow = { username: w.username, @@ -381,47 +443,49 @@ export default function() { clock_in: hmInTZ(start, TZ), clock_out: hmInTZ(end, TZ), work_hours: ((end - start) / 3600000).toFixed(2), - qr_code_name: openQr - }; + qr_code_name: openQr, + } - csvData.push(dailyRow); - perWorkerRows.push(dailyRow); + csvData.push(dailyRow) + perWorkerRows.push(dailyRow) // close the session - open = null; - openQr = 'Manual Entry'; + open = null + openQr = 'Manual Entry' } } } - byWorkerForXlsx.set(`${w.username}||${w.full_name}||${w.department}`, perWorkerRows); + 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'); + 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 }, - ]; + { 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('||'); + const [username, full_name, dept] = key.split('||') - if (ws.lastRow) ws.addRow([]); + 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' }; + 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({ @@ -431,59 +495,83 @@ export default function() { clock_out: 'Clock Out', work_hours: 'Work Hours', qr_code_name: 'QR Code', - }); - hdr.font = { bold: true }; + }) + hdr.font = { bold: true } // Detail rows (one per day) for (const r of rowsForWorker) { - ws.addRow(r); + ws.addRow(r) } } - ws.eachRow(row => { row.alignment = { vertical: 'middle' }; }); + ws.eachRow((row) => { + row.alignment = { vertical: 'middle' } + }) - const buf = await wb.xlsx.writeBuffer(); - res.set('X-Export-TZ', TZ); - 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)); + const buf = await wb.xlsx.writeBuffer() + res.set('X-Export-TZ', TZ) + 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.set('X-Export-TZ', TZ); + fields: [ + 'username', + 'full_name', + 'date', + 'day', + 'clock_in', + 'clock_out', + 'work_hours', + 'qr_code_name', + ], + }) + const csv = json2csvParser.parse(csvData) + res.set('X-Export-TZ', TZ) res .header('Content-Type', 'text/csv') .attachment(`work_hours_${startDate}_to_${endDate}.csv`) - .send(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 }); + console.error('Work hours export error:', error) + res + .status(500) + .json({ message: 'Database error exporting work hours.', details: error.message }) + } finally { + db.release() } - }); + }) router.get('/attendance-records', checkPermission('view_all'), async (req, res) => { + const db = await getConnection() try { - const { workerIds, startDate, endDate, format } = req.query; + const { workerIds, startDate, endDate, format } = req.query if (!workerIds) { - return res.status(400).json({ message: 'Worker IDs are required.' }); + 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)); + const idsArray = workerIds + .split(',') + .map(Number) + .filter((id) => !isNaN(id)) if (idsArray.length === 0) { - return res.json([]); + 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(','); + const inClause = idsArray.join(',') let query = ` SELECT cr.id, w.full_name, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName, @@ -491,97 +579,105 @@ export default function() { 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 + WHERE cr.worker_id IN (${inClause})` // Placeholder is replaced here - const params = []; + const params = [] // --- MODIFICATION END --- if (startDate && endDate) { - query += ' AND cr.timestamp BETWEEN ? AND ?'; - params.push(`${startDate} 00:00:00`, `${endDate} 23:59:59`); + query += ' AND cr.timestamp BETWEEN ? AND ?' + params.push(`${startDate} 00:00:00`, `${endDate} 23:59:59`) } - query += ' ORDER BY w.full_name, cr.timestamp DESC'; + query += ' ORDER BY w.full_name, cr.timestamp DESC' - const [rows] = await db.execute(query, params); + 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); + 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); + res.json(rows) } } catch (error) { - console.error('Attendance records error:', error); - res.status(500).json({ message: 'Database error fetching attendance records.', details: error.message }); + console.error('Attendance records error:', error) + res + .status(500) + .json({ message: 'Database error fetching attendance records.', details: error.message }) + } finally { + db.release() } - }); - + }) // --- All other manager routes remain the same --- // GET a specific manager's permissions router.get('/permissions/:id', async (req, res) => { + const db = await getConnection() try { - const requesterId = req.user.id; - const targetId = parseInt(req.params.id, 10); + 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] - ); + [requesterId], + ) if (permissionRows.length === 0 || !permissionRows[0].can_manage_permissions) { - return res.status(403).json({ message: 'Forbidden: Insufficient permissions to view others\' 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] - ); + 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 [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 + acc[field.Field] = 0 // Use 0 for false } - return acc; - }, {}); - return res.json(defaultPermissions); + 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); + acc[key] = Boolean(value) } - return acc; - }, {}); + return acc + }, {}) - res.json(permissions); + 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 }); + 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) => { + const db = await getConnection() try { - const { id } = req.params; - const permissions = req.body; + 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); + 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 = ` @@ -592,67 +688,75 @@ export default function() { edit_workers = VALUES(edit_workers), manage_resources = VALUES(manage_resources), manager_permissions = VALUES(manager_permissions) - `; + ` - const queryParams = [id, ...values]; + const queryParams = [id, ...values] - await db.execute(query, queryParams); + await db.execute(query, queryParams) - res.status(200).json({ message: 'Permissions updated successfully.' }); + 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 }); + console.error('Update manager permissions error:', error) + res + .status(500) + .json({ message: 'Database error updating manager permissions.', details: error.message }) + } finally { + db.release() } - }); + }) // GET all workers with filtering and pagination router.get('/workers', checkPermission('view_all'), async (req, res) => { + const db = await getConnection() try { - const { search = '', page = 1, limit = 20 } = req.query; - const offset = (parseInt(page) - 1) * parseInt(limit); - const searchTerm = `%${search}%`; + 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`; + ` + 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 + 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); + 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; + const whereString = ` WHERE ${whereClauses.join(' AND ')}` + baseQuery += whereString + countQuery += whereString } - baseQuery += ` ORDER BY w.created_at DESC LIMIT ? OFFSET ?`; - params.push(parseInt(limit), offset); + 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); + const [workers] = await db.execute(baseQuery, params) + const [[{ totalCount }]] = await db.execute(countQuery, countParams) - res.json({ workers, totalCount }); + res.json({ workers, totalCount }) } catch (error) { - console.error('Get workers error:', error); - res.status(500).json({ message: 'Database error fetching workers.', details: error.message }); + console.error('Get workers error:', error) + res.status(500).json({ message: 'Database error fetching workers.', details: error.message }) + } finally { + db.release() } - }); + }) // GET all managers with their permissions router.get('/managers', checkPermission('manager_permissions'), async (req, res) => { + const db = await getConnection() try { - const { search = '', page = 1, limit = 20 } = req.query; - const offset = (parseInt(page) - 1) * parseInt(limit); - const searchTerm = `%${search}%`; + const { search = '', page = 1, limit = 20 } = req.query + const offset = (parseInt(page) - 1) * parseInt(limit) + const searchTerm = `%${search}%` let baseQuery = ` SELECT @@ -660,56 +764,59 @@ export default function() { 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`; + ` + let countQuery = `SELECT COUNT(w.id) as totalCount FROM workers w` - const params = []; - const countParams = []; - let whereClauses = ["w.role = 'manager'", "w.status != 'deleted'"]; + 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); + 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; + const whereString = ` WHERE ${whereClauses.join(' AND ')}` + baseQuery += whereString + countQuery += whereString } - baseQuery += ` ORDER BY w.created_at DESC LIMIT ? OFFSET ?`; - params.push(parseInt(limit), offset); + 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); + const [managers] = await db.execute(baseQuery, params) + const [[{ totalCount }]] = await db.execute(countQuery, countParams) - res.json({ managers, totalCount }); + res.json({ managers, totalCount }) } catch (error) { - console.error('Get managers error:', error); - res.status(500).json({ message: 'Database error fetching managers.', details: error.message }); + console.error('Get managers error:', error) + res.status(500).json({ message: 'Database error fetching managers.', details: error.message }) + } finally { + db.release() } - }); + }) // POST (add) a new manager router.post('/managers', checkPermission('manager_permissions'), async (req, res) => { + const db = await getConnection() try { - const { username, password, fullName, department, position } = req.body; + 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.' }); + return res.status(400).json({ message: 'Username, password, and full name are required.' }) } - const hashedPassword = await bcrypt.hash(password, 10); + 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'] - ); + [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] - ); + await db.execute('INSERT INTO manager_permissions (manager_id, view_all) VALUES (?, ?)', [ + result.insertId, + true, + ]) res.status(201).json({ id: result.insertId, @@ -719,36 +826,39 @@ export default function() { department, position, status: 'active', - view_all: true - }); + view_all: true, + }) } catch (error) { - console.error('Add manager error:', error); + console.error('Add manager error:', error) if (error.code === 'ER_DUP_ENTRY') { - return res.status(409).json({ message: 'Username already exists.' }); + return res.status(409).json({ message: 'Username already exists.' }) } - res.status(500).json({ message: 'Database error adding manager.', details: error.message }); + res.status(500).json({ message: 'Database error adding manager.', details: error.message }) + } finally { + db.release() } - }); + }) // POST (add) a new worker (with soft-deleted reactivation) router.post('/workers', checkPermission('edit_workers'), async (req, res) => { + const db = await getConnection() try { - const { username, password, fullName, department, position, role = 'worker' } = req.body; + 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.' }); + return res.status(400).json({ message: 'Username, password, and full name are required.' }) } // Check for existing worker with this username const [existingRows] = await db.execute( 'SELECT id, status, role FROM workers WHERE username = ?', - [username] - ); + [username], + ) - const hashedPassword = await bcrypt.hash(password, 10); + const hashedPassword = await bcrypt.hash(password, 10) if (existingRows.length > 0) { - const existing = existingRows[0]; + const existing = existingRows[0] if (existing.status === 'deleted' && existing.role === 'worker') { await db.execute( ` @@ -756,8 +866,8 @@ export default function() { SET password_hash = ?, full_name = ?, role = ?, department = ?, position = ?, status = 'active' WHERE id = ? `, - [hashedPassword, fullName, role, department, position, existing.id] - ); + [hashedPassword, fullName, role, department, position, existing.id], + ) return res.status(200).json({ id: existing.id, @@ -767,18 +877,18 @@ export default function() { department, position, status: 'active', - restored: true - }); + restored: true, + }) } - return res.status(409).json({ message: 'Username already exists.' }); + return res.status(409).json({ message: 'Username already exists.' }) } const [result] = await db.execute( ` INSERT INTO workers (username, password_hash, full_name, role, department, position, status) VALUES (?, ?, ?, ?, ?, ?, 'active') `, - [username, hashedPassword, fullName, role, department, position] - ); + [username, hashedPassword, fullName, role, department, position], + ) return res.status(201).json({ id: result.insertId, @@ -787,384 +897,462 @@ export default function() { role, department, position, - status: 'active' - }); + status: 'active', + }) } catch (error) { - console.error('Add worker error:', error); + console.error('Add worker error:', error) if (error.code === 'ER_DUP_ENTRY') { - return res.status(409).json({ message: 'Username already exists.' }); + return res.status(409).json({ message: 'Username already exists.' }) } return res.status(500).json({ message: 'Database error adding worker.', - details: error.message - }); + details: error.message, + }) + } finally { + db.release() } - }); + }) // Soft DELETE a worker (update status to 'deleted') router.delete('/workers/:id', checkPermission('edit_workers'), async (req, res) => { + const db = await getConnection() try { - const { id } = req.params; - const [result] = await db.execute("UPDATE workers SET status = 'deleted' WHERE id = ? AND role = 'worker'", [id]); + 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.' }); + return res.status(404).json({ message: 'Worker not found or already deleted.' }) } - res.status(204).send(); // Maintain existing response for client compatibility + 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 }); + console.error('Soft delete worker error:', error) + res + .status(500) + .json({ message: 'Database error soft deleting worker.', details: error.message }) + } finally { + db.release() } - }); + }) // Soft DELETE a manager (update status to 'deleted') router.delete('/managers/:id', checkPermission('manager_permissions'), async (req, res) => { + const db = await getConnection() try { - const { id } = req.params; - const [result] = await db.execute("UPDATE workers SET status = 'deleted' WHERE id = ? AND role = 'manager'", [id]); + 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.' }); + return res.status(404).json({ message: 'Manager not found or already deleted.' }) } - res.status(204).send(); + 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 }); + console.error('Soft delete manager error:', error) + res + .status(500) + .json({ message: 'Database error soft deleting manager.', details: error.message }) + } finally { + db.release() } - }); + }) // PUT (update) a worker's details (department, position, status) router.put('/workers/:id', checkPermission('edit_workers'), async (req, res) => { + const db = await getConnection() try { - const { id } = req.params; - const { department, position, status, fullName } = req.body; + const { id } = req.params + const { department, position, status, fullName } = req.body // Basic validation if (!department && !position && !status && !fullName) { - return res.status(400).json({ message: 'No update information provided.' }); + 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.' }); + return res.status(400).json({ message: 'Invalid status value.' }) } - let updateQuery = 'UPDATE workers SET'; - const params = []; - const fieldsToUpdate = []; + let updateQuery = 'UPDATE workers SET' + const params = [] + const fieldsToUpdate = [] if (department) { - fieldsToUpdate.push('department = ?'); - params.push(department); + fieldsToUpdate.push('department = ?') + params.push(department) } if (position) { - fieldsToUpdate.push('position = ?'); - params.push(position); + fieldsToUpdate.push('position = ?') + params.push(position) } if (status) { - fieldsToUpdate.push('status = ?'); - params.push(status); + fieldsToUpdate.push('status = ?') + params.push(status) } if (fullName) { - fieldsToUpdate.push('full_name = ?'); - params.push(fullName); + fieldsToUpdate.push('full_name = ?') + params.push(fullName) } - updateQuery += ` ${fieldsToUpdate.join(', ')} WHERE id = ? AND role = 'worker'`; - params.push(id); + updateQuery += ` ${fieldsToUpdate.join(', ')} WHERE id = ? AND role = 'worker'` + params.push(id) - const [result] = await db.execute(updateQuery, params); + const [result] = await db.execute(updateQuery, params) if (result.affectedRows === 0) { - return res.status(404).json({ message: 'Worker not found.' }); + return res.status(404).json({ message: 'Worker not found.' }) } - res.status(200).json({ message: 'Worker details updated successfully.' }); + res.status(200).json({ message: 'Worker details updated successfully.' }) } catch (error) { - console.error('Update worker details error:', error); + console.error('Update worker details error:', error) res.status(500).json({ message: 'Database error updating worker details.', - details: error.message - }); + details: error.message, + }) + } finally { + db.release() } - }); + }) // PUT (update) a manager's details (department, position, status) router.put('/managers/:id', checkPermission('manager_permissions'), async (req, res) => { + const db = await getConnection() try { - const { id } = req.params; - const { department, position, status } = req.body; + 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.' }); + 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.' }); + return res.status(400).json({ message: 'Invalid status value.' }) } - let updateQuery = 'UPDATE workers SET'; - const params = []; - const fieldsToUpdate = []; + let updateQuery = 'UPDATE workers SET' + const params = [] + const fieldsToUpdate = [] if (department) { - fieldsToUpdate.push('department = ?'); - params.push(department); + fieldsToUpdate.push('department = ?') + params.push(department) } if (position) { - fieldsToUpdate.push('position = ?'); - params.push(position); + fieldsToUpdate.push('position = ?') + params.push(position) } if (status) { - fieldsToUpdate.push('status = ?'); - params.push(status); + fieldsToUpdate.push('status = ?') + params.push(status) } - updateQuery += ` ${fieldsToUpdate.join(', ')} WHERE id = ? AND role = 'manager'`; - params.push(id); + updateQuery += ` ${fieldsToUpdate.join(', ')} WHERE id = ? AND role = 'manager'` + params.push(id) - const [result] = await db.execute(updateQuery, params); + const [result] = await db.execute(updateQuery, params) if (result.affectedRows === 0) { - return res.status(404).json({ message: 'Manager not found.' }); + return res.status(404).json({ message: 'Manager not found.' }) } - res.status(200).json({ message: 'Manager details updated successfully.' }); + 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 }); + console.error('Update manager details error:', error) + res + .status(500) + .json({ message: 'Database error updating manager details.', details: error.message }) + } finally { + db.release() } - }); + }) // PUT (update) a worker's password router.put('/workers/:workerId/password', checkPermission('edit_workers'), async (req, res) => { + const db = await getConnection() try { - const { workerId } = req.params; - const { newPassword } = req.body; + 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.' }); + 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]); + 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.' }); + return res.status(404).json({ message: 'Worker not found.' }) } - res.status(200).json({ message: 'Password updated successfully.' }); + 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 }); + console.error('Update password error:', error) + res.status(500).json({ message: 'Database error updating password.', details: error.message }) + } finally { + db.release() } - }); + }) // 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.' }); + router.put( + '/managers/:managerId/password', + checkPermission('manager_permissions'), + async (req, res) => { + const db = await getConnection() + 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 }) + } finally { + db.release() } - 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 + router.put( + '/workers/:workerId/reset-device', + checkPermission('edit_workers'), + async (req, res) => { + const db = await getConnection() + try { + const { workerId } = req.params + const { status } = req.body // Optional status field - let updateQuery = "UPDATE workers SET device_uuid = NULL"; - const params = [workerId]; + 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 + 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, + }) + } finally { + db.release() } - - 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) => { + const db = await getConnection() 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 => ({ + '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); + 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 }); + console.error('Get geofences error:', error) + res + .status(500) + .json({ message: 'Database error fetching geofences.', details: error.message }) + } finally { + db.release() } - }); + }) router.post('/geofences', checkPermission('manage_resources'), async (req, res) => { + const db = await getConnection() try { - const { name, coordinates } = req.body; + const { name, coordinates } = req.body if (!name || !coordinates) { - return res.status(400).json({ message: 'Geofence name and coordinates are required.' }); + 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] - ); + [name, JSON.stringify(coordinates), true], + ) const newGeofence = { id: result.insertId, name, coordinates, is_active: true, - }; - res.status(201).json(newGeofence); + } + 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 }); + console.error('Add geofence error:', error) + res.status(500).json({ message: 'Database error adding geofence.', details: error.message }) + } finally { + db.release() } - }); + }) router.put('/geofences/:id', checkPermission('manage_resources'), async (req, res) => { + const db = await getConnection() try { - const { id } = req.params; - const { is_active } = req.body; + 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.' }); + 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] - ); + 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.' }); + return res.status(404).json({ message: 'Geofence not found.' }) } - res.json({ id, is_active }); + 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 }); + console.error('Update geofence error:', error) + res.status(500).json({ message: 'Database error updating geofence.', details: error.message }) + } finally { + db.release() } - }); + }) router.delete('/geofences/:id', checkPermission('manage_resources'), async (req, res) => { + const db = await getConnection() try { - const { id } = req.params; - const [result] = await db.execute('DELETE FROM geofences WHERE id = ?', [id]); + 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.' }); + return res.status(404).json({ message: 'Geofence not found.' }) } - res.status(204).send(); + res.status(204).send() } catch (error) { - console.error('Delete geofence error:', error); - res.status(500).json({ message: 'Database error deleting geofence.', details: error.message }); + console.error('Delete geofence error:', error) + res.status(500).json({ message: 'Database error deleting geofence.', details: error.message }) + } finally { + db.release() } - }); - + }) // QR Code Management Routes router.get('/qr-codes', checkPermission('view_all'), async (req, res) => { + const db = await getConnection() try { const [rows] = await db.execute( - 'SELECT id, name, is_active, created_at FROM qr_codes ORDER BY created_at DESC' - ); - res.json(rows); + '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.' }); + console.error('Get QR codes error:', error) + res.status(500).json({ message: 'Database error fetching QR codes.' }) + } finally { + db.release() } - }); + }) router.post('/qr-codes', checkPermission('manage_resources'), async (req, res) => { + const db = await getConnection() try { - const { name } = req.body; - if (!name) return res.status(400).json({ message: 'QR Code name is required.' }); + 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 - }; + is_active: true, + } - await db.execute( - 'INSERT INTO qr_codes (id, name, is_active) VALUES (?, ?, ?)', - [newQrCode.id, newQrCode.name, newQrCode.is_active] - ); + await db.execute('INSERT INTO qr_codes (id, name, is_active) VALUES (?, ?, ?)', [ + newQrCode.id, + newQrCode.name, + newQrCode.is_active, + ]) - res.status(201).json(newQrCode); + res.status(201).json(newQrCode) } catch (error) { - console.error('Add QR code error:', error); - res.status(500).json({ message: 'Database error adding QR code.' }); + console.error('Add QR code error:', error) + res.status(500).json({ message: 'Database error adding QR code.' }) + } finally { + db.release() } - }); + }) router.put('/qr-codes/:id', checkPermission('manage_resources'), async (req, res) => { + const db = await getConnection() try { - const { id } = req.params; + const { id } = req.params // Handle both isActive (camelCase) and is_active (snake_case) - const is_active = req.body.is_active ?? req.body.isActive; + 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.' }); + 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] - ); + 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.' }); + return res.status(404).json({ message: 'QR Code not found.' }) } - res.json({ id, is_active }); + res.json({ id, is_active }) } catch (error) { - console.error('Update QR code error:', error); - res.status(500).json({ message: 'Database error updating QR code.' }); + console.error('Update QR code error:', error) + res.status(500).json({ message: 'Database error updating QR code.' }) + } finally { + db.release() } - }); + }) router.delete('/qr-codes/:id', checkPermission('manage_resources'), async (req, res) => { + const db = await getConnection() try { - const { id } = req.params; - const [result] = await db.execute( - 'DELETE FROM qr_codes WHERE id = ?', - [id] - ); + 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.' }); + return res.status(404).json({ message: 'QR Code not found.' }) } - res.status(204).send(); + res.status(204).send() } catch (error) { - console.error('Delete QR code error:', error); - res.status(500).json({ message: 'Database error deleting QR code.' }); + console.error('Delete QR code error:', error) + res.status(500).json({ message: 'Database error deleting QR code.' }) + } finally { + db.release() } - }); + }) - return router; + return router } diff --git a/backend/pool.js b/backend/pool.js index ab064b6..d7a1ed3 100644 --- a/backend/pool.js +++ b/backend/pool.js @@ -16,7 +16,7 @@ const db = mysql.createPool({ connectionLimit: 10, queueLimit: 0, // timezone: '+08:00', - dateStrings: true + dateStrings: true, }); diff --git a/backend/workerRoutes.js b/backend/workerRoutes.js index 60ea2b5..68905aa 100644 --- a/backend/workerRoutes.js +++ b/backend/workerRoutes.js @@ -3,7 +3,6 @@ import { point, polygon, booleanPointInPolygon, pointToLineDistance } from '@tur import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { getConnection } from './pool.js'; -const db = await getConnection(); async function validateDeviceForUser(userId, deviceUuid, db) { const [userRows] = await db.execute('SELECT device_uuid FROM workers WHERE id = ?', [userId]); @@ -32,51 +31,58 @@ export default function() { const AUTO_REGISTER_NEW_DEVICES = true; router.post('/auth/login', async (req, res) => { - const { username, password, deviceUuid } = req.body; - const [rows] = await db.execute('SELECT id, role, password_hash, status FROM workers WHERE username = ?', [username]); - if (rows.length === 0) { - return res.status(401).json({ message: 'Invalid credentials' }); - } - const user = rows[0]; + const db = await getConnection(); + try { + const { username, password, deviceUuid } = req.body; + const [rows] = await db.execute('SELECT id, role, password_hash, status FROM workers WHERE username = ?', [username]); + if (rows.length === 0) { + return res.status(401).json({ message: 'Invalid credentials' }); + } + const user = rows[0]; - // Check if the user's status is 'active' - if (user.status !== 'active') { - return res.status(401).json({ message: 'Invalid credentials' }); - } + // Check if the user's status is 'active' + if (user.status !== 'active') { + return res.status(401).json({ message: 'Invalid credentials' }); + } - const passwordMatch = await bcrypt.compare(password, user.password_hash); - if (!passwordMatch) { - return res.status(401).json({ message: 'Invalid credentials' }); - } + const passwordMatch = await bcrypt.compare(password, user.password_hash); + if (!passwordMatch) { + return res.status(401).json({ message: 'Invalid credentials' }); + } - // Device UUID handling - controlled by configuration flags above - if (DEVICE_UUID_ENABLED && user.role === 'worker') { - const [deviceRows] = await db.execute('SELECT device_uuid FROM workers WHERE id = ?', [user.id]); - const existingDeviceUuid = deviceRows[0].device_uuid; + // Device UUID handling - controlled by configuration flags above + if (DEVICE_UUID_ENABLED && user.role === 'worker') { + const [deviceRows] = await db.execute('SELECT device_uuid FROM workers WHERE id = ?', [user.id]); + const existingDeviceUuid = deviceRows[0].device_uuid; - if (existingDeviceUuid) { - if (deviceUuid && deviceUuid !== existingDeviceUuid) { - return res.status(403).json({ message: 'deviceMismatch' }); - } else if (!deviceUuid) { - return res.status(403).json({ message: 'useMobileApp' }); - } - } else { - // User has no registered device - if (deviceUuid && AUTO_REGISTER_NEW_DEVICES) { - const deviceResult = await validateDeviceForUser(user.id, deviceUuid, db); - if (!deviceResult.valid) { - return res.status(500).json({ message: 'deviceRegistrationFailed' }); + if (existingDeviceUuid) { + if (deviceUuid && deviceUuid !== existingDeviceUuid) { + return res.status(403).json({ message: 'deviceMismatch' }); + } else if (!deviceUuid) { + return res.status(403).json({ message: 'useMobileApp' }); + } + } else { + // User has no registered device + if (deviceUuid && AUTO_REGISTER_NEW_DEVICES) { + const deviceResult = await validateDeviceForUser(user.id, deviceUuid, db); + if (!deviceResult.valid) { + return res.status(500).json({ message: 'deviceRegistrationFailed' }); + } + } else if (!deviceUuid && REQUIRE_DEVICE_FOR_WORKERS) { + return res.status(403).json({ message: 'deviceRequired' }); } - // console.log(`Device UUID registered for worker ${user.id}: ${deviceUuid}`); - } else if (!deviceUuid && REQUIRE_DEVICE_FOR_WORKERS) { - return res.status(403).json({ message: 'deviceRequired' }); } } - } - // Managers can always login, workers without device_uuid can login - const token = jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h' }); - res.json({ token }); + // Managers can always login, workers without device_uuid can login + const token = jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h' }); + res.json({ token }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ message: 'Server error during login' }); + } finally { + db.release(); + } }); const authenticateJWT = (req, res, next) => { @@ -87,7 +93,7 @@ export default function() { if (err) { return res.status(403).json({ message: 'Invalid or expired token' }); } - req.user = { ...user, id: user.userId }; // Correctly map userId to id + req.user = { ...user, id: user.userId }; next(); }); } else { @@ -98,15 +104,15 @@ export default function() { router.use(authenticateJWT); router.post('/clock', async (req, res) => { - // NEW: borrow a connection so we can set session time_zone + const db = await getConnection(); try { const { userId, eventType, qrCodeValue, latitude, longitude } = req.body; // 1) Kill Switch — now evaluated in the session's local day - const clockingAllowed = await isClockingEnabled(db); // CHANGED: pass conn + const clockingAllowed = await isClockingEnabled(db); if (!clockingAllowed) { const note = 'Clock-in/out function is not enabled for today.'; - await db.execute( // CHANGED: use conn + await db.execute( `INSERT INTO clock_records (worker_id, event_type, qr_code_id, latitude, longitude, notes, timestamp) VALUES (?, "failed", ?, ?, ?, ?, CURRENT_TIME())`, @@ -115,13 +121,13 @@ export default function() { return res.status(403).json({ message: 'error.clockingDisabled' }); } - // 2) Geofence Validation (unchanged logic, just switch db -> conn) + // 2) Geofence Validation if (latitude != null && longitude != null) { - const [activeFences] = await db.execute('SELECT coordinates FROM geofences WHERE is_active = 1'); // CHANGED + const [activeFences] = await db.execute('SELECT coordinates FROM geofences WHERE is_active = 1'); if (activeFences.length === 0) { const note = 'Cannot clock in: No active work area is defined.'; - await db.execute( // CHANGED + await db.execute( `INSERT INTO clock_records (worker_id, event_type, qr_code_id, latitude, longitude, notes, timestamp) VALUES (?, "failed", ?, ?, ?, ?, CURRENT_TIME())`, @@ -157,7 +163,7 @@ export default function() { } const distanceString = minDistance.toFixed(2); const note = `Outside geofence by ${distanceString}m`; - await db.execute( // CHANGED + await db.execute( `INSERT INTO clock_records (worker_id, event_type, qr_code_id, latitude, longitude, notes, timestamp) VALUES (?, "failed", ?, ?, ?, ?, CURRENT_TIME())`, @@ -167,15 +173,15 @@ export default function() { } } - // 3) QR Code and Status Validation (switch db -> conn; logic unchanged) + // 3) QR Code and Status Validation if (qrCodeValue !== 'FORCE_CLOCK_OUT') { - const [qrRows] = await db.execute('SELECT is_active FROM qr_codes WHERE id = ?', [qrCodeValue]); // CHANGED + const [qrRows] = await db.execute('SELECT is_active FROM qr_codes WHERE id = ?', [qrCodeValue]); if (qrRows.length === 0 || !qrRows[0].is_active) { return res.status(400).json({ message: 'error.invalidQrCode' }); } } - const [lastEvent] = await db.execute( // CHANGED + const [lastEvent] = await db.execute( 'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1', [userId] ); @@ -184,7 +190,7 @@ export default function() { return res.status(400).json({ message: errorKey }); } - // 4) Record Successful Event — store UTC via SQL conversion (no JS date math) + // 4) Record Successful Event await db.execute( `INSERT INTO clock_records (worker_id, event_type, qr_code_id, latitude, longitude, timestamp) @@ -197,50 +203,82 @@ export default function() { console.error('!!! CRITICAL ERROR in /clock route !!!:', error); res.status(500).json({ message: 'error.criticalServer' }); } finally { - ; + db.release(); } }); router.get('/workers/:id', async (req, res) => { - const { id } = req.params; - const [rows] = await db.execute("SELECT full_name FROM workers WHERE id = ? AND role = 'worker'", [id]); - if (rows.length === 0) { - return res.status(404).json({ message: 'Worker not found.' }); + const db = await getConnection(); + try { + const { id } = req.params; + const [rows] = await db.execute("SELECT full_name FROM workers WHERE id = ? AND role = 'worker'", [id]); + if (rows.length === 0) { + return res.status(404).json({ message: 'Worker not found.' }); + } + res.json(rows[0]); + } catch (error) { + console.error('Get worker error:', error); + res.status(500).json({ message: 'Server error fetching worker' }); + } finally { + db.release(); } - res.json(rows[0]); }); router.get('/worker/status/:userId', async (req, res) => { - const { userId } = req.params; - const [rows] = await db.execute('SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1', [userId]); - res.json({ eventType: rows.length > 0 ? rows[0].event_type : 'clock_out' }); + const db = await getConnection(); + try { + const { userId } = req.params; + const [rows] = await db.execute('SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1', [userId]); + res.json({ eventType: rows.length > 0 ? rows[0].event_type : 'clock_out' }); + } catch (error) { + console.error('Get worker status error:', error); + res.status(500).json({ message: 'Server error fetching worker status' }); + } finally { + db.release(); + } }); router.get('/worker/clock-history/:userId', async (req, res) => { - const { userId } = req.params; - const [rows] = await db.execute(` - SELECT cr.id, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName - FROM clock_records cr - LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id - WHERE cr.worker_id = ? ORDER BY cr.timestamp DESC - `, [userId]); - res.json(rows); + const db = await getConnection(); + try { + const { userId } = req.params; + const [rows] = await db.execute(` + SELECT cr.id, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName + FROM clock_records cr + LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id + WHERE cr.worker_id = ? ORDER BY cr.timestamp DESC + `, [userId]); + res.json(rows); + } catch (error) { + console.error('Get clock history error:', error); + res.status(500).json({ message: 'Server error fetching clock history' }); + } finally { + db.release(); + } }); router.put('/worker/change-password', async (req, res) => { - const { userId } = req.user; - const { currentPassword, newPassword } = req.body; - if (!currentPassword || !newPassword || newPassword.length < 6) { - return res.status(400).json({ message: 'Invalid input.' }); + const db = await getConnection(); + try { + const { userId } = req.user; + const { currentPassword, newPassword } = req.body; + if (!currentPassword || !newPassword || newPassword.length < 6) { + return res.status(400).json({ message: 'Invalid input.' }); + } + const [rows] = await db.execute('SELECT password_hash FROM workers WHERE id = ?', [userId]); + const passwordMatch = await bcrypt.compare(currentPassword, rows[0].password_hash); + if (!passwordMatch) { + return res.status(401).json({ message: 'Incorrect current password.' }); + } + const newHashedPassword = await bcrypt.hash(newPassword, 10); + await db.execute('UPDATE workers SET password_hash = ? WHERE id = ?', [newHashedPassword, userId]); + res.json({ message: 'Password updated successfully.' }); + } catch (error) { + console.error('Change password error:', error); + res.status(500).json({ message: 'Server error changing password' }); + } finally { + db.release(); } - const [rows] = await db.execute('SELECT password_hash FROM workers WHERE id = ?', [userId]); - const passwordMatch = await bcrypt.compare(currentPassword, rows[0].password_hash); - if (!passwordMatch) { - return res.status(401).json({ message: 'Incorrect current password.' }); - } - const newHashedPassword = await bcrypt.hash(newPassword, 10); - await db.execute('UPDATE workers SET password_hash = ? WHERE id = ?', [newHashedPassword, userId]); - res.json({ message: 'Password updated successfully.' }); }); router.post('/location/update', async (req, res) => { @@ -249,30 +287,62 @@ export default function() { }); router.post('/device/register', async (req, res) => { - const { userId, deviceUuid } = req.body; - const result = await validateDeviceForUser(userId, deviceUuid, db); - res.status(result.valid ? 200 : 409).json(result); + const db = await getConnection(); + try { + const { userId, deviceUuid } = req.body; + const result = await validateDeviceForUser(userId, deviceUuid, db); + res.status(result.valid ? 200 : 409).json(result); + } catch (error) { + console.error('Device register error:', error); + res.status(500).json({ message: 'Server error registering device' }); + } finally { + db.release(); + } }); router.post('/device/validate', async (req, res) => { - const { userId, deviceUuid } = req.body; - const result = await validateDeviceForUser(userId, deviceUuid, db); - res.json(result); + const db = await getConnection(); + try { + const { userId, deviceUuid } = req.body; + const result = await validateDeviceForUser(userId, deviceUuid, db); + res.json(result); + } catch (error) { + console.error('Device validate error:', error); + res.status(500).json({ message: 'Server error validating device' }); + } finally { + db.release(); + } }); router.get('/security/status/:userId', async (req, res) => { - const { userId } = req.params; - const [securityRows] = await db.execute('SELECT * FROM security_checks WHERE user_id = ? ORDER BY created_at DESC LIMIT 1', [userId]); - const [alertRows] = await db.execute('SELECT * FROM security_alerts WHERE user_id = ? AND created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)', [userId]); - res.json({ - latestSecurityCheck: securityRows[0] || null, - recentAlerts: alertRows, - }); + const db = await getConnection(); + try { + const { userId } = req.params; + const [securityRows] = await db.execute('SELECT * FROM security_checks WHERE user_id = ? ORDER BY created_at DESC LIMIT 1', [userId]); + const [alertRows] = await db.execute('SELECT * FROM security_alerts WHERE user_id = ? AND created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)', [userId]); + res.json({ + latestSecurityCheck: securityRows[0] || null, + recentAlerts: alertRows, + }); + } catch (error) { + console.error('Security status error:', error); + res.status(500).json({ message: 'Server error fetching security status' }); + } finally { + db.release(); + } }); router.get('/security/app-blacklist', async (req, res) => { - const [rows] = await db.execute('SELECT package_name FROM app_blacklist'); - res.json(rows.map(row => row.package_name)); + const db = await getConnection(); + try { + const [rows] = await db.execute('SELECT package_name FROM app_blacklist'); + res.json(rows.map(row => row.package_name)); + } catch (error) { + console.error('App blacklist error:', error); + res.status(500).json({ message: 'Server error fetching app blacklist' }); + } finally { + db.release(); + } }); return router;