Files
solelog/apps/api/src/routes/sessions.ts
2026-06-17 15:49:20 +02:00

191 lines
6.5 KiB
TypeScript

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));
});