Add OSHA Form 301 and Osha page components
- 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:
1 parent
3e964d1176
commit
61c876edec
59 files changed
+9180
-183
No files matched your search
+11
-49
@@ -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))
|
||||
})
|
||||
Reference in new issue
Block a user