diff --git a/backend/managerRoutes.js b/backend/managerRoutes.js index f1929e0..af9e322 100644 --- a/backend/managerRoutes.js +++ b/backend/managerRoutes.js @@ -317,7 +317,8 @@ const TZ = tz || process.env.EXPORT_TZ || 'Asia/Kuala_Lumpur'; 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 + AND cr.event_type IN ('clock_in','clock_out') +ORDER BY cr.worker_id, cr.timestamp ASC `; const [rows] = await db.execute(query, params); @@ -345,46 +346,52 @@ rows.forEach(row => { }); }); - // ---- Build ONE row per worker/day (no double rows) ---- - const csvData = []; - const byWorkerForXlsx = new Map(); // key = "username||full_name||department" → daily rows + // ---- 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 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); + 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); - // 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; } - } + let open = null; + let openQr = 'Manual Entry'; - const firstIn = events.find(e => e.type === 'clock_in') || null; - const lastOut = [...events].reverse().find(e => e.type === 'clock_out') || null; + 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: 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') : '' + 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); } + } + 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();