diff --git a/backend/managerRoutes.js b/backend/managerRoutes.js index 696478c..73e2f59 100644 --- a/backend/managerRoutes.js +++ b/backend/managerRoutes.js @@ -272,13 +272,8 @@ export default function(db) { const [rows] = await db.execute(query, params); + // ---- Group events by worker/day ---- const workByDay = {}; - const fmtTS = (d) => { - const p = (n) => String(n).padStart(2, '0'); - return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`; - }; - - // group events rows.forEach(row => { const day = new Date(row.timestamp).toISOString().split('T')[0]; if (!workByDay[row.worker_id]) { @@ -299,18 +294,29 @@ export default function(db) { }); }); - // build two rows per day: first clock_in (no hours) and last clock_out (with hours) + // ---- Helpers for formatting ---- + const fmtHM = (d) => { + const p = (n) => String(n).padStart(2, '0'); + return `${p(d.getHours())}:${p(d.getMinutes())}`; + }; + const dayName = (yyyyMmDd) => { + const [y, m, dd] = yyyyMmDd.split('-').map(Number); + const d = new Date(y, m - 1, dd); + return ['SUN','MON','TUE','WED','THU','FRI','SAT'][d.getDay()]; + }; + + // ---- Build ONE row per worker/day (no double rows) ---- const csvData = []; - const byWorkerForXlsx = new Map(); + 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) { @@ -321,132 +327,85 @@ export default function(db) { const firstIn = events.find(e => e.type === 'clock_in') || null; const lastOut = [...events].reverse().find(e => e.type === 'clock_out') || null; - if (firstIn) { - const row = { - username: w.username, - full_name: w.full_name, - department: w.department, - date: day, - work_hours: '', - event_type: 'clock_in', - timestamp: fmtTS(firstIn.time), - qr_code_name: firstIn.qr_code_name || 'Manual Entry' - }; - csvData.push(row); - perWorkerRows.push(row); - } + const dailyRow = { + username: w.username, + full_name: w.full_name, + date: day, + day: dayName(day), + clock_in: firstIn ? fmtHM(firstIn.time) : '', + clock_out: lastOut ? fmtHM(lastOut.time) : '', + work_hours: (firstIn && lastOut) ? (totalSec / 3600).toFixed(2) : '', + qr_code_name: firstIn ? (firstIn.qr_code_name || 'Manual Entry') : '' + }; - if (lastOut) { - const row = { - username: w.username, - full_name: w.full_name, - department: w.department, - date: day, - work_hours: (totalSec / 3600).toFixed(2), - event_type: 'clock_out', - timestamp: fmtTS(lastOut.time), - qr_code_name: lastOut.qr_code_name || 'Manual Entry' - }; - csvData.push(row); - perWorkerRows.push(row); - } + 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'); + if (wantXlsx) { + const wb = new ExcelJS.Workbook(); + const ws = wb.addWorksheet('Attendance'); - // helper formatters - const fmtHM = (d) => { - const p = (n) => String(n).padStart(2, '0'); - return `${p(d.getHours())}:${p(d.getMinutes())}`; - }; - const dayName = (yyyyMmDd) => { - const [y, m, dd] = yyyyMmDd.split('-').map(Number); - const d = new Date(y, m - 1, dd); - return ['SUN','MON','TUE','WED','THU','FRI','SAT'][d.getDay()]; - }; + 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 }, + ]; - 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('||'); - for (const workerId in workByDay) { - const w = workByDay[workerId]; + if (ws.lastRow) ws.addRow([]); - if (ws.lastRow) ws.addRow([]); + // Bold merged group header: "username full_name Dept: X" + const titleRowIdx = (ws.lastRow ? ws.lastRow.number : 0) + 1; + ws.mergeCells(`A${titleRowIdx}:F${titleRowIdx}`); + const titleCell = ws.getCell(`A${titleRowIdx}`); + titleCell.value = dept ? `${username} ${full_name} Dept: ${dept}` : `${username} ${full_name}`; + titleCell.font = { bold: true, size: 12 }; + titleCell.alignment = { horizontal: 'left', vertical: 'middle' }; - // 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 = w.department - ? `${w.username} ${w.full_name} Dept: ${w.department}` - : `${w.username} ${w.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 }; - // header row under the group (styled) - 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 }; - - // one row per day: first clock_in, last clock_out, total hours, QR from clock_in - 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; } + // Detail rows (one per day) + for (const r of rowsForWorker) { + ws.addRow(r); + } } - const firstIn = events.find(e => e.type === 'clock_in') || null; - const lastOut = [...events].reverse().find(e => e.type === 'clock_out') || null; + ws.eachRow(row => { row.alignment = { vertical: 'middle' }; }); - ws.addRow({ - date: day, - day: dayName(day), - clock_in: firstIn ? fmtHM(firstIn.time) : '', - clock_out: lastOut ? fmtHM(lastOut.time) : '', - work_hours: (firstIn && lastOut) ? (totalSec / 3600).toFixed(2) : '', - qr_code_name: firstIn ? (firstIn.qr_code_name || 'Manual Entry') : '', - }); + 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)); } - } - 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 (unchanged field order) - const { Parser } = await import('json2csv'); + // ===== CSV fallback: one row per day; include identity columns ===== const json2csvParser = new Parser({ - fields: ['username', 'full_name', 'date', 'work_hours', 'event_type', 'timestamp', 'qr_code_name'] + 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); + res + .header('Content-Type', 'text/csv') + .attachment(`work_hours_${startDate}_to_${endDate}.csv`) + .send(csv); } catch (error) { console.error('Work hours export error:', error);