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) export const authTokenPurposeEnum = pgEnum('auth_token_purpose', ['magic_link', 'password_reset']) export const reportChannelEnum = pgEnum('report_channel', ['web', 'kiosk']) export const oshaCaseTypeEnum = pgEnum('osha_case_type', vocab.oshaCaseTypes) export const oshaOutcomeEnum = pgEnum('osha_outcome', vocab.oshaOutcomes) export const oshaSpecialCaseEnum = pgEnum('osha_special_case', vocab.oshaSpecialCases) 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)], ) /** Single-use links sent by email (magic sign-in, password reset/invite). */ export const authTokens = pgTable( 'auth_tokens', { /** SHA-256 of the token sent in the link. */ id: text().primaryKey(), userId: uuid() .notNull() .references(() => users.id, { onDelete: 'cascade' }), purpose: authTokenPurposeEnum().notNull(), expiresAt: timestamptz().notNull(), usedAt: timestamptz(), createdAt: createdAt(), }, (t) => [index('auth_tokens_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'), // OSHA establishment details (Form 300A and the ITA upload) streetAddress: text(), city: text(), state: text(), zip: text(), naicsCode: text(), industryDescription: text(), ein: text(), /** ITA establishment_type: 1 private, 2 state government, 3 local government. */ establishmentType: integer().notNull().default(1), 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)], ) /** * Reporting links let people without accounts file reports: on a shared kiosk tablet, or by scanning * a QR code. The token only allows creating reports, so it is stored as-is to let admins reprint QR codes. */ export const kiosks = pgTable( 'kiosks', { id: id(), organizationId: uuid() .notNull() .references(() => organizations.id), name: text().notNull(), token: text().notNull(), /** Limits the link to one site; null allows any. */ siteId: uuid().references(() => sites.id, { onDelete: 'set null' }), createdById: uuid().references(() => users.id, { onDelete: 'set null' }), lastUsedAt: timestamptz(), revokedAt: timestamptz(), createdAt: createdAt(), }, (t) => [uniqueIndex('kiosks_token_unique').on(t.token), index('kiosks_organization_idx').on(t.organizationId)], ) /** 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), /** Name typed by someone reporting without an account (kiosk/QR). */ reporterName: text(), channel: reportChannelEnum().notNull().default('web'), kioskId: uuid().references(() => kiosks.id, { onDelete: 'set null' }), /** Generated on the reporting device so retries after a lost response don't create duplicates. */ clientId: uuid(), closedAt: timestamptz(), createdAt: createdAt(), updatedAt: updatedAt(), }, (t) => [ uniqueIndex('incidents_organization_number_unique').on(t.organizationId, t.number), uniqueIndex('incidents_organization_client_id_unique').on(t.organizationId, t.clientId), 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), // OSHA recordability (29 CFR 1904). Null answers mean "not assessed yet". caseNumber: text(), workRelated: boolean(), newCase: boolean(), died: boolean().notNull().default(false), dateOfDeath: date({ mode: 'string' }), daysAwayFromWork: boolean().notNull().default(false), restrictedOrTransfer: boolean().notNull().default(false), medicalTreatment: boolean().notNull().default(false), lossOfConsciousness: boolean().notNull().default(false), significantDiagnosis: boolean().notNull().default(false), specialCase: oshaSpecialCaseEnum().notNull().default('none'), caseType: oshaCaseTypeEnum().notNull().default('injury'), /** Derived from the answers above with shared/osha.ts, stored for reporting. */ recordable: boolean().notNull().default(false), outcome: oshaOutcomeEnum(), // Severe injury reporting (1904.39) inpatientHospitalization: boolean().notNull().default(false), amputation: boolean().notNull().default(false), lossOfEye: boolean().notNull().default(false), reportedToOshaAt: timestamptz(), // Form 301 details employeeAddress: text(), dateOfBirth: date({ mode: 'string' }), dateOfHire: date({ mode: 'string' }), sex: text(), physicianName: text(), treatmentFacility: text(), treatedInEmergencyRoom: boolean().notNull().default(false), timeStartedWork: text(), activityBefore: text(), whatHappened: text(), objectSubstance: text(), 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(), /** Date the last due/overdue reminder was emailed, so it goes out at most once a day. */ reminderSentOn: date({ mode: 'string' }), 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(), /** Average number of employees on the payroll that month (for the annual average). */ averageEmployees: integer(), createdAt: createdAt(), updatedAt: updatedAt(), }, (t) => [uniqueIndex('hours_worked_site_month_unique').on(t.siteId, t.month)], ) /** Named filter sets for the Analytics and Incidents pages; private, or shared with the organization. */ export const savedViews = pgTable( 'saved_views', { id: id(), organizationId: uuid() .notNull() .references(() => organizations.id), userId: uuid() .notNull() .references(() => users.id, { onDelete: 'cascade' }), page: text().notNull(), name: text().notNull(), filters: jsonb().$type>().notNull(), shared: boolean().notNull().default(false), createdAt: createdAt(), }, (t) => [index('saved_views_organization_page_idx').on(t.organizationId, t.page)], )