high priority medium complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

All active affiliations from the cubit's loaded state are rendered as a list with chapter name, role, and an 'End Affiliation' button per row
Affiliations already marked as pending removal are shown with a visual 'Pending removal' indicator and the End Affiliation button is disabled or replaced with an 'Undo' option
Tapping 'End Affiliation' opens a bottom sheet modal
The bottom sheet contains a date picker pre-filled with today's date for the end date field
The date picker enforces a minimum selectable date of the affiliation's start date and a maximum of today
The bottom sheet contains an optional text note field with a character limit of 500
Tapping Confirm in the bottom sheet calls Cubit.trackPendingRemove(affiliationId, endDate, note) exactly once and closes the sheet
Tapping Cancel in the bottom sheet closes the sheet with no cubit call and no state change
Swiping down to dismiss the bottom sheet behaves identically to tapping Cancel
After trackPendingRemove is called, the row updates to show the 'Pending removal' state immediately (optimistic UI)
All interactive elements meet 48x48dp minimum tap target size and WCAG 2.2 AA contrast requirements
Design System v3 tokens used exclusively

Technical Requirements

frameworks
Flutter
BLoC
flutter_bloc
apis
ChapterMembershipCubit.trackPendingRemove(affiliationId, endDate, note)
showModalBottomSheet (Flutter)
data models
ChapterAffiliation
PendingRemoval
ChapterMembershipState
performance requirements
Bottom sheet must animate open within one frame cycle using standard Flutter bottom sheet transition
Date picker must be responsive with no perceptible lag when scrolling months
security requirements
End date must be validated on the server side at save time — client-side date constraint is UX only, not a security boundary
Note field input must be sanitized of control characters before passing to cubit
Only coordinators with edit permission for the contact may see and interact with End Affiliation buttons — enforce via RBAC check before rendering
ui components
ActiveAffiliationRow (row widget: chapter name + role + End Affiliation button or Pending indicator)
EndAffiliationBottomSheet (modal bottom sheet: date picker + note field + Confirm/Cancel buttons)
AppDatePicker (date picker using Design System v3 styling)
AppTextField (optional note field)
AppButton (Confirm — primary style)
AppButton (Cancel — secondary style)
PendingRemovalBadge (visual indicator for pending rows)

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use showModalBottomSheet with isDismissible: true so swipe-to-dismiss works, and handle the null return value from the future (user dismissed without confirming) identically to Cancel. Model the bottom sheet as a self-contained StatefulWidget so local form state (selectedDate, noteText) is encapsulated — do not push this transient state into the cubit. Pre-fill the date picker with DateTime.now().toUtc() and clamp selectable range using firstDate: affiliation.startDate, lastDate: DateTime.now(). Use the design system AppDatePicker widget for consistent styling.

After Confirm, call cubit.trackPendingRemove before Navigator.pop so the cubit state update triggers before the sheet closes (prevents brief flicker of un-updated row). For the note field, use a TextEditingController with a maxLength enforced in the AppTextField widget. Ensure the PendingRemovalBadge on pending rows clearly communicates the pending state to screen readers via a Semantics label such as 'Affiliation end pending on {endDate}'.

Testing Requirements

Write widget tests using flutter_test: (1) mount the end-affiliation list section with a mock cubit emitting a loaded state containing 3 active affiliations and assert 3 rows with End Affiliation buttons are rendered, (2) tap End Affiliation on one row and assert the bottom sheet opens, (3) select a date in the date picker and enter a note, tap Confirm, assert trackPendingRemove is called with correct affiliationId, endDate, and note, (4) open the bottom sheet and tap Cancel, assert no cubit call was made, (5) simulate a row where pendingRemoval is already set and assert the button is disabled/replaced. Write bloc_test unit tests for trackPendingRemove cubit method. Write a golden test for the bottom sheet in its default state. Test that the date picker rejects dates before the affiliation start date.

Target 90% branch coverage.

Component
Chapter Assignment Editor
ui medium
Epic Risks (3)
high impact medium prob technical

The Duplicate Activity Warning Dialog must announce itself to VoiceOver and TalkBack immediately on appearance. Flutter's default modal semantics do not guarantee focus shift to the dialog on all platform versions, risking silent appearance for screen reader users — a WCAG 2.2 failure.

Mitigation & Contingency

Mitigation: Wrap the dialog content in a Semantics node with liveRegion: true and explicitly request focus via FocusScope.of(context).requestFocus() on the dialog's primary action button in the post-frame callback. Test on physical iOS (VoiceOver) and Android (TalkBack) devices, not only simulators.

Contingency: If automatic focus fails on a specific platform version, add a platform-specific fallback using SemanticsService.announce() to force a live region announcement of the dialog's headline text.

medium impact medium prob technical

The Chapter Membership Cubit tracks pending changes before commit to support the two-step add/confirm flow. If the user navigates away mid-edit or the app is backgrounded, uncommitted pending state could be replayed incorrectly on return, causing phantom affiliation additions or removals.

Mitigation & Contingency

Mitigation: Design the Cubit to hold pending changes only in transient in-memory state with no persistence. On any navigation-away event, emit a reset state that discards pending changes. Prevent accidental navigation during an active edit by showing a discard-changes confirmation dialog.

Contingency: If state desync is reported in production, add an explicit state reconciliation step in the Cubit's onResume handler that re-fetches the authoritative affiliation list from the repository and resets all pending state before re-rendering.

medium impact high prob technical

The Chapter Assignment Editor's searchable chapter list must load quickly. If the organisation has hundreds of chapters (NHF has 1,400 local chapters) and the full list is fetched on dialog open, the editor will be slow to display and the search will be sluggish.

Mitigation & Contingency

Mitigation: Scope the chapter list to only chapters within the coordinator's administrative scope (not all 1,400), leveraging the existing hierarchy access scope service. Implement server-side search with a minimum 2-character threshold and debounce to avoid excessive Supabase queries.

Contingency: If the scoped list is still too large, add local caching of the chapter list with a 15-minute TTL and an explicit refresh button, ensuring the editor is always responsive even on poor network conditions.