Files
ims/lib/db/schema.ts
T

235 lines
11 KiB
TypeScript

import {
pgTable,
pgEnum,
uuid,
text,
boolean,
timestamp,
smallint,
integer,
date,
jsonb,
customType,
} from 'drizzle-orm/pg-core'
// ---------------------------------------------------------------------------
// Custom type: pgvector
// ---------------------------------------------------------------------------
const vector = customType<{ data: number[]; driverData: string }>({
dataType(config) {
return `vector(${(config as { dimensions?: number }).dimensions ?? 1536})`
},
})
// ---------------------------------------------------------------------------
// Enums
// ---------------------------------------------------------------------------
export const userRoleEnum = pgEnum('user_role', [
'reporter', 'supervisor', 'hse', 'capa_owner', 'management', 'admin',
])
export const incidentTypeEnum = pgEnum('incident_type', [
'injury', 'near_miss', 'hazard', 'asset_damage', 'environmental',
'security', 'fire', 'transport',
])
export const incidentStatusEnum = pgEnum('incident_status', [
'reported', 'triaged', 'investigating', 'capa_pending', 'verification', 'closed',
])
export const medicalStatusEnum = pgEnum('medical_status', [
'none', 'first_aid', 'medical_treatment', 'lti',
])
export const evidenceStageEnum = pgEnum('evidence_stage', [
'report', 'response', 'investigation', 'capa', 'verification',
])
export const rcaMethodEnum = pgEnum('rca_method', ['five_why', 'fishbone', 'other'])
export const capaPriorityEnum = pgEnum('capa_priority', ['low', 'med', 'high'])
export const capaStatusEnum = pgEnum('capa_status', [
'open', 'in_progress', 'overdue', 'pending_verification',
'verified', 'reopened', 'closed',
])
export const doshFormTypeEnum = pgEnum('dosh_form_type', ['jkkp6', 'jkkp7', 'jkkp8'])
export const doshStatusEnum = pgEnum('dosh_status', ['not_required', 'pending', 'submitted'])
export const notificationChannelEnum = pgEnum('notification_channel', [
'email', 'whatsapp', 'in_app',
])
// ---------------------------------------------------------------------------
// Tables (dependency order)
// ---------------------------------------------------------------------------
export const sites = pgTable('sites', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
address: text('address'),
region: text('region'),
active: boolean('active').notNull().default(true),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const zones = pgTable('zones', {
id: uuid('id').primaryKey().defaultRandom(),
siteId: uuid('site_id').notNull().references(() => sites.id),
name: text('name').notNull(),
qrCodeToken: text('qr_code_token').notNull().unique(),
active: boolean('active').notNull().default(true),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull().default(''),
email: text('email').notNull().default('').unique(),
phone: text('phone'),
role: userRoleEnum('role').notNull().default('reporter'),
department: text('department'),
siteId: uuid('site_id').references(() => sites.id),
active: boolean('active').notNull().default(true),
passwordHash: text('password_hash').notNull().default(''),
emailVerifiedAt: timestamp('email_verified_at', { withTimezone: true }),
lastLoginAt: timestamp('last_login_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const trucks = pgTable('trucks', {
id: uuid('id').primaryKey().defaultRandom(),
truckNo: text('truck_no').notNull().unique(),
carrier: text('carrier'),
active: boolean('active').notNull().default(true),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const incidents = pgTable('incidents', {
id: uuid('id').primaryKey().defaultRandom(),
referenceNo: text('reference_no').unique(),
incidentType: incidentTypeEnum('incident_type').notNull(),
siteId: uuid('site_id').notNull().references(() => sites.id),
zoneId: uuid('zone_id').references(() => zones.id),
truckId: uuid('truck_id').references(() => trucks.id),
reportedBy: uuid('reported_by').notNull().references(() => users.id),
reportedAt: timestamp('reported_at', { withTimezone: true }).notNull().defaultNow(),
description: text('description').notNull(),
severity: smallint('severity'),
status: incidentStatusEnum('status').notNull().default('reported'),
injuryInvolved: boolean('injury_involved').notNull().default(false),
assetInvolved: boolean('asset_involved').notNull().default(false),
medicalStatus: medicalStatusEnum('medical_status'),
lostDays: integer('lost_days'),
isFatality: boolean('is_fatality').notNull().default(false),
isSeriousBodilyInjury: boolean('is_serious_bodily_injury').notNull().default(false),
isDangerousOccurrence: boolean('is_dangerous_occurrence').notNull().default(false),
isOccupationalDisease: boolean('is_occupational_disease').notNull().default(false),
triageNotes: text('triage_notes'),
triagedBy: uuid('triaged_by').references(() => users.id),
triagedAt: timestamp('triaged_at', { withTimezone: true }),
typeDetails: jsonb('type_details'),
embedding: vector('embedding', { dimensions: 768 }),
closedAt: timestamp('closed_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const evidenceFiles = pgTable('evidence_files', {
id: uuid('id').primaryKey().defaultRandom(),
incidentId: uuid('incident_id').notNull().references(() => incidents.id),
stage: evidenceStageEnum('stage').notNull(),
fileUrl: text('file_url').notNull(),
fileType: text('file_type').notNull(),
fileHash: text('file_hash').notNull(),
uploadedBy: uuid('uploaded_by').notNull().references(() => users.id),
uploadedAt: timestamp('uploaded_at', { withTimezone: true }).notNull().defaultNow(),
deleted: boolean('deleted').notNull().default(false),
})
export const investigations = pgTable('investigations', {
id: uuid('id').primaryKey().defaultRandom(),
incidentId: uuid('incident_id').notNull().references(() => incidents.id),
investigatorId: uuid('investigator_id').notNull().references(() => users.id),
method: rcaMethodEnum('method').notNull().default('five_why'),
findingsText: text('findings_text'),
rootCauseSummary: text('root_cause_summary'),
alcoholTestResult: text('alcohol_test_result'),
urineTestResult: text('urine_test_result'),
witnessStatementRefs: text('witness_statement_refs').array(),
fiveWhySteps: jsonb('five_why_steps'),
fishboneCategories: jsonb('fishbone_categories'),
completedAt: timestamp('completed_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const capaActions = pgTable('capa_actions', {
id: uuid('id').primaryKey().defaultRandom(),
incidentId: uuid('incident_id').notNull().references(() => incidents.id),
rootCauseRef: text('root_cause_ref'),
description: text('description').notNull(),
ownerUserId: uuid('owner_user_id').notNull().references(() => users.id),
department: text('department').notNull(),
dueDate: date('due_date').notNull(),
priority: capaPriorityEnum('priority').notNull().default('med'),
status: capaStatusEnum('status').notNull().default('open'),
completedAt: timestamp('completed_at', { withTimezone: true }),
verifiedBy: uuid('verified_by').references(() => users.id),
verifiedAt: timestamp('verified_at', { withTimezone: true }),
effectivenessRecheckDate: date('effectiveness_recheck_date'),
effectivenessRecheckRound: integer('effectiveness_recheck_round').notNull().default(0),
ownerNotes: text('owner_notes'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const doshReports = pgTable('dosh_reports', {
id: uuid('id').primaryKey().defaultRandom(),
incidentId: uuid('incident_id').notNull().references(() => incidents.id),
formType: doshFormTypeEnum('form_type').notNull(),
status: doshStatusEnum('status').notNull().default('not_required'),
submittedAt: timestamp('submitted_at', { withTimezone: true }),
submittedBy: uuid('submitted_by').references(() => users.id),
fileUrl: text('file_url'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const notificationsLog = pgTable('notifications_log', {
id: uuid('id').primaryKey().defaultRandom(),
incidentId: uuid('incident_id').references(() => incidents.id),
capaId: uuid('capa_id').references(() => capaActions.id),
channel: notificationChannelEnum('channel').notNull(),
recipient: text('recipient').notNull(),
recipientUserId: uuid('recipient_user_id').references(() => users.id),
readAt: timestamp('read_at', { withTimezone: true }),
title: text('title'),
link: text('link'),
sentAt: timestamp('sent_at', { withTimezone: true }).notNull().defaultNow(),
status: text('status').notNull().default('sent'),
})
export const auditLog = pgTable('audit_log', {
id: uuid('id').primaryKey().defaultRandom(),
tableName: text('table_name').notNull(),
recordId: uuid('record_id').notNull(),
action: text('action').notNull(),
changedBy: uuid('changed_by').references(() => users.id),
changedAt: timestamp('changed_at', { withTimezone: true }).notNull().defaultNow(),
oldValue: jsonb('old_value'),
newValue: jsonb('new_value'),
})
export const appSettings = pgTable('app_settings', {
key: text('key').primaryKey(),
value: text('value').notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
updatedBy: uuid('updated_by').references(() => users.id),
})
export const incidentAddenda = pgTable('incident_addenda', {
id: uuid('id').primaryKey().defaultRandom(),
incidentId: uuid('incident_id').notNull().references(() => incidents.id),
author: uuid('author').notNull().references(() => users.id),
body: text('body').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})