added txt file export as extra export.

This commit is contained in:
Edison
2026-03-19 16:33:22 +08:00
parent 9f65883534
commit 99f80a25d0
2 changed files with 75 additions and 0 deletions
+34
View File
@@ -392,6 +392,7 @@ export default function () {
}
const wantXlsx = String(req.query.format || 'csv').toLowerCase() === 'xlsx'
const wantTxt = String(req.query.format || '').toLowerCase() === 'txt'
let workerIdClause = ''
let departmentClause = ''
@@ -522,6 +523,39 @@ export default function () {
byWorkerForXlsx.set(`${w.username}||${w.full_name}||${w.department}`, perWorkerRows)
}
// ===== TXT branch =====
if (wantTxt) {
const lines = []
for (const wId in workByDay) {
const w = workByDay[wId]
const seen = new Set()
for (const day of Object.keys(w.days).sort()) {
const events = w.days[day].slice().sort((a, b) => a.time - b.time)
for (const e of events) {
const code = e.type === 'clock_in' ? '1' : '0'
const date = ymdInTZ(e.time, TZ)
const timeStr = new Intl.DateTimeFormat('en-GB', {
timeZone: TZ,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}).format(e.time)
const line = `${w.username},"${code}","${date} ${timeStr}";"${w.full_name}"`
if (!seen.has(line)) {
seen.add(line)
lines.push(line)
}
}
}
}
res.set('X-Export-TZ', TZ)
res.header('Content-Type', 'text/plain')
.attachment(`attendance_${startDate}_to_${endDate}.txt`)
.send(lines.join('\n'))
return
}
// ===== XLSX branch: grouped header + per-day summary sheet =====
if (wantXlsx) {
const wb = new ExcelJS.Workbook()
+41
View File
@@ -95,6 +95,13 @@
{{ exportLoading ? $t('exporting') : $t('exportAll') }}
</button>
</div>
<div class="shrink-0">
<button @click="exportTxt"
:disabled="!exportFilters.startDate || !exportFilters.endDate || txtExportLoading"
class="bg-blue-600 hover:bg-blue-700 text-white font-semibold px-4 py-2 rounded-md transition-colors duration-200 disabled:opacity-50">
{{ txtExportLoading ? $t('exporting') : 'Export TXT' }}
</button>
</div>
</div>
<div class="overflow-x-auto">
@@ -471,6 +478,7 @@ const confirmMessage = ref('');
const isConfirmModalVisible = ref(false);
const exportFilters = ref({ startDate: '', endDate: '' });
const exportLoading = ref(false);
const txtExportLoading = ref(false);
const showClearDeviceConfirm = ref(false);
const showDeleteConfirm = ref(false);
const departments = ref([]);
@@ -762,6 +770,39 @@ const exportWorkHours = async () => {
}
};
const exportTxt = async () => {
const toast = useToast();
txtExportLoading.value = true;
const { startDate, endDate } = exportFilters.value;
const workerIds = selectedWorkerIds.value.join(',');
let exportUrl = `${import.meta.env.VITE_API_BASE_URL}/api/managers/attendance-records/export?format=txt&startDate=${startDate}&endDate=${endDate}&workerIds=${workerIds}`;
if (selectedDepartment.value) {
exportUrl += `&department=${encodeURIComponent(selectedDepartment.value)}`;
}
try {
const response = await fetch(exportUrl, {
headers: {
Authorization: `Bearer ${sessionStorage.getItem('token')}`,
'X-User-Timezone': getUserTimezone(),
},
});
if (!response.ok) throw new Error('Network response was not ok.');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `attendance_${startDate}_to_${endDate}.txt`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (_err) {
toast.showToast('Export TXT failed.', 'error');
} finally {
txtExportLoading.value = false;
}
};
onMounted(() => {
fetchDepartments();
const savedSearchState = sessionStorage.getItem('personnelSearchState');