Files
jayjennings 3f6dff5f24
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s
feat: implement permissions and actions management
- Add permissions logic for status options in corrective actions.
- Create tests for permissions functionality.
- Implement queries for user authentication and actions management.
- Develop Actions page to display and manage corrective actions.
- Create authentication pages for login, setup, and password reset.
- Add IncidentDetail page to view incident details and associated actions.
- Implement Settings page for user management and password changes.
2026-10-06 11:20:44 -07:00

88 lines
4.7 KiB
TypeScript

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<AppEnv>()
.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) })
})