feat: implement incident reporting and management system
- 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:
1 parent
a382fa904a
commit
c69eccb5c1
47 files changed
+6297
-45
No files matched your search
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user