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

+73
View File
@@ -0,0 +1,73 @@
// 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()
}