high priority low complexity frontend pending frontend specialist Tier 7

Acceptance Criteria

All `ClaimEvent.created_at` values stored as UTC are converted to Europe/Oslo before any formatting
Events within the last 24 hours are displayed as relative time: '2 hours ago', '45 minutes ago', 'Just now' (threshold: < 60 seconds)
Events older than 24 hours are displayed as absolute date-time in Norwegian format: 'dd. MMM yyyy, HH:mm' using nb_NO locale (e.g., '14. feb. 2025, 09:30')
The formatted string passed to `ClaimEventRow` is always in the Europe/Oslo timezone β€” never raw UTC
Each timestamp widget has a `Semantics` wrapper with a `label` property providing the full absolute date-time string regardless of whether relative or absolute display is used (for screen reader accessibility)
Daylight saving time transitions (CET/CEST) are handled correctly β€” use the `timezone` package, not a fixed UTC+1 offset
The formatting logic is extracted into a standalone `ClaimTimestampFormatter` class with a single public method `format(DateTime utcTimestamp, DateTime now) β†’ String` β€” no Flutter SDK dependency in this class
Norwegian locale (nb_NO) is initialized via `initializeDateFormatting('nb_NO')` at app startup before the timeline is rendered
No hardcoded locale strings β€” all relative time phrases ('hours ago', 'minutes ago') are defined as constants in a single location

Technical Requirements

frameworks
Flutter (Dart)
intl package (DateFormat, nb_NO locale)
timezone package (for Europe/Oslo conversion)
data models
DateTime (UTC, from ClaimEvent.created_at)
performance requirements
Formatting computation is synchronous and completes in under 1 ms per timestamp
security requirements
Semantics label must use the full absolute date-time, never a relative string, to avoid ambiguity for assistive technology users
ui components
AccessibleTimestamp (Semantics-wrapped Text widget displaying formatted timestamp)

Execution Context

Execution Tier
Tier 7

Tier 7 - 84 tasks

Can start after Tier 6 completes

Implementation Notes

Create `ClaimTimestampFormatter` as a pure Dart class (no BuildContext, no Flutter imports) so it can be unit-tested without a widget tree. Inject `DateTime now` as a parameter rather than calling `DateTime.now()` inside β€” this is critical for deterministic tests. Use `tz.TZDateTime.from(utcTimestamp, tz.getLocation('Europe/Oslo'))` from the `timezone` package for conversion. Do NOT use `DateTime.toLocal()` β€” device timezone may not be Europe/Oslo.

For relative labels, define a private enum or sealed class: `_RelativeInterval { justNow, minutes, hours }` to avoid magic string comparisons. Register the `nb_NO` locale in `main.dart` alongside other locales to avoid the `MissingLocaleError` at runtime. When constructing the `Semantics` label, format the full absolute datetime even if the display shows relative time β€” this is required for WCAG 2.2 AA compliance.

Testing Requirements

Pure Dart unit tests (no Flutter widgets needed) for `ClaimTimestampFormatter`: (1) UTC timestamp 30 seconds ago β†’ 'Just now'; (2) UTC timestamp 90 minutes ago β†’ '1 time siden' (nb_NO locale); (3) UTC timestamp 25 hours ago β†’ absolute format in nb_NO; (4) UTC timestamp exactly at 24-hour boundary; (5) a timestamp during CET (winter, UTC+1) displays correct local time; (6) a timestamp during CEST (summer, UTC+2) displays correct local time β€” this tests DST handling; (7) assert semantics label is always absolute format. All tests use a fixed `now` parameter β€” never `DateTime.now()` in test logic.

Component
Claim Status Audit Timeline
ui low
Epic Risks (3)
high impact high prob technical

The ThresholdEvaluationService is described as shared Dart logic used both client-side and in the Edge Function. Supabase Edge Functions run Deno/TypeScript, not Dart, meaning the threshold logic must be maintained in two languages and can diverge, causing the server to reject legitimate client submissions.

Mitigation & Contingency

Mitigation: Implement the threshold logic as a single TypeScript module in the Edge Function and call it via a thin Dart HTTP client wrapper for client-side preview feedback only. The server is always authoritative; the client version is purely for UX (showing the user whether their claim will auto-approve before they submit).

Contingency: If dual-language maintenance is unavoidable, create a shared golden test file (JSON fixtures with inputs and expected outputs) that is run against both implementations in CI to detect divergence immediately.

medium impact medium prob technical

A peer mentor could double-tap the submit button or a network retry could trigger a duplicate submission, causing the ApprovalWorkflowService to attempt two concurrent state transitions from draft→submitted for the same claim, potentially resulting in two audit events or conflicting statuses.

Mitigation & Contingency

Mitigation: Implement idempotency in the ApprovalWorkflowService using a database-level unique constraint on (claim_id, from_status, to_status) per transition, combined with a UI-level submission lock (disable button after first tap until response returns).

Contingency: Add a deduplication check at the start of every state transition method that returns the existing state if an identical transition is already in progress or completed within the last 10 seconds.

high impact medium prob scope

Claims with multiple expense lines (e.g., mileage + parking) must have their combined total evaluated against the threshold. If individual lines are added asynchronously or the evaluation runs before all lines are persisted, the auto-approval decision may be computed on an incomplete set of expense lines.

Mitigation & Contingency

Mitigation: The Edge Function always fetches all expense lines from the database (not from the client payload) before computing the threshold decision. Define a clear claim submission contract that requires all expense lines to be persisted before the submit action is called.

Contingency: Add a validation step in ApprovalWorkflowService that counts expected vs. persisted expense lines before allowing the transition, returning a validation error if lines are missing.