added txt file export as extra export.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user