feat: implement incident reporting and management system
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s

- Add incident reporting routes and logic to handle creation, updating, and retrieval of incidents.
- Implement visibility rules for users based on roles (reporters, supervisors).
- Create tests for incident reporting functionality to ensure correctness and reliability.
- Introduce site and area management routes, including validation for area hierarchy.
- Establish user management routes with role-based access control for creating and updating users.
- Add setup routes for initializing the application with an organization and admin user.
- Implement shared enums and schemas for consistent validation and data handling across the application.
- Create a test helper for setting up an in-memory database and API client for testing.
- Add TypeScript configuration for server-side code with strict type checking and module resolution.
This commit is contained in:
jayjennings committed 2026-10-06 10:51:18 -07:00
1 parent a382fa904a
commit c69eccb5c1
47 files changed
+6297 -45

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
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<AppEnv>()
.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<AppEnv>().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 })
})