import { and, asc, eq, inArray, isNull } from 'drizzle-orm' import { Hono } from 'hono' import { z } from 'zod' import { createAreaSchema, createSiteSchema, updateAreaSchema, updateSiteSchema } from '../../shared/schemas.ts' import { requireAuth, requireRole } from '../auth/middleware.ts' import { isUniqueViolation, type Db } from '../db/client.ts' import { areas, sites } from '../db/schema.ts' import { audit, diff } from '../lib/audit.ts' import { currentUser, fail, validate, type AppEnv } from '../lib/http.ts' const idParam = z.object({ id: z.uuid() }) const listQuery = z.object({ includeArchived: z.enum(['true', 'false']).optional() }) async function findSite(db: Db, organizationId: string, id: string) { const [site] = await db.select().from(sites).where(and(eq(sites.id, id), eq(sites.organizationId, organizationId))) if (!site) throw fail(404, 'Site not found') return site } const archivedAt = (archived: boolean | undefined) => (archived === undefined ? undefined : archived ? new Date() : null) export const siteRoutes = new Hono() .get('/', requireAuth, validate('query', listQuery), async (c) => { const me = currentUser(c) const includeArchived = c.req.valid('query').includeArchived === 'true' const db = c.get('db') const siteRows = await db .select() .from(sites) .where(and(eq(sites.organizationId, me.organizationId), includeArchived ? undefined : isNull(sites.archivedAt))) .orderBy(asc(sites.name)) const areaRows = siteRows.length ? await db .select() .from(areas) .where(and(inArray(areas.siteId, siteRows.map((site) => site.id)), includeArchived ? undefined : isNull(areas.archivedAt))) .orderBy(asc(areas.name)) : [] return c.json({ sites: siteRows.map((site) => ({ ...site, areas: areaRows.filter((area) => area.siteId === site.id) })) }) }) .post('/', requireRole('safety_manager'), validate('json', createSiteSchema), async (c) => { const me = currentUser(c) const db = c.get('db') try { const [site] = await db .insert(sites) .values({ ...c.req.valid('json'), organizationId: me.organizationId }) .returning() await audit(db, { organizationId: me.organizationId, actorId: me.id, entityType: 'site', entityId: site.id, action: 'create', changes: { name: site.name } }) return c.json({ site: { ...site, areas: [] } }, 201) } catch (error) { if (isUniqueViolation(error)) throw fail(409, 'A site with that name already exists') throw error } }) .patch('/:id', requireRole('safety_manager'), validate('param', idParam), validate('json', updateSiteSchema), async (c) => { const me = currentUser(c) const db = c.get('db') const site = await findSite(db, me.organizationId, c.req.valid('param').id) const { archived, ...fields } = c.req.valid('json') const update = { ...fields, archivedAt: archivedAt(archived) } const changes = diff(site, update) if (Object.keys(changes).length === 0) return c.json({ site }) try { const [updated] = await db.update(sites).set(update).where(eq(sites.id, site.id)).returning() await audit(db, { organizationId: me.organizationId, actorId: me.id, entityType: 'site', entityId: site.id, action: 'update', changes }) return c.json({ site: updated }) } catch (error) { if (isUniqueViolation(error)) throw fail(409, 'A site with that name already exists') throw error } }) .post('/:id/areas', requireRole('safety_manager'), validate('param', idParam), validate('json', createAreaSchema), async (c) => { const me = currentUser(c) const db = c.get('db') const site = await findSite(db, me.organizationId, c.req.valid('param').id) const input = c.req.valid('json') if (input.parentId) { const [parent] = await db.select({ id: areas.id }).from(areas).where(and(eq(areas.id, input.parentId), eq(areas.siteId, site.id))) if (!parent) throw fail(400, 'Parent area must belong to the same site') } const [area] = await db .insert(areas) .values({ ...input, siteId: site.id }) .returning() await audit(db, { organizationId: me.organizationId, actorId: me.id, entityType: 'area', entityId: area.id, action: 'create', changes: { name: area.name, siteId: site.id } }) return c.json({ area }, 201) }) export const areaRoutes = new Hono().patch('/:id', requireRole('safety_manager'), validate('param', idParam), validate('json', updateAreaSchema), async (c) => { const me = currentUser(c) const db = c.get('db') const [row] = await db .select({ area: areas }) .from(areas) .innerJoin(sites, eq(sites.id, areas.siteId)) .where(and(eq(areas.id, c.req.valid('param').id), eq(sites.organizationId, me.organizationId))) if (!row) throw fail(404, 'Area not found') const { archived, ...fields } = c.req.valid('json') const update = { ...fields, archivedAt: archivedAt(archived) } const changes = diff(row.area, update) if (Object.keys(changes).length === 0) return c.json({ area: row.area }) const [area] = await db.update(areas).set(update).where(eq(areas.id, row.area.id)).returning() await audit(db, { organizationId: me.organizationId, actorId: me.id, entityType: 'area', entityId: area.id, action: 'update', changes }) return c.json({ area }) })