no fall back
This commit is contained in:
+66
-107
@@ -272,13 +272,8 @@ export default function(db) {
|
|||||||
|
|
||||||
const [rows] = await db.execute(query, params);
|
const [rows] = await db.execute(query, params);
|
||||||
|
|
||||||
|
// ---- Group events by worker/day ----
|
||||||
const workByDay = {};
|
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 => {
|
rows.forEach(row => {
|
||||||
const day = new Date(row.timestamp).toISOString().split('T')[0];
|
const day = new Date(row.timestamp).toISOString().split('T')[0];
|
||||||
if (!workByDay[row.worker_id]) {
|
if (!workByDay[row.worker_id]) {
|
||||||
@@ -299,68 +294,7 @@ export default function(db) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// build two rows per day: first clock_in (no hours) and last clock_out (with hours)
|
// ---- Helpers for formatting ----
|
||||||
const csvData = [];
|
|
||||||
const byWorkerForXlsx = new Map();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
// helper formatters
|
|
||||||
const fmtHM = (d) => {
|
const fmtHM = (d) => {
|
||||||
const p = (n) => String(n).padStart(2, '0');
|
const p = (n) => String(n).padStart(2, '0');
|
||||||
return `${p(d.getHours())}:${p(d.getMinutes())}`;
|
return `${p(d.getHours())}:${p(d.getMinutes())}`;
|
||||||
@@ -371,42 +305,14 @@ if (wantXlsx) {
|
|||||||
return ['SUN','MON','TUE','WED','THU','FRI','SAT'][d.getDay()];
|
return ['SUN','MON','TUE','WED','THU','FRI','SAT'][d.getDay()];
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.columns = [
|
// ---- Build ONE row per worker/day (no double rows) ----
|
||||||
{ header: 'Date', key: 'date', width: 12 },
|
const csvData = [];
|
||||||
{ header: 'Day', key: 'day', width: 8 },
|
const byWorkerForXlsx = new Map(); // key = "username||full_name||department" → daily rows
|
||||||
{ 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 workerId in workByDay) {
|
for (const workerId in workByDay) {
|
||||||
const w = workByDay[workerId];
|
const w = workByDay[workerId];
|
||||||
|
const perWorkerRows = [];
|
||||||
|
|
||||||
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 = 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 (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()) {
|
for (const day of Object.keys(w.days).sort()) {
|
||||||
const events = w.days[day].slice().sort((a, b) => a.time - b.time);
|
const events = w.days[day].slice().sort((a, b) => a.time - b.time);
|
||||||
|
|
||||||
@@ -421,14 +327,65 @@ if (wantXlsx) {
|
|||||||
const firstIn = events.find(e => e.type === 'clock_in') || null;
|
const firstIn = events.find(e => e.type === 'clock_in') || null;
|
||||||
const lastOut = [...events].reverse().find(e => e.type === 'clock_out') || null;
|
const lastOut = [...events].reverse().find(e => e.type === 'clock_out') || null;
|
||||||
|
|
||||||
ws.addRow({
|
const dailyRow = {
|
||||||
|
username: w.username,
|
||||||
|
full_name: w.full_name,
|
||||||
date: day,
|
date: day,
|
||||||
day: dayName(day),
|
day: dayName(day),
|
||||||
clock_in: firstIn ? fmtHM(firstIn.time) : '',
|
clock_in: firstIn ? fmtHM(firstIn.time) : '',
|
||||||
clock_out: lastOut ? fmtHM(lastOut.time) : '',
|
clock_out: lastOut ? fmtHM(lastOut.time) : '',
|
||||||
work_hours: (firstIn && lastOut) ? (totalSec / 3600).toFixed(2) : '',
|
work_hours: (firstIn && lastOut) ? (totalSec / 3600).toFixed(2) : '',
|
||||||
qr_code_name: firstIn ? (firstIn.qr_code_name || 'Manual Entry') : '',
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,15 +395,17 @@ if (wantXlsx) {
|
|||||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
res.setHeader('Content-Disposition', `attachment; filename="work_hours_${startDate}_to_${endDate}.xlsx"`);
|
res.setHeader('Content-Disposition', `attachment; filename="work_hours_${startDate}_to_${endDate}.xlsx"`);
|
||||||
return res.send(Buffer.from(buf));
|
return res.send(Buffer.from(buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSV fallback (unchanged field order)
|
// ===== CSV fallback: one row per day; include identity columns =====
|
||||||
const { Parser } = await import('json2csv');
|
|
||||||
const json2csvParser = new Parser({
|
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);
|
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) {
|
} catch (error) {
|
||||||
console.error('Work hours export error:', error);
|
console.error('Work hours export error:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user