Skip to main content

Catalog Restructure — Phase D: Recommendations Relocation + Inline Review + Badge — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Relocate member "suggestions" (ChoreRequests): move the SUBMIT affordance onto the Earn tab (chore/bounty) + Rewards tab (activity/reward), render pending suggestions inline at the TOP of each admin Manage tab (approve/reject), show a pending-count badge on the More tab's admin entry, and remove the suggestion section from the Household tab.

Architecture: No SDK/service change — requestChore, approveChoreRequest/rejectChoreRequest, and the itemType→create routing (ChoreService._promote) are already built and tested. Phase D extracts the existing all-four-types suggest dialog and the approve/reject review tile into shared widgets, adds a ChoreRequestsManageBloc to the Phase C ManagePage MultiBlocProvider for inline review, wires direct-repo requestChore submit from Earn/Rewards, adds a pending-count field to MoreBloc + a Material-3 Badge on the More admin row, and deletes the Household ChoreRequestsSection.

Tech Stack: Flutter 3.44 / Dart 3.9 (FVM), flutter_bloc, auto_route, design_system atoms, flow_test harness, json_serializable. Reuses the Phase C ManagePage/RewardsManageBody/ActivitiesManageBody/ChoresManageBody.

Global Constraints

  • FVM only: fvm flutter ... / fvm dart ... — never bare flutter/dart. Run from app/.
  • One data path: Bloc/Cubit → Repository → Client facade → Service → Adapter. Presentation NEVER imports drift/supabase. The suggest dialog submitting directly via ChoresRepository.requestChore (read from context) is the sanctioned precedent set by showRecommendSheet — the repository IS the seam; this is not a violation.
  • No SDK/service change required: requestChore (client.dart:499-508), approveChoreRequest (514-517), rejectChoreRequest (521-524), getChoreRequests (490, one-shot Future<List<ChoreRequest>>), and ChoreService._promote itemType→create routing (chore_service.dart:1032-1059) are all built + tested. Phase D only consumes them.
  • Poll-on-open, no realtime: pending requests load one-shot via getChoreRequests() (filter status == pending), refreshed on surface open — matching the existing Household poll pattern. No watchChoreRequests stream is added.
  • @JsonSerializable cubit/bloc states where a state is introduced (per the E1 admin-CRUD decision), following ChoresManagementState.
  • Codegen scoping: any state.g.dart/router.gr.dart regen uses a scoped --build-filter; after each, git status and restore any collateral generated file from HEAD. Commands given per task.
  • Brand-neutral: no brand strings in names; all copy via Strings.
  • Suite baseline: app 785 green (207 golden skips) on feat/mvp1-personas-authz. Run the FULL app suite (cd app && fvm flutter test) at the end of any task that changes a constructor signature, a route, a provider set, or removes a widget. Report exact counts.
  • Reuse Phase C surfaces: ManagePage (app/lib/inside/routes/authenticated/manage/page.dart), the three manage bodies, and ChoresManagementBloc are the integration points; do not duplicate them.

Resolved ambiguities (controller decisions — do not re-litigate)

  1. Badge → More tab admin entry only (MoreTileGatedAdmin, key MorePage.adminEntry): aggregate pending count across all 4 item-types, sourced from MoreBloc, poll-on-open. The in-hub AdminHubPage.manageEntry badge is deferred (redundant).
  2. Earn/Rewards suggest = additive: a distinct "Suggest" affordance in each tab's TopLevelHeader.actions; the existing Earn AI-recommend button is untouched.
  3. Submit = direct ChoresRepository.requestChore from the scoped shared dialog (reads ChoresRepository + CurrentMemberRepository from context), following showRecommendSheet. No new submit bloc.
  4. Pending source = poll-on-open one-shot (getChoreRequests() filtered to pending + by itemType). No realtime.

Phase C follow-up folded in

  • Task 8 sweeps the now-dead Strings.adminRewardsTitle / adminActivitiesTitle (orphaned when Phase C replaced the two hub rows). The orphaned standalone RewardsManagementPage/ActivitiesManagementPage removal + the catalog-wide stale-list-on-reload decision remain deferred (out of Phase D scope; noted in the ledger).

