1359 lines
45 KiB
JavaScript
1359 lines
45 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'
|
|
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()
|
|
})
|
|
|
|
/* === 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) => {
|
|
const db = await getConnection()
|
|
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.' })
|
|
} finally {
|
|
db.release()
|
|
}
|
|
}
|
|
}
|
|
|
|
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',
|
|
)
|
|
// 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.' })
|
|
} finally {
|
|
db.release()
|
|
}
|
|
})
|
|
|
|
router.post('/enabled-dates/update', checkPermission('manage_resources'), async (req, res) => {
|
|
const db = await getConnection()
|
|
try {
|
|
const { datesToEnable, datesToDisable } = req.body
|
|
|
|
if (!Array.isArray(datesToEnable) || !Array.isArray(datesToDisable)) {
|
|
return res.status(400).json({ message: 'Invalid input format.' })
|
|
}
|
|
|
|
// Process all deletions sequentially
|
|
for (const date of datesToDisable) {
|
|
await db.execute('DELETE FROM enabled_dates WHERE enabled_date = ?', [date])
|
|
}
|
|
|
|
// Process all insertions sequentially
|
|
for (const date of datesToEnable) {
|
|
await db.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 {
|
|
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
|
|
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,
|
|
})
|
|
} 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
|
|
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,
|
|
})
|
|
} finally {
|
|
db.release()
|
|
}
|
|
})
|
|
|
|
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
|
|
|
|
if (!startDate || !endDate) {
|
|
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`]
|
|
|
|
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 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,
|
|
}))
|
|
|
|
const json2csvParser = new Parser({
|
|
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')
|
|
.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 })
|
|
} 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
|
|
|
|
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}.` })
|
|
}
|
|
|
|
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
|
|
|
|
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} 00:00:00`, `${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}
|
|
AND cr.event_type IN ('clock_in','clock_out')
|
|
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 rows: one per successful [clock_in, clock_out] session ----
|
|
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()) {
|
|
// events for this day in ascending time
|
|
const events = w.days[day].slice().sort((a, b) => a.time - b.time)
|
|
|
|
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'
|
|
} else if (e.type === 'clock_out' && open != null) {
|
|
const start = open
|
|
const end = e.time
|
|
|
|
const dailyRow = {
|
|
username: w.username,
|
|
full_name: w.full_name,
|
|
date: day,
|
|
day: dayNameFromYMD(day),
|
|
clock_in: hmInTZ(start, TZ),
|
|
clock_out: hmInTZ(end, TZ),
|
|
work_hours: ((end - start) / 3600000).toFixed(2),
|
|
qr_code_name: openQr,
|
|
}
|
|
|
|
csvData.push(dailyRow)
|
|
perWorkerRows.push(dailyRow)
|
|
|
|
// close the session
|
|
open = null
|
|
openQr = 'Manual Entry'
|
|
}
|
|
}
|
|
}
|
|
|
|
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.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)
|
|
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 })
|
|
} 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
|
|
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 ?'
|
|
params.push(`${startDate} 00:00:00`, `${endDate} 23:59:59`)
|
|
}
|
|
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 })
|
|
} 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)
|
|
|
|
// 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) => {
|
|
const db = await getConnection()
|
|
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 })
|
|
} 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}%`
|
|
|
|
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 })
|
|
} 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}%`
|
|
|
|
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 })
|
|
} 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
|
|
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 })
|
|
} 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
|
|
|
|
if (!username || !password || !fullName) {
|
|
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],
|
|
)
|
|
|
|
const hashedPassword = await bcrypt.hash(password, 10)
|
|
|
|
if (existingRows.length > 0) {
|
|
const existing = existingRows[0]
|
|
if (existing.status === 'deleted' && existing.role === 'worker') {
|
|
await db.execute(
|
|
`
|
|
UPDATE workers
|
|
SET password_hash = ?, full_name = ?, role = ?, department = ?, position = ?, status = 'active'
|
|
WHERE id = ?
|
|
`,
|
|
[hashedPassword, fullName, role, department, position, existing.id],
|
|
)
|
|
|
|
return res.status(200).json({
|
|
id: existing.id,
|
|
username,
|
|
fullName,
|
|
role,
|
|
department,
|
|
position,
|
|
status: 'active',
|
|
restored: true,
|
|
})
|
|
}
|
|
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],
|
|
)
|
|
|
|
return 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.' })
|
|
}
|
|
return res.status(500).json({
|
|
message: 'Database error adding worker.',
|
|
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],
|
|
)
|
|
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 })
|
|
} 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],
|
|
)
|
|
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 })
|
|
} 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
|
|
|
|
// Basic validation
|
|
if (!department && !position && !status && !fullName) {
|
|
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)
|
|
}
|
|
if (fullName) {
|
|
fieldsToUpdate.push('full_name = ?')
|
|
params.push(fullName)
|
|
}
|
|
|
|
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,
|
|
})
|
|
} 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
|
|
|
|
// 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 })
|
|
} 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
|
|
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 })
|
|
} finally {
|
|
db.release()
|
|
}
|
|
})
|
|
|
|
// PUT (update) a manager's password
|
|
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()
|
|
}
|
|
},
|
|
)
|
|
|
|
// PUT (clear) a worker's device UUID and/or update status
|
|
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]
|
|
|
|
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()
|
|
}
|
|
},
|
|
)
|
|
|
|
// 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) => ({
|
|
...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 })
|
|
} finally {
|
|
db.release()
|
|
}
|
|
})
|
|
|
|
router.post('/geofences', checkPermission('manage_resources'), async (req, res) => {
|
|
const db = await getConnection()
|
|
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 })
|
|
} 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
|
|
|
|
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 })
|
|
} 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])
|
|
|
|
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 })
|
|
} 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)
|
|
} catch (error) {
|
|
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 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.' })
|
|
} finally {
|
|
db.release()
|
|
}
|
|
})
|
|
|
|
router.put('/qr-codes/:id', checkPermission('manage_resources'), async (req, res) => {
|
|
const db = await getConnection()
|
|
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.' })
|
|
} 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])
|
|
|
|
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.' })
|
|
} finally {
|
|
db.release()
|
|
}
|
|
})
|
|
|
|
return router
|
|
}
|