feat(api): orderable activities + admin reorder endpoint

This commit is contained in:
Bas van Rossem
2026-06-17 20:58:06 +02:00
parent 974ecb120d
commit 56e0162230
2 changed files with 155 additions and 4 deletions

View File

@@ -1,6 +1,6 @@
import { Hono } from 'hono';
import { eq, asc } from 'drizzle-orm';
import { CreateActivityInput, UpdateActivityInput } from '@solelog/shared';
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';
@@ -24,7 +24,10 @@ 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.name));
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))
@@ -40,13 +43,47 @@ activitiesRoutes.post('/api/activities', async (c) => {
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<number>`COALESCE(MAX(${activities.sortOrder}), -1)` })
.from(activities);
const [row] = await db
.insert(activities)
.values({ name: parsed.data.name, insoleTypes: parsed.data.insole_types })
.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);