From cff5f9e67bc4b3faef7619bc6dc608897e5a6ea9 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 18 Mar 2026 20:45:08 +0100 Subject: [PATCH] docs(08-task-delete): create phase plan --- .planning/ROADMAP.md | 6 +- .planning/phases/08-task-delete/08-01-PLAN.md | 310 ++++++++++++++++++ .planning/phases/08-task-delete/08-02-PLAN.md | 290 ++++++++++++++++ 3 files changed, 605 insertions(+), 1 deletion(-) create mode 100644 .planning/phases/08-task-delete/08-01-PLAN.md create mode 100644 .planning/phases/08-task-delete/08-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 56b3f01..6362e52 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -43,6 +43,10 @@ See `milestones/v1.1-ROADMAP.md` for full phase details. **Goal**: Users can remove tasks they no longer need, with smart preservation of completion history for future statistics **Depends on**: Phase 7 (v1.1 shipped — calendar, history, and sorting all in place) **Requirements**: DEL-01, DEL-02, DEL-03, DEL-04 +**Plans:** 2 plans +Plans: +- [ ] 08-01-PLAN.md — Data layer: isActive column, schema migration, DAO filters and methods +- [ ] 08-02-PLAN.md — UI layer: delete button, confirmation dialog, smart delete provider **Success Criteria** (what must be TRUE): 1. The task edit form has a clearly visible delete action (button or icon) 2. Deleting a task with zero completions removes it from the database entirely @@ -82,6 +86,6 @@ See `milestones/v1.1-ROADMAP.md` for full phase details. | 5. Calendar Strip | v1.1 | 2/2 | Complete | 2026-03-16 | | 6. Task History | v1.1 | 1/1 | Complete | 2026-03-16 | | 7. Task Sorting | v1.1 | 2/2 | Complete | 2026-03-16 | -| 8. Task Delete | v1.2 | - | Planned | - | +| 8. Task Delete | v1.2 | 0/2 | In Progress | - | | 9. Task Creation UX | v1.2 | - | Planned | - | | 10. Dead Code Cleanup | v1.2 | - | Planned | - | diff --git a/.planning/phases/08-task-delete/08-01-PLAN.md b/.planning/phases/08-task-delete/08-01-PLAN.md new file mode 100644 index 0000000..6a107e9 --- /dev/null +++ b/.planning/phases/08-task-delete/08-01-PLAN.md @@ -0,0 +1,310 @@ +--- +phase: 08-task-delete +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - lib/core/database/database.dart + - lib/features/tasks/data/tasks_dao.dart + - lib/features/home/data/calendar_dao.dart + - lib/features/home/data/daily_plan_dao.dart + - lib/features/rooms/data/rooms_dao.dart + - test/features/tasks/data/tasks_dao_test.dart +autonomous: true +requirements: [DEL-02, DEL-03] + +must_haves: + truths: + - "Active tasks appear in all views (calendar, room task lists, daily plan)" + - "Deactivated tasks are hidden from all views" + - "Hard delete removes task and completions from DB entirely" + - "Soft delete sets isActive to false without removing data" + - "Existing tasks default to active after migration" + artifacts: + - path: "lib/core/database/database.dart" + provides: "isActive BoolColumn on Tasks table, schema v3, migration" + contains: "isActive" + - path: "lib/features/tasks/data/tasks_dao.dart" + provides: "softDeleteTask, getCompletionCount, isActive filter on watchTasksInRoom" + exports: ["softDeleteTask", "getCompletionCount"] + - path: "lib/features/home/data/calendar_dao.dart" + provides: "isActive=true filter on all 6 task queries + getTaskCount" + contains: "isActive" + - path: "lib/features/home/data/daily_plan_dao.dart" + provides: "isActive=true filter on watchAllTasksWithRoomName and count queries" + contains: "isActive" + - path: "lib/features/rooms/data/rooms_dao.dart" + provides: "isActive=true filter on task queries in watchRoomWithStats" + contains: "isActive" + - path: "test/features/tasks/data/tasks_dao_test.dart" + provides: "Tests for softDeleteTask, getCompletionCount, isActive filtering" + key_links: + - from: "lib/core/database/database.dart" + to: "All DAOs" + via: "Tasks table schema with isActive column" + pattern: "BoolColumn.*isActive.*withDefault.*true" + - from: "lib/features/tasks/data/tasks_dao.dart" + to: "lib/features/tasks/presentation/task_providers.dart" + via: "softDeleteTask and getCompletionCount methods" + pattern: "softDeleteTask|getCompletionCount" +--- + + +Add isActive column to the Tasks table and filter all DAO queries to exclude deactivated tasks. + +Purpose: Foundation for smart task deletion — the isActive column enables soft-delete behavior where completed tasks are hidden but preserved for statistics, while hard-delete removes tasks with no history entirely. + +Output: Schema v3 with isActive column, all DAO queries filtering active-only, softDeleteTask and getCompletionCount DAO methods, passing tests. + + + +@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md +@/home/jlmak/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/08-task-delete/08-CONTEXT.md + + + + +From lib/core/database/database.dart (current schema): +```dart +class Tasks extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get roomId => integer().references(Rooms, #id)(); + TextColumn get name => text().withLength(min: 1, max: 200)(); + TextColumn get description => text().nullable()(); + IntColumn get intervalType => intEnum()(); + IntColumn get intervalDays => integer().withDefault(const Constant(1))(); + IntColumn get anchorDay => integer().nullable()(); + IntColumn get effortLevel => intEnum()(); + DateTimeColumn get nextDueDate => dateTime()(); + DateTimeColumn get createdAt => dateTime().clientDefault(() => DateTime.now())(); +} + +// Current schema version +int get schemaVersion => 2; + +// Current migration strategy +MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (Migrator m) async { await m.createAll(); }, + onUpgrade: (Migrator m, int from, int to) async { + if (from < 2) { + await m.createTable(rooms); + await m.createTable(tasks); + await m.createTable(taskCompletions); + } + }, + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON'); + }, + ); +} +``` + +From lib/features/tasks/data/tasks_dao.dart (existing methods): +```dart +class TasksDao extends DatabaseAccessor with _$TasksDaoMixin { + Stream> watchTasksInRoom(int roomId); + Future insertTask(TasksCompanion task); + Future updateTask(Task task); + Future deleteTask(int taskId); // hard delete with cascade + Future completeTask(int taskId, {DateTime? now}); + Stream> watchCompletionsForTask(int taskId); + Future getOverdueTaskCount(int roomId, {DateTime? today}); +} +``` + +From lib/features/home/data/calendar_dao.dart (6 queries needing filter): +```dart +class CalendarDao extends DatabaseAccessor with _$CalendarDaoMixin { + Stream> watchTasksForDate(DateTime date); + Stream> watchTasksForDateInRoom(DateTime date, int roomId); + Stream> watchOverdueTasks(DateTime referenceDate); + Stream> watchOverdueTasksInRoom(DateTime referenceDate, int roomId); + Future getTaskCount(); + Future getTaskCountInRoom(int roomId); +} +``` + +From lib/features/home/data/daily_plan_dao.dart (3 queries needing filter): +```dart +class DailyPlanDao extends DatabaseAccessor with _$DailyPlanDaoMixin { + Stream> watchAllTasksWithRoomName(); + Future getOverdueAndTodayTaskCount({DateTime? today}); + Future getOverdueTaskCount({DateTime? today}); +} +``` + +From lib/features/rooms/data/rooms_dao.dart (task query in watchRoomWithStats): +```dart +// Inside watchRoomWithStats: +final taskList = await (select(tasks) + ..where((t) => t.roomId.equals(room.id))) + .get(); +``` + +Test pattern from test/features/tasks/data/tasks_dao_test.dart: +```dart +late AppDatabase db; +late int roomId; +setUp(() async { + db = AppDatabase(NativeDatabase.memory()); + roomId = await db.roomsDao.insertRoom( + RoomsCompanion.insert(name: 'Kueche', iconName: 'kitchen'), + ); +}); +tearDown(() async { await db.close(); }); +``` + + + + + + + Task 1: Add isActive column, migration, and new DAO methods + + lib/core/database/database.dart, + lib/features/tasks/data/tasks_dao.dart, + test/features/tasks/data/tasks_dao_test.dart + + + - Test: softDeleteTask sets isActive to false (task remains in DB but isActive == false) + - Test: getCompletionCount returns 0 for task with no completions + - Test: getCompletionCount returns correct count for task with completions + - Test: watchTasksInRoom excludes tasks where isActive is false + - Test: getOverdueTaskCount excludes tasks where isActive is false + - Test: existing hard deleteTask still works (removes task and completions) + + +1. In database.dart Tasks table, add: `BoolColumn get isActive => boolean().withDefault(const Constant(true))();` + +2. Bump schemaVersion to 3. + +3. Update migration onUpgrade — add `from < 3` block: + ```dart + if (from < 3) { + await m.addColumn(tasks, tasks.isActive); + } + ``` + This uses Drift's addColumn which handles the ALTER TABLE and the default value for existing rows. + +4. In tasks_dao.dart, add isActive filter to watchTasksInRoom: + ```dart + ..where((t) => t.roomId.equals(roomId) & t.isActive.equals(true)) + ``` + +5. In tasks_dao.dart, add isActive filter to getOverdueTaskCount task query: + ```dart + ..where((t) => t.roomId.equals(roomId) & t.isActive.equals(true)) + ``` + +6. Add softDeleteTask method to TasksDao: + ```dart + Future softDeleteTask(int taskId) { + return (update(tasks)..where((t) => t.id.equals(taskId))) + .write(const TasksCompanion(isActive: Value(false))); + } + ``` + +7. Add getCompletionCount method to TasksDao: + ```dart + Future getCompletionCount(int taskId) async { + final count = taskCompletions.id.count(); + final query = selectOnly(taskCompletions) + ..addColumns([count]) + ..where(taskCompletions.taskId.equals(taskId)); + final result = await query.getSingle(); + return result.read(count) ?? 0; + } + ``` + +8. Run `dart run build_runner build --delete-conflicting-outputs` to regenerate Drift code. + +9. Write tests in tasks_dao_test.dart following existing test patterns (NativeDatabase.memory, setUp/tearDown). + + + cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/tasks/data/tasks_dao_test.dart --reporter compact + + + - Tasks table has isActive BoolColumn with default true + - Schema version is 3 with working migration + - softDeleteTask sets isActive=false without removing data + - getCompletionCount returns accurate count + - watchTasksInRoom only returns active tasks + - getOverdueTaskCount only counts active tasks + - All new tests pass, all existing tests pass + + + + + Task 2: Add isActive filters to CalendarDao, DailyPlanDao, and RoomsDao + + lib/features/home/data/calendar_dao.dart, + lib/features/home/data/daily_plan_dao.dart, + lib/features/rooms/data/rooms_dao.dart + + +1. In calendar_dao.dart, add `& tasks.isActive.equals(true)` to the WHERE clause of ALL 6 query methods: + - watchTasksForDate: add to existing `query.where(...)` expression + - watchTasksForDateInRoom: add to existing `query.where(...)` expression + - watchOverdueTasks: add to existing `query.where(...)` expression + - watchOverdueTasksInRoom: add to existing `query.where(...)` expression + - getTaskCount: add `..where(tasks.isActive.equals(true))` to selectOnly + - getTaskCountInRoom: add `& tasks.isActive.equals(true)` to existing where + +2. In daily_plan_dao.dart, add isActive filter to all 3 query methods: + - watchAllTasksWithRoomName: add `query.where(tasks.isActive.equals(true));` after the join + - getOverdueAndTodayTaskCount: add `& tasks.isActive.equals(true)` to existing where + - getOverdueTaskCount: add `& tasks.isActive.equals(true)` to existing where + +3. In rooms_dao.dart watchRoomWithStats method, filter the task query to active-only: + ```dart + final taskList = await (select(tasks) + ..where((t) => t.roomId.equals(room.id) & t.isActive.equals(true))) + .get(); + ``` + +4. Run `dart run build_runner build --delete-conflicting-outputs` to regenerate if needed. + +5. Run `dart analyze` to confirm no issues. + + + cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test --reporter compact && dart analyze --fatal-infos + + + - All 6 CalendarDao queries filter by isActive=true + - All 3 DailyPlanDao queries filter by isActive=true + - RoomsDao watchRoomWithStats only counts active tasks + - All 137+ existing tests still pass + - dart analyze reports zero issues + + + + + + +- Schema version is 3, migration adds isActive column with default true +- softDeleteTask and getCompletionCount methods exist on TasksDao +- Every query across TasksDao, CalendarDao, DailyPlanDao, and RoomsDao that returns tasks filters by isActive=true +- Hard deleteTask (cascade) still works unchanged +- All tests pass: `flutter test --reporter compact` +- Code quality: `dart analyze --fatal-infos` reports zero issues + + + +- Deactivated tasks (isActive=false) are excluded from ALL active views: calendar day tasks, overdue tasks, room task lists, daily plan, room stats +- Existing tasks default to active after schema migration +- New DAO methods (softDeleteTask, getCompletionCount) are available for the UI layer +- All 137+ tests pass, new DAO tests pass + + + +After completion, create `.planning/phases/08-task-delete/08-01-SUMMARY.md` + diff --git a/.planning/phases/08-task-delete/08-02-PLAN.md b/.planning/phases/08-task-delete/08-02-PLAN.md new file mode 100644 index 0000000..3c13b14 --- /dev/null +++ b/.planning/phases/08-task-delete/08-02-PLAN.md @@ -0,0 +1,290 @@ +--- +phase: 08-task-delete +plan: 02 +type: execute +wave: 2 +depends_on: ["08-01"] +files_modified: + - lib/features/tasks/presentation/task_providers.dart + - lib/features/tasks/presentation/task_form_screen.dart +autonomous: true +requirements: [DEL-01, DEL-04] + +must_haves: + truths: + - "User sees a red delete button at the bottom of the task edit form" + - "Tapping delete shows a confirmation dialog before any action" + - "Confirming delete on a task with no completions removes it from the database" + - "Confirming delete on a task with completions deactivates it (hidden from views)" + - "After deletion the user is navigated back to the room task list" + artifacts: + - path: "lib/features/tasks/presentation/task_form_screen.dart" + provides: "Delete button and confirmation dialog in edit mode" + contains: "taskDeleteConfirmTitle" + - path: "lib/features/tasks/presentation/task_providers.dart" + provides: "Smart delete method using getCompletionCount" + contains: "softDeleteTask" + key_links: + - from: "lib/features/tasks/presentation/task_form_screen.dart" + to: "lib/features/tasks/presentation/task_providers.dart" + via: "TaskActions.smartDeleteTask call from delete button callback" + pattern: "smartDeleteTask" + - from: "lib/features/tasks/presentation/task_providers.dart" + to: "lib/features/tasks/data/tasks_dao.dart" + via: "getCompletionCount + conditional deleteTask or softDeleteTask" + pattern: "getCompletionCount.*softDeleteTask|deleteTask" +--- + + +Add the delete button and confirmation dialog to the task edit form, with smart delete logic in the provider layer. + +Purpose: Users can remove tasks they no longer need. The smart behavior (hard vs soft delete) is invisible to the user -- they just see "delete" with a confirmation. + +Output: Working delete flow on the task edit form: red button -> confirmation dialog -> smart delete -> navigate back. + + + +@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md +@/home/jlmak/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/08-task-delete/08-CONTEXT.md +@.planning/phases/08-task-delete/08-01-SUMMARY.md + + + + +From lib/features/tasks/presentation/task_providers.dart (existing TaskActions): +```dart +@riverpod +class TaskActions extends _$TaskActions { + @override + FutureOr build() {} + + Future createTask({...}) async { ... } + Future updateTask(Task task) async { ... } + Future deleteTask(int taskId) async { ... } // calls DAO hard delete + Future completeTask(int taskId) async { ... } +} +``` + +From lib/features/tasks/data/tasks_dao.dart (after Plan 01): +```dart +class TasksDao { + Future deleteTask(int taskId); // hard delete (cascade) + Future softDeleteTask(int taskId); // sets isActive = false + Future getCompletionCount(int taskId); // count completions +} +``` + +From lib/features/tasks/presentation/task_form_screen.dart (edit mode section): +```dart +// History section (edit mode only) — delete button goes AFTER this +if (widget.isEditing) ...[ + 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!, + ), + ), +], +``` + +From lib/l10n/app_de.arb (existing delete l10n strings): +```json +"taskDeleteConfirmTitle": "Aufgabe l\u00f6schen?", +"taskDeleteConfirmMessage": "Die Aufgabe wird unwiderruflich gel\u00f6scht.", +"taskDeleteConfirmAction": "L\u00f6schen" +``` + +Room delete dialog pattern (from lib/features/rooms/presentation/rooms_screen.dart:165-189): +```dart +showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(l10n.roomDeleteConfirmTitle), + content: Text(l10n.roomDeleteConfirmMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text(l10n.cancel), + ), + FilledButton( + style: FilledButton.styleFrom( + backgroundColor: Theme.of(ctx).colorScheme.error, + ), + onPressed: () { ... }, + child: Text(l10n.roomDeleteConfirmAction), + ), + ], + ), +); +``` + + + + + + + Task 1: Add smartDeleteTask to TaskActions provider + lib/features/tasks/presentation/task_providers.dart + +Add a `smartDeleteTask` method to the `TaskActions` class in task_providers.dart. This method checks the completion count and routes to hard delete or soft delete accordingly: + +```dart +/// Smart delete: hard-deletes tasks with no completions, soft-deletes tasks with completions. +Future smartDeleteTask(int taskId) async { + final db = ref.read(appDatabaseProvider); + final completionCount = await db.tasksDao.getCompletionCount(taskId); + if (completionCount == 0) { + await db.tasksDao.deleteTask(taskId); + } else { + await db.tasksDao.softDeleteTask(taskId); + } +} +``` + +Keep the existing `deleteTask` method unchanged (it is still a valid hard delete for other uses like room cascade delete). + +Run `dart run build_runner build --delete-conflicting-outputs` to regenerate the provider code. + + + cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && dart analyze --fatal-infos lib/features/tasks/presentation/task_providers.dart + + + - smartDeleteTask method exists on TaskActions + - Method checks completion count and routes to hard or soft delete + - dart analyze passes with zero issues + + + + + Task 2: Add delete button and confirmation dialog to TaskFormScreen + lib/features/tasks/presentation/task_form_screen.dart + +1. In the TaskFormScreen build method's ListView children, AFTER the history section (the existing `if (widget.isEditing) ...` block ending at line ~204), add the delete button section inside the same `if (widget.isEditing)` block: + +```dart +if (widget.isEditing) ...[ + const SizedBox(height: 24), + const Divider(), + // History ListTile (existing) + ListTile( + leading: const Icon(Icons.history), + title: Text(l10n.taskHistoryTitle), + trailing: const Icon(Icons.chevron_right), + onTap: () => showTaskHistorySheet( + context: context, + taskId: widget.taskId!, + ), + ), + // DELETE BUTTON — new + const Divider(), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + style: FilledButton.styleFrom( + backgroundColor: theme.colorScheme.error, + foregroundColor: theme.colorScheme.onError, + ), + onPressed: _isLoading ? null : _onDelete, + icon: const Icon(Icons.delete_outline), + label: Text(l10n.taskDeleteConfirmAction), + ), + ), +], +``` + +2. Add a `_onDelete` method to _TaskFormScreenState: + +```dart +Future _onDelete() async { + final l10n = AppLocalizations.of(context); + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(l10n.taskDeleteConfirmTitle), + content: Text(l10n.taskDeleteConfirmMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text(l10n.cancel), + ), + FilledButton( + style: FilledButton.styleFrom( + backgroundColor: Theme.of(ctx).colorScheme.error, + ), + onPressed: () => Navigator.pop(ctx, true), + child: Text(l10n.taskDeleteConfirmAction), + ), + ], + ), + ); + + if (confirmed != true || !mounted) return; + + setState(() => _isLoading = true); + try { + await ref.read(taskActionsProvider.notifier).smartDeleteTask(widget.taskId!); + if (mounted) { + context.pop(); + } + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } +} +``` + +Note: The `l10n.cancel` string should already exist from the room delete dialog. If not, use `MaterialLocalizations.of(context).cancelButtonLabel`. + +3. Verify `cancel` l10n key exists. If it does not exist in app_de.arb, check for the existing cancel button pattern in rooms_screen.dart and use the same approach. + + + cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test --reporter compact && dart analyze --fatal-infos + + + - Red delete button visible at bottom of task edit form (below history, separated by divider) + - Delete button only shows in edit mode (not create mode) + - Tapping delete shows AlertDialog with title "Aufgabe loschen?" and error-colored confirm button + - Canceling dialog does nothing + - Confirming dialog calls smartDeleteTask and pops back to room task list + - Button is disabled while loading (_isLoading) + - All existing tests pass, dart analyze clean + + + + + + +- Task edit form shows red delete button below history section with divider separator +- Delete button is NOT shown in create mode +- Tapping delete shows confirmation dialog matching room delete dialog pattern +- Confirming deletes/deactivates the task and navigates back +- Canceling returns to the form without changes +- All tests pass: `flutter test --reporter compact` +- Code quality: `dart analyze --fatal-infos` reports zero issues + + + +- Complete delete flow works: open task -> scroll to bottom -> tap delete -> confirm -> back to room task list +- Smart delete is invisible to user: tasks with completions are deactivated, tasks without are removed +- Delete button follows Material 3 error color pattern +- Confirmation dialog uses existing German l10n strings +- All 137+ tests pass + + + +After completion, create `.planning/phases/08-task-delete/08-02-SUMMARY.md` +