Files
Nilai_Clock/backend/managerRoutes.js
T
2025-10-17 09:52:53 +08:00

1104 lines
41 KiB
JavaScript

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