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:
312
.planning/milestones/v1.1-phases/06-task-history/06-01-PLAN.md
Normal file
312
.planning/milestones/v1.1-phases/06-task-history/06-01-PLAN.md
Normal file
@@ -0,0 +1,312 @@
|
||||
---
|
||||
phase: 06-task-history
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- lib/features/tasks/data/tasks_dao.dart
|
||||
- lib/features/tasks/data/tasks_dao.g.dart
|
||||
- lib/features/tasks/presentation/task_history_sheet.dart
|
||||
- lib/features/tasks/presentation/task_form_screen.dart
|
||||
- lib/features/home/presentation/calendar_task_row.dart
|
||||
- lib/l10n/app_de.arb
|
||||
- lib/l10n/app_localizations.dart
|
||||
- lib/l10n/app_localizations_de.dart
|
||||
- test/features/tasks/data/task_history_dao_test.dart
|
||||
autonomous: true
|
||||
requirements: [HIST-01, HIST-02]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Every task completion is recorded with a timestamp and persists across app restarts"
|
||||
- "User can open a history view from the task edit form showing all past completion dates in reverse-chronological order"
|
||||
- "History view shows a meaningful empty state if the task has never been completed"
|
||||
artifacts:
|
||||
- path: "lib/features/tasks/data/tasks_dao.dart"
|
||||
provides: "watchCompletionsForTask(int taskId) stream method"
|
||||
contains: "watchCompletionsForTask"
|
||||
- path: "lib/features/tasks/presentation/task_history_sheet.dart"
|
||||
provides: "Bottom sheet displaying task completion history"
|
||||
exports: ["showTaskHistorySheet"]
|
||||
- path: "lib/features/tasks/presentation/task_form_screen.dart"
|
||||
provides: "Verlauf button in edit mode opening history sheet"
|
||||
contains: "showTaskHistorySheet"
|
||||
- path: "lib/features/home/presentation/calendar_task_row.dart"
|
||||
provides: "onTap navigation to task edit form"
|
||||
contains: "context.go"
|
||||
- path: "test/features/tasks/data/task_history_dao_test.dart"
|
||||
provides: "Tests for completion history DAO query"
|
||||
min_lines: 30
|
||||
key_links:
|
||||
- from: "lib/features/tasks/presentation/task_form_screen.dart"
|
||||
to: "lib/features/tasks/presentation/task_history_sheet.dart"
|
||||
via: "showTaskHistorySheet call in Verlauf button onTap"
|
||||
pattern: "showTaskHistorySheet"
|
||||
- from: "lib/features/tasks/presentation/task_history_sheet.dart"
|
||||
to: "lib/features/tasks/data/tasks_dao.dart"
|
||||
via: "watchCompletionsForTask stream consumption"
|
||||
pattern: "watchCompletionsForTask"
|
||||
- from: "lib/features/home/presentation/calendar_task_row.dart"
|
||||
to: "TaskFormScreen"
|
||||
via: "GoRouter navigation on row tap"
|
||||
pattern: "context\\.go.*tasks"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add task completion history: a DAO query to fetch completions, a bottom sheet to display them, integration into the task edit form, and CalendarTaskRow onTap navigation.
|
||||
|
||||
Purpose: Users can see exactly when each task was completed in the past, building trust that the scheduling loop is working correctly.
|
||||
Output: Working history view accessible from task edit form, completion data surfaced from existing TaskCompletions table.
|
||||
</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/06-task-history/06-CONTEXT.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
<!-- Executor should use these directly -- no codebase exploration needed. -->
|
||||
|
||||
From lib/core/database/database.dart:
|
||||
```dart
|
||||
/// TaskCompletions table: records when a task was completed.
|
||||
class TaskCompletions extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get taskId => integer().references(Tasks, #id)();
|
||||
DateTimeColumn get completedAt => dateTime()();
|
||||
}
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [Rooms, Tasks, TaskCompletions],
|
||||
daos: [RoomsDao, TasksDao, DailyPlanDao, CalendarDao],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase { ... }
|
||||
```
|
||||
|
||||
From lib/features/tasks/data/tasks_dao.dart:
|
||||
```dart
|
||||
@DriftAccessor(tables: [Tasks, TaskCompletions])
|
||||
class TasksDao extends DatabaseAccessor<AppDatabase> with _$TasksDaoMixin {
|
||||
TasksDao(super.attachedDatabase);
|
||||
|
||||
Stream<List<Task>> watchTasksInRoom(int roomId) { ... }
|
||||
Future<int> insertTask(TasksCompanion task) => into(tasks).insert(task);
|
||||
Future<bool> updateTask(Task task) => update(tasks).replace(task);
|
||||
Future<void> deleteTask(int taskId) { ... }
|
||||
Future<void> completeTask(int taskId, {DateTime? now}) { ... }
|
||||
Future<int> getOverdueTaskCount(int roomId, {DateTime? today}) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
From lib/features/tasks/presentation/task_form_screen.dart:
|
||||
```dart
|
||||
class TaskFormScreen extends ConsumerStatefulWidget {
|
||||
final int? roomId;
|
||||
final int? taskId;
|
||||
const TaskFormScreen({super.key, this.roomId, this.taskId});
|
||||
bool get isEditing => taskId != null;
|
||||
}
|
||||
// build() returns Scaffold with AppBar + Form > ListView with fields
|
||||
// In edit mode: _existingTask is loaded via _loadExistingTask()
|
||||
```
|
||||
|
||||
From lib/features/home/presentation/calendar_task_row.dart:
|
||||
```dart
|
||||
class CalendarTaskRow extends StatelessWidget {
|
||||
const CalendarTaskRow({
|
||||
super.key,
|
||||
required this.taskWithRoom,
|
||||
required this.onCompleted,
|
||||
this.isOverdue = false,
|
||||
});
|
||||
final TaskWithRoom taskWithRoom;
|
||||
final VoidCallback onCompleted;
|
||||
final bool isOverdue;
|
||||
}
|
||||
// TaskWithRoom has: task (Task), roomName (String), roomId (int)
|
||||
```
|
||||
|
||||
From lib/features/home/domain/daily_plan_models.dart:
|
||||
```dart
|
||||
class TaskWithRoom {
|
||||
final Task task;
|
||||
final String roomName;
|
||||
final int roomId;
|
||||
const TaskWithRoom({required this.task, required this.roomName, required this.roomId});
|
||||
}
|
||||
```
|
||||
|
||||
Bottom sheet pattern from lib/features/rooms/presentation/icon_picker_sheet.dart:
|
||||
```dart
|
||||
Future<String?> showIconPickerSheet({
|
||||
required BuildContext context,
|
||||
String? selectedIconName,
|
||||
}) {
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => IconPickerSheet(...),
|
||||
);
|
||||
}
|
||||
// Sheet uses SafeArea > Padding > Column(mainAxisSize: MainAxisSize.min) with drag handle
|
||||
```
|
||||
|
||||
Router pattern from lib/core/router/router.dart:
|
||||
```dart
|
||||
// Task edit route: /rooms/:roomId/tasks/:taskId
|
||||
GoRoute(
|
||||
path: 'tasks/:taskId',
|
||||
builder: (context, state) {
|
||||
final taskId = int.parse(state.pathParameters['taskId']!);
|
||||
return TaskFormScreen(taskId: taskId);
|
||||
},
|
||||
),
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Add DAO query, provider, localization, and tests for completion history</name>
|
||||
<files>
|
||||
lib/features/tasks/data/tasks_dao.dart,
|
||||
lib/features/tasks/data/tasks_dao.g.dart,
|
||||
lib/l10n/app_de.arb,
|
||||
lib/l10n/app_localizations.dart,
|
||||
lib/l10n/app_localizations_de.dart,
|
||||
test/features/tasks/data/task_history_dao_test.dart
|
||||
</files>
|
||||
<behavior>
|
||||
- watchCompletionsForTask(taskId) returns Stream of TaskCompletion list ordered by completedAt DESC (newest first)
|
||||
- Empty list returned when no completions exist for a given taskId
|
||||
- After completeTask(taskId) is called, watchCompletionsForTask(taskId) emits a list containing the new completion with correct timestamp
|
||||
- Completions for different tasks are isolated (taskId=1 completions do not appear in taskId=2 stream)
|
||||
- Multiple completions for the same task are all returned in reverse-chronological order
|
||||
</behavior>
|
||||
<action>
|
||||
RED phase:
|
||||
Create test/features/tasks/data/task_history_dao_test.dart with tests for the behaviors above.
|
||||
Use the existing in-memory database test pattern: AppDatabase(NativeDatabase.memory()), get TasksDao, insert a room and tasks, then test.
|
||||
Run tests -- they MUST fail (watchCompletionsForTask does not exist yet).
|
||||
|
||||
GREEN phase:
|
||||
1. In lib/features/tasks/data/tasks_dao.dart, add:
|
||||
```dart
|
||||
/// Watch all completions for a task, newest first.
|
||||
Stream<List<TaskCompletion>> watchCompletionsForTask(int taskId) {
|
||||
return (select(taskCompletions)
|
||||
..where((c) => c.taskId.equals(taskId))
|
||||
..orderBy([(c) => OrderingTerm.desc(c.completedAt)]))
|
||||
.watch();
|
||||
}
|
||||
```
|
||||
2. Run `dart run build_runner build --delete-conflicting-outputs` to regenerate tasks_dao.g.dart.
|
||||
3. Run tests -- they MUST pass.
|
||||
|
||||
Then add localization strings to lib/l10n/app_de.arb:
|
||||
- "taskHistoryTitle": "Verlauf"
|
||||
- "taskHistoryEmpty": "Noch nie erledigt"
|
||||
- "taskHistoryCount": "{count} Mal erledigt" with @taskHistoryCount placeholder for count (int)
|
||||
|
||||
Run `flutter gen-l10n` to regenerate app_localizations.dart and app_localizations_de.dart.
|
||||
|
||||
NOTE: No separate Riverpod provider is needed -- the bottom sheet will access the DAO directly via appDatabaseProvider (same pattern as _loadExistingTask in TaskFormScreen). This keeps it simple since the sheet is a one-shot modal, not a long-lived screen.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/tasks/data/task_history_dao_test.dart -r expanded && flutter analyze --no-fatal-infos</automated>
|
||||
</verify>
|
||||
<done>
|
||||
watchCompletionsForTask method exists on TasksDao, returns Stream of completions sorted newest-first.
|
||||
All new DAO tests pass. All 101+ existing tests still pass.
|
||||
Three German localization strings (taskHistoryTitle, taskHistoryEmpty, taskHistoryCount) are available via AppLocalizations.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Build history bottom sheet, wire into TaskFormScreen, add CalendarTaskRow navigation</name>
|
||||
<files>
|
||||
lib/features/tasks/presentation/task_history_sheet.dart,
|
||||
lib/features/tasks/presentation/task_form_screen.dart,
|
||||
lib/features/home/presentation/calendar_task_row.dart
|
||||
</files>
|
||||
<action>
|
||||
1. Create lib/features/tasks/presentation/task_history_sheet.dart:
|
||||
- Export a top-level function: `Future<void> showTaskHistorySheet({required BuildContext context, required int taskId})`
|
||||
- Uses `showModalBottomSheet` with `isScrollControlled: true` following icon_picker_sheet.dart pattern
|
||||
- The sheet widget is a ConsumerWidget (needs ref to access DAO)
|
||||
- Uses `ref.read(appDatabaseProvider).tasksDao.watchCompletionsForTask(taskId)` wrapped in a StreamBuilder
|
||||
- Layout: SafeArea > Padding(16) > Column(mainAxisSize: min):
|
||||
a. Drag handle (same as icon_picker_sheet: Container 32x4, onSurfaceVariant 0.4 alpha, rounded)
|
||||
b. Title: AppLocalizations.of(context).taskHistoryTitle (i.e. "Verlauf"), titleMedium style
|
||||
c. Optional: completion count summary below title using taskHistoryCount string -- show only when count > 0
|
||||
d. SizedBox(height: 16)
|
||||
e. StreamBuilder on watchCompletionsForTask:
|
||||
- Loading: Center(CircularProgressIndicator())
|
||||
- Empty data: centered Column with Icon(Icons.history, size: 48, color: onSurfaceVariant) + SizedBox(8) + Text(taskHistoryEmpty), style: bodyLarge, color: onSurfaceVariant
|
||||
- Has data: ConstrainedBox(maxHeight: MediaQuery.of(context).size.height * 0.4) > ListView.builder:
|
||||
Each item: ListTile with leading Icon(Icons.check_circle_outline, color: primary), title: DateFormat('dd.MM.yyyy', 'de').format(completion.completedAt), subtitle: DateFormat('HH:mm', 'de').format(completion.completedAt)
|
||||
f. SizedBox(height: 8) at bottom
|
||||
|
||||
2. Modify lib/features/tasks/presentation/task_form_screen.dart:
|
||||
- Import task_history_sheet.dart
|
||||
- In the build() method's ListView children, AFTER the due date picker section and ONLY when `widget.isEditing` is true, add:
|
||||
```
|
||||
const SizedBox(height: 24),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.history),
|
||||
title: Text(l10n.taskHistoryTitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => showTaskHistorySheet(context: context, taskId: widget.taskId!),
|
||||
),
|
||||
```
|
||||
- This adds a "Verlauf" row that opens the history bottom sheet
|
||||
|
||||
3. Modify lib/features/home/presentation/calendar_task_row.dart:
|
||||
- Add an onTap callback to the ListTile that navigates to the task edit form
|
||||
- The CalendarTaskRow already has access to taskWithRoom.task.id and taskWithRoom.roomId
|
||||
- Add to ListTile: `onTap: () => context.go('/rooms/${taskWithRoom.roomId}/tasks/${taskWithRoom.task.id}')`
|
||||
- This enables: CalendarTaskRow tap -> TaskFormScreen (edit mode) -> "Verlauf" button -> history sheet
|
||||
- Keep the existing onCompleted checkbox behavior unchanged
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test && flutter analyze --no-fatal-infos</automated>
|
||||
</verify>
|
||||
<done>
|
||||
History bottom sheet opens from TaskFormScreen in edit mode via "Verlauf" row.
|
||||
Sheet shows completion dates in dd.MM.yyyy + HH:mm format, reverse-chronological.
|
||||
Empty state shows Icons.history + "Noch nie erledigt" message.
|
||||
CalendarTaskRow tapping navigates to TaskFormScreen for that task.
|
||||
All existing tests still pass. dart analyze clean.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
Phase 6 verification checks:
|
||||
1. `flutter test` -- all tests pass (101 existing + new DAO tests)
|
||||
2. `flutter analyze --no-fatal-infos` -- zero issues
|
||||
3. Manual flow: Open app > tap a task in calendar > task edit form opens > "Verlauf" row visible > tap it > bottom sheet shows history or empty state
|
||||
4. Manual flow: Complete a task via checkbox > navigate to that task's edit form > tap "Verlauf" > new completion entry appears with timestamp
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- HIST-01: Task completion recording verified via DAO tests (completions already written by completeTask; new query surfaces them)
|
||||
- HIST-02: History bottom sheet accessible from task edit form, shows all past completions reverse-chronologically with German date/time formatting, shows meaningful empty state
|
||||
- CalendarTaskRow tapping navigates to task edit form (history one tap away)
|
||||
- Zero regressions: all existing tests pass, dart analyze clean
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/06-task-history/06-01-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user