refactor(api): extract buildSessionsCsv shared by worker + admin exports

This commit is contained in:
Bas van Rossem
2026-06-24 16:22:06 +02:00
parent eac1eed71a
commit 01fa18f401
3 changed files with 106 additions and 40 deletions

View File

@@ -11,3 +11,62 @@ export function formatDuration(totalSeconds: number): string {
const sec = s % 60;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
}
// One completed session, flattened for CSV. Times accept Date | number | string.
export interface SessionCsvRow {
id: number;
activityName: string | null;
userName?: string | null;
insoleType: string | null;
pairCount: number;
startTime: Date | number | string;
endTime: Date | number | string | null;
durationSeconds: number | null;
pausedSeconds: number | null;
}
// Build the CSV body shared by the worker self-export and the admin all-users export.
// includeWorker prepends a Worker column; everything else matches the legacy format byte-for-byte.
export function buildSessionsCsv(
rows: SessionCsvRow[],
opts: { includeWorker?: boolean } = {},
): string {
const includeWorker = opts.includeWorker ?? false;
const header = [
...(includeWorker ? ['Worker'] : []),
'ID',
'Task',
'Insole Type',
'No. of Insoles',
'Date',
'Total Duration',
'Paused Duration',
'Start Time',
'End Time',
]
.map(quote)
.join(',');
const dataLines = rows.map((row) => {
const start = new Date(row.startTime);
const end = row.endTime ? new Date(row.endTime) : null;
return [
...(includeWorker ? [row.userName ?? ''] : []),
row.id,
row.activityName ?? '',
row.insoleType ?? 'Kurk',
row.pairCount ?? 2,
start.toLocaleDateString('nl-BE', { day: '2-digit', month: '2-digit', year: 'numeric' }),
formatDuration(row.durationSeconds ?? 0),
formatDuration(row.pausedSeconds ?? 0),
start.toLocaleTimeString('nl-BE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
end
? end.toLocaleTimeString('nl-BE', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
: '',
]
.map(quote)
.join(',');
});
return [header, ...dataLines].join('\n');
}

View File

@@ -5,7 +5,7 @@ import { db } from '../db/client';
import { activities, workSessions } from '../db/schema';
import { getSessionUser } from '../lib/require-user';
import { toWorkSession } from '../lib/work-session';
import { quote, formatDuration } from '../lib/csv';
import { buildSessionsCsv } from '../lib/csv';
export const sessionsRoutes = new Hono();
@@ -20,45 +20,18 @@ sessionsRoutes.get('/api/export', async (c) => {
.where(and(eq(workSessions.userId, sessionUser.id), eq(workSessions.status, 'completed')))
.orderBy(asc(workSessions.startTime));
const header = [
'ID',
'Task',
'Insole Type',
'No. of Insoles',
'Date',
'Total Duration',
'Paused Duration',
'Start Time',
'End Time',
]
.map(quote)
.join(',');
const dataLines = rows.map(({ session, activityName }) => {
const start = new Date(session.startTime);
const end = session.endTime ? new Date(session.endTime) : null;
return [
session.id,
activityName ?? '',
session.insoleType ?? 'Kurk',
session.pairCount ?? 2,
start.toLocaleDateString('nl-BE', { day: '2-digit', month: '2-digit', year: 'numeric' }),
formatDuration(session.durationSeconds ?? 0),
formatDuration(session.pausedSeconds ?? 0),
start.toLocaleTimeString('nl-BE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}),
end
? end.toLocaleTimeString('nl-BE', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
: '',
]
.map(quote)
.join(',');
});
const csv = [header, ...dataLines].join('\n');
const csv = buildSessionsCsv(
rows.map(({ session, activityName }) => ({
id: session.id,
activityName,
insoleType: session.insoleType,
pairCount: session.pairCount,
startTime: session.startTime,
endTime: session.endTime,
durationSeconds: session.durationSeconds,
pausedSeconds: session.pausedSeconds,
})),
);
return c.body(csv, 200, {
'Content-Type': 'text/csv; charset=utf-8',

34
apps/api/test/csv.test.ts Normal file
View File

@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { buildSessionsCsv, type SessionCsvRow } from '../src/lib/csv';
const row: SessionCsvRow = {
id: 7,
activityName: 'Frezen',
userName: 'Jan',
insoleType: 'Kurk',
pairCount: 2,
startTime: new Date('2026-06-17T08:00:00Z'),
endTime: new Date('2026-06-17T08:01:30Z'),
durationSeconds: 90,
pausedSeconds: 0,
};
describe('buildSessionsCsv', () => {
it('omits the Worker column by default (legacy header)', () => {
const lines = buildSessionsCsv([row]).split('\n');
expect(lines[0]).toBe(
'"ID","Task","Insole Type","No. of Insoles","Date","Total Duration","Paused Duration","Start Time","End Time"',
);
expect(lines[1]).toContain('"Frezen"');
expect(lines[1]).toContain('"00:01:30"');
expect(lines[1]).not.toContain('"Jan"');
});
it('prepends a Worker column when includeWorker is true', () => {
const lines = buildSessionsCsv([row], { includeWorker: true }).split('\n');
expect(lines[0]).toBe(
'"Worker","ID","Task","Insole Type","No. of Insoles","Date","Total Duration","Paused Duration","Start Time","End Time"',
);
expect(lines[1].startsWith('"Jan"')).toBe(true);
});
});