File Structure

Create:

  • app/lib/inside/routes/authenticated/shared/suggest_catalog_item_dialog.dartSuggestCatalogItemDialog (extracted from _RequestChoreDialog, gains allowedTypes) + showSuggestCatalogItemDialog(...).
  • app/lib/inside/routes/authenticated/shared/chore_request_review_tile.dartChoreRequestReviewTile (extracted from _ChoreRequestTile, callback-driven).
  • app/lib/inside/blocs/chore_requests_manage/events.dart / state.dart / state.g.dart / bloc.dartChoreRequestsManageBloc (load/approve/reject; exposes pending list).
  • app/lib/inside/routes/authenticated/manage/pending_requests_section.dartPendingRequestsSection({required RequestItemType itemType}) inline block for the Manage bodies.
  • app/test/unit/blocs/chore_requests_manage_bloc_test.dart, app/test/flows/manage_recommendations_test.dart, app/test/flows/earn_suggest_test.dart (+ rewards suggest).

Modify:

  • app/lib/outside/repositories/chores/chores_repository.dart — confirm requestChore/getChoreRequests/approveChoreRequest/rejectChoreRequest delegates exist (they do, 150-202); no change unless a signature gap is found.
  • app/lib/inside/routes/authenticated/manage/page.dart — add ChoreRequestsManageBloc to the MultiBlocProvider.
  • app/lib/inside/routes/authenticated/rewards/rewards_manage_body.dart, activities/activities_manage_body.dart, manage/chores_manage_body.dart — insert PendingRequestsSection above the add button.
  • app/lib/inside/routes/authenticated/earn/page.dart, rewards_tab/page.dart — add the "Suggest" header affordance.
  • app/lib/inside/routes/authenticated/household/page.dart — remove ChoreRequestsSection.
  • app/lib/inside/routes/authenticated/household/chore_requests_section.dart — DELETE after its dialog+tile are extracted and it's unhosted.
  • app/lib/inside/blocs/household/members_bloc.dart + state — retire the ChoreRequest* events/handlers/state (keep currentMemberId).
  • app/lib/inside/blocs/more/bloc.dart + state.dart — inject ChoresRepository, load pending count, add pendingRequestsCount.
  • app/lib/inside/routes/authenticated/more/widgets/more_tile_gated_admin.dart — add optional pendingCount badge.
  • app/lib/inside/i18n/strings.dart — new suggest* / recommendations* copy; remove dead adminRewardsTitle/adminActivitiesTitle (Task 8).

Task 1: Extract SuggestCatalogItemDialog (scoped by allowedTypes)

Files:

  • Create: app/lib/inside/routes/authenticated/shared/suggest_catalog_item_dialog.dart
  • Modify: app/lib/inside/routes/authenticated/household/chore_requests_section.dart (its trigger calls the shared dialog)
  • Test: app/test/widget/suggest_catalog_item_dialog_test.dart

Interfaces:

  • Consumes: ChoresRepository.requestChore({required String actingMemberId, required String name, required ChoreKind kind, required ChoreFrequency frequency, RequestItemType itemType, int tokenValue, String? emoji, String? note}); CurrentMemberRepository.current (authed member id); RequestItemType {chore,bounty,activity,reward}.

  • Produces: Future<void> showSuggestCatalogItemDialog(BuildContext context, {required List<RequestItemType> allowedTypes}) + SuggestCatalogItemDialog widget. On submit it calls context.read<ChoresRepository>().requestChore(actingMemberId: <authed>, ...) directly (fire-and-forget with success/error SnackBar or DsBanner), reading the authed member from CurrentMemberRepository.

  • Step 1: Write the failing test

Create app/test/widget/suggest_catalog_item_dialog_test.dart. Pump SuggestCatalogItemDialog(allowedTypes: [RequestItemType.chore, RequestItemType.bounty]) inside a MaterialApp with RepositoryProviders for a MockChoresRepository + a MockCurrentMemberRepository (returning an authed member). Assert: (a) only the two allowed type segments render (no activity/reward segment); (b) filling the name + tapping submit calls requestChore with itemType: RequestItemType.chore (default of the allowed set) and the authed actingMemberId. Use the codebase's real widget-test pattern (mocktail; check an existing dialog test for the harness).

