docs(07-task-sorting): create phase plan

This commit is contained in:
2026-03-16 22:23:24 +01:00
parent a94d41b7f7
commit 27f18d4f39
3 changed files with 495 additions and 2 deletions

View File

@@ -63,7 +63,10 @@ Plans:
2. Selecting alphabetical sort orders tasks A-Z by name within the visible list
3. Selecting interval sort orders tasks from most-frequent (daily) to least-frequent (yearly/custom) intervals
4. Selecting effort sort orders tasks from lowest effort to highest effort level
**Plans**: TBD
**Plans:** 2 plans
Plans:
- [ ] 07-01-PLAN.md — Sort model, persistence notifier, localization, provider integration
- [ ] 07-02-PLAN.md — Sort dropdown widget, HomeScreen AppBar, TaskListScreen integration, tests
## Progress
@@ -75,4 +78,4 @@ Plans:
| 4. Notifications | v1.0 | 3/3 | Complete | 2026-03-16 |
| 5. Calendar Strip | 2/2 | Complete | 2026-03-16 | - |
| 6. Task History | 1/1 | Complete | 2026-03-16 | - |
| 7. Task Sorting | v1.1 | 0/? | Not started | - |
| 7. Task Sorting | v1.1 | 0/2 | Not started | - |

View File

