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().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)], )