Add OSHA Form 301 and Osha page components
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s

- Implemented Form301 component for displaying OSHA Form 301 equivalent, including employee and case information.
- Created Osha component to manage OSHA recordkeeping, including tabs for 300 Log, 300A Summary, Hours & rates, and Submit to OSHA.
- Added functionality for printing and downloading CSV files for OSHA submissions.
- Integrated loading and error handling states for data fetching.
This commit is contained in:
jayjennings committed 2026-10-06 13:09:57 -07:00
1 parent 3e964d1176
commit 61c876edec
59 files changed
+9180 -183

No files matched your search

+11 -49
View File
@@ -1,12 +1,13 @@
import { and, asc, count, desc, eq, gte, inArray, isNotNull, isNull, lt, ne, sql } from 'drizzle-orm'
import { Hono } from 'hono'
import { z } from 'zod'
import type { AnalyticsStats, OverviewStats } from '../../shared/api-types.ts'
import type { OverviewStats } from '../../shared/api-types.ts'
import { incidentStatuses, type IncidentStatus } from '../../shared/enums.ts'
import { requireAuth } from '../auth/middleware.ts'
import { correctiveActions, incidents, investigations, rootCauses, sites } from '../db/schema.ts'
import { correctiveActions, incidents, sites } from '../db/schema.ts'
import { addDays, localDate } from '../lib/dates.ts'
import { currentUser, validate, type AppEnv } from '../lib/http.ts'
import { computeAnalytics } from '../lib/analytics.ts'
import { currentUser, fail, validate, type AppEnv } from '../lib/http.ts'
import { analyticsQuery } from '../../shared/schemas.ts'
import { visibleTo } from '../lib/incidents.ts'
const DAY_MS = 86_400_000
@@ -97,49 +98,10 @@ export const statsRoutes = new Hono<AppEnv>()
return c.json(stats)
})
.get('/analytics', requireAuth, validate('query', z.object({ days: z.coerce.number().int().min(7).max(730).default(90) })), async (c) => {
const me = currentUser(c)
const db = c.get('db')
const { days } = c.req.valid('query')
const since = new Date(Date.now() - days * DAY_MS)
const inPeriod = and(visibleTo(db, me), gte(incidents.occurredAt, since))
const byType = await db.select({ type: incidents.type, count: count() }).from(incidents).where(inPeriod).groupBy(incidents.type).orderBy(desc(count()))
const bySeverity = await db.select({ severity: incidents.severity, count: count() }).from(incidents).where(inPeriod).groupBy(incidents.severity)
const causes = await db
.select({ category: rootCauses.category, count: count() })
.from(rootCauses)
.innerJoin(investigations, eq(investigations.id, rootCauses.investigationId))
.innerJoin(incidents, eq(incidents.id, investigations.incidentId))
.where(inPeriod)
.groupBy(rootCauses.category)
.orderBy(desc(count()))
const totalCauses = causes.reduce((sum, row) => sum + row.count, 0)
const monthExpr = sql<string>`to_char(${correctiveActions.completedAt}, 'YYYY-MM')`
const onTimeExpr = sql<boolean>`(${correctiveActions.completedAt})::date <= ${correctiveActions.dueDate}`
const completed = await db
.select({ month: monthExpr, onTime: onTimeExpr, count: count() })
.from(correctiveActions)
.innerJoin(incidents, eq(incidents.id, correctiveActions.incidentId))
.where(and(visibleTo(db, me), isNotNull(correctiveActions.completedAt), isNotNull(correctiveActions.dueDate), gte(correctiveActions.completedAt, since)))
.groupBy(monthExpr, onTimeExpr)
const onTime = completed.filter((row) => row.onTime).reduce((sum, row) => sum + row.count, 0)
const allCompleted = completed.reduce((sum, row) => sum + row.count, 0)
const months = [...new Set(completed.map((row) => row.month))].sort()
const stats: AnalyticsStats = {
periodDays: days,
total: byType.reduce((sum, row) => sum + row.count, 0),
byType,
bySeverity,
topRootCause: causes[0] ? { category: causes[0].category, count: causes[0].count, share: causes[0].count / totalCauses } : null,
onTimeActionRate: allCompleted ? onTime / allCompleted : null,
monthlyCompletedActions: months.map((month) => ({
month,
onTime: completed.filter((row) => row.month === month && row.onTime).reduce((sum, row) => sum + row.count, 0),
late: completed.filter((row) => row.month === month && !row.onTime).reduce((sum, row) => sum + row.count, 0),
})),
}
return c.json(stats)
.get('/analytics', requireAuth, validate('query', analyticsQuery), async (c) => {
const filters = c.req.valid('query')
if (filters.from > filters.to) throw fail(400, 'The start date must be before the end date')
const days = (Date.parse(filters.to) - Date.parse(filters.from)) / 86_400_000
if (days > 3 * 366) throw fail(400, 'Choose a range of up to three years')
return c.json(await computeAnalytics(c.get('db'), currentUser(c), filters))
})