docs(08-task-delete): create phase plan
This commit is contained in:
@@ -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
|
**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)
|
**Depends on**: Phase 7 (v1.1 shipped — calendar, history, and sorting all in place)
|
||||||
**Requirements**: DEL-01, DEL-02, DEL-03, DEL-04
|
**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):
|
**Success Criteria** (what must be TRUE):
|
||||||
1. The task edit form has a clearly visible delete action (button or icon)
|
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
|
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 |
|
| 5. Calendar Strip | v1.1 | 2/2 | Complete | 2026-03-16 |
|
||||||
| 6. Task History | v1.1 | 1/1 | 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 |
|
| 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 | - |
|
| 9. Task Creation UX | v1.2 | - | Planned | - |
|
||||||
| 10. Dead Code Cleanup | v1.2 | - | Planned | - |
|
| 10. Dead Code Cleanup | v1.2 | - | Planned | - |
|
||||||
|
|||||||
310
.planning/phases/08-task-delete/08-01-PLAN.md
Normal file
310
.planning/phases/08-task-delete/08-01-PLAN.md
Normal file
@@ -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"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
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.
|
||||||
|
</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/08-task-delete/08-CONTEXT.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||||
|
|
||||||
|
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<IntervalType>()();
|
||||||
|
IntColumn get intervalDays => integer().withDefault(const Constant(1))();
|
||||||
|
IntColumn get anchorDay => integer().nullable()();
|
||||||
|
IntColumn get effortLevel => intEnum<EffortLevel>()();
|
||||||
|
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<AppDatabase> with _$TasksDaoMixin {
|
||||||
|
Stream<List<Task>> watchTasksInRoom(int roomId);
|
||||||
|
Future<int> insertTask(TasksCompanion task);
|
||||||
|
Future<bool> updateTask(Task task);
|
||||||
|
Future<void> deleteTask(int taskId); // hard delete with cascade
|
||||||
|
Future<void> completeTask(int taskId, {DateTime? now});
|
||||||
|
Stream<List<TaskCompletion>> watchCompletionsForTask(int taskId);
|
||||||
|
Future<int> getOverdueTaskCount(int roomId, {DateTime? today});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From lib/features/home/data/calendar_dao.dart (6 queries needing filter):
|
||||||
|
```dart
|
||||||
|
class CalendarDao extends DatabaseAccessor<AppDatabase> with _$CalendarDaoMixin {
|
||||||
|
Stream<List<TaskWithRoom>> watchTasksForDate(DateTime date);
|
||||||
|
Stream<List<TaskWithRoom>> watchTasksForDateInRoom(DateTime date, int roomId);
|
||||||
|
Stream<List<TaskWithRoom>> watchOverdueTasks(DateTime referenceDate);
|
||||||
|
Stream<List<TaskWithRoom>> watchOverdueTasksInRoom(DateTime referenceDate, int roomId);
|
||||||
|
Future<int> getTaskCount();
|
||||||
|
Future<int> getTaskCountInRoom(int roomId);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From lib/features/home/data/daily_plan_dao.dart (3 queries needing filter):
|
||||||
|
```dart
|
||||||
|
class DailyPlanDao extends DatabaseAccessor<AppDatabase> with _$DailyPlanDaoMixin {
|
||||||
|
Stream<List<TaskWithRoom>> watchAllTasksWithRoomName();
|
||||||
|
Future<int> getOverdueAndTodayTaskCount({DateTime? today});
|
||||||
|
Future<int> 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(); });
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Add isActive column, migration, and new DAO methods</name>
|
||||||
|
<files>
|
||||||
|
lib/core/database/database.dart,
|
||||||
|
lib/features/tasks/data/tasks_dao.dart,
|
||||||
|
test/features/tasks/data/tasks_dao_test.dart
|
||||||
|
</files>
|
||||||
|
<behavior>
|
||||||
|
- 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)
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
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<void> 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<int> 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).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/tasks/data/tasks_dao_test.dart --reporter compact</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- 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
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Add isActive filters to CalendarDao, DailyPlanDao, and RoomsDao</name>
|
||||||
|
<files>
|
||||||
|
lib/features/home/data/calendar_dao.dart,
|
||||||
|
lib/features/home/data/daily_plan_dao.dart,
|
||||||
|
lib/features/rooms/data/rooms_dao.dart
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test --reporter compact && dart analyze --fatal-infos</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- 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
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- 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
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- 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
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/08-task-delete/08-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
290
.planning/phases/08-task-delete/08-02-PLAN.md
Normal file
290
.planning/phases/08-task-delete/08-02-PLAN.md
Normal file
@@ -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"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
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.
|
||||||
|
</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/08-task-delete/08-CONTEXT.md
|
||||||
|
@.planning/phases/08-task-delete/08-01-SUMMARY.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key types and contracts the executor needs. -->
|
||||||
|
|
||||||
|
From lib/features/tasks/presentation/task_providers.dart (existing TaskActions):
|
||||||
|
```dart
|
||||||
|
@riverpod
|
||||||
|
class TaskActions extends _$TaskActions {
|
||||||
|
@override
|
||||||
|
FutureOr<void> build() {}
|
||||||
|
|
||||||
|
Future<int> createTask({...}) async { ... }
|
||||||
|
Future<void> updateTask(Task task) async { ... }
|
||||||
|
Future<void> deleteTask(int taskId) async { ... } // calls DAO hard delete
|
||||||
|
Future<void> completeTask(int taskId) async { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From lib/features/tasks/data/tasks_dao.dart (after Plan 01):
|
||||||
|
```dart
|
||||||
|
class TasksDao {
|
||||||
|
Future<void> deleteTask(int taskId); // hard delete (cascade)
|
||||||
|
Future<void> softDeleteTask(int taskId); // sets isActive = false
|
||||||
|
Future<int> 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add smartDeleteTask to TaskActions provider</name>
|
||||||
|
<files>lib/features/tasks/presentation/task_providers.dart</files>
|
||||||
|
<action>
|
||||||
|
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<void> 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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && dart analyze --fatal-infos lib/features/tasks/presentation/task_providers.dart</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- smartDeleteTask method exists on TaskActions
|
||||||
|
- Method checks completion count and routes to hard or soft delete
|
||||||
|
- dart analyze passes with zero issues
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Add delete button and confirmation dialog to TaskFormScreen</name>
|
||||||
|
<files>lib/features/tasks/presentation/task_form_screen.dart</files>
|
||||||
|
<action>
|
||||||
|
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<void> _onDelete() async {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test --reporter compact && dart analyze --fatal-infos</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- 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
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- 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
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- 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
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/08-task-delete/08-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user