high priority medium complexity frontend pending frontend specialist Tier 6

Acceptance Criteria

ClaimStatusAuditTimeline is a StatelessWidget that accepts a `List<ClaimEvent>` parameter β€” it has no Riverpod dependency itself (data loading is separated)
Each ClaimEvent is rendered as a timeline row with a vertical connector line between rows
State transition label is displayed using a localized string map (e.g., 'submitted' β†’ 'Innsendt til godkjenning' in nb_NO); English fallback is acceptable for Phase 1
Actor display name is shown in the body typography style using the design token `AppTypography.bodyMedium`
Actor role is shown as a colored badge: peer mentor (design token accent color), coordinator (design token primary color), system (design token neutral color)
Timestamp is rendered as a formatted string β€” exact formatting is handled by the composing widget (task-015 provides the formatter); this widget accepts a pre-formatted `String` to keep it pure
Optional coordinator comment is displayed in a visually distinct style (italic or card inset) and is completely absent from the DOM when null or empty
Empty state (empty list): renders a centered message 'No activity recorded yet' with the appropriate design token text style
The widget is wrapped in a `SingleChildScrollView` with vertical axis β€” the timeline scrolls independently if taller than its parent
No hardcoded colors β€” all colors come from the design token system (`AppColors.*`)
No hardcoded spacing β€” all padding/margin values come from `AppSpacing.*`
WCAG 2.2 AA contrast ratio met for all text/background combinations
Widget renders correctly at text scale factors 1.0, 1.5, and 2.0 without overflow

Technical Requirements

frameworks
Flutter (Dart)
Design token system (AppColors, AppTypography, AppSpacing, AppRadii from project constants)
data models
ClaimEvent (state_transition, actor_display_name, actor_role, formatted_timestamp: String, coordinator_comment)
performance requirements
Widget builds in under 16 ms per frame for lists up to 50 events
Use ListView.builder if event count can exceed 20 to avoid building all rows at once
security requirements
All user-generated text (actor_display_name, coordinator_comment) must be treated as untrusted and rendered with Flutter Text widget only β€” no HTML rendering
Do not display internal IDs (claim_id, actor_id, event_id) in the UI
ui components
ClaimStatusAuditTimeline (root widget)
ClaimEventRow (private sub-widget for a single event row)
ActorRoleBadge (private sub-widget for the colored role indicator)
TimelineConnector (private sub-widget for the vertical line between rows)

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Implementation Notes

Keep ClaimStatusAuditTimeline as a pure, dumb widget β€” it receives `List` where `ClaimEvent` has a `formattedTimestamp` field already computed by the provider layer or a composing widget. This separation makes the widget trivially testable and reusable. Implement the vertical timeline connector as a `CustomPaint` or a simple `Container` with `BoxDecoration` border on the left β€” avoid third-party timeline packages to keep dependency count low. Use `const` constructors wherever possible.

The `ClaimEventRow` sub-widget should be a `const`-eligible StatelessWidget. For the actor role badge, define a simple mapping: `ActorRole.peerMentor β†’ AppColors.accent`, `ActorRole.coordinator β†’ AppColors.primary`, `ActorRole.system β†’ AppColors.neutral`. Match the existing card/badge pattern in the codebase (AppButton, custom fields table) for visual consistency.

Testing Requirements

Widget tests using `flutter_test`: (1) render with a list of 3 events β€” assert all actor names, role badges, and pre-formatted timestamps appear via `find.text()`; (2) render with empty list β€” assert empty state message is present; (3) coordinator comment present β€” assert comment text visible; (4) coordinator comment null β€” assert comment text absent; (5) text overflow test at scale factor 2.0 β€” wrap in `MediaQuery` override and assert no `RenderFlex` overflow exceptions. Golden image tests are covered in task-016.

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.