feat(api): user-scoped CSV export matching legacy format
This commit is contained in:
13
apps/api/src/lib/csv.ts
Normal file
13
apps/api/src/lib/csv.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// CSV helpers ported verbatim from the legacy export route (docs/reference/legacy-backend.md §4).
|
||||
|
||||
// Wrap a value in double quotes, doubling any embedded double quote (standard CSV escaping).
|
||||
export const quote = (value: unknown) => `"${String(value).replace(/"/g, '""')}"`;
|
||||
|
||||
// Format a total number of seconds as zero-padded HH:MM:SS (hours can exceed 99).
|
||||
export function formatDuration(totalSeconds: number): string {
|
||||
const s = totalSeconds || 0;
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Hono } from 'hono';
|
||||
import { and, desc, eq } from 'drizzle-orm';
|
||||
import { and, asc, desc, eq } from 'drizzle-orm';
|
||||
import { StartSessionInput } from '@solelog/shared';
|
||||
import type { WorkSession } from '@solelog/shared';
|
||||
import { db } from '../db/client';
|
||||
import { activities, workSessions } from '../db/schema';
|
||||
import { getSessionUser } from '../lib/require-user';
|
||||
import { quote, formatDuration } from '../lib/csv';
|
||||
|
||||
export const sessionsRoutes = new Hono();
|
||||
|
||||
@@ -28,6 +29,61 @@ function toWorkSession(row: WorkSessionRow, activityName?: string | null): WorkS
|
||||
};
|
||||
}
|
||||
|
||||
sessionsRoutes.get('/api/export', async (c) => {
|
||||
const sessionUser = await getSessionUser(c);
|
||||
if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401);
|
||||
|
||||
const rows = await db
|
||||
.select({ session: workSessions, activityName: activities.name })
|
||||
.from(workSessions)
|
||||
.leftJoin(activities, eq(workSessions.activityId, activities.id))
|
||||
.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',
|
||||
'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),
|
||||
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');
|
||||
|
||||
return c.body(csv, 200, {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': 'attachment; filename="insole-production-report.csv"',
|
||||
});
|
||||
});
|
||||
|
||||
sessionsRoutes.get('/api/sessions/active', async (c) => {
|
||||
const sessionUser = await getSessionUser(c);
|
||||
if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401);
|
||||
|
||||
Reference in New Issue
Block a user