feat(i18n): add internationalization support with English and Spanish translations
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s

feat(kiosk): implement kiosk report submission functionality with offline support

test(outbox): add tests for outbox functionality including report submission and error handling

feat(outbox): create outbox management for queued reports with IndexedDB persistence

feat(uuid): add UUID generation utility for client IDs

feat(qr): implement QR code generation for reporting links by site and area

feat(kiosk): create Kiosk page for reporting incidents without an account
This commit is contained in:
jayjennings committed 2026-10-06 11:38:40 -07:00
1 parent 3f6dff5f24
commit 3e964d1176
64 files changed
+10056 -267

No files matched your search

+16 -15
View File
@@ -6,20 +6,21 @@ import { Page } from '../components/Page'
import { PageIntro } from '../components/PageIntro'
import { Empty, ErrorMessage, Loading } from '../components/QueryState'
import { panel } from '../components/styles'
import { formatDay } from '../lib/dates'
import { useI18n } from '../i18n'
import { useActions, useUpdateAction, type Me } from '../lib/queries'
const filters = [
{ value: 'open', label: 'Open' },
{ value: 'overdue', label: 'Overdue' },
{ value: 'done', label: 'Done' },
{ value: 'all', label: 'All' },
{ value: 'open', label: 'filterOpen' },
{ value: 'overdue', label: 'filterOverdue' },
{ value: 'done', label: 'filterDone' },
{ value: 'all', label: 'filterAll' },
] as const
const tab = (active: boolean) => `rounded-[5px] px-3 py-1.5 text-2xs font-semibold ${active ? 'bg-forest-950 text-white' : 'text-muted hover:bg-white hover:text-ink'}`
export function Actions() {
const me = useOutletContext<Me>()
const { t, formatDay } = useI18n()
const [params, setParams] = useSearchParams()
const canSeeAll = hasRole(me.user.role, 'supervisor')
const scope = canSeeAll && params.get('scope') === 'all' ? 'all' : 'mine'
@@ -36,25 +37,25 @@ export function Actions() {
return (
<Page>
<PageIntro
eyebrow="Corrective actions"
title={scope === 'mine' ? 'My actions' : 'All actions'}
copy={scope === 'mine' ? 'Corrective actions assigned to you. Mark them done when the fix is in place.' : 'Every corrective action across the organization.'}
eyebrow={t.actions.eyebrow}
title={scope === 'mine' ? t.actions.mine : t.actions.all}
copy={scope === 'mine' ? t.actions.mineCopy : t.actions.allCopy}
/>
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<div className="flex gap-1 rounded-md bg-line-soft p-1" role="tablist" aria-label="Filter actions">
{filters.map((f) => (
<button key={f.value} type="button" role="tab" aria-selected={filter === f.value} className={tab(filter === f.value)} onClick={() => set('filter', f.value)}>
{f.label}
{t.actions[f.label]}
</button>
))}
</div>
{canSeeAll && (
<div className="flex gap-1 rounded-md bg-line-soft p-1" role="tablist" aria-label="Whose actions">
<button type="button" role="tab" aria-selected={scope === 'mine'} className={tab(scope === 'mine')} onClick={() => set('scope', 'mine')}>
Assigned to me
{t.actions.assignedToMe}
</button>
<button type="button" role="tab" aria-selected={scope === 'all'} className={tab(scope === 'all')} onClick={() => set('scope', 'all')}>
Everyone
{t.actions.everyone}
</button>
</div>
)}
@@ -64,7 +65,7 @@ export function Actions() {
{actions.isPending ? (
<Loading />
) : !actions.data?.length ? (
<Empty>{filter === 'open' ? 'Nothing open. Nice work.' : 'No actions here.'}</Empty>
<Empty>{filter === 'open' ? t.actions.nothingOpen : t.actions.none}</Empty>
) : (
<ul className="divide-y divide-line-soft">
{actions.data.map((action) => (
@@ -76,12 +77,12 @@ export function Actions() {
{action.incident.reference}
</Link>{' '}
{action.incident.title}
{scope === 'all' && ` · ${action.assignee?.name ?? 'Unassigned'}`}
{scope === 'all' && ` · ${action.assignee?.name ?? t.actions.unassigned}`}
</p>
</div>
<div className="flex items-center gap-3 text-2xs text-body">
{action.overdue && <OverdueBadge />}
<span className="w-[100px] text-right">{action.dueDate ? `Due ${formatDay(action.dueDate)}` : 'No due date'}</span>
{action.overdue && <OverdueBadge label={t.actions.overdue} />}
<span className="w-[100px] text-right">{action.dueDate ? t.actions.due(formatDay(action.dueDate)) : t.actions.noDueDate}</span>
<ActionStatusControl action={action} disabled={update.isPending} onChange={(status) => update.mutate({ id: action.id, status })} />
</div>
</li>