critical priority medium complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

Bottom sheet renders with correct claim metadata: submitter full name, total amount (NOK formatted), list of expense types, and thumbnail grid of attached receipts
Approve button triggers ApprovalWorkflowService.approveClaim() with optimistic UI update and loading indicator; success closes the sheet and emits an approved event to the parent queue
Reject button reveals an optional justification text field (AppTextField) before confirming; rejection without text is permitted but a confirmation dialog is shown
Justification text is passed to ApprovalWorkflowService.rejectClaim() and persisted via ClaimApprovalDecisionRepository
Audit timeline section renders all prior state transitions with actor name, role, timestamp (dd.MM.yyyy HH:mm), and transition label in chronological order
All interactive elements meet WCAG 2.2 AA contrast ratios; Approve and Reject buttons have distinct color tokens (success/danger from design token system)
Semantic labels are set on all interactive elements for VoiceOver/TalkBack compatibility; bottom sheet focus is trapped inside the modal while open
Back/close navigation is handled via a visible close button (not swipe-only) per universal design requirements
Error state from ApprovalWorkflowService is surfaced as an inline error message, not a silent failure
Receipt thumbnails open a full-screen image viewer on tap with a close affordance
Sheet is fully responsive and scrollable when content exceeds viewport height on small devices

Technical Requirements

frameworks
Flutter
BLoC
apis
ApprovalWorkflowService.approveClaim()
ApprovalWorkflowService.rejectClaim()
ClaimApprovalDecisionRepository
ExpenseClaimStatusRepository
data models
ExpenseClaim
ExpenseLine
ClaimApprovalDecision
AuditEvent
Receipt
performance requirements
Bottom sheet open animation must complete within 300ms
Receipt thumbnails must load from Supabase Storage with aggressive caching; no visible loading flicker for cached images
Approve/reject API calls must show loading state within 100ms of tap
security requirements
Coordinator role must be verified on the client before rendering approve/reject actions; server-side role validation in ApprovalWorkflowService is authoritative
Justification text must be sanitized before persistence to prevent injection
Receipt images must be fetched via signed Supabase Storage URLs with short expiry; URLs must not be cached to disk in plain text
ui components
AppButton (primary/danger variants)
AppTextField (justification input)
ClaimStatusBadge
ReceiptThumbnailGrid
AuditTimelineList
ModalCloseButton
FullScreenImageViewer

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use DraggableScrollableSheet or showModalBottomSheet with isScrollControlled: true to handle variable content height. Wrap the sheet in a BlocProvider scoped to the sheet's lifecycle — do not share the parent queue BLoC. Use a local ApprovalActionCubit with states: ApprovalActionIdle, ApprovalActionLoading, ApprovalActionSuccess, ApprovalActionError. Emit a result event (approved/rejected claimId) back to the parent via a callback or BlocListener before popping.

Receipt thumbnails should use CachedNetworkImage with a placeholder shimmer. For the audit timeline, use a ListView.builder with a custom TimelineTile widget — avoid nested scrolling conflicts by using NeverScrollableScrollPhysics on the inner list and letting the outer DraggableScrollableSheet handle scrolling. Justification field should auto-focus when Reject is tapped using FocusNode.requestFocus() after the animation settles. Design tokens must be used for all colors — never hardcode hex values.

Testing Requirements

Unit tests (flutter_test): test BLoC state transitions for approve/reject actions including loading, success, and error states; mock ApprovalWorkflowService to verify correct method calls with claim ID and justification text. Widget tests: verify all metadata fields render correctly for a fixture claim; verify Approve button is present and Reject reveals text field; verify audit timeline renders N events in correct order; verify close button dismisses the sheet. Accessibility tests: verify Semantics tree has labels on all interactive widgets; verify focus order is logical. Edge case tests: empty audit timeline renders gracefully; very long justification text does not overflow layout; network error during approve/reject surfaces error widget.

Epic Risks (3)
medium impact medium prob technical

Maintaining multi-select state across paginated list pages is architecturally complex in Flutter with Riverpod/BLoC. If the selection state is stored in the widget tree rather than the state layer, page transitions and list redraws can silently clear selections, causing coordinators to lose their multi-select and re-enter it.

Mitigation & Contingency

Mitigation: Store the selected claim ID set in a dedicated Riverpod StateNotifier outside the paginated list widget tree. The paginated list reads selection state from this provider and does not own it. Selection persists independently of list scroll position or page loads.

Contingency: If cross-page selection proves prohibitively complex, limit bulk selection to the currently visible page (add a clear warning in the UI) and prioritise single-page bulk approval for the initial release.

medium impact medium prob integration

If a coordinator has the queue open while another coordinator approves claims from the same queue (possible in large organisations with shared chapter coverage), the Realtime update may arrive out of order or be missed during a reconnect, leaving the first coordinator's view stale and allowing them to attempt to approve an already-actioned claim.

Mitigation & Contingency

Mitigation: The ApprovalWorkflowService's optimistic locking (from the foundation epic) will catch the concurrent edit at the database level. The CoordinatorReviewQueueScreen should handle the resulting ConcurrencyException by removing the claim from the local list and showing a brief snackbar: 'This claim was already actioned by another coordinator.'

Contingency: Add a queue staleness indicator (a subtle 'last updated X seconds ago' label) and a manual refresh button as a fallback for coordinators who notice inconsistencies.

low impact high prob dependency

The end-to-end test requirement that a peer mentor receives a push notification within 30 seconds of coordinator approval depends on FCM delivery latency, which is outside the application's control and can vary significantly in CI/CD environments.

Mitigation & Contingency

Mitigation: Structure end-to-end tests to verify notification intent (correct FCM payload dispatched, correct Realtime event emitted) rather than actual device delivery timing. Use test doubles for FCM delivery in automated tests and reserve real-device delivery tests for manual pre-release validation.

Contingency: If notification timing requirements must be validated in automation, instrument the ApprovalNotificationService with a test hook that records dispatch timestamps and assert against those rather than actual FCM callbacks.