@@ -0,0 +1,276 @@
---
phase: 07-task-sorting
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- lib/features/tasks/domain/task_sort_option.dart
- lib/features/tasks/presentation/sort_preference_notifier.dart
- lib/features/tasks/presentation/sort_preference_notifier.g.dart
- lib/l10n/app_de.arb
- lib/l10n/app_localizations.dart
- lib/l10n/app_localizations_de.dart
- lib/features/home/presentation/calendar_providers.dart
- lib/features/tasks/presentation/task_providers.dart
- test/features/tasks/presentation/sort_preference_notifier_test.dart
autonomous: true
requirements: [SORT-01, SORT-02, SORT-03]
must_haves:
truths:
- "Sort preference persists across app restarts"
- "CalendarDayList tasks are sorted according to the active sort preference"
- "TaskListScreen tasks are sorted according to the active sort preference"
- "Default sort is alphabetical (matches current CalendarDayList behavior)"
artifacts:
- path: "lib/features/tasks/domain/task_sort_option.dart"
provides: "TaskSortOption enum with alphabetical, interval, effort values"
exports: ["TaskSortOption"]
- path: "lib/features/tasks/presentation/sort_preference_notifier.dart"
provides: "SortPreferenceNotifier with SharedPreferences persistence"
exports: ["SortPreferenceNotifier", "sortPreferenceProvider"]
- path: "lib/features/home/presentation/calendar_providers.dart"
provides: "calendarDayProvider sorts dayTasks by active sort preference"
contains: "sortPreferenceProvider"
- path: "lib/features/tasks/presentation/task_providers.dart"
provides: "tasksInRoomProvider sorts tasks by active sort preference"
contains: "sortPreferenceProvider"
- path: "test/features/tasks/presentation/sort_preference_notifier_test.dart"
provides: "Unit tests for sort preference persistence and default"
key_links:
- from: "lib/features/home/presentation/calendar_providers.dart"
to: "sortPreferenceProvider"
via: "ref.watch in calendarDayProvider"
pattern: "ref\\.watch\\(sortPreferenceProvider\\)"
- from: "lib/features/tasks/presentation/task_providers.dart"
to: "sortPreferenceProvider"
via: "ref.watch in tasksInRoomProvider"
pattern: "ref\\.watch\\(sortPreferenceProvider\\)"
---
<objective>
Create the task sort domain model, SharedPreferences-backed persistence provider, and integrate sort logic into both task list providers (calendarDayProvider and tasksInRoomProvider).
Purpose: Establishes the data layer and sort logic so that task lists react to sort preference changes. The UI plan (07-02) will add the dropdown widget that writes to this provider.
Output: TaskSortOption enum, SortPreferenceNotifier, updated calendarDayProvider and tasksInRoomProvider with in-memory sorting, German localization strings for sort labels.
</objective>
<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/07-task-sorting/07-CONTEXT.md
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
From lib/features/tasks/domain/effort_level.dart:
```dart
enum EffortLevel {
low, // 0
medium, // 1
high, // 2
}
```
From lib/features/tasks/domain/frequency.dart:
```dart
enum IntervalType {
daily, // 0
everyNDays, // 1
weekly, // 2
biweekly, // 3
monthly, // 4
everyNMonths, // 5
quarterly, // 6
yearly, // 7
}
```
From lib/features/home/domain/daily_plan_models.dart:
```dart
class TaskWithRoom {
final Task task;
final String roomName;
final int roomId;
}
```
From lib/features/home/domain/calendar_models.dart:
```dart
class CalendarDayState {
final DateTime selectedDate;
final List<TaskWithRoom> dayTasks;
final List<TaskWithRoom> overdueTasks;
final int totalTaskCount;
}
```
From lib/core/theme/theme_provider.dart (pattern to follow for SharedPreferences notifier):
```dart
@riverpod
class ThemeNotifier extends _$ThemeNotifier {
@override
ThemeMode build() {
_loadPersistedThemeMode();
return ThemeMode.system; // sync default, async load overrides
}
Future<void> _loadPersistedThemeMode() async { ... }
Future<void> setThemeMode(ThemeMode mode) async {
state = mode;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_themeModeKey, _themeModeToString(mode));
}
}
```
From lib/features/home/presentation/calendar_providers.dart:
```dart
final calendarDayProvider = StreamProvider.autoDispose<CalendarDayState>((ref) {
final db = ref.watch(appDatabaseProvider);
final selectedDate = ref.watch(selectedDateProvider);
// ... fetches dayTasks, overdueTasks, totalTaskCount
// dayTasks come from watchTasksForDate which sorts alphabetically in SQL
});
```
From lib/features/tasks/presentation/task_providers.dart:
```dart
final tasksInRoomProvider = StreamProvider.family.autoDispose<List<Task>, int>((ref, roomId) {
final db = ref.watch(appDatabaseProvider);
return db.tasksDao.watchTasksInRoom(roomId);
// watchTasksInRoom sorts by nextDueDate in SQL
});
```
From lib/core/database/database.dart (Task table columns relevant to sorting):
```dart
class Tasks extends Table {
TextColumn get name => text().withLength(min: 1, max: 200)();
IntColumn get intervalType => intEnum<IntervalType>()();
IntColumn get intervalDays => integer().withDefault(const Constant(1))();
IntColumn get effortLevel => intEnum<EffortLevel>()();
}
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create TaskSortOption enum, SortPreferenceNotifier, and localization strings</name>
<files>
lib/features/tasks/domain/task_sort_option.dart,
lib/features/tasks/presentation/sort_preference_notifier.dart,
lib/features/tasks/presentation/sort_preference_notifier.g.dart,
lib/l10n/app_de.arb,
lib/l10n/app_localizations.dart,
lib/l10n/app_localizations_de.dart,
test/features/tasks/presentation/sort_preference_notifier_test.dart
</files>
<behavior>
- Default sort preference is TaskSortOption.alphabetical
- setSortOption(TaskSortOption.interval) updates state to interval
- Sort preference persists: after setSortOption(effort), a fresh notifier reads back effort from SharedPreferences
- TaskSortOption enum has exactly 3 values: alphabetical, interval, effort
</behavior>
<action>
1. Create `lib/features/tasks/domain/task_sort_option.dart`:
- `enum TaskSortOption { alphabetical, interval, effort }` — three values only, no index stability concern since this is NOT stored as intEnum in drift (stored as string in SharedPreferences)
2. Create `lib/features/tasks/presentation/sort_preference_notifier.dart`:
- Follow the exact ThemeNotifier pattern from `lib/core/theme/theme_provider.dart`
- `@riverpod class SortPreferenceNotifier extends _$SortPreferenceNotifier`
- `build()` returns `TaskSortOption.alphabetical` synchronously (default = alphabetical per user decision for continuity with current A-Z sort in CalendarDayList), then calls `_loadPersisted()` async
- `_loadPersisted()` reads `SharedPreferences.getString('task_sort_option')` and maps to enum
- `setSortOption(TaskSortOption option)` sets state immediately then persists string to SharedPreferences
- Static helpers `_fromString` / `_toString` for serialization (use enum .name property)
- The generated provider will be named `sortPreferenceProvider` (Riverpod 3 naming convention, consistent with themeProvider)
3. Run `dart run build_runner build --delete-conflicting-outputs` to generate `.g.dart`
4. Add localization strings to `lib/l10n/app_de.arb`:
- `"sortAlphabetical": "A\u2013Z"` (A-Z with en-dash, concise label per user decision)
- `"sortInterval": "Intervall"` (German for interval/frequency)
- `"sortEffort": "Aufwand"` (German for effort, matches existing taskFormEffortLabel context)
- `"sortLabel": "Sortierung"` (label for accessibility/semantics on the dropdown)
5. Run `flutter gen-l10n` to regenerate localization files
6. Write tests in `test/features/tasks/presentation/sort_preference_notifier_test.dart`:
- Follow the pattern from notification_settings test: `makeContainer()` helper that creates ProviderContainer, awaits `Future.delayed(Duration.zero)` for async load
- `SharedPreferences.setMockInitialValues({})` in setUp
- Test: default is alphabetical
- Test: setSortOption updates state
- Test: persisted value is loaded on restart (set mock initial values with key, verify state after load)
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/tasks/presentation/sort_preference_notifier_test.dart -x && flutter analyze --no-fatal-infos</automated>
</verify>
<done>TaskSortOption enum exists with 3 values. SortPreferenceNotifier persists to SharedPreferences. 3+ unit tests pass. ARB file has 4 new sort strings. dart analyze clean.</done>
</task>
<task type="auto">
<name>Task 2: Integrate sort logic into calendarDayProvider and tasksInRoomProvider</name>
<files>
lib/features/home/presentation/calendar_providers.dart,
lib/features/tasks/presentation/task_providers.dart
</files>
<action>
1. Edit `lib/features/home/presentation/calendar_providers.dart`:
- Add import for `sort_preference_notifier.dart` and `task_sort_option.dart`
- Inside `calendarDayProvider`, add `final sortOption = ref.watch(sortPreferenceProvider);`
- After constructing `CalendarDayState`, apply in-memory sort to `dayTasks` list before returning. Do NOT sort overdueTasks (overdue section stays pinned at top in its existing order per user discretion decision).
- Sort implementation — create a top-level helper function `List<TaskWithRoom> _sortTasks(List<TaskWithRoom> tasks, TaskSortOption sortOption)` that returns a new sorted list:
- `alphabetical`: sort by `task.name.toLowerCase()` (case-insensitive A-Z)
- `interval`: sort by `task.intervalType.index` ascending (daily=0 is most frequent, yearly=7 is least), then by `task.intervalDays` ascending as tiebreaker
- `effort`: sort by `task.effortLevel.index` ascending (low=0, medium=1, high=2)
- Apply: `dayTasks: _sortTasks(dayTasks, sortOption)` in the CalendarDayState constructor call
- Note: The SQL `orderBy([OrderingTerm.asc(tasks.name)])` in CalendarDao.watchTasksForDate still runs, but the in-memory sort overrides it. This is intentional — the SQL sort provides a stable baseline, the in-memory sort applies the user's preference.
2. Edit `lib/features/tasks/presentation/task_providers.dart`:
- Add import for `sort_preference_notifier.dart` and `task_sort_option.dart`
- In `tasksInRoomProvider`, add `final sortOption = ref.watch(sortPreferenceProvider);`
- Map the stream to apply in-memory sorting: `return db.tasksDao.watchTasksInRoom(roomId).map((tasks) => _sortTasksRaw(tasks, sortOption));`
- Create a top-level helper `List<Task> _sortTasksRaw(List<Task> tasks, TaskSortOption sortOption)` that sorts raw Task objects (not TaskWithRoom):
- `alphabetical`: sort by `task.name.toLowerCase()`
- `interval`: sort by `task.intervalType.index`, then `task.intervalDays`
- `effort`: sort by `task.effortLevel.index`
- Returns a new sorted list (do not mutate the original)
3. Verify both providers react to sort preference changes by running existing tests (they should still pass since default sort is alphabetical and current data is already alphabetically sorted or test data is single-item).
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test && flutter analyze --no-fatal-infos</automated>
</verify>
<done>calendarDayProvider watches sortPreferenceProvider and sorts dayTasks accordingly. tasksInRoomProvider watches sortPreferenceProvider and sorts tasks accordingly. All 106+ existing tests pass. dart analyze clean.</done>
</task>
</tasks>
<verification>
- `flutter test` — all 106+ tests pass (existing + new sort preference tests)
- `flutter analyze --no-fatal-infos` — zero issues
- `sortPreferenceProvider` is watchable and defaults to alphabetical
- Both calendarDayProvider and tasksInRoomProvider react to sort preference changes
</verification>
<success_criteria>
- TaskSortOption enum exists with alphabetical, interval, effort values
- SortPreferenceNotifier persists sort preference to SharedPreferences
- Default sort is alphabetical (continuity with existing A-Z sort)
- calendarDayProvider sorts dayTasks by active sort (overdue section unsorted)
- tasksInRoomProvider sorts tasks by active sort
- All tests pass, analyze clean
</success_criteria>
<output>
After completion, create `.planning/phases/07-task-sorting/07-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,214 @@
---
phase: 07-task-sorting
plan: 02
type: execute
wave: 2
depends_on: ["07-01"]
files_modified:
- lib/features/tasks/presentation/sort_dropdown.dart
- lib/features/home/presentation/home_screen.dart
- lib/features/tasks/presentation/task_list_screen.dart
- test/features/home/presentation/home_screen_test.dart
autonomous: true
requirements: [SORT-01, SORT-02, SORT-03]
must_haves:
truths:
- "A sort dropdown is visible in the HomeScreen AppBar showing the current sort label"
- "A sort dropdown is visible in the TaskListScreen AppBar showing the current sort label"
- "Tapping the dropdown shows three options: A-Z, Intervall, Aufwand"
- "Selecting a sort option updates the task list order immediately"
- "The sort preference persists across screen navigations and app restarts"
artifacts:
- path: "lib/features/tasks/presentation/sort_dropdown.dart"
provides: "Reusable SortDropdown ConsumerWidget"
exports: ["SortDropdown"]
- path: "lib/features/home/presentation/home_screen.dart"
provides: "HomeScreen with AppBar containing SortDropdown"
contains: "SortDropdown"
- path: "lib/features/tasks/presentation/task_list_screen.dart"
provides: "TaskListScreen AppBar with SortDropdown alongside edit/delete"
contains: "SortDropdown"
key_links:
- from: "lib/features/tasks/presentation/sort_dropdown.dart"
to: "sortPreferenceProvider"
via: "ref.watch for display, ref.read for mutation"
pattern: "ref\\.watch\\(sortPreferenceProvider\\)"
- from: "lib/features/home/presentation/home_screen.dart"
to: "lib/features/tasks/presentation/sort_dropdown.dart"
via: "SortDropdown widget in AppBar actions"
pattern: "SortDropdown"
- from: "lib/features/tasks/presentation/task_list_screen.dart"
to: "lib/features/tasks/presentation/sort_dropdown.dart"
via: "SortDropdown widget in AppBar actions"
pattern: "SortDropdown"
---
<objective>
Build the sort dropdown widget and wire it into both task list screens (HomeScreen and TaskListScreen), adding an AppBar to HomeScreen.
Purpose: Gives users visible access to the sort controls. The data layer from Plan 01 already sorts reactively; this plan adds the UI trigger.
Output: SortDropdown reusable widget, updated HomeScreen with AppBar, updated TaskListScreen with dropdown in existing AppBar, updated tests.
</objective>
<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/07-task-sorting/07-CONTEXT.md
@.planning/phases/07-task-sorting/07-01-SUMMARY.md
<interfaces>
<!-- Interfaces from Plan 01 that this plan depends on -->
From lib/features/tasks/domain/task_sort_option.dart (created in 07-01):
```dart
enum TaskSortOption { alphabetical, interval, effort }
```
From lib/features/tasks/presentation/sort_preference_notifier.dart (created in 07-01):
```dart
@riverpod
class SortPreferenceNotifier extends _$SortPreferenceNotifier {
TaskSortOption build(); // returns alphabetical by default
Future<void> setSortOption(TaskSortOption option);
}
// Generated as: sortPreferenceProvider
```
From lib/l10n/app_de.arb (strings added in 07-01):
```
sortAlphabetical: "A-Z"
sortInterval: "Intervall"
sortEffort: "Aufwand"
sortLabel: "Sortierung"
```
<!-- Existing interfaces being modified -->
From lib/features/home/presentation/home_screen.dart:
```dart
class HomeScreen extends ConsumerStatefulWidget {
// Currently: Stack with CalendarStrip + CalendarDayList + floating Today FAB
// No AppBar — body sits directly inside AppShell's Scaffold
}
```
From lib/features/tasks/presentation/task_list_screen.dart:
```dart
class TaskListScreen extends ConsumerWidget {
// Has its own Scaffold with AppBar containing edit + delete IconButtons
// AppBar actions: [edit, delete]
}
```
From lib/features/home/presentation/calendar_strip.dart:
```dart
class CalendarStrip extends StatefulWidget {
const CalendarStrip({super.key, required this.controller, this.onTodayVisibilityChanged});
final CalendarStripController controller;
final ValueChanged<bool>? onTodayVisibilityChanged;
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Build SortDropdown widget and integrate into HomeScreen and TaskListScreen</name>
<files>
lib/features/tasks/presentation/sort_dropdown.dart,
lib/features/home/presentation/home_screen.dart,
lib/features/tasks/presentation/task_list_screen.dart
</files>
<action>
1. Create `lib/features/tasks/presentation/sort_dropdown.dart`:
- A `ConsumerWidget` named `SortDropdown`
- Uses `PopupMenuButton<TaskSortOption>` (Material 3, better than DropdownButton for AppBar trailing actions — it opens a menu overlay rather than inline expansion)
- `ref.watch(sortPreferenceProvider)` to get current sort option
- The button child shows the current sort label as a Text widget using l10n strings:
- `alphabetical` -> `l10n.sortAlphabetical` (A-Z)
- `interval` -> `l10n.sortInterval` (Intervall)
- `effort` -> `l10n.sortEffort` (Aufwand)
- Style the button child as a Row with `Icon(Icons.sort)` + `SizedBox(width: 4)` + label Text. Use `theme.textTheme.labelLarge` for the text.
- `itemBuilder` returns 3 `PopupMenuItem<TaskSortOption>` entries with check marks: for each option, show a Row with `Icon(Icons.check, size: 18)` (visible only when selected, invisible when not via `Opacity(opacity: isSelected ? 1 : 0)`) + `SizedBox(width: 8)` + label Text
- `onSelected`: `ref.read(sortPreferenceProvider.notifier).setSortOption(value)`
- Helper method `String _label(TaskSortOption option, AppLocalizations l10n)` that maps enum to l10n string
2. Edit `lib/features/home/presentation/home_screen.dart`:
- HomeScreen currently returns a `Stack` with `Column(CalendarStrip, Expanded(CalendarDayList))` + optional floating Today button
- Wrap the entire current Stack in a `Scaffold` with an `AppBar`:
- `AppBar(title: Text(l10n.tabHome), actions: [const SortDropdown()])`
- The `tabHome` l10n string already exists ("Ubersicht") — reuse it as the AppBar title for the home screen
- body: the existing Stack content
- Keep CalendarStrip, CalendarDayList, and floating Today FAB exactly as they are
- Import `sort_dropdown.dart`
- Note: HomeScreen is inside AppShell's Scaffold body. Adding a nested Scaffold is fine and standard for per-tab AppBars in StatefulShellRoute.indexedStack. The AppShell Scaffold provides the bottom nav; the inner Scaffold provides the AppBar.
3. Edit `lib/features/tasks/presentation/task_list_screen.dart`:
- In the existing `AppBar.actions` list, add `const SortDropdown()` BEFORE the edit and delete IconButtons. Order: [SortDropdown, edit, delete].
- Import `sort_dropdown.dart`
- No other changes to TaskListScreen
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter analyze --no-fatal-infos</automated>
</verify>
<done>SortDropdown widget exists showing current sort label with sort icon. HomeScreen has AppBar with title "Ubersicht" and SortDropdown. TaskListScreen AppBar has SortDropdown before edit/delete buttons. dart analyze clean.</done>
</task>
<task type="auto">
<name>Task 2: Update tests for HomeScreen AppBar and sort dropdown</name>
<files>
test/features/home/presentation/home_screen_test.dart
</files>
<action>
1. Edit `test/features/home/presentation/home_screen_test.dart`:
- Add import for `sort_preference_notifier.dart` and `task_sort_option.dart`
- In the `_buildApp` helper, add a provider override for `sortPreferenceProvider`:
```dart
sortPreferenceProvider.overrideWith(SortPreferenceNotifier.new),
```
This will use the real notifier with mock SharedPreferences (already set up in setUp).
- Add a new test group `'HomeScreen sort dropdown'`:
- Test: "shows sort dropdown in AppBar" — pump the app with tasks, verify `find.byType(PopupMenuButton<TaskSortOption>)` findsOneWidget
- Test: "shows AppBar with title" — verify `find.text('Ubersicht')` findsOneWidget (the tabHome l10n string)
- Verify all existing tests still pass. The addition of an AppBar wrapping the existing content should not break existing assertions since they look for specific widgets/text within the tree.
2. Run full test suite to confirm no regressions.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test && flutter analyze --no-fatal-infos</automated>
</verify>
<done>Home screen tests verify AppBar with sort dropdown is present. All 108+ tests pass (106 existing + 2+ new). dart analyze clean.</done>
</task>
</tasks>
<verification>
- `flutter test` — all tests pass including new sort dropdown tests
- `flutter analyze --no-fatal-infos` — zero issues
- HomeScreen has AppBar with SortDropdown visible
- TaskListScreen has SortDropdown in AppBar actions
- Tapping dropdown shows 3 options with check mark on current selection
- Selecting a different sort option reorders the task list reactively
</verification>
<success_criteria>
- SortDropdown widget is reusable and shows current sort with icon
- HomeScreen has AppBar titled "Ubersicht" with SortDropdown in trailing actions
- TaskListScreen has SortDropdown before edit/delete buttons in AppBar
- Sort selection updates task list order immediately (reactive via provider)
- Sort preference persists (set in one screen, visible in another after navigation)
- All tests pass, analyze clean
</success_criteria>
<output>
After completion, create `.planning/phases/07-task-sorting/07-02-SUMMARY.md`
</output>