high priority medium complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

Screen is accessible only to users with coordinator or org_admin role — non-coordinators receive a no-access screen
Initial load fetches the first page (20 items) of unresolved duplicate pairs ordered by flagged_date descending
Each list item displays: activity A summary (date, type, contact name), activity B summary (date, type, contact name), flagged date, and a 'Review' link/button
Infinite scroll triggers next page load when the user scrolls within 200px of the list bottom
Pull-to-refresh resets pagination to page 1 and reloads the queue
Empty state is shown with a descriptive message when there are no unresolved duplicates in the queue
Loading skeleton is shown for the first page load — not a spinner blocking the full screen
Error state is shown with a retry button if DeduplicationQueueService fails
Each list item is tappable and navigates to the duplicate detail/resolution view
Queue count badge on the navigation entry point (coordinator home or tools tab) reflects the current unresolved count
Screen passes WCAG 2.2 AA contrast requirements and all list items are accessible via TalkBack/VoiceOver
Organization_id scoping: coordinator only sees duplicates within their own organization

Technical Requirements

frameworks
Flutter
BLoC
Riverpod
apis
Supabase PostgreSQL 15
Supabase Auth
data models
activity
assignment
performance requirements
First page renders within 1.5 seconds on a 4G connection
Pagination append must not rebuild existing list items — use ListView.builder with a stable key per pair
Supabase query uses .range() for offset-based pagination — no cursor required for this use case
security requirements
Supabase query must filter by organization_id from JWT claims — RLS enforces this server-side but the query must also include the filter client-side for clarity
Coordinator role check must be performed on the server side via RLS, not only on the client navigation guard
Contact names and activity details in list items are PII — screen must not be accessible via deep link without valid coordinator session
ui components
DeduplicationQueueScreen scaffold with AppBar
DuplicatePairListItem widget
Loading skeleton list
Empty state widget
Error state widget with retry button
Pull-to-refresh (RefreshIndicator)
Infinite scroll trigger (scroll listener or SliverList)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use a DeduplicationQueueCubit (or BLoC) with states: QueueInitial, QueueLoading, QueueLoaded(List pairs, bool hasMore), QueueLoadingMore, QueueEmpty, QueueError. DuplicatePair is a value object containing activityA (Activity), activityB (Activity), flaggedAt (DateTime), and pairId (String). In the repository, query a view or join on the activities table filtered by duplicate_reviewed = false and has a linked candidate — define the exact Supabase query shape when the DB schema for flagged pairs is finalized. Use ListView.builder for efficient rendering; add a bottom loader widget as the last item when hasMore = true.

For the queue count badge, expose a stream from DeduplicationQueueService that emits the unresolved count — update via Supabase Realtime subscription on the duplicates table for live updates.

Testing Requirements

Write BLoC unit tests: (1) QueueLoading emitted on load; (2) QueueLoaded with items emitted on service success; (3) QueueEmpty emitted when service returns empty list; (4) QueueError emitted on service failure; (5) QueueLoadingMore emitted on pagination trigger; (6) new page appended to existing items on success. Write widget tests: (7) list items render correct activity pair data; (8) tapping a list item triggers navigation; (9) pull-to-refresh calls reload; (10) empty state visible when list is empty; (11) retry button calls reload on error. Test pagination: verify .range(0,19) on first load and .range(20,39) on second page. Mock DeduplicationQueueService with fake returning 20, then 5 items.

Component
Deduplication Queue Screen
ui medium
Epic Risks (3)
high impact medium prob technical

The merge resolution path requires identifying which fields from the draft differ from the existing record, applying those differences to the existing record, and cancelling the draft — all as an atomic operation. Partial failures (e.g., update succeeds but draft cancellation fails) could leave the system in an inconsistent state.

Mitigation & Contingency

Mitigation: Implement the merge path as a Supabase RPC transaction that updates the existing record and soft-deletes the draft in a single atomic call. The DuplicateResolutionHandler should never attempt field-level merge at the application layer.

Contingency: If the atomic RPC approach proves too complex for the initial release, simplify the merge path to: mark existing record as the canonical record and cancel the new submission without field merging, displaying a message to the user to manually verify the existing record's fields. Log a follow-up ticket for full field-merge in a later sprint.

medium impact medium prob integration

The DuplicateWarningBottomSheet must intercept the activity wizard's save action without disrupting the wizard's existing navigation stack. If the bottom sheet is implemented as a separate route rather than an overlay, back navigation could break the wizard's step state.

Mitigation & Contingency

Mitigation: Use a `showModalBottomSheet` overlay pattern so the bottom sheet sits above the wizard route without pushing a new route onto the Navigator stack. The wizard's CuBit/BLoC retains all draft state while the sheet is visible. Test this integration with the existing activity-registration-cubit before merging.

Contingency: If the overlay approach causes Z-order or focus issues with the wizard's keyboard-aware layout, route the duplicate check result back to the wizard as a state event (DuplicateDetected), and let the wizard render a local inline warning banner instead of a bottom sheet.

medium impact medium prob technical

The bottom sheet and comparison panel involve complex layouts with multiple interactive elements. Screen reader users (particularly relevant for Blindeforbundet) may struggle with the side-by-side comparison layout if semantics are not carefully ordered.

Mitigation & Contingency

Mitigation: Design the comparison panel with a single-column semantic order (read record A fully, then record B fully) regardless of visual layout. Use Flutter's `Semantics` widget with `sortKey` to enforce correct traversal order. Test with TalkBack and VoiceOver against the WCAG 2.2 AA reading order criteria.

Contingency: If side-by-side layout cannot achieve acceptable screen reader ordering, switch to a stacked tab layout (Tab A / Tab B) for the comparison panel that is semantically simple even if less visually immediate for sighted users.