critical priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

ChapterMembershipCubit constructor accepts a MultiChapterMembershipService via constructor injection (not located via service locator inside the class body)
loadAffiliations() emits ChapterMembershipLoading, then on success emits ChapterMembershipLoaded with the fetched affiliations and empty pending lists
loadAffiliations() emits ChapterMembershipError with the appropriate ChapterMembershipFailure type on any service exception
loadAffiliations() is idempotent: calling it a second time while already in a loaded state re-fetches and replaces the current state
trackPendingAdd(chapterId) adds the chapter ID to pendingAddChapterIds in the current loaded state without triggering a service call
trackPendingAdd(chapterId) is a no-op if the chapter is already in pendingAddChapterIds (no duplicates)
trackPendingAdd(chapterId) is a no-op if the contact is already affiliated with that chapter in currentAffiliations
trackPendingRemove(affiliationId) adds the affiliation ID to pendingRemoveAffiliationIds without triggering a service call
trackPendingRemove(affiliationId) is a no-op if the ID is already in pendingRemoveAffiliationIds
trackPendingAdd and trackPendingRemove throw a StateError (not silently fail) if called when the current state is not ChapterMembershipLoaded

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
Dart async/await
apis
MultiChapterMembershipService (internal service layer)
SupabaseContactChapterAdapter (via service)
data models
ChapterAffiliation
ChapterMembershipState
ChapterMembershipFailure
performance requirements
loadAffiliations() must not block the UI thread — use async/await with emit inside try/catch
trackPendingAdd and trackPendingRemove must be synchronous and complete in O(n) where n is current list length (typically ≤5)
security requirements
Service injection must be via constructor — never use a global singleton directly in the Cubit to allow test mocking
Do not log affiliation IDs or chapter IDs at info level in production builds
ui components
ChapterMembershipLoaded.pendingAddChapterIds drives the 'pending add' visual indicators in the chapter selection list
ChapterMembershipLoaded.pendingRemoveAffiliationIds drives the 'pending remove' strikethrough/indicator in the current affiliations list

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use `Cubit` from `flutter_bloc`. In loadAffiliations, always emit Loading first, then wrap the service call in try/catch mapping exceptions to ChapterMembershipFailure types. For trackPendingAdd/trackPendingRemove, cast the current state to ChapterMembershipLoaded (throw StateError if cast fails) and emit a copyWith of the loaded state with the updated list — do NOT create a new loaded state from scratch to preserve other loaded-state fields. Use List.unmodifiable when emitting new state to prevent callers from mutating state lists.

Keep the Cubit lean: no direct Supabase imports, no BuildContext references, no Navigator calls — it is pure business state logic. Follow the project convention for Cubit file location: `lib/features/chapter_membership/cubit/chapter_membership_cubit.dart`. If the project uses Riverpod alongside BLoC, the Cubit should still be instantiated via a Riverpod provider wrapping it, but the Cubit class itself must have no Riverpod dependencies.

Testing Requirements

Unit tests (flutter_test with bloc_test package): use `blocTest()` for all Cubit tests. Test scenarios: (1) loadAffiliations happy path — assert [Loading, Loaded] emission sequence. (2) loadAffiliations service throws — assert [Loading, Error] with correct failure type. (3) loadAffiliations while already loaded re-emits [Loading, Loaded] with fresh data.

(4) trackPendingAdd adds to pending list without service call. (5) trackPendingAdd duplicate is ignored. (6) trackPendingAdd for already-affiliated chapter is ignored. (7) trackPendingRemove adds to pending list.

(8) trackPendingRemove duplicate is ignored. (9) trackPendingAdd called in non-loaded state throws StateError. Mock MultiChapterMembershipService with mocktail. Aim for 100% branch coverage on the Cubit methods.

Component
Chapter Membership Cubit
service 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.