// sketch — match the real MockChoresRepository / member seed / DsSegmented finder
testWidgets('scopes type selector to allowedTypes and submits requestChore', (tester) async {
final repo = MockChoresRepository();
when(() => repo.requestChore(
actingMemberId: any(named: 'actingMemberId'), name: any(named: 'name'),
kind: any(named: 'kind'), frequency: any(named: 'frequency'),
itemType: any(named: 'itemType'), tokenValue: any(named: 'tokenValue'),
emoji: any(named: 'emoji'), note: any(named: 'note'),
)).thenAnswer((_) async => seedChoreRequest(id: 'r1'));
// ... pump dialog with allowedTypes:[chore,bounty], enter name, tap submit ...
verify(() => repo.requestChore(itemType: RequestItemType.chore, ...)).called(1);
});

Verify the real MockChoresRepository/MockCurrentMemberRepository names + a seedChoreRequest factory (in client_sdk_testing); adjust. Read chore_requests_section.dart:295-521 (_RequestChoreDialog) as the extraction source.

  • Step 2: Run test to verify it fails

Run: cd app && fvm flutter test test/widget/suggest_catalog_item_dialog_test.dart Expected: FAIL — the shared dialog doesn't exist.

  • Step 3: Extract + parameterize

Move _RequestChoreDialog (chore_requests_section.dart:295-521) into SuggestCatalogItemDialog in the new shared file. Changes from the original:

  • Add final List<RequestItemType> allowedTypes; (required). The DsSegmented<RequestItemType> renders only allowedTypes (not all four); the initial selected type is allowedTypes.first.

  • Replace the current MembersBloc-event submit path with a DIRECT call: read context.read<ChoresRepository>() + the authed member id from context.read<CurrentMemberRepository>().current (match how showRecommendSheet reaches its repository), call requestChore(...), then close the dialog and show a success/failure affordance. Preserve the existing kind/frequency filler derivation for activity/reward.

  • Provide showSuggestCatalogItemDialog(context, {required allowedTypes}) that showDialogs it.

  • Step 4: Point Household's trigger at the shared dialog

In chore_requests_section.dart, change the trailing IconButton (lines 40-46) to call showSuggestCatalogItemDialog(context, allowedTypes: RequestItemType.values) (all four — Household keeps full scope until Task 6 removes it). Delete the now-moved _RequestChoreDialog from this file. Leave the review list (_ChoreRequestTile) in place for now (Task 2 extracts it; Task 6 removes the section).

  • Step 5: Run tests + full suite

Run: cd app && fvm flutter test test/widget/suggest_catalog_item_dialog_test.dart Expected: PASS. Run: cd app && fvm flutter test Expected: baseline (785) green; any existing Household test that opened _RequestChoreDialog still passes (it now opens the shared dialog with the same fields/keys — preserve the field Keys during extraction).

  • Step 6: Commit
git add app/lib/inside/routes/authenticated/shared/suggest_catalog_item_dialog.dart app/lib/inside/routes/authenticated/household/chore_requests_section.dart app/test/widget/suggest_catalog_item_dialog_test.dart
git commit -m "feat: extract SuggestCatalogItemDialog (allowedTypes-scoped) from Household"

Task 2: Extract ChoreRequestReviewTile

Files:

  • Create: app/lib/inside/routes/authenticated/shared/chore_request_review_tile.dart
  • Modify: app/lib/inside/routes/authenticated/household/chore_requests_section.dart (use the shared tile)
  • Test: app/test/widget/chore_request_review_tile_test.dart

Interfaces:

  • Produces: ChoreRequestReviewTile({required ChoreRequest request, required VoidCallback onApprove, required VoidCallback onReject, String? requesterLabel}) — a StatelessWidget rendering the request (emoji, name, proposer, tokenValue, note) with Approve + Reject buttons that invoke the callbacks. Callback-driven so it works in both the Household section (until removed) and the Manage inline block.

  • Step 1: Write the failing test

