critical priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

DuplicateComparisonPanel renders inside DuplicateWarningBottomSheet with correct candidate activity data passed as props
Comparison panel is fully visible in the scrollable content area above action buttons before user scrolls
Both the draft activity and candidate duplicate activity fields are displayed side-by-side or stacked with clear labels
Bottom sheet opens and the panel renders within 300ms on low-end Android devices
Action buttons (Proceed, Merge, Cancel) remain sticky at the bottom of the sheet, not scrolled out of view
Panel displays all relevant activity fields: date, duration, contact name, activity type
Bottom sheet handles empty candidate list gracefully — fallback UI shown if no candidate data is passed
VoiceOver/TalkBack announces panel content in a logical reading order (draft first, then candidate)
Panel layout passes WCAG 2.2 AA contrast ratio requirements for all text and background color pairings
Widget tree disposes properly when bottom sheet is dismissed — no memory leaks from mounted state

Technical Requirements

frameworks
Flutter
BLoC
Riverpod
data models
activity
performance requirements
Bottom sheet open animation completes within 300ms
Panel widget rebuilds only when candidate data changes — use const constructors where possible
No jank during scroll within bottom sheet on 60fps devices
security requirements
Contact names and activity details displayed in panel must be masked if the user lacks read permission on the candidate activity's record
No PII logged to console during panel rendering
ui components
DuplicateComparisonPanel
DuplicateWarningBottomSheet
Sticky action button row
Scrollable content area with SingleChildScrollView or CustomScrollView
Field comparison row widget (label + draft value + candidate value)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use a DraggableScrollableSheet or showModalBottomSheet with isScrollControlled: true to allow the sheet to expand sufficiently for the comparison panel. Place DuplicateComparisonPanel inside a Flexible or Expanded widget within a Column, with the action buttons in a non-scrolling footer. Pass candidate data as an immutable value object (Activity or a dedicated DuplicateCandidate DTO) — avoid passing raw Maps. Use design token colors for the field comparison highlight (e.g., amber for differing fields).

Ensure the panel widget is const-constructable for tree stability. Do not use MediaQuery inside the panel widget — let the parent sheet control sizing.

Testing Requirements

Write widget tests using flutter_test that verify: (1) DuplicateComparisonPanel receives and renders correct candidate activity props; (2) action buttons are rendered below the panel and remain visible after scroll; (3) panel shows fallback UI when candidate list is null or empty; (4) Semantics tree exposes panel content to screen readers in correct order. Golden tests for comparison panel layout on 375px and 414px screen widths. No integration tests required for this task — covered by task-003.

Component
Duplicate Warning Bottom Sheet
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.