14d412544e
- Added login route with JWT token generation and device validation for non-manager roles. - Implemented clocking functionality with geofence validation and distance calculation. - Created routes for managing workers, including password change and device registration. - Added security status and app blacklist retrieval endpoints. feat: Develop Geofence Management component - Created a Vue component for managing geofences with map integration using Leaflet. - Implemented functionality to draw, save, activate/deactivate, and delete geofences. - Added UI for displaying existing geofences in a table format. feat: Introduce Kill Switch Management component - Developed a calendar-based UI for managing enabled/disabled dates. - Implemented functionality to apply or discard changes to the work schedule. - Added visual indicators for pending changes in the calendar. feat: Create Warning Reporting component - Implemented a reporting interface for failed clock records with search and filter options. - Added detail modal for viewing specific failed record details. - Implemented sorting functionality for the records table.
636 lines
22 KiB
JavaScript
636 lines
22 KiB
JavaScript
import express from 'express';
|
|
import { Parser } from 'json2csv';
|
|
import bcrypt from 'bcrypt';
|
|
import jwt from 'jsonwebtoken';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
export default function(db) {
|
|
const router = express.Router();
|
|
|
|
// Middleware to authenticate and authorize managers
|
|
const authenticateJWT = (req, res, next) => {
|
|
const authHeader = req.headers.authorization;
|
|
if (authHeader) {
|
|
const token = authHeader.split(' ')[1];
|
|
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
|
|
if (err || user.role !== 'manager') {
|
|
return res.status(403).json({ message: 'Forbidden' });
|
|
}
|
|
req.user = user;
|
|
next();
|
|
});
|
|
} else {
|
|
res.status(401).json({ message: 'Unauthorized' });
|
|
}
|
|
};
|
|
router.use(authenticateJWT);
|
|
|
|
// --- START: Date Management Routes ---
|
|
router.get('/enabled-dates', async (req, res) => {
|
|
try {
|
|
const [rows] = await db.execute('SELECT YEAR(enabled_date) as year, MONTH(enabled_date) as month, DAY(enabled_date) as day FROM enabled_dates');
|
|
// Format date safely using components from the database to avoid timezone shifts
|
|
const dates = rows.map(r => `${r.year}-${String(r.month).padStart(2, '0')}-${String(r.day).padStart(2, '0')}`);
|
|
res.json(dates);
|
|
} catch (error) {
|
|
console.error('Error fetching enabled dates:', error);
|
|
res.status(500).json({ message: 'Database error fetching enabled dates.' });
|
|
}
|
|
});
|
|
|
|
// Definitive version using a dedicated database connection
|
|
router.post('/enabled-dates/update', async (req, res) => {
|
|
let connection; // Define connection here to ensure it's accessible in the 'finally' block
|
|
try {
|
|
const { datesToEnable, datesToDisable } = req.body;
|
|
|
|
if (!Array.isArray(datesToEnable) || !Array.isArray(datesToDisable)) {
|
|
return res.status(400).json({ message: 'Invalid input format.' });
|
|
}
|
|
|
|
// 1. Get a single, dedicated connection from the pool
|
|
connection = await db.getConnection();
|
|
|
|
// 2. Process all deletions sequentially on the dedicated connection
|
|
for (const date of datesToDisable) {
|
|
await connection.execute('DELETE FROM enabled_dates WHERE enabled_date = ?', [date]);
|
|
}
|
|
|
|
// 3. Process all insertions sequentially on the dedicated connection
|
|
for (const date of datesToEnable) {
|
|
await connection.execute('INSERT IGNORE INTO enabled_dates (enabled_date) VALUES (?)', [date]);
|
|
}
|
|
|
|
res.status(200).json({ message: 'Work schedule updated successfully.' });
|
|
|
|
} catch (error) {
|
|
console.error('Error updating work schedule:', error);
|
|
res.status(500).json({ message: 'Database error during schedule update.' });
|
|
} finally {
|
|
// 4. Ensure the dedicated connection is always released back to the pool
|
|
if (connection) {
|
|
connection.release();
|
|
}
|
|
}
|
|
});
|
|
// --- END: Date Management Routes ---
|
|
|
|
// --- ATTENDANCE & REPORTING ---
|
|
|
|
router.get('/failed-records', async (req, res) => {
|
|
try {
|
|
const { search = '', startDate, endDate } = req.query;
|
|
if (!startDate || !endDate) {
|
|
return res.status(400).json({ message: 'Start date and end date are required.' });
|
|
}
|
|
|
|
const searchTerm = `%${search}%`;
|
|
const params = [startDate, `${endDate} 23:59:59`];
|
|
|
|
let searchQuery = '';
|
|
if (search) {
|
|
searchQuery = `AND (w.full_name LIKE ? OR w.department LIKE ?)`;
|
|
params.push(searchTerm, searchTerm);
|
|
}
|
|
|
|
const query = `
|
|
SELECT cr.worker_id, w.full_name, COUNT(*) as count
|
|
FROM clock_records cr
|
|
JOIN workers w ON cr.worker_id = w.id
|
|
WHERE cr.event_type = 'failed'
|
|
AND cr.timestamp BETWEEN ? AND ?
|
|
${searchQuery}
|
|
GROUP BY cr.worker_id, w.full_name
|
|
ORDER BY count DESC
|
|
`;
|
|
|
|
const [rows] = await db.execute(query, params);
|
|
res.json(rows);
|
|
} catch (error) {
|
|
console.error('Failed records summary error:', error);
|
|
res.status(500).json({ message: 'Database error fetching failed records summary.', details: error.message });
|
|
}
|
|
});
|
|
|
|
router.get('/failed-records/details', async (req, res) => {
|
|
try {
|
|
const { workerId, startDate, endDate } = req.query;
|
|
if (!workerId || !startDate || !endDate) {
|
|
return res.status(400).json({ message: 'Worker ID, start date, and end date are required.' });
|
|
}
|
|
|
|
const query = `
|
|
SELECT cr.id, cr.timestamp, cr.event_type, COALESCE(qc.name, 'N/A') as qrCodeUsedName, cr.notes
|
|
FROM clock_records cr
|
|
LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id
|
|
WHERE cr.worker_id = ?
|
|
AND cr.event_type = 'failed'
|
|
AND cr.timestamp BETWEEN ? AND ?
|
|
ORDER BY cr.timestamp DESC
|
|
`;
|
|
|
|
const params = [workerId, startDate, `${endDate} 23:59:59`];
|
|
const [rows] = await db.execute(query, params);
|
|
res.json(rows);
|
|
} catch (error) {
|
|
console.error('Failed records details error:', error);
|
|
res.status(500).json({ message: 'Database error fetching failed records details.', details: error.message });
|
|
}
|
|
});
|
|
|
|
// GET attendance records with a modified query to avoid the MySQL 5.7 bug
|
|
router.get('/attendance-records/export-raw', async (req, res) => {
|
|
try {
|
|
const { workerIds, startDate, endDate } = req.query;
|
|
if (!startDate || !endDate) {
|
|
return res.status(400).json({ message: 'Start date and end date are required.' });
|
|
}
|
|
|
|
let workerIdClause = '';
|
|
const params = [startDate, `${endDate} 23:59:59`];
|
|
|
|
if (workerIds) {
|
|
const idsArray = workerIds.split(',').map(Number).filter(id => !isNaN(id));
|
|
if (idsArray.length > 0) {
|
|
workerIdClause = `AND cr.worker_id IN (${idsArray.join(',')})`;
|
|
}
|
|
}
|
|
|
|
const query = `
|
|
SELECT w.username, w.full_name, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qr_code_name, cr.notes
|
|
FROM clock_records cr
|
|
JOIN workers w ON cr.worker_id = w.id
|
|
LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id
|
|
WHERE cr.timestamp BETWEEN ? AND ? ${workerIdClause}
|
|
ORDER BY cr.timestamp DESC
|
|
`;
|
|
|
|
const [rows] = await db.execute(query, params);
|
|
|
|
const json2csvParser = new Parser({ fields: ['username', 'full_name', 'event_type', 'timestamp', 'qr_code_name', 'notes'] });
|
|
const csv = json2csvParser.parse(rows);
|
|
res.header('Content-Type', 'text/csv').attachment(`raw_attendance_${startDate}_to_${endDate}.csv`).send(csv);
|
|
|
|
} catch (error) {
|
|
console.error('Raw attendance export error:', error);
|
|
res.status(500).json({ message: 'Database error exporting raw attendance.', details: error.message });
|
|
}
|
|
});
|
|
|
|
router.post('/add-record', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { workerId, eventType, timestamp, notes } = req.body
|
|
|
|
if (!workerId || !eventType || !timestamp) {
|
|
return res
|
|
.status(400)
|
|
.json({ message: 'Worker ID, event type, and timestamp are required.' })
|
|
}
|
|
|
|
// Check last event to prevent adding a duplicate event type
|
|
const [lastEventRows] = await db.execute(
|
|
'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1',
|
|
[workerId],
|
|
)
|
|
|
|
if (lastEventRows.length > 0 && lastEventRows[0].event_type === eventType) {
|
|
const status = eventType === 'clock_in' ? 'in' : 'out'
|
|
return res.status(409).json({ message: `Worker is already clocked ${status}.` })
|
|
}
|
|
// --- THIS IS THE FIX ---
|
|
const sanitizedTimestamp = timestamp.replace('T', ' ')
|
|
|
|
await db.execute(
|
|
'INSERT INTO clock_records (worker_id, event_type, timestamp, notes, qr_code_id, latitude, longitude) VALUES (?, ?, ?, ?, NULL, NULL, NULL)',
|
|
[workerId, eventType, sanitizedTimestamp, notes],
|
|
)
|
|
|
|
res.status(201).json({ message: 'Manual record added successfully.' })
|
|
} catch (error) {
|
|
console.error('Add manual record error:', error)
|
|
res.status(500).json({ message: 'Database error adding manual record.' })
|
|
}
|
|
})
|
|
|
|
router.get('/attendance-records/export', async (req, res) => {
|
|
try {
|
|
const { workerIds, startDate, endDate } = req.query;
|
|
if (!startDate || !endDate) {
|
|
return res.status(400).json({ message: 'Start date and end date are required.' });
|
|
}
|
|
|
|
let workerIdClause = '';
|
|
const params = [startDate, `${endDate} 23:59:59`];
|
|
|
|
if (workerIds) {
|
|
const idsArray = workerIds.split(',').map(Number).filter(id => !isNaN(id));
|
|
if (idsArray.length > 0) {
|
|
workerIdClause = `AND cr.worker_id IN (${idsArray.join(',')})`;
|
|
}
|
|
}
|
|
|
|
const query = `
|
|
SELECT cr.worker_id, w.username, w.full_name, cr.event_type, cr.timestamp
|
|
FROM clock_records cr
|
|
JOIN workers w ON cr.worker_id = w.id
|
|
WHERE cr.timestamp BETWEEN ? AND ? ${workerIdClause}
|
|
ORDER BY cr.worker_id, cr.timestamp ASC
|
|
`;
|
|
|
|
const [rows] = await db.execute(query, params);
|
|
|
|
const workHoursByWorkerAndDay = {};
|
|
|
|
rows.forEach(row => {
|
|
const day = new Date(row.timestamp).toISOString().split('T')[0];
|
|
if (!workHoursByWorkerAndDay[row.worker_id]) {
|
|
workHoursByWorkerAndDay[row.worker_id] = {
|
|
username: row.username,
|
|
full_name: row.full_name,
|
|
days: {}
|
|
};
|
|
}
|
|
if (!workHoursByWorkerAndDay[row.worker_id].days[day]) {
|
|
workHoursByWorkerAndDay[row.worker_id].days[day] = [];
|
|
}
|
|
workHoursByWorkerAndDay[row.worker_id].days[day].push({
|
|
type: row.event_type,
|
|
time: new Date(row.timestamp)
|
|
});
|
|
});
|
|
|
|
const csvData = [];
|
|
for (const workerId in workHoursByWorkerAndDay) {
|
|
const workerData = workHoursByWorkerAndDay[workerId];
|
|
for (const day in workerData.days) {
|
|
const events = workerData.days[day];
|
|
let dailyTotalSeconds = 0;
|
|
let lastClockIn = null;
|
|
|
|
events.forEach(event => {
|
|
if (event.type === 'clock_in') {
|
|
lastClockIn = event.time;
|
|
} else if (event.type === 'clock_out' && lastClockIn) {
|
|
dailyTotalSeconds += (event.time - lastClockIn) / 1000;
|
|
lastClockIn = null;
|
|
}
|
|
});
|
|
|
|
csvData.push({
|
|
username: workerData.username,
|
|
full_name: workerData.full_name,
|
|
date: day,
|
|
work_hours: (dailyTotalSeconds / 3600).toFixed(2)
|
|
});
|
|
}
|
|
}
|
|
|
|
const json2csvParser = new Parser({ fields: ['username', 'full_name', 'date', 'work_hours'] });
|
|
const csv = json2csvParser.parse(csvData);
|
|
res.header('Content-Type', 'text/csv').attachment(`work_hours_${startDate}_to_${endDate}.csv`).send(csv);
|
|
|
|
} catch (error) {
|
|
console.error('Work hours export error:', error);
|
|
res.status(500).json({ message: 'Database error exporting work hours.', details: error.message });
|
|
}
|
|
});
|
|
|
|
router.get('/attendance-records', async (req, res) => {
|
|
try {
|
|
const { workerIds, startDate, endDate, format } = req.query;
|
|
if (!workerIds) {
|
|
return res.status(400).json({ message: 'Worker IDs are required.' });
|
|
}
|
|
|
|
// Ensure all IDs are numbers to prevent SQL injection.
|
|
const idsArray = workerIds.split(',').map(Number).filter(id => !isNaN(id));
|
|
if (idsArray.length === 0) {
|
|
return res.json([]);
|
|
}
|
|
|
|
// --- MODIFICATION START ---
|
|
// Instead of using a '?' placeholder for the IN clause, we build it directly.
|
|
// This is safe because we have already sanitized idsArray to be only numbers.
|
|
// This change is intended to bypass the specific bug in your MySQL version.
|
|
const inClause = idsArray.join(',');
|
|
let query = `
|
|
SELECT cr.id, w.full_name, cr.event_type, cr.timestamp,
|
|
COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName,
|
|
cr.latitude, cr.longitude, cr.notes
|
|
FROM clock_records cr
|
|
JOIN workers w ON cr.worker_id = w.id
|
|
LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id
|
|
WHERE cr.worker_id IN (${inClause})`; // Placeholder is replaced here
|
|
|
|
const params = [];
|
|
// --- MODIFICATION END ---
|
|
|
|
if (startDate && endDate) {
|
|
query += ' AND cr.timestamp BETWEEN ? AND ?';
|
|
const endOfDay = new Date(endDate);
|
|
endOfDay.setHours(23, 59, 59, 999);
|
|
params.push(startDate, endOfDay);
|
|
}
|
|
query += ' ORDER BY w.full_name, cr.timestamp DESC';
|
|
|
|
const [rows] = await db.execute(query, params);
|
|
|
|
if (format === 'csv') {
|
|
const json2csvParser = new Parser({ fields: ['full_name', 'event_type', 'timestamp', 'qrCodeUsedName', 'notes'] });
|
|
const csv = json2csvParser.parse(rows);
|
|
res.header('Content-Type', 'text/csv').attachment('attendance.csv').send(csv);
|
|
} else {
|
|
res.json(rows);
|
|
}
|
|
} catch (error) {
|
|
console.error('Attendance records error:', error);
|
|
res.status(500).json({ message: 'Database error fetching attendance records.', details: error.message });
|
|
}
|
|
});
|
|
|
|
|
|
// --- All other manager routes remain the same ---
|
|
|
|
// GET all workers with filtering and pagination
|
|
router.get('/workers', async (req, res) => {
|
|
try {
|
|
const { search = '', page = 1, limit = 20 } = req.query;
|
|
const offset = (parseInt(page) - 1) * parseInt(limit);
|
|
const searchTerm = `%${search}%`;
|
|
|
|
let baseQuery = `
|
|
SELECT w.id, w.username, w.full_name, w.department, w.position, w.created_at
|
|
FROM workers w
|
|
`;
|
|
let countQuery = `SELECT COUNT(w.id) as totalCount FROM workers w`;
|
|
|
|
const params = [];
|
|
const countParams = [];
|
|
let whereClauses = ["w.role = 'worker'"];
|
|
|
|
if (search) {
|
|
whereClauses.push(`(w.full_name LIKE ? OR w.department LIKE ?)`);
|
|
params.push(searchTerm, searchTerm);
|
|
countParams.push(searchTerm, searchTerm);
|
|
}
|
|
|
|
if (whereClauses.length > 0) {
|
|
const whereString = ` WHERE ${whereClauses.join(' AND ')}`;
|
|
baseQuery += whereString;
|
|
countQuery += whereString;
|
|
}
|
|
|
|
baseQuery += ` ORDER BY w.created_at DESC LIMIT ? OFFSET ?`;
|
|
params.push(parseInt(limit), offset);
|
|
|
|
const [workers] = await db.execute(baseQuery, params);
|
|
const [[{ totalCount }]] = await db.execute(countQuery, countParams);
|
|
|
|
res.json({ workers, totalCount });
|
|
} catch (error) {
|
|
console.error('Get workers error:', error);
|
|
res.status(500).json({ message: 'Database error fetching workers.', details: error.message });
|
|
}
|
|
});
|
|
|
|
// POST (add) a new worker
|
|
router.post('/workers', async (req, res) => {
|
|
try {
|
|
const { username, password, fullName, department, position, role = 'worker' } = req.body;
|
|
if (!username || !password || !fullName) {
|
|
return res.status(400).json({ message: 'Username, password, and full name are required.' });
|
|
}
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
const [result] = await db.execute(
|
|
'INSERT INTO workers (username, password_hash, full_name, role, department, position) VALUES (?, ?, ?, ?, ?, ?)',
|
|
[username, hashedPassword, fullName, role, department, position]
|
|
);
|
|
res.status(201).json({ id: result.insertId, username, fullName, role, department, position });
|
|
} catch (error) {
|
|
console.error('Add worker error:', error);
|
|
if (error.code === 'ER_DUP_ENTRY') {
|
|
return res.status(409).json({ message: 'Username already exists.' });
|
|
}
|
|
res.status(500).json({ message: 'Database error adding worker.', details: error.message });
|
|
}
|
|
});
|
|
|
|
// DELETE a worker
|
|
router.delete('/workers/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const [result] = await db.execute("DELETE FROM workers WHERE id = ? AND role = 'worker'", [id]);
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ message: 'Worker not found.' });
|
|
}
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
console.error('Delete worker error:', error);
|
|
res.status(500).json({ message: 'Database error deleting worker.', details: error.message });
|
|
}
|
|
});
|
|
|
|
// PUT (update) a worker's password
|
|
router.put('/workers/:workerId/password', async (req, res) => {
|
|
try {
|
|
const { workerId } = req.params;
|
|
const { newPassword } = req.body;
|
|
if (!newPassword || newPassword.length < 6) {
|
|
return res.status(400).json({ message: 'Password must be at least 6 characters long.' });
|
|
}
|
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
const [result] = await db.execute("UPDATE workers SET password_hash = ? WHERE id = ? AND role = 'worker'", [hashedPassword, workerId]);
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ message: 'Worker not found.' });
|
|
}
|
|
res.status(200).json({ message: 'Password updated successfully.' });
|
|
} catch (error) {
|
|
console.error('Update password error:', error);
|
|
res.status(500).json({ message: 'Database error updating password.', details: error.message });
|
|
}
|
|
});
|
|
|
|
// PUT (clear) a worker's device UUID
|
|
router.put('/workers/:workerId/reset-device', async (req, res) => {
|
|
try {
|
|
const { workerId } = req.params;
|
|
const [result] = await db.execute("UPDATE workers SET device_uuid = NULL WHERE id = ?", [workerId]);
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ message: 'Worker not found.' });
|
|
}
|
|
res.status(200).json({ message: 'Device registration cleared.' });
|
|
} catch (error) {
|
|
console.error('Reset device error:', error);
|
|
res.status(500).json({ message: 'Database error resetting device.', details: error.message });
|
|
}
|
|
});
|
|
|
|
|
|
// Geofence Management Routes
|
|
router.get('/geofences', async (req, res) => {
|
|
try {
|
|
const [rows] = await db.execute(
|
|
'SELECT id, name, coordinates, is_active, created_at FROM geofences ORDER BY created_at DESC'
|
|
);
|
|
const geofences = rows.map(row => ({
|
|
...row,
|
|
coordinates: JSON.parse(row.coordinates || '[]')
|
|
}));
|
|
res.json(geofences);
|
|
} catch (error) {
|
|
console.error('Get geofences error:', error);
|
|
res.status(500).json({ message: 'Database error fetching geofences.', details: error.message });
|
|
}
|
|
});
|
|
|
|
router.post('/geofences', async (req, res) => {
|
|
try {
|
|
const { name, coordinates } = req.body;
|
|
if (!name || !coordinates) {
|
|
return res.status(400).json({ message: 'Geofence name and coordinates are required.' });
|
|
}
|
|
|
|
const [result] = await db.execute(
|
|
'INSERT INTO geofences (name, coordinates, is_active) VALUES (?, ?, ?)',
|
|
[name, JSON.stringify(coordinates), true]
|
|
);
|
|
|
|
const newGeofence = {
|
|
id: result.insertId,
|
|
name,
|
|
coordinates,
|
|
is_active: true,
|
|
};
|
|
res.status(201).json(newGeofence);
|
|
} catch (error) {
|
|
console.error('Add geofence error:', error);
|
|
res.status(500).json({ message: 'Database error adding geofence.', details: error.message });
|
|
}
|
|
});
|
|
|
|
router.put('/geofences/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { is_active } = req.body;
|
|
|
|
if (typeof is_active !== 'boolean') {
|
|
return res.status(400).json({ message: 'is_active must be a boolean.' });
|
|
}
|
|
|
|
const [result] = await db.execute(
|
|
'UPDATE geofences SET is_active = ? WHERE id = ?',
|
|
[is_active, id]
|
|
);
|
|
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ message: 'Geofence not found.' });
|
|
}
|
|
|
|
res.json({ id, is_active });
|
|
} catch (error) {
|
|
console.error('Update geofence error:', error);
|
|
res.status(500).json({ message: 'Database error updating geofence.', details: error.message });
|
|
}
|
|
});
|
|
|
|
router.delete('/geofences/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const [result] = await db.execute('DELETE FROM geofences WHERE id = ?', [id]);
|
|
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ message: 'Geofence not found.' });
|
|
}
|
|
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
console.error('Delete geofence error:', error);
|
|
res.status(500).json({ message: 'Database error deleting geofence.', details: error.message });
|
|
}
|
|
});
|
|
|
|
|
|
// QR Code Management Routes
|
|
router.get('/qr-codes', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const [rows] = await db.execute(
|
|
'SELECT id, name, is_active, created_at FROM qr_codes ORDER BY created_at DESC'
|
|
);
|
|
res.json(rows);
|
|
} catch (error) {
|
|
console.error('Get QR codes error:', error);
|
|
res.status(500).json({ message: 'Database error fetching QR codes.' });
|
|
}
|
|
});
|
|
|
|
router.post('/qr-codes', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { name } = req.body;
|
|
if (!name) return res.status(400).json({ message: 'QR Code name is required.' });
|
|
|
|
const newQrCode = {
|
|
id: uuidv4(),
|
|
name,
|
|
is_active: true
|
|
};
|
|
|
|
await db.execute(
|
|
'INSERT INTO qr_codes (id, name, is_active) VALUES (?, ?, ?)',
|
|
[newQrCode.id, newQrCode.name, newQrCode.is_active]
|
|
);
|
|
|
|
res.status(201).json(newQrCode);
|
|
} catch (error) {
|
|
console.error('Add QR code error:', error);
|
|
res.status(500).json({ message: 'Database error adding QR code.' });
|
|
}
|
|
});
|
|
|
|
router.put('/qr-codes/:id', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
// Handle both isActive (camelCase) and is_active (snake_case)
|
|
const is_active = req.body.is_active ?? req.body.isActive;
|
|
|
|
if (typeof is_active !== 'boolean') {
|
|
return res.status(400).json({ message: 'Status must be a boolean value.' });
|
|
}
|
|
|
|
const [result] = await db.execute(
|
|
'UPDATE qr_codes SET is_active = ? WHERE id = ?',
|
|
[is_active, id]
|
|
);
|
|
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ message: 'QR Code not found.' });
|
|
}
|
|
|
|
res.json({ id, is_active });
|
|
} catch (error) {
|
|
console.error('Update QR code error:', error);
|
|
res.status(500).json({ message: 'Database error updating QR code.' });
|
|
}
|
|
});
|
|
|
|
router.delete('/qr-codes/:id', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const [result] = await db.execute(
|
|
'DELETE FROM qr_codes WHERE id = ?',
|
|
[id]
|
|
);
|
|
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ message: 'QR Code not found.' });
|
|
}
|
|
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
console.error('Delete QR code error:', error);
|
|
res.status(500).json({ message: 'Database error deleting QR code.' });
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|