high priority medium complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

The coordinator layout (_CoordinatorDetailLayout) renders all peer mentor content fields (certificate name, expiry date, ExpiryStatusIndicator in full mode) plus two additional action buttons: 'Trigger course enrollment' and 'Acknowledge lapse'
Tapping 'Trigger course enrollment' calls CourseEnrollmentPromptService and dispatches the relevant BLoC event; the button shows a loading indicator while the async call is in progress
Tapping 'Acknowledge lapse' dispatches AcknowledgeLapse BLoC event with the current notificationId; the button shows a loading indicator while the async call is in progress
After AcknowledgeLapse succeeds, the BLoC emits an updated Loaded state with isAcknowledged=true; the UI reflects this (e.g., button disabled or hidden, confirmation message shown)
After TriggerCourseEnrollment succeeds, a confirmation snackbar or inline message is shown to the coordinator
Both action buttons are disabled (not just invisible) while their respective async operations are in progress to prevent double-taps
Peer mentors (non-coordinator role) never see the coordinator action buttons — they are completely absent from the widget tree, not just hidden with Visibility(visible: false)
Role detection uses the project's existing role-resolution service; no hardcoded role string comparisons
Error handling: if either service call fails, an error message (snackbar or inline) is shown; the button is re-enabled so the coordinator can retry
All coordinator-specific buttons have Semantics labels so screen readers can identify them
WCAG 2.2 AA contrast met for button text and loading indicator against the screen background

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc — BlocConsumer for side-effects)
Riverpod (if CourseEnrollmentPromptService is provided via Riverpod)
apis
CourseEnrollmentPromptService (existing or new service)
CoordinatorAcknowledgementService (existing or new service)
Role-resolution service (existing)
data models
ExpiryNotificationRecord
AcknowledgeLapse BLoC event
CertificateExpiryNotificationLoaded
performance requirements
Button loading indicators must appear within one frame of tap (synchronous state update before async call)
Async service calls should have a timeout (e.g., 10 seconds) with error handling to prevent indefinite loading state
security requirements
Server-side Supabase RLS must enforce coordinator-only access for acknowledgement and enrollment endpoints — client-side role check is a UX guard only, not a security boundary
AcknowledgeLapse must be idempotent — if already acknowledged, the UI should reflect this and not allow re-acknowledgement
Do not expose internal record IDs or user IDs in button labels or visible UI text
ui components
AppButton (primary + secondary variants)
CircularProgressIndicator (or project loading indicator)
SnackBar (project standard for confirmations/errors)
ExpiryStatusIndicator (full mode)

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use BlocConsumer instead of BlocBuilder for the coordinator layout so you can respond to state changes as side-effects (snackbars, navigation) via the listener callback while the builder handles UI rebuilds. Manage the in-flight loading state for each button separately using local widget state (StatefulWidget or Riverpod StateProvider) so they can independently show/hide their loading indicators without conflating their states. Define CourseEnrollmentPromptService and CoordinatorAcknowledgementService as abstract classes (interfaces) injected via constructor or Riverpod provider — this enables clean mocking in tests. Ensure both services are behind the same role-resolution check in the BLoC (task-005 already enforces AcknowledgeLapse role check).

Add an isAcknowledged guard on the acknowledge button: if record.isAcknowledged == true, render the button as disabled with an 'Already acknowledged' label rather than removing it, to give coordinators clear feedback on current state.

Testing Requirements

Widget tests using flutter_test and MockBloc. Cover: (1) coordinator role renders both action buttons, (2) peer mentor role does not render either button (assert find.byKey or find.text returns nothing), (3) tapping 'Trigger course enrollment' disables button and shows loading indicator while async call is in progress, (4) successful enrollment shows confirmation snackbar, (5) failed enrollment shows error snackbar and re-enables button, (6) tapping 'Acknowledge lapse' dispatches AcknowledgeLapse event to BLoC, (7) after BLoC emits updated Loaded state with isAcknowledged=true, the acknowledge button is disabled/hidden and confirmation is visible, (8) double-tap on either button does not dispatch event twice (debounce or disable check). Use fake implementations of CourseEnrollmentPromptService and CoordinatorAcknowledgementService in tests.

Epic Risks (2)
medium impact medium prob technical

The persistent banner must remain visible across app sessions and only disappear when a specific backend condition is met (renewal or coordinator acknowledgement). If the BLoC state is not properly sourced from the notification record repository on every app launch, the banner may disappear prematurely or fail to reappear after a session restart.

Mitigation & Contingency

Mitigation: Drive the banner's visibility exclusively from a Supabase real-time subscription on the notification records table filtered by mentor_id and acknowledged_at IS NULL. Never persist banner visibility state locally. Write an integration test that restarts the BLoC and verifies the banner reappears from the database source.

Contingency: If real-time subscriptions introduce latency or connection reliability issues in offline-first scenarios, add a local cache flag that is only cleared when the repository confirms the acknowledgement write succeeded, with a cache TTL of 24 hours as a fallback.

high impact low prob security

The notification detail view must conditionally render coordinator-specific actions based on the authenticated user's role. Incorrect role resolution could expose the 'Acknowledge Lapse' action to peer mentors or hide it from coordinators, breaking the workflow and potentially allowing unauthorised state changes.

Mitigation & Contingency

Mitigation: Source the role check from the existing role_state_manager BLoC that is already authenticated against Supabase role claims. Do not rely on a local flag. The coordinator acknowledgement service backend also validates role server-side, providing defence in depth. Add widget tests that render the detail view with mentor and coordinator role fixtures and assert the presence or absence of coordinator actions.

Contingency: If a role resolution bug is found in production, immediately disable the acknowledge action via a feature flag and patch the role check in a hotfix release. The server-side validation in the coordinator acknowledgement service ensures no actual state change can occur even if the button is incorrectly rendered.