chore: complete v1.1 milestone
Archive v1.1 Calendar & Polish milestone artifacts (roadmap, requirements, phase directories) to milestones/. Evolve PROJECT.md with validated requirements and new key decisions. Update RETROSPECTIVE.md with v1.1 section and cross-milestone trends. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
276
.planning/milestones/v1.1-phases/07-task-sorting/07-01-PLAN.md
Normal file
276
.planning/milestones/v1.1-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>
|
||||
Reference in New Issue
Block a user