import { Hono } from 'hono'; import { eq, asc, sql } from 'drizzle-orm'; import { CreateActivityInput, UpdateActivityInput, ReorderActivitiesInput } from '@solelog/shared'; import type { Activity } from '@solelog/shared'; import { db } from '../db/client'; import { activities, workSessions } from '../db/schema'; import { getSessionUser, isAdmin } from '../lib/require-user'; export const activitiesRoutes = new Hono(); type ActivityRow = typeof activities.$inferSelect; function toActivity(row: ActivityRow): Activity { return { id: row.id, name: row.name, insole_types: row.insoleTypes as Activity['insole_types'], created_at: new Date(row.createdAt).toISOString(), sort_order: row.sortOrder ?? 0, }; } activitiesRoutes.get('/api/activities', async (c) => { const sessionUser = await getSessionUser(c); if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401); const rows = await db .select() .from(activities) .orderBy(asc(activities.sortOrder), asc(activities.name)); const insoleType = c.req.query('insole_type'); const filtered = insoleType ? rows.filter((r) => (r.insoleTypes as string[]).includes(insoleType)) : rows; return c.json(filtered.map(toActivity)); }); activitiesRoutes.post('/api/activities', async (c) => { const sessionUser = await getSessionUser(c); if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401); if (!isAdmin(sessionUser)) return c.json({ error: 'Forbidden' }, 403); const parsed = CreateActivityInput.safeParse(await c.req.json().catch(() => null)); if (!parsed.success) return c.json({ error: 'Invalid input' }, 400); // Append: place the new activity after the current highest sort_order. const [{ max }] = await db .select({ max: sql`COALESCE(MAX(${activities.sortOrder}), -1)` }) .from(activities); const [row] = await db .insert(activities) .values({ name: parsed.data.name, insoleTypes: parsed.data.insole_types, sortOrder: max + 1, }) .returning(); return c.json(toActivity(row)); }); activitiesRoutes.put('/api/activities/reorder', async (c) => { const sessionUser = await getSessionUser(c); if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401); if (!isAdmin(sessionUser)) return c.json({ error: 'Forbidden' }, 403); const parsed = ReorderActivitiesInput.safeParse(await c.req.json().catch(() => null)); if (!parsed.success) return c.json({ error: 'Invalid input' }, 400); // The payload must be the full ordered set: same size, same ids. const existing = await db.select({ id: activities.id }).from(activities); const known = new Set(existing.map((r) => r.id)); if (parsed.data.ids.length !== known.size || parsed.data.ids.some((id) => !known.has(id))) return c.json({ error: 'Invalid input' }, 400); for (let i = 0; i < parsed.data.ids.length; i++) { await db.update(activities).set({ sortOrder: i }).where(eq(activities.id, parsed.data.ids[i])); } const rows = await db .select() .from(activities) .orderBy(asc(activities.sortOrder), asc(activities.name)); return c.json(rows.map(toActivity)); }); activitiesRoutes.put('/api/activities/:id', async (c) => { const sessionUser = await getSessionUser(c); if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401); if (!isAdmin(sessionUser)) return c.json({ error: 'Forbidden' }, 403); const id = Number.parseInt(c.req.param('id'), 10); if (Number.isNaN(id)) return c.json({ error: 'Activity not found' }, 404); const parsed = UpdateActivityInput.safeParse(await c.req.json().catch(() => null)); if (!parsed.success) return c.json({ error: 'Invalid input' }, 400); const [row] = await db .update(activities) .set({ name: parsed.data.name, insoleTypes: parsed.data.insole_types }) .where(eq(activities.id, id)) .returning(); if (!row) return c.json({ error: 'Activity not found' }, 404); return c.json(toActivity(row)); }); activitiesRoutes.delete('/api/activities/:id', async (c) => { const sessionUser = await getSessionUser(c); if (!sessionUser) return c.json({ error: 'Unauthorized' }, 401); if (!isAdmin(sessionUser)) return c.json({ error: 'Forbidden' }, 403); const id = Number.parseInt(c.req.param('id'), 10); if (Number.isNaN(id)) return c.json({ error: 'Activity not found' }, 404); // FK has no cascade declared: delete the activity's sessions first, then the activity. await db.delete(workSessions).where(eq(workSessions.activityId, id)); await db.delete(activities).where(eq(activities.id, id)); return c.json({ success: true }); });