Compare commits
12 Commits
2a4b14cb43
...
v1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ea79e0853 | |||
| 772034cba1 | |||
| a3e4d0224b | |||
| e5eccb74e5 | |||
| 9398193c1e | |||
| 3697e4efc4 | |||
| 13c7d623ba | |||
| a9f298350e | |||
| a44f2b80b5 | |||
| 27f18d4f39 | |||
| a94d41b7f7 | |||
| 99358ed704 |
@@ -22,9 +22,9 @@ Requirements for milestone v1.1 Calendar & Polish. Each maps to roadmap phases.
|
|||||||
|
|
||||||
### Task Sorting
|
### Task Sorting
|
||||||
|
|
||||||
- [ ] **SORT-01**: User can sort tasks alphabetically
|
- [x] **SORT-01**: User can sort tasks alphabetically
|
||||||
- [ ] **SORT-02**: User can sort tasks by frequency interval
|
- [x] **SORT-02**: User can sort tasks by frequency interval
|
||||||
- [ ] **SORT-03**: User can sort tasks by effort level
|
- [x] **SORT-03**: User can sort tasks by effort level
|
||||||
|
|
||||||
## Future Requirements
|
## Future Requirements
|
||||||
|
|
||||||
@@ -67,9 +67,9 @@ Which phases cover which requirements. Updated during roadmap creation.
|
|||||||
| CAL-05 | Phase 5 | Complete |
|
| CAL-05 | Phase 5 | Complete |
|
||||||
| HIST-01 | Phase 6 | Complete |
|
| HIST-01 | Phase 6 | Complete |
|
||||||
| HIST-02 | Phase 6 | Complete |
|
| HIST-02 | Phase 6 | Complete |
|
||||||
| SORT-01 | Phase 7 | Pending |
|
| SORT-01 | Phase 7 | Complete |
|
||||||
| SORT-02 | Phase 7 | Pending |
|
| SORT-02 | Phase 7 | Complete |
|
||||||
| SORT-03 | Phase 7 | Pending |
|
| SORT-03 | Phase 7 | Complete |
|
||||||
|
|
||||||
**Coverage:**
|
**Coverage:**
|
||||||
- v1.1 requirements: 10 total
|
- v1.1 requirements: 10 total
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ See `milestones/v1.0-ROADMAP.md` for full phase details.
|
|||||||
|
|
||||||
- [x] **Phase 5: Calendar Strip** - Replace the stacked daily plan home screen with a horizontal scrollable date-strip and day-task list (completed 2026-03-16)
|
- [x] **Phase 5: Calendar Strip** - Replace the stacked daily plan home screen with a horizontal scrollable date-strip and day-task list (completed 2026-03-16)
|
||||||
- [x] **Phase 6: Task History** - Record every task completion with a timestamp and expose a per-task history view (completed 2026-03-16)
|
- [x] **Phase 6: Task History** - Record every task completion with a timestamp and expose a per-task history view (completed 2026-03-16)
|
||||||
- [ ] **Phase 7: Task Sorting** - Add alphabetical, interval, and effort sort options to task lists
|
- [x] **Phase 7: Task Sorting** - Add alphabetical, interval, and effort sort options to task lists (completed 2026-03-16)
|
||||||
|
|
||||||
## Phase Details
|
## Phase Details
|
||||||
|
|
||||||
@@ -63,7 +63,10 @@ Plans:
|
|||||||
2. Selecting alphabetical sort orders tasks A-Z by name within the visible list
|
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
|
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
|
4. Selecting effort sort orders tasks from lowest effort to highest effort level
|
||||||
**Plans**: TBD
|
**Plans:** 2/2 plans complete
|
||||||
|
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
|
## Progress
|
||||||
|
|
||||||
@@ -75,4 +78,4 @@ Plans:
|
|||||||
| 4. Notifications | v1.0 | 3/3 | Complete | 2026-03-16 |
|
| 4. Notifications | v1.0 | 3/3 | Complete | 2026-03-16 |
|
||||||
| 5. Calendar Strip | 2/2 | Complete | 2026-03-16 | - |
|
| 5. Calendar Strip | 2/2 | Complete | 2026-03-16 | - |
|
||||||
| 6. Task History | 1/1 | Complete | 2026-03-16 | - |
|
| 6. Task History | 1/1 | Complete | 2026-03-16 | - |
|
||||||
| 7. Task Sorting | v1.1 | 0/? | Not started | - |
|
| 7. Task Sorting | 2/2 | Complete | 2026-03-16 | - |
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ gsd_state_version: 1.0
|
|||||||
milestone: v1.0
|
milestone: v1.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: completed
|
status: completed
|
||||||
stopped_at: Completed Phase 6 Plan 01 (task history)
|
stopped_at: Completed 07-task-sorting/07-02-PLAN.md
|
||||||
last_updated: "2026-03-16T21:01:58.162Z"
|
last_updated: "2026-03-16T21:43:23.009Z"
|
||||||
last_activity: 2026-03-16 — Completed Phase 6 Plan 01 (task completion history)
|
last_activity: 2026-03-16 — Completed Phase 6 Plan 01 (task completion history)
|
||||||
progress:
|
progress:
|
||||||
total_phases: 3
|
total_phases: 3
|
||||||
completed_phases: 2
|
completed_phases: 3
|
||||||
total_plans: 3
|
total_plans: 5
|
||||||
completed_plans: 3
|
completed_plans: 5
|
||||||
percent: 100
|
percent: 100
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -45,6 +45,8 @@ Progress: [██████████] 100% (1/1 plans in Phase 6)
|
|||||||
| Phase 05-calendar-strip P01 | 5 | 2 tasks | 10 files |
|
| Phase 05-calendar-strip P01 | 5 | 2 tasks | 10 files |
|
||||||
| Phase 05-calendar-strip P02 | 8 | 3 tasks | 9 files |
|
| Phase 05-calendar-strip P02 | 8 | 3 tasks | 9 files |
|
||||||
| Phase 06-task-history P01 | 5 | 2 tasks | 9 files |
|
| Phase 06-task-history P01 | 5 | 2 tasks | 9 files |
|
||||||
|
| Phase 07-task-sorting P01 | 4 | 2 tasks | 9 files |
|
||||||
|
| Phase 07-task-sorting P02 | 4 | 2 tasks | 5 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -63,6 +65,11 @@ Progress: [██████████] 100% (1/1 plans in Phase 6)
|
|||||||
| Tests use pump()+pump(Duration) instead of pumpAndSettle() | CalendarStrip animation controllers cause pumpAndSettle timeout — fixed-duration pump steps are reliable |
|
| Tests use pump()+pump(Duration) instead of pumpAndSettle() | CalendarStrip animation controllers cause pumpAndSettle timeout — fixed-duration pump steps are reliable |
|
||||||
| No separate Riverpod provider for history sheet | ref.read(appDatabaseProvider) directly in ConsumerWidget — one-shot modals do not need a dedicated provider |
|
| No separate Riverpod provider for history sheet | ref.read(appDatabaseProvider) directly in ConsumerWidget — one-shot modals do not need a dedicated provider |
|
||||||
| CalendarTaskRow onTap navigates to task edit form | Makes history accessible in one tap from home screen, consistent with GoRouter route patterns |
|
| CalendarTaskRow onTap navigates to task edit form | Makes history accessible in one tap from home screen, consistent with GoRouter route patterns |
|
||||||
|
- [Phase 07-task-sorting]: Default sort is alphabetical — continuity with existing A-Z SQL sort in CalendarDayList
|
||||||
|
- [Phase 07-task-sorting]: overdueTasks are NOT sorted — pinned at top in existing order per design decision
|
||||||
|
- [Phase 07-task-sorting]: Sort preference stored as enum.name string in SharedPreferences (not intEnum) — enum reordering safe
|
||||||
|
- [Phase 07-task-sorting]: Used PopupMenuButton for SortDropdown in AppBar — menu overlay vs inline expansion, Material 3 pattern
|
||||||
|
- [Phase 07-task-sorting]: HomeScreen uses nested Scaffold for AppBar — standard StatefulShellRoute.indexedStack per-tab AppBar pattern
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -74,7 +81,7 @@ None.
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-16T20:57:30Z
|
Last session: 2026-03-16T21:40:24.556Z
|
||||||
Stopped at: Completed Phase 6 Plan 01 (task history)
|
Stopped at: Completed 07-task-sorting/07-02-PLAN.md
|
||||||
Resume file: .planning/phases/06-task-history/06-01-SUMMARY.md
|
Resume file: None
|
||||||
Next action: Phase 7 (task sorting) or release
|
Next action: Phase 7 (task sorting) or release
|
||||||
|
|||||||
276
.planning/phases/07-task-sorting/07-01-PLAN.md
Normal file
276
.planning/phases/07-task-sorting/07-01-PLAN.md
Normal 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 && 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>
|
||||||
138
.planning/phases/07-task-sorting/07-01-SUMMARY.md
Normal file
138
.planning/phases/07-task-sorting/07-01-SUMMARY.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
---
|
||||||
|
phase: 07-task-sorting
|
||||||
|
plan: 01
|
||||||
|
subsystem: ui
|
||||||
|
tags: [flutter, riverpod, shared_preferences, sorting, localization]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 05-calendar-strip
|
||||||
|
provides: calendarDayProvider and CalendarDayState used by sort integration
|
||||||
|
- phase: 06-task-history
|
||||||
|
provides: task domain model and CalendarTaskRow context
|
||||||
|
|
||||||
|
provides:
|
||||||
|
- TaskSortOption enum (alphabetical, interval, effort)
|
||||||
|
- SortPreferenceNotifier with SharedPreferences persistence
|
||||||
|
- sortPreferenceProvider (keepAlive Riverpod provider)
|
||||||
|
- calendarDayProvider with in-memory sort of dayTasks
|
||||||
|
- tasksInRoomProvider with in-memory sort via stream.map
|
||||||
|
- German localization strings: sortAlphabetical, sortInterval, sortEffort, sortLabel
|
||||||
|
|
||||||
|
affects: [07-02-sort-ui, any phase using calendarDayProvider or tasksInRoomProvider]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "SortPreferenceNotifier: sync default return, async _loadPersisted() — same pattern as ThemeNotifier"
|
||||||
|
- "In-memory sort helper functions (_sortTasks, _sortTasksRaw) applied after DB stream emit"
|
||||||
|
- "overdueTasks intentionally unsorted — only dayTasks sorted"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- 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
|
||||||
|
- test/features/tasks/presentation/sort_preference_notifier_test.dart
|
||||||
|
modified:
|
||||||
|
- lib/features/home/presentation/calendar_providers.dart
|
||||||
|
- lib/features/tasks/presentation/task_providers.dart
|
||||||
|
- lib/l10n/app_de.arb
|
||||||
|
- lib/l10n/app_localizations.dart
|
||||||
|
- lib/l10n/app_localizations_de.dart
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Default sort is alphabetical — continuity with existing A-Z SQL sort in CalendarDayList"
|
||||||
|
- "overdueTasks are NOT sorted — they stay pinned at the top in existing order"
|
||||||
|
- "Sort stored as string (enum.name) in SharedPreferences — not intEnum, so reordering enum is safe"
|
||||||
|
- "SortPreferenceNotifier uses keepAlive: true — global preference should never be disposed"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "SortPreferenceNotifier pattern: sync default + async _loadPersisted() — matches ThemeNotifier"
|
||||||
|
- "In-memory sort via stream.map in StreamProvider — DB SQL sort provides stable baseline, in-memory overrides"
|
||||||
|
|
||||||
|
requirements-completed: [SORT-01, SORT-02, SORT-03]
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 4min
|
||||||
|
completed: 2026-03-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 07 Plan 01: Task Sort Domain and Provider Summary
|
||||||
|
|
||||||
|
**TaskSortOption enum + SharedPreferences-backed SortPreferenceNotifier wired into calendarDayProvider and tasksInRoomProvider with in-memory alphabetical/interval/effort sorting**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 4 min
|
||||||
|
- **Started:** 2026-03-16T21:29:32Z
|
||||||
|
- **Completed:** 2026-03-16T21:33:37Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 9
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- TaskSortOption enum (alphabetical, interval, effort) with SharedPreferences persistence via SortPreferenceNotifier
|
||||||
|
- calendarDayProvider now watches sortPreferenceProvider and sorts dayTasks in-memory; overdueTasks intentionally unsorted
|
||||||
|
- tasksInRoomProvider now watches sortPreferenceProvider and applies sort via stream.map
|
||||||
|
- 7 new unit tests for SortPreferenceNotifier covering default, state update, persistence, and restart recovery
|
||||||
|
- 4 German localization strings added (sortAlphabetical, sortInterval, sortEffort, sortLabel)
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **TDD RED: Failing sort preference tests** - `a9f2983` (test)
|
||||||
|
2. **Task 1: TaskSortOption enum, SortPreferenceNotifier, localization** - `13c7d62` (feat)
|
||||||
|
3. **Task 2: Sort integration into calendarDayProvider and tasksInRoomProvider** - `3697e4e` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `lib/features/tasks/domain/task_sort_option.dart` - TaskSortOption enum with alphabetical/interval/effort values
|
||||||
|
- `lib/features/tasks/presentation/sort_preference_notifier.dart` - SortPreferenceNotifier with SharedPreferences persistence
|
||||||
|
- `lib/features/tasks/presentation/sort_preference_notifier.g.dart` - Generated Riverpod provider code
|
||||||
|
- `lib/features/home/presentation/calendar_providers.dart` - Added sortPreferenceProvider watch + _sortTasks helper
|
||||||
|
- `lib/features/tasks/presentation/task_providers.dart` - Added sortPreferenceProvider watch + _sortTasksRaw helper + stream.map
|
||||||
|
- `lib/l10n/app_de.arb` - Added sortAlphabetical, sortInterval, sortEffort, sortLabel strings
|
||||||
|
- `lib/l10n/app_localizations.dart` - Regenerated with sort string getters
|
||||||
|
- `lib/l10n/app_localizations_de.dart` - Regenerated with German sort string implementations
|
||||||
|
- `test/features/tasks/presentation/sort_preference_notifier_test.dart` - 7 unit tests for sort preference
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Default sort is alphabetical for continuity with existing SQL A-Z sort in CalendarDayList
|
||||||
|
- overdueTasks section is explicitly NOT sorted — stays pinned at top in existing order
|
||||||
|
- Sort preference stored as enum.name string in SharedPreferences (not intEnum) so enum reordering is always safe
|
||||||
|
- SortPreferenceNotifier uses `keepAlive: true` — global app preference must not be disposed
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None - no external service configuration required.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- sortPreferenceProvider is live and defaults to alphabetical
|
||||||
|
- Both task list providers react to sort preference changes immediately
|
||||||
|
- Ready for 07-02: sort UI (dropdown in AppBar) to write to sortPreferenceProvider
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 07-task-sorting*
|
||||||
|
*Completed: 2026-03-16*
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- FOUND: lib/features/tasks/domain/task_sort_option.dart
|
||||||
|
- FOUND: lib/features/tasks/presentation/sort_preference_notifier.dart
|
||||||
|
- FOUND: lib/features/tasks/presentation/sort_preference_notifier.g.dart
|
||||||
|
- FOUND: test/features/tasks/presentation/sort_preference_notifier_test.dart
|
||||||
|
- FOUND: .planning/phases/07-task-sorting/07-01-SUMMARY.md
|
||||||
|
- Commits a9f2983, 13c7d62, 3697e4e all verified in git log
|
||||||
214
.planning/phases/07-task-sorting/07-02-PLAN.md
Normal file
214
.planning/phases/07-task-sorting/07-02-PLAN.md
Normal 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>
|
||||||
125
.planning/phases/07-task-sorting/07-02-SUMMARY.md
Normal file
125
.planning/phases/07-task-sorting/07-02-SUMMARY.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
phase: 07-task-sorting
|
||||||
|
plan: 02
|
||||||
|
subsystem: ui
|
||||||
|
tags: [flutter, riverpod, material3, popup-menu, sort-ui, localization]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 07-task-sorting
|
||||||
|
plan: 01
|
||||||
|
provides: sortPreferenceProvider, TaskSortOption enum, German sort l10n strings
|
||||||
|
|
||||||
|
provides:
|
||||||
|
- SortDropdown ConsumerWidget (PopupMenuButton<TaskSortOption> with check marks)
|
||||||
|
- HomeScreen with AppBar (title: Übersicht, actions: SortDropdown)
|
||||||
|
- TaskListScreen AppBar with SortDropdown before edit/delete buttons
|
||||||
|
|
||||||
|
affects: [home_screen_test.dart, app_shell_test.dart, any screen showing HomeScreen]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "PopupMenuButton<TaskSortOption> with Opacity check mark — avoids layout shift vs conditional Icon"
|
||||||
|
- "Nested Scaffold inside AppShell tab body — standard pattern for per-tab AppBars in StatefulShellRoute"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- lib/features/tasks/presentation/sort_dropdown.dart
|
||||||
|
modified:
|
||||||
|
- lib/features/home/presentation/home_screen.dart
|
||||||
|
- lib/features/tasks/presentation/task_list_screen.dart
|
||||||
|
- test/features/home/presentation/home_screen_test.dart
|
||||||
|
- test/shell/app_shell_test.dart
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Used PopupMenuButton instead of DropdownButton for AppBar — menu overlay vs inline expansion, consistent with Material 3 AppBar action patterns"
|
||||||
|
- "Opacity(opacity: isSelected ? 1 : 0) for check mark — preserves item width alignment vs conditional show/hide"
|
||||||
|
- "HomeScreen Scaffold is nested inside AppShell Scaffold — standard StatefulShellRoute pattern for per-tab AppBars"
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 4min
|
||||||
|
completed: 2026-03-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 07 Plan 02: Sort Dropdown UI Summary
|
||||||
|
|
||||||
|
**SortDropdown ConsumerWidget using PopupMenuButton wired into HomeScreen AppBar (title: Übersicht) and TaskListScreen AppBar before edit/delete actions**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 4 min
|
||||||
|
- **Started:** 2026-03-16T21:35:56Z
|
||||||
|
- **Completed:** 2026-03-16T21:39:24Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 5 (1 created, 4 modified)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- SortDropdown ConsumerWidget: PopupMenuButton<TaskSortOption> with sort icon, current label, and check mark on active option
|
||||||
|
- HomeScreen wrapped in Scaffold with AppBar titled "Übersicht" and SortDropdown in trailing actions
|
||||||
|
- TaskListScreen AppBar has SortDropdown before the existing edit/delete IconButtons
|
||||||
|
- 2 new tests in HomeScreen test suite: verifies PopupMenuButton and AppBar title presence
|
||||||
|
- Auto-fixed app_shell_test regression caused by "Übersicht" now appearing twice (AppBar + bottom nav)
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: SortDropdown widget and HomeScreen/TaskListScreen integration** - `e5eccb7` (feat)
|
||||||
|
2. **Task 2: Sort dropdown tests and AppShell test fix** - `a3e4d02` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `lib/features/tasks/presentation/sort_dropdown.dart` - Reusable SortDropdown ConsumerWidget with PopupMenuButton<TaskSortOption>
|
||||||
|
- `lib/features/home/presentation/home_screen.dart` - Added Scaffold with AppBar (Übersicht title + SortDropdown)
|
||||||
|
- `lib/features/tasks/presentation/task_list_screen.dart` - Added SortDropdown before edit/delete in AppBar actions
|
||||||
|
- `test/features/home/presentation/home_screen_test.dart` - Added sortPreferenceProvider override + 2 new sort dropdown tests
|
||||||
|
- `test/shell/app_shell_test.dart` - Fixed findsOneWidget -> findsWidgets for 'Übersicht' (now in AppBar + bottom nav)
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Used PopupMenuButton instead of DropdownButton for AppBar actions — menu overlay is cleaner in AppBar context (Material 3)
|
||||||
|
- Opacity trick for check mark: `Opacity(opacity: isSelected ? 1 : 0)` preserves item width so labels align regardless of selection
|
||||||
|
- HomeScreen uses nested Scaffold for AppBar — standard pattern in StatefulShellRoute.indexedStack; AppShell provides bottom nav, HomeScreen provides AppBar
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 1 - Bug] Fixed AppShell test regression from 'Übersicht' duplicate**
|
||||||
|
- **Found during:** Task 2 test run
|
||||||
|
- **Issue:** `app_shell_test.dart` expected `findsOneWidget` for 'Übersicht'. Adding the HomeScreen AppBar title caused the string to appear twice (AppBar + bottom nav label).
|
||||||
|
- **Fix:** Changed `findsOneWidget` to `findsWidgets` in `app_shell_test.dart` line 67. Applied same fix to new `home_screen_test.dart` AppBar title test.
|
||||||
|
- **Files modified:** `test/shell/app_shell_test.dart`, `test/features/home/presentation/home_screen_test.dart`
|
||||||
|
- **Commit:** `a3e4d02`
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None beyond the auto-fixed regression.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None - no external service configuration required.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- Phase 07 (task sorting) is now complete: data layer (07-01) + UI layer (07-02)
|
||||||
|
- Sort dropdown is live in both HomeScreen and TaskListScreen AppBars
|
||||||
|
- Selecting a sort option reactively reorders task lists via sortPreferenceProvider
|
||||||
|
- Preference persists across app restarts via SharedPreferences
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 07-task-sorting*
|
||||||
|
*Completed: 2026-03-16*
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- FOUND: lib/features/tasks/presentation/sort_dropdown.dart
|
||||||
|
- FOUND: lib/features/home/presentation/home_screen.dart (modified)
|
||||||
|
- FOUND: lib/features/tasks/presentation/task_list_screen.dart (modified)
|
||||||
|
- FOUND: test/features/home/presentation/home_screen_test.dart (modified)
|
||||||
|
- FOUND: test/shell/app_shell_test.dart (modified)
|
||||||
|
- Commits e5eccb7, a3e4d02 verified in git log
|
||||||
|
- All 115 tests pass, dart analyze clean
|
||||||
91
.planning/phases/07-task-sorting/07-CONTEXT.md
Normal file
91
.planning/phases/07-task-sorting/07-CONTEXT.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Phase 7: Task Sorting - Context
|
||||||
|
|
||||||
|
**Gathered:** 2026-03-16
|
||||||
|
**Status:** Ready for planning
|
||||||
|
|
||||||
|
<domain>
|
||||||
|
## Phase Boundary
|
||||||
|
|
||||||
|
Add sort controls to task list screens so users can reorder tasks by name (alphabetical), frequency interval, or effort level. The sort preference persists across app restarts. Requirements: SORT-01, SORT-02, SORT-03.
|
||||||
|
|
||||||
|
</domain>
|
||||||
|
|
||||||
|
<decisions>
|
||||||
|
## Implementation Decisions
|
||||||
|
|
||||||
|
### Sort control widget
|
||||||
|
- Dropdown button in the AppBar, right side (trailing actions position)
|
||||||
|
- When collapsed, shows the current sort name as text (e.g., "A-Z", "Intervall", "Aufwand")
|
||||||
|
- Expands to show the 3 sort options as a standard dropdown menu
|
||||||
|
|
||||||
|
### Sort option labels
|
||||||
|
- Claude's discretion — pick German labels that fit the app's existing localization style (concise but clear)
|
||||||
|
|
||||||
|
### Sort scope
|
||||||
|
- One global sort preference applies to all task list screens
|
||||||
|
- Same dropdown appears in both the home screen (CalendarDayList) and per-room (TaskListScreen) AppBars
|
||||||
|
|
||||||
|
### Persistence
|
||||||
|
- Store the sort preference in SharedPreferences (simple key-value for a single enum)
|
||||||
|
- No database schema change needed
|
||||||
|
- Persists across app restarts per success criteria
|
||||||
|
|
||||||
|
### Default sort
|
||||||
|
- Claude's discretion — pick the least disruptive default (likely alphabetical to match current CalendarDayList behavior)
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
- Sort option label text (German, concise)
|
||||||
|
- Default sort order (recommend alphabetical for continuity)
|
||||||
|
- Whether TaskListScreen also gets the dropdown (recommend yes for consistency with global setting, since the success criteria says "task list screens" plural)
|
||||||
|
- Sort direction (always ascending — A-Z, daily→yearly, low→high — no toggle needed for MVP)
|
||||||
|
- Dropdown styling (Material 3 DropdownButton or PopupMenuButton variant)
|
||||||
|
- Sort icon or visual indicator in the dropdown
|
||||||
|
- How overdue section interacts with sorting (recommend: overdue section stays pinned at top regardless of sort, only day tasks are sorted)
|
||||||
|
|
||||||
|
</decisions>
|
||||||
|
|
||||||
|
<specifics>
|
||||||
|
## Specific Ideas
|
||||||
|
|
||||||
|
No specific requirements — open to standard approaches that match existing app patterns.
|
||||||
|
|
||||||
|
</specifics>
|
||||||
|
|
||||||
|
<code_context>
|
||||||
|
## Existing Code Insights
|
||||||
|
|
||||||
|
### Reusable Assets
|
||||||
|
- `EffortLevel` enum (low/medium/high) with `.index` for ordering — directly usable for effort sort
|
||||||
|
- `IntervalType` enum with `.index` ordered roughly by frequency (daily=0 through yearly=7) — usable for interval sort
|
||||||
|
- `FrequencyInterval.presets` list ordered most-frequent to least — reference for sort order
|
||||||
|
- `Task.name` field — direct alphabetical sort target
|
||||||
|
- `CalendarDayList` and `TaskListScreen` — the two list widgets that need sort integration
|
||||||
|
- `AppLocalizations` + `.arb` files — existing German localization pipeline
|
||||||
|
|
||||||
|
### Established Patterns
|
||||||
|
- Manual `StreamProvider.autoDispose` for drift types (riverpod_generator issue) — sort provider follows same pattern
|
||||||
|
- `calendarDayProvider` watches `selectedDateProvider` — can also watch a sort preference provider
|
||||||
|
- `tasksInRoomProvider` family provider — can be extended with sort parameter or read global sort
|
||||||
|
- Feature folder structure: `features/home/`, `features/tasks/` — sort logic may live in a shared location or in each feature
|
||||||
|
|
||||||
|
### Integration Points
|
||||||
|
- `HomeScreen` AppBar — add dropdown to trailing actions
|
||||||
|
- `TaskListScreen` AppBar — already has edit/delete actions; add dropdown alongside
|
||||||
|
- `CalendarDao.watchTasksForDate()` — currently sorts alphabetically; needs sort-aware query or in-memory sort
|
||||||
|
- `TasksDao.watchTasksInRoom()` — currently sorts by nextDueDate; needs sort-aware query or in-memory sort
|
||||||
|
- `SharedPreferences` — not yet used in the app; needs package addition and provider setup
|
||||||
|
- `app_de.arb` — add localization strings for sort labels
|
||||||
|
|
||||||
|
</code_context>
|
||||||
|
|
||||||
|
<deferred>
|
||||||
|
## Deferred Ideas
|
||||||
|
|
||||||
|
None — discussion stayed within phase scope
|
||||||
|
|
||||||
|
</deferred>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Phase: 07-task-sorting*
|
||||||
|
*Context gathered: 2026-03-16*
|
||||||
135
.planning/phases/07-task-sorting/7-VERIFICATION.md
Normal file
135
.planning/phases/07-task-sorting/7-VERIFICATION.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
phase: 07-task-sorting
|
||||||
|
verified: 2026-03-16T22:00:00Z
|
||||||
|
status: passed
|
||||||
|
score: 9/9 must-haves verified
|
||||||
|
re_verification: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 7: Task Sorting Verification Report
|
||||||
|
|
||||||
|
**Phase Goal:** Users can reorder task lists by the dimension most useful to them — name, how often the task recurs, or how much effort it requires
|
||||||
|
**Verified:** 2026-03-16T22:00:00Z
|
||||||
|
**Status:** passed
|
||||||
|
**Re-verification:** No — initial verification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal Achievement
|
||||||
|
|
||||||
|
### Observable Truths
|
||||||
|
|
||||||
|
| # | Truth | Status | Evidence |
|
||||||
|
|----|--------------------------------------------------------------------------------|------------|------------------------------------------------------------------------------------------------------|
|
||||||
|
| 1 | Sort preference persists across app restarts | VERIFIED | `SortPreferenceNotifier._loadPersisted()` reads `SharedPreferences.getString('task_sort_option')` on build; 2 restart-recovery tests pass |
|
||||||
|
| 2 | CalendarDayList tasks are sorted according to the active sort preference | VERIFIED | `calendarDayProvider` calls `ref.watch(sortPreferenceProvider)` and applies `_sortTasks(dayTasks, sortOption)` before returning `CalendarDayState` |
|
||||||
|
| 3 | TaskListScreen tasks are sorted according to the active sort preference | VERIFIED | `tasksInRoomProvider` calls `ref.watch(sortPreferenceProvider)` and applies `stream.map((tasks) => _sortTasksRaw(tasks, sortOption))` |
|
||||||
|
| 4 | Default sort is alphabetical (matches current CalendarDayList behavior) | VERIFIED | `SortPreferenceNotifier.build()` returns `TaskSortOption.alphabetical` synchronously; test "build() returns default state of alphabetical" confirms |
|
||||||
|
| 5 | A sort dropdown is visible in the HomeScreen AppBar showing the current label | VERIFIED | `HomeScreen.build()` returns `Scaffold(appBar: AppBar(actions: const [SortDropdown()]))` — wired and rendered |
|
||||||
|
| 6 | A sort dropdown is visible in the TaskListScreen AppBar | VERIFIED | `TaskListScreen.build()` AppBar actions list: `[const SortDropdown(), edit IconButton, delete IconButton]` |
|
||||||
|
| 7 | Tapping the dropdown shows three options: A-Z, Intervall, Aufwand | VERIFIED | `SortDropdown` builds `PopupMenuButton` from `TaskSortOption.values` (3 items), labels map to `l10n.sortAlphabetical/sortInterval/sortEffort` |
|
||||||
|
| 8 | Selecting a sort option updates the task list order immediately | VERIFIED | `onSelected` calls `ref.read(sortPreferenceProvider.notifier).setSortOption(value)`; providers watch `sortPreferenceProvider` and rebuild reactively |
|
||||||
|
| 9 | The sort preference persists across screen navigations and app restarts | VERIFIED | `@Riverpod(keepAlive: true)` prevents disposal during navigation; SharedPreferences stores and reloads value |
|
||||||
|
|
||||||
|
**Score:** 9/9 truths verified
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Required Artifacts
|
||||||
|
|
||||||
|
### Plan 07-01 Artifacts
|
||||||
|
|
||||||
|
| Artifact | Expected | Status | Details |
|
||||||
|
|----------|----------|--------|---------|
|
||||||
|
| `lib/features/tasks/domain/task_sort_option.dart` | `TaskSortOption` enum with alphabetical, interval, effort | VERIFIED | Exactly 3 values, comments match intent. No stubs. |
|
||||||
|
| `lib/features/tasks/presentation/sort_preference_notifier.dart` | `SortPreferenceNotifier` with SharedPreferences persistence | VERIFIED | `build()` returns `alphabetical` synchronously, `_loadPersisted()` async, `setSortOption()` sets state + persists. Pattern matches `ThemeNotifier`. |
|
||||||
|
| `lib/features/tasks/presentation/sort_preference_notifier.g.dart` | Generated Riverpod provider file | VERIFIED | Generated correctly; `sortPreferenceProvider` declared as `SortPreferenceNotifierProvider._()` with `isAutoDispose: false` (keepAlive). |
|
||||||
|
| `lib/features/home/presentation/calendar_providers.dart` | `calendarDayProvider` sorts `dayTasks` by active sort preference | VERIFIED | `ref.watch(sortPreferenceProvider)` present. `_sortTasks()` helper implements all 3 sort modes. `overdueTasks` intentionally unsorted. |
|
||||||
|
| `lib/features/tasks/presentation/task_providers.dart` | `tasksInRoomProvider` sorts tasks by active sort preference | VERIFIED | `ref.watch(sortPreferenceProvider)` present. `_sortTasksRaw()` helper + `stream.map()` applied correctly. |
|
||||||
|
| `test/features/tasks/presentation/sort_preference_notifier_test.dart` | Unit tests for sort preference persistence and default | VERIFIED | 7 tests: default alphabetical, setSortOption interval, setSortOption effort, persist to SharedPreferences, restart recovery (effort), restart recovery (interval), unknown value fallback. |
|
||||||
|
|
||||||
|
### Plan 07-02 Artifacts
|
||||||
|
|
||||||
|
| Artifact | Expected | Status | Details |
|
||||||
|
|----------|----------|--------|---------|
|
||||||
|
| `lib/features/tasks/presentation/sort_dropdown.dart` | Reusable `SortDropdown` `ConsumerWidget` | VERIFIED | `ConsumerWidget`, `PopupMenuButton<TaskSortOption>`, Opacity check mark pattern, `ref.watch` for display, `ref.read` for mutation, `_label()` helper. |
|
||||||
|
| `lib/features/home/presentation/home_screen.dart` | HomeScreen with AppBar containing `SortDropdown` | VERIFIED | `Scaffold(appBar: AppBar(title: Text(l10n.tabHome), actions: const [SortDropdown()]))`. Existing Stack body preserved. |
|
||||||
|
| `lib/features/tasks/presentation/task_list_screen.dart` | TaskListScreen AppBar with `SortDropdown` before edit/delete | VERIFIED | `actions: [const SortDropdown(), IconButton(edit), IconButton(delete)]`. Correct order. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Link Verification
|
||||||
|
|
||||||
|
### Plan 07-01 Key Links
|
||||||
|
|
||||||
|
| From | To | Via | Status | Details |
|
||||||
|
|------|----|-----|--------|---------|
|
||||||
|
| `calendar_providers.dart` | `sortPreferenceProvider` | `ref.watch(sortPreferenceProvider)` in `calendarDayProvider` | WIRED | Line 77: `final sortOption = ref.watch(sortPreferenceProvider);`. Applied at line 101: `dayTasks: _sortTasks(dayTasks, sortOption)`. |
|
||||||
|
| `task_providers.dart` | `sortPreferenceProvider` | `ref.watch(sortPreferenceProvider)` in `tasksInRoomProvider` | WIRED | Line 43: `final sortOption = ref.watch(sortPreferenceProvider);`. Applied at lines 44-46 via `stream.map`. |
|
||||||
|
|
||||||
|
### Plan 07-02 Key Links
|
||||||
|
|
||||||
|
| From | To | Via | Status | Details |
|
||||||
|
|------|----|-----|--------|---------|
|
||||||
|
| `sort_dropdown.dart` | `sortPreferenceProvider` | `ref.watch` for display, `ref.read` for mutation | WIRED | Line 21: `ref.watch(sortPreferenceProvider)`. Line 27: `ref.read(sortPreferenceProvider.notifier).setSortOption(value)`. |
|
||||||
|
| `home_screen.dart` | `sort_dropdown.dart` | `SortDropdown` widget in AppBar actions | WIRED | Import on line 7. Used in `AppBar(actions: const [SortDropdown()])` on line 37. |
|
||||||
|
| `task_list_screen.dart` | `sort_dropdown.dart` | `SortDropdown` widget in AppBar actions | WIRED | Import on line 7. Used in `actions: [const SortDropdown(), ...]` on line 31. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements Coverage
|
||||||
|
|
||||||
|
| Requirement | Source Plan | Description | Status | Evidence |
|
||||||
|
|-------------|-------------|-------------|--------|----------|
|
||||||
|
| SORT-01 | 07-01, 07-02 | User can sort tasks alphabetically | SATISFIED | `TaskSortOption.alphabetical` is the default. `_sortTasks()` and `_sortTasksRaw()` implement case-insensitive A-Z sort. `SortDropdown` displays "A–Z" label (l10n). |
|
||||||
|
| SORT-02 | 07-01, 07-02 | User can sort tasks by frequency interval | SATISFIED | `TaskSortOption.interval` sort implemented: `intervalType.index` ascending with `intervalDays` tiebreaker. Displayed as "Intervall" in `SortDropdown`. |
|
||||||
|
| SORT-03 | 07-01, 07-02 | User can sort tasks by effort level | SATISFIED | `TaskSortOption.effort` sort implemented: `effortLevel.index` ascending (low=0, medium=1, high=2). Displayed as "Aufwand" in `SortDropdown`. |
|
||||||
|
|
||||||
|
No orphaned requirements. All three SORT requirements are claimed by both plans and fully implemented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-Patterns Found
|
||||||
|
|
||||||
|
None. All seven implementation files scanned — no TODO, FIXME, XXX, HACK, PLACEHOLDER, return null, return {}, return [], or empty arrow functions found.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Human Verification Required
|
||||||
|
|
||||||
|
### 1. Visual check: Sort dropdown appearance in AppBar
|
||||||
|
|
||||||
|
**Test:** Launch the app, navigate to HomeScreen. Verify the AppBar shows a sort icon (Icons.sort) followed by the current sort label text "A–Z".
|
||||||
|
**Expected:** Sort icon and "A–Z" text visible in the top-right AppBar area.
|
||||||
|
**Why human:** Widget rendering and visual layout cannot be verified programmatically.
|
||||||
|
|
||||||
|
### 2. Popup menu interaction: Check marks on active option
|
||||||
|
|
||||||
|
**Test:** Tap the sort dropdown, verify three items appear with a check mark next to the currently selected option and no check mark on the other two.
|
||||||
|
**Expected:** Check mark visible on "A–Z" (default), invisible (but space-preserving) on "Intervall" and "Aufwand".
|
||||||
|
**Why human:** Opacity(0) vs Opacity(1) rendering and visual alignment cannot be verified with grep.
|
||||||
|
|
||||||
|
### 3. Reactive reorder on selection
|
||||||
|
|
||||||
|
**Test:** With tasks loaded in HomeScreen, tap the sort dropdown and select "Aufwand". Verify the task list reorders immediately without a page reload.
|
||||||
|
**Expected:** Task list updates instantly, sorted low-effort first.
|
||||||
|
**Why human:** Real-time Riverpod reactive rebuild requires a running app to observe.
|
||||||
|
|
||||||
|
### 4. Cross-screen persistence of sort preference
|
||||||
|
|
||||||
|
**Test:** Select "Intervall" in HomeScreen, then navigate to a room's TaskListScreen. Verify the sort dropdown there also shows "Intervall".
|
||||||
|
**Expected:** Sort preference is shared across screens (same `sortPreferenceProvider`, `keepAlive: true`).
|
||||||
|
**Why human:** Cross-screen navigation state cannot be verified statically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gaps Summary
|
||||||
|
|
||||||
|
None. All 9 observable truths verified. All artifacts exist, are substantive, and are correctly wired. All 3 requirements (SORT-01, SORT-02, SORT-03) are fully satisfied. All 5 commits (a9f2983, 13c7d62, 3697e4e, e5eccb7, a3e4d02) confirmed present in git log. No anti-patterns detected in implementation files.
|
||||||
|
|
||||||
|
The phase delivers its stated goal: users can reorder task lists by name (A–Z), frequency interval, or effort level via a persistent, reactive sort preference accessible from both HomeScreen and TaskListScreen AppBars.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Verified: 2026-03-16T22:00:00Z_
|
||||||
|
_Verifier: Claude (gsd-verifier)_
|
||||||
@@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:household_keeper/core/providers/database_provider.dart';
|
import 'package:household_keeper/core/providers/database_provider.dart';
|
||||||
import 'package:household_keeper/features/home/domain/calendar_models.dart';
|
import 'package:household_keeper/features/home/domain/calendar_models.dart';
|
||||||
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/domain/task_sort_option.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/presentation/sort_preference_notifier.dart';
|
||||||
|
|
||||||
/// Notifier that manages the currently selected date in the calendar strip.
|
/// Notifier that manages the currently selected date in the calendar strip.
|
||||||
///
|
///
|
||||||
@@ -27,17 +29,52 @@ final selectedDateProvider =
|
|||||||
SelectedDateNotifier.new,
|
SelectedDateNotifier.new,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Sort a list of [TaskWithRoom] by the given [sortOption].
|
||||||
|
///
|
||||||
|
/// Returns a new sorted list; never mutates the original.
|
||||||
|
/// Only [dayTasks] are sorted — the overdue section stays in its existing
|
||||||
|
/// order per user decision.
|
||||||
|
List<TaskWithRoom> _sortTasks(
|
||||||
|
List<TaskWithRoom> tasks,
|
||||||
|
TaskSortOption sortOption,
|
||||||
|
) {
|
||||||
|
final sorted = List<TaskWithRoom>.from(tasks);
|
||||||
|
switch (sortOption) {
|
||||||
|
case TaskSortOption.alphabetical:
|
||||||
|
sorted.sort((a, b) => a.task.name.toLowerCase().compareTo(
|
||||||
|
b.task.name.toLowerCase(),
|
||||||
|
));
|
||||||
|
case TaskSortOption.interval:
|
||||||
|
sorted.sort((a, b) {
|
||||||
|
final cmp = a.task.intervalType.index.compareTo(
|
||||||
|
b.task.intervalType.index,
|
||||||
|
);
|
||||||
|
if (cmp != 0) return cmp;
|
||||||
|
return a.task.intervalDays.compareTo(b.task.intervalDays);
|
||||||
|
});
|
||||||
|
case TaskSortOption.effort:
|
||||||
|
sorted.sort((a, b) => a.task.effortLevel.index.compareTo(
|
||||||
|
b.task.effortLevel.index,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
/// Reactive calendar day state: tasks for the selected date + overdue tasks.
|
/// Reactive calendar day state: tasks for the selected date + overdue tasks.
|
||||||
///
|
///
|
||||||
/// Overdue tasks are only included when the selected date is today.
|
/// Overdue tasks are only included when the selected date is today.
|
||||||
/// Past and future dates show only tasks originally due on that day.
|
/// Past and future dates show only tasks originally due on that day.
|
||||||
///
|
///
|
||||||
|
/// dayTasks are sorted in-memory according to the active [sortPreferenceProvider].
|
||||||
|
/// overdueTasks retain their existing order (pinned at top, unsorted per design).
|
||||||
|
///
|
||||||
/// Defined manually (not @riverpod) because riverpod_generator has trouble
|
/// Defined manually (not @riverpod) because riverpod_generator has trouble
|
||||||
/// with drift's generated [Task] type. Same pattern as [dailyPlanProvider].
|
/// with drift's generated [Task] type. Same pattern as [dailyPlanProvider].
|
||||||
final calendarDayProvider =
|
final calendarDayProvider =
|
||||||
StreamProvider.autoDispose<CalendarDayState>((ref) {
|
StreamProvider.autoDispose<CalendarDayState>((ref) {
|
||||||
final db = ref.watch(appDatabaseProvider);
|
final db = ref.watch(appDatabaseProvider);
|
||||||
final selectedDate = ref.watch(selectedDateProvider);
|
final selectedDate = ref.watch(selectedDateProvider);
|
||||||
|
final sortOption = ref.watch(sortPreferenceProvider);
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
@@ -61,7 +98,7 @@ final calendarDayProvider =
|
|||||||
|
|
||||||
return CalendarDayState(
|
return CalendarDayState(
|
||||||
selectedDate: selectedDate,
|
selectedDate: selectedDate,
|
||||||
dayTasks: dayTasks,
|
dayTasks: _sortTasks(dayTasks, sortOption),
|
||||||
overdueTasks: overdueTasks,
|
overdueTasks: overdueTasks,
|
||||||
totalTaskCount: totalTaskCount,
|
totalTaskCount: totalTaskCount,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:household_keeper/features/home/presentation/calendar_day_list.dart';
|
import 'package:household_keeper/features/home/presentation/calendar_day_list.dart';
|
||||||
import 'package:household_keeper/features/home/presentation/calendar_providers.dart';
|
import 'package:household_keeper/features/home/presentation/calendar_providers.dart';
|
||||||
import 'package:household_keeper/features/home/presentation/calendar_strip.dart';
|
import 'package:household_keeper/features/home/presentation/calendar_strip.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/presentation/sort_dropdown.dart';
|
||||||
import 'package:household_keeper/l10n/app_localizations.dart';
|
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||||
|
|
||||||
/// The app's primary screen: a horizontal calendar strip at the top with a
|
/// The app's primary screen: a horizontal calendar strip at the top with a
|
||||||
@@ -30,40 +31,46 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
||||||
return Stack(
|
return Scaffold(
|
||||||
children: [
|
appBar: AppBar(
|
||||||
Column(
|
title: Text(l10n.tabHome),
|
||||||
children: [
|
actions: const [SortDropdown()],
|
||||||
CalendarStrip(
|
),
|
||||||
controller: _stripController,
|
body: Stack(
|
||||||
onTodayVisibilityChanged: (visible) {
|
children: [
|
||||||
setState(() => _showTodayButton = !visible);
|
Column(
|
||||||
},
|
children: [
|
||||||
),
|
CalendarStrip(
|
||||||
const Expanded(child: CalendarDayList()),
|
controller: _stripController,
|
||||||
],
|
onTodayVisibilityChanged: (visible) {
|
||||||
),
|
setState(() => _showTodayButton = !visible);
|
||||||
if (_showTodayButton)
|
|
||||||
Positioned(
|
|
||||||
bottom: 16,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Center(
|
|
||||||
child: FloatingActionButton.extended(
|
|
||||||
onPressed: () {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
|
||||||
ref
|
|
||||||
.read(selectedDateProvider.notifier)
|
|
||||||
.selectDate(today);
|
|
||||||
_stripController.scrollToToday();
|
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.today),
|
),
|
||||||
label: Text(l10n.calendarTodayButton),
|
const Expanded(child: CalendarDayList()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_showTodayButton)
|
||||||
|
Positioned(
|
||||||
|
bottom: 16,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(
|
||||||
|
child: FloatingActionButton.extended(
|
||||||
|
onPressed: () {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
ref
|
||||||
|
.read(selectedDateProvider.notifier)
|
||||||
|
.selectDate(today);
|
||||||
|
_stripController.scrollToToday();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.today),
|
||||||
|
label: Text(l10n.calendarTodayButton),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
lib/features/tasks/domain/task_sort_option.dart
Normal file
9
lib/features/tasks/domain/task_sort_option.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/// Sort options for task lists.
|
||||||
|
///
|
||||||
|
/// Stored as a string in SharedPreferences (not as intEnum in the database),
|
||||||
|
/// so reordering these values is safe.
|
||||||
|
enum TaskSortOption {
|
||||||
|
alphabetical, // A–Z by task name
|
||||||
|
interval, // by frequency interval (most frequent first)
|
||||||
|
effort, // by effort level (low → medium → high)
|
||||||
|
}
|
||||||
71
lib/features/tasks/presentation/sort_dropdown.dart
Normal file
71
lib/features/tasks/presentation/sort_dropdown.dart
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:household_keeper/features/tasks/domain/task_sort_option.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/presentation/sort_preference_notifier.dart';
|
||||||
|
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
/// A reusable sort dropdown widget for use in AppBar actions.
|
||||||
|
///
|
||||||
|
/// Displays the current sort option as a labelled button with a sort icon.
|
||||||
|
/// Tapping opens a popup menu with three options: A–Z, Intervall, Aufwand.
|
||||||
|
/// The active option is indicated with a visible check mark.
|
||||||
|
///
|
||||||
|
/// Reads sort state from [sortPreferenceProvider] and writes via
|
||||||
|
/// [SortPreferenceNotifier.setSortOption].
|
||||||
|
class SortDropdown extends ConsumerWidget {
|
||||||
|
const SortDropdown({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final current = ref.watch(sortPreferenceProvider);
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return PopupMenuButton<TaskSortOption>(
|
||||||
|
onSelected: (value) =>
|
||||||
|
ref.read(sortPreferenceProvider.notifier).setSortOption(value),
|
||||||
|
itemBuilder: (context) => TaskSortOption.values.map((option) {
|
||||||
|
final isSelected = option == current;
|
||||||
|
return PopupMenuItem<TaskSortOption>(
|
||||||
|
value: option,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Opacity(
|
||||||
|
opacity: isSelected ? 1.0 : 0.0,
|
||||||
|
child: const Icon(Icons.check, size: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(_label(option, l10n)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.sort),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_label(current, l10n),
|
||||||
|
style: theme.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _label(TaskSortOption option, AppLocalizations l10n) {
|
||||||
|
switch (option) {
|
||||||
|
case TaskSortOption.alphabetical:
|
||||||
|
return l10n.sortAlphabetical;
|
||||||
|
case TaskSortOption.interval:
|
||||||
|
return l10n.sortInterval;
|
||||||
|
case TaskSortOption.effort:
|
||||||
|
return l10n.sortEffort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'package:household_keeper/features/tasks/domain/task_sort_option.dart';
|
||||||
|
|
||||||
|
part 'sort_preference_notifier.g.dart';
|
||||||
|
|
||||||
|
const _sortOptionKey = 'task_sort_option';
|
||||||
|
|
||||||
|
/// Notifier that manages the active task sort preference.
|
||||||
|
///
|
||||||
|
/// Defaults to [TaskSortOption.alphabetical] synchronously (matching the
|
||||||
|
/// existing A-Z sort in CalendarDayList), then loads the persisted value
|
||||||
|
/// asynchronously on first build.
|
||||||
|
///
|
||||||
|
/// Follows the same pattern as [ThemeNotifier] in
|
||||||
|
/// `lib/core/theme/theme_provider.dart`.
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class SortPreferenceNotifier extends _$SortPreferenceNotifier {
|
||||||
|
@override
|
||||||
|
TaskSortOption build() {
|
||||||
|
_loadPersisted();
|
||||||
|
return TaskSortOption.alphabetical;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPersisted() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final persisted = prefs.getString(_sortOptionKey);
|
||||||
|
if (persisted != null) {
|
||||||
|
state = _fromString(persisted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the active sort preference and persist it.
|
||||||
|
Future<void> setSortOption(TaskSortOption option) async {
|
||||||
|
state = option;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(_sortOptionKey, _toString(option));
|
||||||
|
}
|
||||||
|
|
||||||
|
static TaskSortOption _fromString(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'alphabetical':
|
||||||
|
return TaskSortOption.alphabetical;
|
||||||
|
case 'interval':
|
||||||
|
return TaskSortOption.interval;
|
||||||
|
case 'effort':
|
||||||
|
return TaskSortOption.effort;
|
||||||
|
default:
|
||||||
|
return TaskSortOption.alphabetical;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _toString(TaskSortOption option) {
|
||||||
|
return option.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'sort_preference_notifier.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
/// Notifier that manages the active task sort preference.
|
||||||
|
///
|
||||||
|
/// Defaults to [TaskSortOption.alphabetical] synchronously (matching the
|
||||||
|
/// existing A-Z sort in CalendarDayList), then loads the persisted value
|
||||||
|
/// asynchronously on first build.
|
||||||
|
///
|
||||||
|
/// Follows the same pattern as [ThemeNotifier] in
|
||||||
|
/// `lib/core/theme/theme_provider.dart`.
|
||||||
|
|
||||||
|
@ProviderFor(SortPreferenceNotifier)
|
||||||
|
final sortPreferenceProvider = SortPreferenceNotifierProvider._();
|
||||||
|
|
||||||
|
/// Notifier that manages the active task sort preference.
|
||||||
|
///
|
||||||
|
/// Defaults to [TaskSortOption.alphabetical] synchronously (matching the
|
||||||
|
/// existing A-Z sort in CalendarDayList), then loads the persisted value
|
||||||
|
/// asynchronously on first build.
|
||||||
|
///
|
||||||
|
/// Follows the same pattern as [ThemeNotifier] in
|
||||||
|
/// `lib/core/theme/theme_provider.dart`.
|
||||||
|
final class SortPreferenceNotifierProvider
|
||||||
|
extends $NotifierProvider<SortPreferenceNotifier, TaskSortOption> {
|
||||||
|
/// Notifier that manages the active task sort preference.
|
||||||
|
///
|
||||||
|
/// Defaults to [TaskSortOption.alphabetical] synchronously (matching the
|
||||||
|
/// existing A-Z sort in CalendarDayList), then loads the persisted value
|
||||||
|
/// asynchronously on first build.
|
||||||
|
///
|
||||||
|
/// Follows the same pattern as [ThemeNotifier] in
|
||||||
|
/// `lib/core/theme/theme_provider.dart`.
|
||||||
|
SortPreferenceNotifierProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'sortPreferenceProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$sortPreferenceNotifierHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
SortPreferenceNotifier create() => SortPreferenceNotifier();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(TaskSortOption value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<TaskSortOption>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$sortPreferenceNotifierHash() =>
|
||||||
|
r'5d7f2c5d06b82b4114262ee05cf890ebe717fe2a';
|
||||||
|
|
||||||
|
/// Notifier that manages the active task sort preference.
|
||||||
|
///
|
||||||
|
/// Defaults to [TaskSortOption.alphabetical] synchronously (matching the
|
||||||
|
/// existing A-Z sort in CalendarDayList), then loads the persisted value
|
||||||
|
/// asynchronously on first build.
|
||||||
|
///
|
||||||
|
/// Follows the same pattern as [ThemeNotifier] in
|
||||||
|
/// `lib/core/theme/theme_provider.dart`.
|
||||||
|
|
||||||
|
abstract class _$SortPreferenceNotifier extends $Notifier<TaskSortOption> {
|
||||||
|
TaskSortOption build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref = this.ref as $Ref<TaskSortOption, TaskSortOption>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<TaskSortOption, TaskSortOption>,
|
||||||
|
TaskSortOption,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
import 'package:household_keeper/core/database/database.dart';
|
import 'package:household_keeper/core/database/database.dart';
|
||||||
import 'package:household_keeper/core/providers/database_provider.dart';
|
import 'package:household_keeper/core/providers/database_provider.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/presentation/sort_dropdown.dart';
|
||||||
import 'package:household_keeper/features/tasks/presentation/task_providers.dart';
|
import 'package:household_keeper/features/tasks/presentation/task_providers.dart';
|
||||||
import 'package:household_keeper/features/tasks/presentation/task_row.dart';
|
import 'package:household_keeper/features/tasks/presentation/task_row.dart';
|
||||||
import 'package:household_keeper/l10n/app_localizations.dart';
|
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||||
@@ -27,6 +28,7 @@ class TaskListScreen extends ConsumerWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: _RoomTitle(roomId: roomId),
|
title: _RoomTitle(roomId: roomId),
|
||||||
actions: [
|
actions: [
|
||||||
|
const SortDropdown(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
onPressed: () => context.go('/rooms/$roomId/edit'),
|
onPressed: () => context.go('/rooms/$roomId/edit'),
|
||||||
|
|||||||
@@ -6,17 +6,44 @@ import 'package:household_keeper/core/database/database.dart';
|
|||||||
import 'package:household_keeper/core/providers/database_provider.dart';
|
import 'package:household_keeper/core/providers/database_provider.dart';
|
||||||
import 'package:household_keeper/features/tasks/domain/effort_level.dart';
|
import 'package:household_keeper/features/tasks/domain/effort_level.dart';
|
||||||
import 'package:household_keeper/features/tasks/domain/frequency.dart';
|
import 'package:household_keeper/features/tasks/domain/frequency.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/domain/task_sort_option.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/presentation/sort_preference_notifier.dart';
|
||||||
|
|
||||||
part 'task_providers.g.dart';
|
part 'task_providers.g.dart';
|
||||||
|
|
||||||
/// Stream provider family for tasks in a specific room, sorted by due date.
|
/// Sort a list of [Task] by the given [sortOption].
|
||||||
|
///
|
||||||
|
/// Returns a new sorted list; never mutates the original.
|
||||||
|
List<Task> _sortTasksRaw(List<Task> tasks, TaskSortOption sortOption) {
|
||||||
|
final sorted = List<Task>.from(tasks);
|
||||||
|
switch (sortOption) {
|
||||||
|
case TaskSortOption.alphabetical:
|
||||||
|
sorted.sort((a, b) => a.name.toLowerCase().compareTo(
|
||||||
|
b.name.toLowerCase(),
|
||||||
|
));
|
||||||
|
case TaskSortOption.interval:
|
||||||
|
sorted.sort((a, b) {
|
||||||
|
final cmp = a.intervalType.index.compareTo(b.intervalType.index);
|
||||||
|
if (cmp != 0) return cmp;
|
||||||
|
return a.intervalDays.compareTo(b.intervalDays);
|
||||||
|
});
|
||||||
|
case TaskSortOption.effort:
|
||||||
|
sorted.sort((a, b) => a.effortLevel.index.compareTo(b.effortLevel.index));
|
||||||
|
}
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stream provider family for tasks in a specific room, sorted by active sort preference.
|
||||||
///
|
///
|
||||||
/// Defined manually because riverpod_generator has trouble with drift's
|
/// Defined manually because riverpod_generator has trouble with drift's
|
||||||
/// generated [Task] type in family provider return types.
|
/// generated [Task] type in family provider return types.
|
||||||
final tasksInRoomProvider =
|
final tasksInRoomProvider =
|
||||||
StreamProvider.family.autoDispose<List<Task>, int>((ref, roomId) {
|
StreamProvider.family.autoDispose<List<Task>, int>((ref, roomId) {
|
||||||
final db = ref.watch(appDatabaseProvider);
|
final db = ref.watch(appDatabaseProvider);
|
||||||
return db.tasksDao.watchTasksInRoom(roomId);
|
final sortOption = ref.watch(sortPreferenceProvider);
|
||||||
|
return db.tasksDao
|
||||||
|
.watchTasksInRoom(roomId)
|
||||||
|
.map((tasks) => _sortTasksRaw(tasks, sortOption));
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Notifier for task mutations: create, update, delete, complete.
|
/// Notifier for task mutations: create, update, delete, complete.
|
||||||
|
|||||||
@@ -115,5 +115,9 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": { "type": "int" }
|
"count": { "type": "int" }
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"sortAlphabetical": "A\u2013Z",
|
||||||
|
"sortInterval": "Intervall",
|
||||||
|
"sortEffort": "Aufwand",
|
||||||
|
"sortLabel": "Sortierung"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -537,6 +537,30 @@ abstract class AppLocalizations {
|
|||||||
/// In de, this message translates to:
|
/// In de, this message translates to:
|
||||||
/// **'{count} Mal erledigt'**
|
/// **'{count} Mal erledigt'**
|
||||||
String taskHistoryCount(int count);
|
String taskHistoryCount(int count);
|
||||||
|
|
||||||
|
/// No description provided for @sortAlphabetical.
|
||||||
|
///
|
||||||
|
/// In de, this message translates to:
|
||||||
|
/// **'A–Z'**
|
||||||
|
String get sortAlphabetical;
|
||||||
|
|
||||||
|
/// No description provided for @sortInterval.
|
||||||
|
///
|
||||||
|
/// In de, this message translates to:
|
||||||
|
/// **'Intervall'**
|
||||||
|
String get sortInterval;
|
||||||
|
|
||||||
|
/// No description provided for @sortEffort.
|
||||||
|
///
|
||||||
|
/// In de, this message translates to:
|
||||||
|
/// **'Aufwand'**
|
||||||
|
String get sortEffort;
|
||||||
|
|
||||||
|
/// No description provided for @sortLabel.
|
||||||
|
///
|
||||||
|
/// In de, this message translates to:
|
||||||
|
/// **'Sortierung'**
|
||||||
|
String get sortLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -250,4 +250,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String taskHistoryCount(int count) {
|
String taskHistoryCount(int count) {
|
||||||
return '$count Mal erledigt';
|
return '$count Mal erledigt';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sortAlphabetical => 'A–Z';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sortInterval => 'Intervall';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sortEffort => 'Aufwand';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sortLabel => 'Sortierung';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import 'package:household_keeper/features/home/presentation/calendar_providers.d
|
|||||||
import 'package:household_keeper/features/rooms/presentation/room_providers.dart';
|
import 'package:household_keeper/features/rooms/presentation/room_providers.dart';
|
||||||
import 'package:household_keeper/features/tasks/domain/effort_level.dart';
|
import 'package:household_keeper/features/tasks/domain/effort_level.dart';
|
||||||
import 'package:household_keeper/features/tasks/domain/frequency.dart';
|
import 'package:household_keeper/features/tasks/domain/frequency.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/domain/task_sort_option.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/presentation/sort_preference_notifier.dart';
|
||||||
import 'package:household_keeper/l10n/app_localizations.dart';
|
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||||
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ Widget _buildApp(CalendarDayState dayState) {
|
|||||||
roomWithStatsListProvider.overrideWith(
|
roomWithStatsListProvider.overrideWith(
|
||||||
(ref) => Stream.value([]),
|
(ref) => Stream.value([]),
|
||||||
),
|
),
|
||||||
|
sortPreferenceProvider.overrideWith(SortPreferenceNotifier.new),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return UncontrolledProviderScope(
|
return UncontrolledProviderScope(
|
||||||
@@ -238,4 +241,38 @@ void main() {
|
|||||||
expect(find.byType(ListView), findsWidgets);
|
expect(find.byType(ListView), findsWidgets);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('HomeScreen sort dropdown', () {
|
||||||
|
testWidgets('shows sort dropdown in AppBar', (tester) async {
|
||||||
|
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||||
|
selectedDate: today,
|
||||||
|
dayTasks: const [],
|
||||||
|
overdueTasks: const [],
|
||||||
|
totalTaskCount: 0,
|
||||||
|
)));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
// SortDropdown wraps a PopupMenuButton<TaskSortOption>
|
||||||
|
expect(
|
||||||
|
find.byType(PopupMenuButton<TaskSortOption>),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows AppBar with title', (tester) async {
|
||||||
|
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||||
|
selectedDate: today,
|
||||||
|
dayTasks: const [],
|
||||||
|
overdueTasks: const [],
|
||||||
|
totalTaskCount: 0,
|
||||||
|
)));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
// tabHome l10n string is 'Übersicht'. It appears in the AppBar title
|
||||||
|
// and also in the bottom navigation bar label — use findsWidgets.
|
||||||
|
expect(find.text('\u00dcbersicht'), findsWidgets);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'package:household_keeper/features/tasks/domain/task_sort_option.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/presentation/sort_preference_notifier.dart';
|
||||||
|
|
||||||
|
/// Helper: create a container and wait for the initial async _loadPersisted()
|
||||||
|
/// to finish.
|
||||||
|
Future<ProviderContainer> makeContainer() async {
|
||||||
|
final container = ProviderContainer();
|
||||||
|
// Trigger build
|
||||||
|
container.read(sortPreferenceProvider);
|
||||||
|
// Allow the async _loadPersisted() to complete
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUp(() {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('SortPreferenceNotifier', () {
|
||||||
|
test('build() returns default state of alphabetical', () async {
|
||||||
|
final container = ProviderContainer();
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
final state = container.read(sortPreferenceProvider);
|
||||||
|
|
||||||
|
expect(state, TaskSortOption.alphabetical);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setSortOption(interval) updates state to interval', () async {
|
||||||
|
final container = await makeContainer();
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
await container
|
||||||
|
.read(sortPreferenceProvider.notifier)
|
||||||
|
.setSortOption(TaskSortOption.interval);
|
||||||
|
|
||||||
|
expect(container.read(sortPreferenceProvider), TaskSortOption.interval);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setSortOption(effort) updates state to effort', () async {
|
||||||
|
final container = await makeContainer();
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
await container
|
||||||
|
.read(sortPreferenceProvider.notifier)
|
||||||
|
.setSortOption(TaskSortOption.effort);
|
||||||
|
|
||||||
|
expect(container.read(sortPreferenceProvider), TaskSortOption.effort);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setSortOption persists to SharedPreferences', () async {
|
||||||
|
final container = await makeContainer();
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
await container
|
||||||
|
.read(sortPreferenceProvider.notifier)
|
||||||
|
.setSortOption(TaskSortOption.effort);
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
expect(prefs.getString('task_sort_option'), 'effort');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('persisted value is loaded on restart (effort)', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'task_sort_option': 'effort',
|
||||||
|
});
|
||||||
|
|
||||||
|
final container = await makeContainer();
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
expect(container.read(sortPreferenceProvider), TaskSortOption.effort);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('persisted value is loaded on restart (interval)', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'task_sort_option': 'interval',
|
||||||
|
});
|
||||||
|
|
||||||
|
final container = await makeContainer();
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
expect(container.read(sortPreferenceProvider), TaskSortOption.interval);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unknown persisted value falls back to alphabetical', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'task_sort_option': 'unknown_value',
|
||||||
|
});
|
||||||
|
|
||||||
|
final container = await makeContainer();
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
expect(container.read(sortPreferenceProvider), TaskSortOption.alphabetical);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -63,8 +63,9 @@ void main() {
|
|||||||
// Verify 3 NavigationDestination widgets are rendered
|
// Verify 3 NavigationDestination widgets are rendered
|
||||||
expect(find.byType(NavigationDestination), findsNWidgets(3));
|
expect(find.byType(NavigationDestination), findsNWidgets(3));
|
||||||
|
|
||||||
// Verify correct German labels from ARB (with umlauts)
|
// Verify correct German labels from ARB (with umlauts).
|
||||||
expect(find.text('\u00dcbersicht'), findsOneWidget);
|
// 'Übersicht' appears in both the bottom nav and the HomeScreen AppBar.
|
||||||
|
expect(find.text('\u00dcbersicht'), findsWidgets);
|
||||||
expect(find.text('R\u00e4ume'), findsOneWidget);
|
expect(find.text('R\u00e4ume'), findsOneWidget);
|
||||||
expect(find.text('Einstellungen'), findsOneWidget);
|
expect(find.text('Einstellungen'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user