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

+286
View File
@@ -0,0 +1,286 @@
import { bigserial, boolean, date, index, integer, jsonb, numeric, pgEnum, pgTable, text, timestamp, uniqueIndex, uuid, type AnyPgColumn } from 'drizzle-orm/pg-core'
import * as vocab from '../../shared/enums.ts'
export const roleEnum = pgEnum('role', vocab.roles)
export const incidentTypeEnum = pgEnum('incident_type', vocab.incidentTypes)
export const severityEnum = pgEnum('severity', vocab.severities)
export const incidentStatusEnum = pgEnum('incident_status', vocab.incidentStatuses)
export const areaKindEnum = pgEnum('area_kind', vocab.areaKinds)
export const employmentTypeEnum = pgEnum('employment_type', vocab.employmentTypes)
export const treatmentLevelEnum = pgEnum('treatment_level', vocab.treatmentLevels)
export const rcaMethodEnum = pgEnum('rca_method', vocab.rcaMethods)
export const rootCauseCategoryEnum = pgEnum('root_cause_category', vocab.rootCauseCategories)
export const actionKindEnum = pgEnum('action_kind', vocab.actionKinds)
export const actionStatusEnum = pgEnum('action_status', vocab.actionStatuses)
const id = () => uuid().primaryKey().defaultRandom()
const timestamptz = () => timestamp({ withTimezone: true })
const createdAt = () => timestamptz().notNull().defaultNow()
const updatedAt = () =>
timestamptz()
.notNull()
.defaultNow()
.$onUpdate(() => new Date())
export const organizations = pgTable('organizations', {
id: id(),
name: text().notNull(),
createdAt: createdAt(),
})
export const users = pgTable(
'users',
{
id: id(),
organizationId: uuid()
.notNull()
.references(() => organizations.id),
/** Stored lower-cased. */
email: text().notNull(),
name: text().notNull(),
role: roleEnum().notNull().default('reporter'),
passwordHash: text(),
active: boolean().notNull().default(true),
lastLoginAt: timestamptz(),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [uniqueIndex('users_email_unique').on(t.email), index('users_organization_idx').on(t.organizationId)],
)
export const sessions = pgTable(
'sessions',
{
/** SHA-256 of the session token; the token itself is never stored. */
id: text().primaryKey(),
userId: uuid()
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
expiresAt: timestamptz().notNull(),
createdAt: createdAt(),
},
(t) => [index('sessions_user_idx').on(t.userId)],
)
export const sites = pgTable(
'sites',
{
id: id(),
organizationId: uuid()
.notNull()
.references(() => organizations.id),
name: text().notNull(),
address: text(),
timezone: text().notNull().default('UTC'),
archivedAt: timestamptz(),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [uniqueIndex('sites_organization_name_unique').on(t.organizationId, t.name)],
)
/** Location tree within a site: building → area → line. */
export const areas = pgTable(
'areas',
{
id: id(),
siteId: uuid()
.notNull()
.references(() => sites.id, { onDelete: 'cascade' }),
parentId: uuid().references((): AnyPgColumn => areas.id, { onDelete: 'cascade' }),
name: text().notNull(),
kind: areaKindEnum().notNull().default('area'),
archivedAt: timestamptz(),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [index('areas_site_idx').on(t.siteId), index('areas_parent_idx').on(t.parentId)],
)
/** Per-organization counter for gap-free, human-friendly incident numbers. */
export const incidentCounters = pgTable('incident_counters', {
organizationId: uuid()
.primaryKey()
.references(() => organizations.id),
lastNumber: integer().notNull().default(0),
})
export const incidents = pgTable(
'incidents',
{
id: id(),
organizationId: uuid()
.notNull()
.references(() => organizations.id),
number: integer().notNull(),
title: text().notNull(),
type: incidentTypeEnum().notNull(),
severity: severityEnum().notNull(),
status: incidentStatusEnum().notNull().default('reported'),
occurredAt: timestamptz().notNull(),
siteId: uuid()
.notNull()
.references(() => sites.id),
areaId: uuid().references(() => areas.id, { onDelete: 'set null' }),
locationDetail: text(),
description: text(),
immediateAction: text(),
/** Null when reported anonymously. */
reporterId: uuid().references(() => users.id, { onDelete: 'set null' }),
anonymous: boolean().notNull().default(false),
closedAt: timestamptz(),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [
uniqueIndex('incidents_organization_number_unique').on(t.organizationId, t.number),
index('incidents_organization_occurred_idx').on(t.organizationId, t.occurredAt),
index('incidents_organization_status_idx').on(t.organizationId, t.status),
index('incidents_reporter_idx').on(t.reporterId),
],
)
export const injuredPersons = pgTable(
'injured_persons',
{
id: id(),
incidentId: uuid()
.notNull()
.references(() => incidents.id, { onDelete: 'cascade' }),
/** Set when the person has a user account. */
userId: uuid().references(() => users.id, { onDelete: 'set null' }),
name: text().notNull(),
jobTitle: text(),
employmentType: employmentTypeEnum().notNull().default('employee'),
bodyPart: text(),
natureOfInjury: text(),
treatment: treatmentLevelEnum().notNull().default('none'),
daysAway: integer().notNull().default(0),
daysRestricted: integer().notNull().default(0),
/** OSHA privacy case: name withheld from the 300 log. */
privacyCase: boolean().notNull().default(false),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [index('injured_persons_incident_idx').on(t.incidentId)],
)
export const attachments = pgTable(
'attachments',
{
id: id(),
incidentId: uuid()
.notNull()
.references(() => incidents.id, { onDelete: 'cascade' }),
fileName: text().notNull(),
contentType: text().notNull(),
sizeBytes: integer().notNull(),
storageKey: text().notNull().unique(),
uploadedById: uuid().references(() => users.id, { onDelete: 'set null' }),
createdAt: createdAt(),
},
(t) => [index('attachments_incident_idx').on(t.incidentId)],
)
export const investigations = pgTable('investigations', {
id: id(),
incidentId: uuid()
.notNull()
.unique()
.references(() => incidents.id, { onDelete: 'cascade' }),
leadId: uuid().references(() => users.id, { onDelete: 'set null' }),
method: rcaMethodEnum().notNull().default('five_whys'),
summary: text(),
startedAt: timestamptz().notNull().defaultNow(),
completedAt: timestamptz(),
createdAt: createdAt(),
updatedAt: updatedAt(),
})
export const rootCauses = pgTable(
'root_causes',
{
id: id(),
investigationId: uuid()
.notNull()
.references(() => investigations.id, { onDelete: 'cascade' }),
category: rootCauseCategoryEnum().notNull(),
description: text().notNull(),
/** 5 Whys chain, first "why" first. */
whys: jsonb().$type<string[]>().notNull().default([]),
createdAt: createdAt(),
},
(t) => [index('root_causes_investigation_idx').on(t.investigationId)],
)
export const correctiveActions = pgTable(
'corrective_actions',
{
id: id(),
incidentId: uuid()
.notNull()
.references(() => incidents.id, { onDelete: 'cascade' }),
rootCauseId: uuid().references(() => rootCauses.id, { onDelete: 'set null' }),
kind: actionKindEnum().notNull().default('corrective'),
title: text().notNull(),
description: text(),
assigneeId: uuid().references(() => users.id, { onDelete: 'set null' }),
dueDate: date({ mode: 'string' }),
status: actionStatusEnum().notNull().default('open'),
completedAt: timestamptz(),
verifiedById: uuid().references(() => users.id, { onDelete: 'set null' }),
verifiedAt: timestamptz(),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [index('corrective_actions_incident_idx').on(t.incidentId), index('corrective_actions_assignee_status_idx').on(t.assigneeId, t.status)],
)
export const comments = pgTable(
'comments',
{
id: id(),
incidentId: uuid()
.notNull()
.references(() => incidents.id, { onDelete: 'cascade' }),
authorId: uuid().references(() => users.id, { onDelete: 'set null' }),
body: text().notNull(),
createdAt: createdAt(),
},
(t) => [index('comments_incident_idx').on(t.incidentId)],
)
export const auditLog = pgTable(
'audit_log',
{
id: bigserial({ mode: 'number' }).primaryKey(),
organizationId: uuid()
.notNull()
.references(() => organizations.id),
actorId: uuid().references(() => users.id, { onDelete: 'set null' }),
entityType: text().notNull(),
entityId: text().notNull(),
action: text().notNull(),
/** `{ field: [before, after] }` for updates, the created record for creates. */
changes: jsonb(),
createdAt: createdAt(),
},
(t) => [index('audit_log_entity_idx').on(t.organizationId, t.entityType, t.entityId)],
)
/** Monthly hours worked per site, for TRIR / DART rate calculations. */
export const hoursWorked = pgTable(
'hours_worked',
{
id: id(),
siteId: uuid()
.notNull()
.references(() => sites.id, { onDelete: 'cascade' }),
/** First day of the month, e.g. `2026-10-01`. */
month: date({ mode: 'string' }).notNull(),
hours: numeric({ precision: 12, scale: 2 }).notNull(),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [uniqueIndex('hours_worked_site_month_unique').on(t.siteId, t.month)],
)