import { and, asc, eq, inArray, lt, sql } from 'drizzle-orm' import { Hono } from 'hono' import { z } from 'zod' import { hasRole } from '../../shared/enums.ts' import { actionListQuery, updateActionSchema } from '../../shared/schemas.ts' import { requireAuth } from '../auth/middleware.ts' import { correctiveActions, incidents, investigations, rootCauses } from '../db/schema.ts' import { diff } from '../lib/audit.ts' import { localDate } from '../lib/dates.ts' import { currentUser, fail, validate, type AppEnv } from '../lib/http.ts' import { assertActiveUser, auditIncident, selectActions, toAction } from '../lib/incidents.ts' const idParam = z.object({ id: z.uuid() }) const OPEN = ['open', 'in_progress'] as const export const actionRoutes = new Hono() .get('/', requireAuth, validate('query', actionListQuery), async (c) => { const me = currentUser(c) const { scope, filter } = c.req.valid('query') if (scope === 'all' && !hasRole(me.role, 'supervisor')) throw fail(403, 'You do not have permission to do that') const today = localDate() const rows = await selectActions(c.get('db')) .where( and( eq(incidents.organizationId, me.organizationId), scope === 'mine' ? eq(correctiveActions.assigneeId, me.id) : undefined, filter === 'open' ? inArray(correctiveActions.status, OPEN) : undefined, filter === 'overdue' ? and(inArray(correctiveActions.status, OPEN), lt(correctiveActions.dueDate, today)) : undefined, filter === 'done' ? inArray(correctiveActions.status, ['done', 'verified']) : undefined, ), ) .orderBy(sql`${correctiveActions.dueDate} asc nulls last`, asc(correctiveActions.createdAt)) .limit(500) return c.json({ actions: rows.map((row) => toAction(row, today)) }) }) .patch('/:id', requireAuth, validate('param', idParam), validate('json', updateActionSchema), async (c) => { const me = currentUser(c) const db = c.get('db') const [row] = await selectActions(db).where(and(eq(correctiveActions.id, c.req.valid('param').id), eq(incidents.organizationId, me.organizationId))) if (!row) throw fail(404, 'Action not found') const { action } = row const input = c.req.valid('json') const isManager = hasRole(me.role, 'supervisor') const isAssignee = action.assigneeId === me.id if (!isManager) { // Assignees can report progress on their own actions, nothing else. const onlyStatus = Object.keys(input).every((key) => key === 'status') if (!isAssignee || !onlyStatus || (input.status && !['open', 'in_progress', 'done'].includes(input.status))) { throw fail(403, 'You can only update the progress of actions assigned to you') } } if (input.status === 'verified' && !hasRole(me.role, 'safety_manager')) throw fail(403, 'Only safety managers can verify actions') if (action.status === 'verified' && input.status && input.status !== 'verified' && !hasRole(me.role, 'safety_manager')) { throw fail(403, 'Only safety managers can reopen a verified action') } if (input.assigneeId) await assertActiveUser(db, me.organizationId, input.assigneeId) if (input.rootCauseId) { const [cause] = await db .select({ id: rootCauses.id }) .from(rootCauses) .innerJoin(investigations, eq(investigations.id, rootCauses.investigationId)) .where(and(eq(rootCauses.id, input.rootCauseId), eq(investigations.incidentId, action.incidentId))) if (!cause) throw fail(400, 'Root cause must belong to this incident') } const now = new Date() const status = input.status ?? action.status const update = { ...input, completedAt: status === 'done' || status === 'verified' ? (action.completedAt ?? now) : null, verifiedAt: status === 'verified' ? (action.verifiedAt ?? now) : null, verifiedById: status === 'verified' ? (action.verifiedById ?? me.id) : null, // A new due date deserves a fresh reminder reminderSentOn: input.dueDate !== undefined && input.dueDate !== action.dueDate ? null : undefined, } const changes = diff(action, update) delete changes.reminderSentOn if (Object.keys(changes).length > 0) { await db.update(correctiveActions).set(update).where(eq(correctiveActions.id, action.id)) await auditIncident(db, me, me.organizationId, action.incidentId, 'action.update', { actionId: action.id, title: action.title, ...changes }) if (input.assigneeId && input.assigneeId !== action.assigneeId && input.assigneeId !== me.id) c.get('notifier').actionAssigned(action.id, me.name) } const [updated] = await selectActions(db).where(eq(correctiveActions.id, action.id)) return c.json({ action: toAction(updated) }) })