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 bareflutter/dart. Run fromapp/. - One data path: Bloc/Cubit → Repository → Client facade → Service → Adapter. Presentation NEVER imports
drift/supabase. The suggest dialog submitting directly viaChoresRepository.requestChore(read from context) is the sanctioned precedent set byshowRecommendSheet— 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-shotFuture<List<ChoreRequest>>), andChoreService._promoteitemType→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()(filterstatus == pending), refreshed on surface open — matching the existing Household poll pattern. NowatchChoreRequestsstream is added. @JsonSerializablecubit/bloc states where a state is introduced (per the E1 admin-CRUD decision), followingChoresManagementState.- Codegen scoping: any
state.g.dart/router.gr.dartregen uses a scoped--build-filter; after each,git statusand 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, andChoresManagementBlocare the integration points; do not duplicate them.
Resolved ambiguities (controller decisions — do not re-litigate)
- Badge → More tab admin entry only (
MoreTileGatedAdmin, keyMorePage.adminEntry): aggregate pending count across all 4 item-types, sourced fromMoreBloc, poll-on-open. The in-hubAdminHubPage.manageEntrybadge is deferred (redundant). - Earn/Rewards suggest = additive: a distinct "Suggest" affordance in each tab's
TopLevelHeader.actions; the existing Earn AI-recommend button is untouched. - Submit = direct
ChoresRepository.requestChorefrom the scoped shared dialog (readsChoresRepository+CurrentMemberRepositoryfrom context), followingshowRecommendSheet. No new submit bloc. - 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 standaloneRewardsManagementPage/ActivitiesManagementPageremoval + 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.dart—SuggestCatalogItemDialog(extracted from_RequestChoreDialog, gainsallowedTypes) +showSuggestCatalogItemDialog(...).app/lib/inside/routes/authenticated/shared/chore_request_review_tile.dart—ChoreRequestReviewTile(extracted from_ChoreRequestTile, callback-driven).app/lib/inside/blocs/chore_requests_manage/events.dart/state.dart/state.g.dart/bloc.dart—ChoreRequestsManageBloc(load/approve/reject; exposes pending list).app/lib/inside/routes/authenticated/manage/pending_requests_section.dart—PendingRequestsSection({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— confirmrequestChore/getChoreRequests/approveChoreRequest/rejectChoreRequestdelegates exist (they do, 150-202); no change unless a signature gap is found.app/lib/inside/routes/authenticated/manage/page.dart— addChoreRequestsManageBlocto theMultiBlocProvider.app/lib/inside/routes/authenticated/rewards/rewards_manage_body.dart,activities/activities_manage_body.dart,manage/chores_manage_body.dart— insertPendingRequestsSectionabove 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— removeChoreRequestsSection.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 theChoreRequest*events/handlers/state (keepcurrentMemberId).app/lib/inside/blocs/more/bloc.dart+state.dart— injectChoresRepository, load pending count, addpendingRequestsCount.app/lib/inside/routes/authenticated/more/widgets/more_tile_gated_admin.dart— add optionalpendingCountbadge.app/lib/inside/i18n/strings.dart— newsuggest*/recommendations*copy; remove deadadminRewardsTitle/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})+SuggestCatalogItemDialogwidget. On submit it callscontext.read<ChoresRepository>().requestChore(actingMemberId: <authed>, ...)directly (fire-and-forget with success/error SnackBar or DsBanner), reading the authed member fromCurrentMemberRepository. -
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/MockCurrentMemberRepositorynames + aseedChoreRequestfactory (inclient_sdk_testing); adjust. Readchore_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). TheDsSegmented<RequestItemType>renders onlyallowedTypes(not all four); the initial selected type isallowedTypes.first. -
Replace the current
MembersBloc-event submit path with a DIRECT call: readcontext.read<ChoresRepository>()+ the authed member id fromcontext.read<CurrentMemberRepository>().current(match howshowRecommendSheetreaches its repository), callrequestChore(...), 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})thatshowDialogs 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})— aStatelessWidgetrendering 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-shotFuture<List<ChoreRequest>>),.approveChoreRequest({required actingMemberId, required requestId}),.rejectChoreRequest({required actingMemberId, required requestId});CurrentMemberRepository.currentforactingMemberId. -
Produces:
ChoreRequestsManageBloc({required ChoresRepository choresRepository, required CurrentMemberRepository currentMemberRepository}); eventsChoreRequestsManageStarted,ChoreRequestsManageApproved(String id),ChoreRequestsManageRejected(String id); state{ ChoreRequestsManageStatus status, List<ChoreRequest> pending, String? errorMessage }withpendingOf(RequestItemType type) => pending.where((r) => r.itemType == type); status enum{initial,loading,ready,loadFailure,working,actionFailure}. MirrorsChoresManagementBloc(load-after-write,_runGuardedtyped-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, mirrorChoresManagementState), substitutingList<ChoreRequest> pendingforchoresand 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, mirrorChoresManagementBloc):_onStartedloadsgetChoreRequests()filtered tostatus == ChoreRequestStatus.pending;_onApproved/_onRejectedrun_runGuardedcallingapproveChoreRequest/rejectChoreRequestwithactingMemberId: _currentMemberRepository.current?.id(guard null → anactionFailure"no acting member", never a bare crash), then reload. SameValidationException/DomainRuleException/Exceptionladder asChoresManagementBloc.
Read
app/lib/inside/blocs/chores_management/bloc.dartas the exact template; confirmCurrentMemberRepository.currentreturns a member with.id(read the repo).
-
Step 7: Run bloc test + full suite → PASS;
cd app && fvm flutter testgreen (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 byManagePage);ChoreRequestReviewTile(Task 2);RequestItemType. -
Produces:
PendingRequestsSection({required RequestItemType itemType})— reads the bloc, rendersbloc.state.pendingOf(itemType)as a titled block ofChoreRequestReviewTiles (approve →ChoreRequestsManageApproved(id), reject →ChoreRequestsManageRejected(id)); renders NOTHING when that type has no pending requests. -
Step 1: Add strings (
recommendationsSectionTitle,recommendationsApprove,recommendationsReject, etc.) instrings.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'stitle/childparams fromrewards_manage_body.dartusage.
-
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_isBountyderives fromkind). -
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 testgreen (>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(keyEarnPage.suggestButton/RewardsTabPage.suggestButton) in each tab'sTopLevelHeader.actionsopening the dialog scoped to[chore, bounty](Earn) /[activity, reward](Rewards). -
Step 1: Add strings (
suggestTooltip/suggestChoreBountyTitle/suggestActivityRewardTitleas 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'sactionsparam 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 suite →
cd app && fvm flutter testgreen (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:
-
MoreBlocgains aChoresRepositoryconstructor dependency;_onStartedloadsgetChoreRequests(), countsstatus == pending, stores it.MoreStategainsint pendingRequestsCount(default 0).MoreTileGatedAdmingainsint pendingCount = 0and renders a Material-3Badge(or a small count bubble) on its trailing area whenpendingCount > 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/MoreBlocfield wiring + theMorePage.adminEntryconstruction site; keepKey('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 suite →
cd app && fvm flutter testgreen. -
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 statusrestore each time. - i18n mechanism: confirm plain-const vs slang in
strings.dartbefore 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; theChoresManagementBlocDomainRuleExceptiontest — all remain in the ledger as follow-ups.