app/test/widget/chore_request_review_tile_test.dart: pump ChoreRequestReviewTile(request: seedChoreRequest(name: 'Extra dishes'), onApprove: ..., onReject: ...); assert the name renders and that tapping the approve/reject buttons (by their keys) invokes the callbacks exactly once.

  • Step 2: Run test to verify it fails

Run: cd app && fvm flutter test test/widget/chore_request_review_tile_test.dart — FAIL (widget missing).

  • Step 3: Extract

Move _ChoreRequestTile (in chore_requests_section.dart) into ChoreRequestReviewTile, replacing its context.read<MembersBloc>().add(ChoreRequestApproved/Rejected(...)) dispatches with the onApprove/onReject callbacks. Preserve the rendered fields + any Keys. Keep the tile visually identical (reuse the same DS atoms).

  • Step 4: Use the shared tile in Household

In chore_requests_section.dart, render ChoreRequestReviewTile(request: r, onApprove: () => context.read<MembersBloc>().add(ChoreRequestApproved(r.id)), onReject: () => context.read<MembersBloc>().add(ChoreRequestRejected(r.id))) for each pending request (the Household wiring stays via MembersBloc until Task 6). Delete the moved _ChoreRequestTile.

  • Step 5: Run tests + full suite

Run: cd app && fvm flutter test test/widget/chore_request_review_tile_test.dart → PASS. Run: cd app && fvm flutter test → 785 green.

  • Step 6: Commit
git add app/lib/inside/routes/authenticated/shared/chore_request_review_tile.dart app/lib/inside/routes/authenticated/household/chore_requests_section.dart app/test/widget/chore_request_review_tile_test.dart
git commit -m "feat: extract ChoreRequestReviewTile (callback-driven) from Household"

Task 3: ChoreRequestsManageBloc

Files:

  • Create: app/lib/inside/blocs/chore_requests_manage/{events,state,state.g,bloc}.dart
  • Test: app/test/unit/blocs/chore_requests_manage_bloc_test.dart

Interfaces:

  • Consumes: ChoresRepository.getChoreRequests() (one-shot Future<List<ChoreRequest>>), .approveChoreRequest({required actingMemberId, required requestId}), .rejectChoreRequest({required actingMemberId, required requestId}); CurrentMemberRepository.current for actingMemberId.

  • Produces: ChoreRequestsManageBloc({required ChoresRepository choresRepository, required CurrentMemberRepository currentMemberRepository}); events ChoreRequestsManageStarted, ChoreRequestsManageApproved(String id), ChoreRequestsManageRejected(String id); state { ChoreRequestsManageStatus status, List<ChoreRequest> pending, String? errorMessage } with pendingOf(RequestItemType type) => pending.where((r) => r.itemType == type); status enum {initial,loading,ready,loadFailure,working,actionFailure}. Mirrors ChoresManagementBloc (load-after-write, _runGuarded typed-exception surfacing).

  • Step 1: Write the failing test

