191 lines
6.5 KiB
TypeScript
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));
|
|
});
|