import { Hono } from 'hono'; 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(); type WorkSessionRow = typeof workSessions.$inferSelect; function toWorkSession(row: WorkSessionRow, activityName?: string | null): WorkSession { return { id: row.id, user_id: row.userId, activity_id: row.activityId, activity_name: activityName ?? undefined, insole_type: (row.insoleType ?? null) as WorkSession['insole_type'], pair_count: row.pairCount, start_time: new Date(row.startTime).toISOString(), end_time: row.endTime ? new Date(row.endTime).toISOString() : null, duration_seconds: row.durationSeconds ?? null, status: row.status as WorkSession['status'], source: row.source as WorkSession['source'], notes: row.notes ?? null, created_at: new Date(row.createdAt).toISOString(), }; } 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); 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, 'active'))) .orderBy(desc(workSessions.startTime)); return c.json(rows.map((r) => toWorkSession(r.session, r.activityName))); }); sessionsRoutes.get('/api/sessions', 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(eq(workSessions.userId, sessionUser.id)) .orderBy(desc(workSessions.startTime)); return c.json(rows.map((r) => toWorkSession(r.session, r.activityName))); }); sessionsRoutes.post('/api/sessions/start', async (c) => { const sessionUser = await getSessionUser(c); if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401); const parsed = StartSessionInput.safeParse(await c.req.json().catch(() => null)); if (!parsed.success) return c.json({ error: 'Invalid input' }, 400); const [activity] = await db .select() .from(activities) .where(eq(activities.id, parsed.data.activity_id)); if (!activity) return c.json({ error: 'Activity not found' }, 404); const [row] = await db .insert(workSessions) .values({ userId: sessionUser.id, activityId: parsed.data.activity_id, insoleType: parsed.data.insole_type, pairCount: parsed.data.pair_count, startTime: new Date(), endTime: null, durationSeconds: null, status: 'active', source: 'app', }) .returning(); return c.json(toWorkSession(row, activity.name)); }); sessionsRoutes.post('/api/sessions/:id/stop', async (c) => { const sessionUser = await getSessionUser(c); if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401); const id = Number.parseInt(c.req.param('id'), 10); if (Number.isNaN(id)) return c.json({ error: 'Session not found' }, 404); const [row] = await db .select() .from(workSessions) .where(and(eq(workSessions.id, id), eq(workSessions.userId, sessionUser.id))); if (!row) return c.json({ error: 'Session not found' }, 404); if (row.status !== 'active') return c.json({ error: 'Session already closed' }, 409); const endTime = new Date(); const durationSeconds = Math.round((endTime.getTime() - new Date(row.startTime).getTime()) / 1000); const [updated] = await db .update(workSessions) .set({ endTime, durationSeconds, status: 'completed' }) .where(eq(workSessions.id, id)) .returning(); return c.json(toWorkSession(updated)); }); sessionsRoutes.post('/api/sessions/:id/discard', async (c) => { const sessionUser = await getSessionUser(c); if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401); const id = Number.parseInt(c.req.param('id'), 10); if (Number.isNaN(id)) return c.json({ error: 'Session not found' }, 404); const [row] = await db .select() .from(workSessions) .where(and(eq(workSessions.id, id), eq(workSessions.userId, sessionUser.id))); if (!row) return c.json({ error: 'Session not found' }, 404); if (row.status !== 'active') return c.json({ error: 'Session already closed' }, 409); const [updated] = await db .update(workSessions) .set({ status: 'discarded', endTime: new Date() }) .where(eq(workSessions.id, id)) .returning(); return c.json(toWorkSession(updated)); });