app/test/unit/blocs/chore_requests_manage_bloc_test.dart (repo's real bloc-test pattern — flutter_test + stream listening, per Phase C Task 2): Started loads pending (filter status==pending) → ready; pendingOf(RequestItemType.reward) filters correctly; Approved calls approveChoreRequest(actingMemberId: <authed>, requestId: 'r1') then reloads; a ValidationException on approve surfaces actionFailure with .message.

  • Step 2: Run test → FAIL (cd app && fvm flutter test test/unit/blocs/chore_requests_manage_bloc_test.dart).

  • Step 3: Write events (events.dart):

sealed class ChoreRequestsManageEvent {}
class ChoreRequestsManageStarted extends ChoreRequestsManageEvent {}
class ChoreRequestsManageApproved extends ChoreRequestsManageEvent {
ChoreRequestsManageApproved(this.id); final String id;
}
class ChoreRequestsManageRejected extends ChoreRequestsManageEvent {
ChoreRequestsManageRejected(this.id); final String id;
}
  • Step 4: Write state (state.dart, @JsonSerializable, mirror ChoresManagementState), substituting List<ChoreRequest> pending for chores and adding:
Iterable<ChoreRequest> pendingOf(RequestItemType type) =>
pending.where((r) => r.itemType == type);

Status enum + copyWith(status, pending, setErrorMessage) exactly as the ChoresManagementState shape.

  • Step 5: Generate state.g.dart (scoped)
cd app && fvm dart run build_runner build --build-filter "lib/inside/blocs/chore_requests_manage/state.g.dart"

git status → restore any collateral generated file from HEAD.

  • Step 6: Write bloc (bloc.dart, mirror ChoresManagementBloc): _onStarted loads getChoreRequests() filtered to status == ChoreRequestStatus.pending; _onApproved/_onRejected run _runGuarded calling approveChoreRequest/rejectChoreRequest with actingMemberId: _currentMemberRepository.current?.id (guard null → an actionFailure "no acting member", never a bare crash), then reload. Same ValidationException/DomainRuleException/Exception ladder as ChoresManagementBloc.

Read app/lib/inside/blocs/chores_management/bloc.dart as the exact template; confirm CurrentMemberRepository.current returns a member with .id (read the repo).

  • Step 7: Run bloc test + full suite → PASS; cd app && fvm flutter test green (no call sites yet).

  • Step 8: Commit

git add app/lib/inside/blocs/chore_requests_manage/ app/test/unit/blocs/chore_requests_manage_bloc_test.dart
git commit -m "feat: ChoreRequestsManageBloc (load pending + approve/reject, mirrors ChoresManagementBloc)"

Task 4: PendingRequestsSection inline in each Manage body

Files:

  • Create: app/lib/inside/routes/authenticated/manage/pending_requests_section.dart
  • Modify: manage/page.dart (add bloc to MultiBlocProvider), rewards/rewards_manage_body.dart, activities/activities_manage_body.dart, manage/chores_manage_body.dart, strings.dart
  • Test: app/test/flows/manage_recommendations_test.dart

Interfaces:

  • Consumes: ChoreRequestsManageBloc (from context, provided by ManagePage); ChoreRequestReviewTile (Task 2); RequestItemType.

  • Produces: PendingRequestsSection({required RequestItemType itemType}) — reads the bloc, renders bloc.state.pendingOf(itemType) as a titled block of ChoreRequestReviewTiles (approve → ChoreRequestsManageApproved(id), reject → ChoreRequestsManageRejected(id)); renders NOTHING when that type has no pending requests.

  • Step 1: Add strings (recommendationsSectionTitle, recommendationsApprove, recommendationsReject, etc.) in strings.dart's real mechanism.

  • Step 2: Add the bloc to ManagePage

In manage/page.dart wrappedRoute's MultiBlocProvider, add a fourth provider (lazy: false like the others):

BlocProvider(
lazy: false,
create: (context) => ChoreRequestsManageBloc(
choresRepository: context.read<ChoresRepository>(),
currentMemberRepository: context.read<CurrentMemberRepository>(),
)..add(ChoreRequestsManageStarted()),
),
  • Step 3: Write PendingRequestsSection
class PendingRequestsSection extends StatelessWidget {
const PendingRequestsSection({required this.itemType, super.key});
final RequestItemType itemType;

@override
Widget build(BuildContext context) {
return BlocBuilder<ChoreRequestsManageBloc, ChoreRequestsManageState>(
builder: (context, state) {
final pending = state.pendingOf(itemType).toList(growable: false);
if (pending.isEmpty) return const SizedBox.shrink();
final theme = DsTheme.of(context);
return Padding(
padding: EdgeInsets.only(bottom: theme.spacing.s5),
child: DsSection(
title: Strings.recommendationsSectionTitle,
child: Column(
children: <Widget>[
for (final r in pending)
ChoreRequestReviewTile(
request: r,
onApprove: () => context
.read<ChoreRequestsManageBloc>()
.add(ChoreRequestsManageApproved(r.id)),
onReject: () => context
.read<ChoreRequestsManageBloc>()
.add(ChoreRequestsManageRejected(r.id)),
),
],
),
),
);
},
);
}
}

Confirm DsSection's title/child params from rewards_manage_body.dart usage.

  • Step 4: Insert into the three bodies (above the add button)

  • rewards_manage_body.dart (above the "Add reward" DsButton): const PendingRequestsSection(itemType: RequestItemType.reward).

  • activities_manage_body.dart (above "Add activity"): const PendingRequestsSection(itemType: RequestItemType.activity).

  • chores_manage_body.dart (above the add button): PendingRequestsSection(itemType: _isBounty ? RequestItemType.bounty : RequestItemType.chore) (non-const because _isBounty derives from kind).

  • Step 5: Write the flow test

app/test/flows/manage_recommendations_test.dart (mirror manage_test.dart): stub getChoreRequests to return a pending REWARD request; navigate More → adminEntry → manageEntry; assert the pending request renders at the top of the Rewards tab; tap Approve; assert approveChoreRequest was called and (after reload stub) the request is gone. Also assert the Chores tab shows a pending CHORE request and NOT the reward one (type scoping).

  • Step 6: Run flow test + full suite → PASS; cd app && fvm flutter test green (>785).

  • Step 7: Commit

git add app/lib/inside/routes/authenticated/manage/ app/lib/inside/routes/authenticated/rewards/rewards_manage_body.dart app/lib/inside/routes/authenticated/activities/activities_manage_body.dart app/lib/inside/i18n/strings.dart app/test/flows/manage_recommendations_test.dart
git commit -m "feat: inline pending-suggestions review at top of each Manage tab"

Task 5: Earn + Rewards "Suggest" affordance

Files:

  • Modify: app/lib/inside/routes/authenticated/earn/page.dart, rewards_tab/page.dart, strings.dart
  • Test: app/test/flows/earn_suggest_test.dart (+ rewards suggest, same file or sibling)

Interfaces:

  • Consumes: showSuggestCatalogItemDialog(context, {required allowedTypes}) (Task 1).

  • Produces: an IconButton (key EarnPage.suggestButton / RewardsTabPage.suggestButton) in each tab's TopLevelHeader.actions opening the dialog scoped to [chore, bounty] (Earn) / [activity, reward] (Rewards).

  • Step 1: Add strings (suggestTooltip / suggestChoreBountyTitle / suggestActivityRewardTitle as needed).

  • Step 2: Failing flow test

app/test/flows/earn_suggest_test.dart: navigate to Earn; tap EarnPage.suggestButton; assert the dialog opens with only chore/bounty type options; fill + submit; assert requestChore called with an itemType in {chore,bounty}. Sibling assertion for Rewards tab ([activity,reward]).

  • Step 3: Add the affordances

In earn/page.dart, add to TopLevelHeader.actions (alongside the existing AI recommend button, ~line 146-159):

IconButton(
key: const Key('EarnPage.suggestButton'),
tooltip: Strings.suggestTooltip,
icon: const Icon(Icons.lightbulb_outline),
onPressed: () => showSuggestCatalogItemDialog(
context,
allowedTypes: const [RequestItemType.chore, RequestItemType.bounty],
),
),

In rewards_tab/page.dart, add an actions: list to the currently-empty TopLevelHeader() (~line 156-158) with the analogous button (RewardsTabPage.suggestButton, allowedTypes: [activity, reward]).

Confirm TopLevelHeader's actions param name/type from its definition; match how Earn's existing recommend button is passed.

  • Step 4: Run flow tests + full suite → PASS; green.

  • Step 5: Commit

git add app/lib/inside/routes/authenticated/earn/page.dart app/lib/inside/routes/authenticated/rewards_tab/page.dart app/lib/inside/i18n/strings.dart app/test/flows/earn_suggest_test.dart
git commit -m "feat: Suggest affordance on Earn (chore/bounty) + Rewards (activity/reward) tabs"

Task 6: Remove the Household suggestion section

Files:

  • Modify: app/lib/inside/routes/authenticated/household/page.dart (unhost the section)
  • Delete: app/lib/inside/routes/authenticated/household/chore_requests_section.dart
  • Modify: app/lib/inside/blocs/household/members_bloc.dart + its state (retire the request events/handlers/state)
  • Test: migrate/clean any Household flow test asserting the section

Interfaces: removes ChoreRequestSubmitted/ChoreRequestApproved/ChoreRequestRejected events, _onChoreRequestSubmitted/_onChoreRequestApproved/_onChoreRequestRejected/_loadChoreRequests handlers, and MembersState.choreRequests/choreRequestsStatus + the ChoreRequestsStatus enum. KEEP MembersState.currentMemberId (used by family-goal approve + admin-create paths).

  • Step 1: Find dependents

graphify query "ChoreRequestsSection MembersBloc chore request handlers" + grep -rn "ChoreRequestsSection\|ChoreRequestSubmitted\|ChoreRequestApproved\|ChoreRequestRejected\|choreRequests\b\|choreRequestsStatus\|ChoreRequestsStatus" app/lib app/test to enumerate every reference before deleting.

  • Step 2: Unhost + delete the section

Remove the ChoreRequestsSection usage from household/page.dart (both the tailored branch ~268-271 and master branch ~289-293) + its import (~27). Delete chore_requests_section.dart.

  • Step 3: Retire the MembersBloc request members

Remove the three request events, their on<...> registrations + handlers, _loadChoreRequests, and the choreRequests/choreRequestsStatus state fields + ChoreRequestsStatus enum. Keep everything else (currentMemberId, member CRUD, family-goal handlers). Update MembersState.copyWith/props/toJson accordingly and regenerate its state.g.dart if @JsonSerializable:

cd app && fvm dart run build_runner build --build-filter "lib/inside/blocs/household/state.g.dart"

(scoped; git status + restore collateral). Adjust the filter path to the real MembersState .g.dart location.

  • Step 4: Migrate/clean tests

Any flow/bloc test asserting the Household suggestion section or the removed events must be updated: assertions that the SUBMIT works now live in earn_suggest_test.dart (Task 5); assertions that REVIEW works now live in manage_recommendations_test.dart (Task 4). Remove or redirect the Household-side assertions — do NOT delete coverage that isn't reproduced elsewhere; if a unique behavior was only tested here, recreate it on the new surface.

  • Step 5: Full suitecd app && fvm flutter test green (count may drop by the removed Household tests but the relocated coverage in Tasks 4/5 compensates; net behavior preserved). Report the delta and where each removed assertion now lives.

  • Step 6: Commit

git add -A app/lib/inside/routes/authenticated/household/ app/lib/inside/blocs/household/ app/test
git commit -m "refactor: remove Household suggestion section (submit->Earn/Rewards, review->Manage)"

Task 7: More-tab pending-count badge

Files:

  • Modify: app/lib/inside/blocs/more/bloc.dart + state.dart, app/lib/inside/routes/authenticated/more/widgets/more_tile_gated_admin.dart, strings.dart
  • Test: app/test/flows/more_recommendations_badge_test.dart (or a MoreBloc unit test + widget test)

Interfaces:

  • MoreBloc gains a ChoresRepository constructor dependency; _onStarted loads getChoreRequests(), counts status == pending, stores it. MoreState gains int pendingRequestsCount (default 0). MoreTileGatedAdmin gains int pendingCount = 0 and renders a Material-3 Badge (or a small count bubble) on its trailing area when pendingCount > 0.

  • Step 1: Failing test

MoreBloc unit test: with getChoreRequests() stubbed to return 2 pending + 1 approved, _onStarted yields state.pendingRequestsCount == 2. Widget test: MoreTileGatedAdmin(pendingCount: 3) renders a badge showing "3"; pendingCount: 0 renders no badge.

  • Step 2: Run → FAIL.

  • Step 3: MoreBloc + state

Inject ChoresRepository into MoreBloc (add the param + wire it at the construction site — find where MoreBloc is created, likely in the More route's wrappedRoute or a provider; update that site AND any test that constructs MoreBloc). In _onStarted, after the existing loads, final reqs = await _choresRepository.getChoreRequests(); emit(state.copyWith(pendingRequestsCount: reqs.where((r) => r.status == ChoreRequestStatus.pending).length)); (guard failures — a count-load error must not break the More tab; default to 0 on error). Add pendingRequestsCount to MoreState (+ copyWith/props/toJson + regen if serializable, scoped).

  • Step 4: Badge on the row

Add final int pendingCount; (default 0) to MoreTileGatedAdmin; wrap/augment its trailing with Badge.count(count: pendingCount, isLabelVisible: pendingCount > 0, child: <existing trailing>) (Flutter Material 3 Badge), or a DS-token-styled count bubble if the design prefers. At the MorePage.adminEntry construction site, pass pendingCount: context.watch<MoreBloc>().state.pendingRequestsCount (or via a BlocBuilder).

Confirm MoreState/MoreBloc field wiring + the MorePage.adminEntry construction site; keep Key('MorePage.adminEntry') intact (flow tests use it).

  • Step 5: Run tests + full suite → green.

  • Step 6: Commit

git add app/lib/inside/blocs/more/ app/lib/inside/routes/authenticated/more/ app/lib/inside/i18n/strings.dart app/test
git commit -m "feat: pending-suggestions count badge on the More admin entry"

Task 8: Sweep dead admin catalog strings (Phase C follow-up)

Files: app/lib/inside/i18n/strings.dart (+ its generated output if slang)

  • Step 1: Confirm dead

grep -rn "adminRewardsTitle\|adminActivitiesTitle" app/lib app/test — confirm ZERO references outside the declaration (Phase C replaced their rows). If any live reference remains, STOP and report (they're not dead).

  • Step 2: Remove

Delete the two static const String adminRewardsTitle/adminActivitiesTitle declarations (or the slang keys + regen, scoped).

  • Step 3: Full suitecd app && fvm flutter test green.

  • Step 4: Update graphify + commit

git add app/lib/inside/i18n/strings.dart && git commit -m "chore: remove dead admin catalog strings (Phase C follow-up)"
cd .. && graphify update . && git add graphify-out/ && git commit -m "chore: graphify update after Phase D" || true

Self-Review

1. Spec coverage (spec §5 + phasing "Phase D"):

  • Submit relocated to Earn (chore/bounty) + Rewards (activity/reward), reusing requestChore → Tasks 1, 5. ✅
  • Household suggestion section removed → Task 6. ✅
  • Pending suggestions inline at top of each Manage type list, approve/reject routing by itemType → Tasks 3, 4 (routing reuses the built _promote). ✅
  • Pending-count badge on the More admin row → Task 7. ✅

2. Placeholder scan: extraction tasks give exact source line-ranges + new signatures (implementer reads the source); new code (cubit, section, badge) is shown in full; every command is exact + FVM + scoped. > notes are verify-against-codebase instructions, not placeholders. ✅

3. Type consistency: RequestItemType {chore,bounty,activity,reward} used identically across the dialog allowedTypes, the cubit pendingOf, and the section itemType. ChoreRequestsManageStatus mirrors ChoresManagementStatus. pendingRequestsCount named identically across MoreState, MoreBloc, and MoreTileGatedAdmin. Approve/reject use actingMemberId from CurrentMemberRepository everywhere. ✅

Cross-cutting notes for the executor

  • Task ordering is dependency-driven: shared widgets (1,2) → cubit (3) → inline review (4) → suggest (5) → THEN Household removal (6, only after submit+review exist elsewhere) → badge (7) → cleanup (8). Do not remove the Household section before Tasks 4+5 land.
  • Full-suite discipline: Tasks 4, 6, 7 change providers/remove widgets/change constructors → each ends with a full fvm flutter test + reported counts. Task 6 may legitimately change the count (removed Household tests, added relocated tests) — report the delta and the coverage mapping.
  • Codegen: Tasks 3, 6, 7 may regen state.g.dart — scoped filter + git status restore each time.
  • i18n mechanism: confirm plain-const vs slang in strings.dart before adding/removing keys; route through the real mechanism.
  • Deferred (not Phase D): removing the orphaned standalone RewardsManagementPage/ActivitiesManagementPage; the catalog-wide stale-list-on-reload decision; the ChoresManagementBloc DomainRuleException test — all remain in the ledger as follow-ups.