- 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.
351 lines
9.1 KiB
TypeScript
351 lines
9.1 KiB
TypeScript
// Response shapes returned by the API. Dates are ISO 8601 strings.
|
|
import type { SummaryTotals } from './osha.ts'
|
|
import type {
|
|
actionKinds,
|
|
actionStatuses,
|
|
AreaKind,
|
|
employmentTypes,
|
|
IncidentStatus,
|
|
OshaCaseType,
|
|
OshaOutcome,
|
|
OshaSpecialCase,
|
|
IncidentType,
|
|
rcaMethods,
|
|
Role,
|
|
rootCauseCategories,
|
|
Severity,
|
|
} from './enums.ts'
|
|
|
|
export type ActionKind = (typeof actionKinds)[number]
|
|
export type ActionStatus = (typeof actionStatuses)[number]
|
|
export type RcaMethod = (typeof rcaMethods)[number]
|
|
export type RootCauseCategory = (typeof rootCauseCategories)[number]
|
|
|
|
export interface UserRef {
|
|
id: string
|
|
name: string
|
|
}
|
|
|
|
export interface User {
|
|
id: string
|
|
email: string
|
|
name: string
|
|
role: Role
|
|
active: boolean
|
|
lastLoginAt: string | null
|
|
createdAt: string
|
|
}
|
|
|
|
export interface Organization {
|
|
id: string
|
|
name: string
|
|
}
|
|
|
|
export interface Area {
|
|
id: string
|
|
siteId: string
|
|
parentId: string | null
|
|
name: string
|
|
kind: AreaKind
|
|
archivedAt: string | null
|
|
}
|
|
|
|
export interface Establishment {
|
|
streetAddress: string | null
|
|
city: string | null
|
|
state: string | null
|
|
zip: string | null
|
|
naicsCode: string | null
|
|
industryDescription: string | null
|
|
ein: string | null
|
|
establishmentType: number
|
|
}
|
|
|
|
export interface Site extends Partial<Establishment> {
|
|
id: string
|
|
name: string
|
|
address: string | null
|
|
timezone: string
|
|
archivedAt: string | null
|
|
areas: Area[]
|
|
}
|
|
|
|
export interface Incident {
|
|
id: string
|
|
number: number
|
|
reference: string
|
|
title: string
|
|
type: IncidentType
|
|
severity: Severity
|
|
status: IncidentStatus
|
|
occurredAt: string
|
|
reportedAt: string
|
|
updatedAt: string
|
|
closedAt: string | null
|
|
site: { id: string; name: string }
|
|
area: { id: string; name: string } | null
|
|
locationDetail: string | null
|
|
description: string | null
|
|
immediateAction: string | null
|
|
anonymous: boolean
|
|
reporter: UserRef | null
|
|
/** Name given by someone reporting without an account. */
|
|
reporterName: string | null
|
|
/** How the report arrived; `kiosk` covers kiosk tablets and QR codes on reporting links. */
|
|
channel: 'web' | 'kiosk'
|
|
kiosk: { id: string; name: string } | null
|
|
}
|
|
|
|
export interface Kiosk {
|
|
id: string
|
|
name: string
|
|
token: string
|
|
site: { id: string; name: string } | null
|
|
lastUsedAt: string | null
|
|
revokedAt: string | null
|
|
createdAt: string
|
|
}
|
|
|
|
/** What a reporting link may see: just enough to fill in the report form. */
|
|
export interface KioskConfig {
|
|
organization: { name: string }
|
|
kiosk: { name: string }
|
|
sites: Site[]
|
|
}
|
|
|
|
export interface Investigation {
|
|
id: string
|
|
lead: UserRef | null
|
|
method: RcaMethod
|
|
summary: string | null
|
|
startedAt: string
|
|
completedAt: string | null
|
|
}
|
|
|
|
export interface RootCause {
|
|
id: string
|
|
category: RootCauseCategory
|
|
description: string
|
|
whys: string[]
|
|
}
|
|
|
|
export interface CorrectiveAction {
|
|
id: string
|
|
incident: { number: number; reference: string; title: string }
|
|
rootCauseId: string | null
|
|
kind: ActionKind
|
|
title: string
|
|
description: string | null
|
|
assignee: UserRef | null
|
|
dueDate: string | null
|
|
status: ActionStatus
|
|
overdue: boolean
|
|
completedAt: string | null
|
|
verifiedBy: UserRef | null
|
|
verifiedAt: string | null
|
|
createdAt: string
|
|
}
|
|
|
|
export interface Attachment {
|
|
id: string
|
|
fileName: string
|
|
contentType: string
|
|
sizeBytes: number
|
|
uploadedBy: UserRef | null
|
|
createdAt: string
|
|
url: string
|
|
}
|
|
|
|
export interface TransitionOption {
|
|
to: IncidentStatus
|
|
label: string
|
|
/** Why the transition can't happen yet; absent when it is allowed. */
|
|
blockedReason?: string
|
|
}
|
|
|
|
export interface IncidentDetail extends Incident {
|
|
investigation: Investigation | null
|
|
rootCauses: RootCause[]
|
|
actions: CorrectiveAction[]
|
|
attachments: Attachment[]
|
|
transitions: TransitionOption[]
|
|
/** Number of injured or ill people recorded; details are only for safety managers. */
|
|
injuredCount: number
|
|
/** What the signed-in user may do on this incident. */
|
|
can: { edit: boolean; manageInvestigation: boolean; manageActions: boolean; manageOsha: boolean }
|
|
}
|
|
|
|
export interface Comment {
|
|
id: string
|
|
author: UserRef | null
|
|
body: string
|
|
createdAt: string
|
|
}
|
|
|
|
export interface TimelineEntry {
|
|
id: number
|
|
at: string
|
|
actor: UserRef | null
|
|
action: string
|
|
changes: Record<string, unknown> | null
|
|
}
|
|
|
|
export interface Paginated<T> {
|
|
items: T[]
|
|
page: number
|
|
pageSize: number
|
|
total: number
|
|
}
|
|
|
|
export interface OverviewStats {
|
|
openIncidents: number
|
|
reportedLast7Days: number
|
|
actionsDueThisWeek: number
|
|
overdueActions: number
|
|
/** Average days from report to close for incidents closed in the last 90 days. */
|
|
averageCloseDays: number | null
|
|
byStatus: Record<IncidentStatus, number>
|
|
actionProgress: { completed: number; total: number }
|
|
/** Incidents per week for the last 12 weeks, oldest first. */
|
|
weeklyTrend: { weekStart: string; count: number }[]
|
|
bySite: { siteId: string; name: string; count: number }[]
|
|
}
|
|
|
|
/** A number for the selected period and the same-length period before it. */
|
|
export interface Kpi {
|
|
value: number | null
|
|
previous: number | null
|
|
}
|
|
|
|
/** Incident types grouped for trend charts. */
|
|
export type TypeGroup = 'hazards' | 'injuries' | 'damage'
|
|
|
|
export interface AnalyticsStats {
|
|
from: string
|
|
to: string
|
|
previousFrom: string
|
|
bucket: 'week' | 'month'
|
|
kpis: {
|
|
reports: Kpi
|
|
/** Leading: hazards and near misses reported. */
|
|
leadingReports: Kpi
|
|
injuries: Kpi
|
|
/** Leading: hazards and near misses per injury or illness. */
|
|
nearMissRatio: Kpi
|
|
/** Leading: share of reports moved out of "reported" within 24 hours. */
|
|
triagedWithin24h: Kpi
|
|
/** Leading: share of actions completed by their due date. */
|
|
actionsOnTime: Kpi
|
|
averageDaysToClose: Kpi
|
|
/** Lagging: OSHA-recordable cases (supervisors and above). */
|
|
recordables: Kpi | null
|
|
overdueActions: number
|
|
}
|
|
trend: ({ start: string } & Record<TypeGroup, number>)[]
|
|
byLocation: { id: string | null; name: string; count: number }[]
|
|
byRootCause: { category: RootCauseCategory; count: number }[]
|
|
/** heatmap[weekday][hour], weekday 0 = Monday, in the server's time zone. */
|
|
heatmap: number[][]
|
|
actionsByMonth: { month: string; onTime: number; late: number }[]
|
|
/** Rolling 12-month OSHA rates at each month end; null for reporters. */
|
|
rates: { month: string; trir: number | null; dart: number | null }[] | null
|
|
yearly: { year: number; recordables: number; dartCases: number; hours: number; trir: number | null; dart: number | null }[] | null
|
|
}
|
|
|
|
export interface SavedView {
|
|
id: string
|
|
page: 'analytics' | 'incidents'
|
|
name: string
|
|
filters: Record<string, string>
|
|
shared: boolean
|
|
mine: boolean
|
|
}
|
|
|
|
// ---- OSHA recordkeeping ----
|
|
|
|
export interface OshaCase {
|
|
id: string
|
|
incident: { id: string; number: number; reference: string; title: string; occurredAt: string }
|
|
caseNumber: string
|
|
name: string
|
|
jobTitle: string | null
|
|
employmentType: (typeof employmentTypes)[number]
|
|
bodyPart: string | null
|
|
natureOfInjury: string | null
|
|
caseType: OshaCaseType
|
|
workRelated: boolean | null
|
|
newCase: boolean | null
|
|
died: boolean
|
|
dateOfDeath: string | null
|
|
daysAwayFromWork: boolean
|
|
daysAway: number
|
|
restrictedOrTransfer: boolean
|
|
daysRestricted: number
|
|
medicalTreatment: boolean
|
|
lossOfConsciousness: boolean
|
|
significantDiagnosis: boolean
|
|
specialCase: OshaSpecialCase
|
|
privacyCase: boolean
|
|
recordable: boolean
|
|
outcome: OshaOutcome | null
|
|
inpatientHospitalization: boolean
|
|
amputation: boolean
|
|
lossOfEye: boolean
|
|
reportedToOshaAt: string | null
|
|
/** Set when 1904.39 requires a direct report to OSHA. */
|
|
severeReport: { withinHours: 8 | 24; reasons: string[]; dueBy: string } | null
|
|
employeeAddress: string | null
|
|
dateOfBirth: string | null
|
|
dateOfHire: string | null
|
|
sex: 'M' | 'F' | null
|
|
physicianName: string | null
|
|
treatmentFacility: string | null
|
|
treatedInEmergencyRoom: boolean
|
|
timeStartedWork: string | null
|
|
activityBefore: string | null
|
|
whatHappened: string | null
|
|
objectSubstance: string | null
|
|
/** Form 301 / ITA fields still empty for a recordable case. */
|
|
missingFields: string[]
|
|
updatedAt: string
|
|
}
|
|
|
|
/** One line of the Form 300 Log. */
|
|
export interface OshaLogRow {
|
|
caseId: string
|
|
caseNumber: string
|
|
incidentNumber: number
|
|
/** "Privacy case" for privacy concern cases. */
|
|
employeeName: string
|
|
jobTitle: string
|
|
dateOfInjury: string
|
|
location: string
|
|
description: string
|
|
outcome: OshaOutcome
|
|
daysAway: number
|
|
daysRestricted: number
|
|
caseType: OshaCaseType
|
|
missingFields: string[]
|
|
}
|
|
|
|
export interface OshaYear {
|
|
site: { id: string; name: string } & Establishment
|
|
companyName: string
|
|
year: number
|
|
rows: OshaLogRow[]
|
|
totals: SummaryTotals
|
|
months: { month: number; hours: number | null; averageEmployees: number | null }[]
|
|
totalHoursWorked: number
|
|
annualAverageEmployees: number
|
|
maxEmployees: number
|
|
rates: { trir: number | null; dart: number | null; ltir: number | null }
|
|
checks: { errors: string[]; warnings: string[] }
|
|
missingEstablishmentFields: string[]
|
|
}
|
|
|
|
export interface Form301 {
|
|
case: OshaCase
|
|
establishment: { name: string; companyName: string } & Establishment
|
|
incident: { reference: string; occurredAt: string; title: string; description: string | null; site: string; area: string | null; locationDetail: string | null }
|
|
}
|