- 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.
74 lines
3.9 KiB
TypeScript
74 lines
3.9 KiB
TypeScript
// Loads demo data into an empty database: `npm run db:seed`. Not for production use.
|
|
import { hashPassword } from '../auth/password.ts'
|
|
import { loadConfig } from '../config.ts'
|
|
import { openDatabase } from './client.ts'
|
|
import { areas, incidentCounters, incidents, organizations, sites, users } from './schema.ts'
|
|
|
|
const DEMO_PASSWORD = 'safetylog-demo'
|
|
|
|
const config = loadConfig()
|
|
const { db, close } = await openDatabase(config.databaseUrl)
|
|
|
|
try {
|
|
if ((await db.select({ id: users.id }).from(users).limit(1)).length > 0) {
|
|
console.error('Database already has users; seeding is only for empty databases.')
|
|
process.exitCode = 1
|
|
} else {
|
|
await db.transaction(async (tx) => {
|
|
const [org] = await tx.insert(organizations).values({ name: 'Northstar Co.' }).returning()
|
|
const passwordHash = await hashPassword(DEMO_PASSWORD)
|
|
const people = await tx
|
|
.insert(users)
|
|
.values([
|
|
{ name: 'Jordan Lee', email: 'admin@example.com', role: 'admin' as const },
|
|
{ name: 'Priya Shah', email: 'manager@example.com', role: 'safety_manager' as const },
|
|
{ name: 'Sam Rivera', email: 'supervisor@example.com', role: 'supervisor' as const },
|
|
{ name: 'Maya Chen', email: 'reporter@example.com', role: 'reporter' as const },
|
|
].map((u) => ({ ...u, organizationId: org.id, passwordHash })))
|
|
.returning()
|
|
const person = (email: string) => people.find((p) => p.email === email)!.id
|
|
|
|
const layout: Record<string, string[]> = {
|
|
'North Campus': ['Building A', 'Warehouse 04', 'Lab 2'],
|
|
'South Campus': ['Fabrication 02', 'Yard', 'Office block'],
|
|
'West Campus': ['Warehouse 01', 'Loading dock'],
|
|
}
|
|
const areaIds: Record<string, { siteId: string; areaId: string }> = {}
|
|
for (const [siteName, buildings] of Object.entries(layout)) {
|
|
const [site] = await tx.insert(sites).values({ organizationId: org.id, name: siteName }).returning()
|
|
for (const name of buildings) {
|
|
const [area] = await tx.insert(areas).values({ siteId: site.id, name, kind: 'building' }).returning()
|
|
areaIds[name] = { siteId: site.id, areaId: area.id }
|
|
}
|
|
}
|
|
|
|
const daysAgo = (days: number) => new Date(Date.now() - days * 86_400_000)
|
|
const demo = [
|
|
{ title: 'Damaged guardrail on mezzanine', type: 'property_damage', severity: 'low', status: 'closed', building: 'Warehouse 01', locationDetail: 'Mezzanine east', reporter: 'manager@example.com', days: 9 },
|
|
{ title: 'Hand laceration during material handling', type: 'injury', severity: 'high', status: 'investigation', building: 'Fabrication 02', locationDetail: 'Line 3', reporter: 'supervisor@example.com', days: 7 },
|
|
{ title: 'Wet floor near employee entrance', type: 'hazard', severity: 'medium', status: 'actions', building: 'Building A', locationDetail: 'Ground floor lobby', reporter: 'reporter@example.com', days: 6 },
|
|
{ title: 'Forklift reversed into pallet stack', type: 'near_miss', severity: 'high', status: 'triage', building: 'Warehouse 04', locationDetail: 'Loading bay C', reporter: 'admin@example.com', days: 5 },
|
|
] as const
|
|
await tx.insert(incidents).values(
|
|
demo.map((item, index) => ({
|
|
organizationId: org.id,
|
|
number: index + 1,
|
|
title: item.title,
|
|
type: item.type,
|
|
severity: item.severity,
|
|
status: item.status,
|
|
occurredAt: daysAgo(item.days),
|
|
...areaIds[item.building],
|
|
locationDetail: item.locationDetail,
|
|
reporterId: person(item.reporter),
|
|
closedAt: item.status === 'closed' ? daysAgo(item.days - 3) : null,
|
|
})),
|
|
)
|
|
await tx.insert(incidentCounters).values({ organizationId: org.id, lastNumber: demo.length })
|
|
})
|
|
console.log(`Seeded demo data. Sign in as admin@example.com, manager@example.com, supervisor@example.com or reporter@example.com with password "${DEMO_PASSWORD}".`)
|
|
}
|
|
} finally {
|
|
await close()
|
|
}
|