chore: archive v1.2 phase directories to milestones/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
310
.planning/milestones/v1.2-phases/08-task-delete/08-01-PLAN.md
Normal file
310
.planning/milestones/v1.2-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>
|
||||
152
.planning/milestones/v1.2-phases/08-task-delete/08-01-SUMMARY.md
Normal file
152
.planning/milestones/v1.2-phases/08-task-delete/08-01-SUMMARY.md
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
phase: 08-task-delete
|
||||
plan: 01
|
||||
subsystem: database
|
||||
tags: [drift, sqlite, flutter, soft-delete, schema-migration]
|
||||
|
||||
# Dependency graph
|
||||
requires: []
|
||||
provides:
|
||||
- isActive BoolColumn on Tasks table (schema v3)
|
||||
- softDeleteTask(taskId) method on TasksDao
|
||||
- getCompletionCount(taskId) method on TasksDao
|
||||
- isActive=true filter on all task queries across all 4 DAOs
|
||||
- Schema migration from v2 to v3 (addColumn)
|
||||
affects: [08-02, 08-03, delete-dialog, task-providers]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Soft delete via isActive BoolColumn with default true"
|
||||
- "All task-returning DAO queries filter by isActive=true"
|
||||
- "Schema versioning via Drift addColumn migration"
|
||||
- "TDD: RED commit before implementation GREEN commit"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- drift_schemas/household_keeper/drift_schema_v3.json
|
||||
- test/drift/household_keeper/generated/schema_v3.dart
|
||||
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
|
||||
- test/drift/household_keeper/migration_test.dart
|
||||
- test/drift/household_keeper/generated/schema.dart
|
||||
- test/core/database/database_test.dart
|
||||
- test/features/home/presentation/home_screen_test.dart
|
||||
- test/features/tasks/presentation/task_list_screen_test.dart
|
||||
|
||||
key-decisions:
|
||||
- "isActive column uses BoolColumn.withDefault(true) so existing rows are automatically active after migration"
|
||||
- "Migration uses from==2 (not from<3) for addColumn to avoid duplicate-column error when upgrading from v1 (where createTable already includes isActive)"
|
||||
- "Migration tests updated to only test paths ending at v3 (current schemaVersion) since AppDatabase always migrates to its schemaVersion"
|
||||
|
||||
patterns-established:
|
||||
- "Soft-delete pattern: isActive BoolColumn with default true, filter all queries by isActive=true"
|
||||
- "Hard-delete remains via deleteTask(id) which cascades to completions"
|
||||
|
||||
requirements-completed: [DEL-02, DEL-03]
|
||||
|
||||
# Metrics
|
||||
duration: 9min
|
||||
completed: 2026-03-18
|
||||
---
|
||||
|
||||
# Phase 8 Plan 01: isActive Column and DAO Filtering Summary
|
||||
|
||||
**Drift schema v3 with isActive BoolColumn on Tasks, soft-delete DAO methods, and isActive=true filter applied to all 15 task queries across TasksDao, CalendarDao, DailyPlanDao, and RoomsDao**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 9 min
|
||||
- **Started:** 2026-03-18T19:47:32Z
|
||||
- **Completed:** 2026-03-18T19:56:39Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 11
|
||||
|
||||
## Accomplishments
|
||||
- Added `isActive BoolColumn` (default `true`) to Tasks table with schema v3 migration
|
||||
- Added `softDeleteTask(taskId)` and `getCompletionCount(taskId)` to TasksDao
|
||||
- Applied `isActive=true` filter to all task-returning queries across all 4 DAOs (15 total query sites)
|
||||
- 6 new tests passing (softDeleteTask, getCompletionCount, watchTasksInRoom filtering, getOverdueTaskCount filtering, hard deleteTask still works)
|
||||
- All 144 tests pass, dart analyze reports zero issues
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **TDD RED: Failing tests** - `a2cef91` (test)
|
||||
2. **Task 1: Add isActive column, migration v3, softDeleteTask and getCompletionCount** - `4b51f5f` (feat)
|
||||
3. **Task 2: Add isActive filters to CalendarDao, DailyPlanDao, RoomsDao** - `b2f14dc` (feat)
|
||||
|
||||
## Files Created/Modified
|
||||
- `lib/core/database/database.dart` - Added isActive BoolColumn to Tasks, bumped schemaVersion to 3, added from==2 migration
|
||||
- `lib/features/tasks/data/tasks_dao.dart` - Added isActive filter to watchTasksInRoom and getOverdueTaskCount, added softDeleteTask and getCompletionCount methods
|
||||
- `lib/features/home/data/calendar_dao.dart` - Added isActive=true filter to all 6 query methods
|
||||
- `lib/features/home/data/daily_plan_dao.dart` - Added isActive=true filter to all 3 query methods
|
||||
- `lib/features/rooms/data/rooms_dao.dart` - Added isActive=true filter to watchRoomWithStats task query
|
||||
- `test/features/tasks/data/tasks_dao_test.dart` - Added 6 new tests for soft-delete behavior
|
||||
- `test/drift/household_keeper/migration_test.dart` - Updated to test v1→v3 and v2→v3 migrations
|
||||
- `test/drift/household_keeper/generated/schema_v3.dart` - Generated schema snapshot for v3
|
||||
- `test/drift/household_keeper/generated/schema.dart` - Updated to include v3 in versions list
|
||||
- `drift_schemas/household_keeper/drift_schema_v3.json` - v3 schema JSON for Drift migration tooling
|
||||
- `test/core/database/database_test.dart` - Updated schemaVersion assertion from 2 to 3
|
||||
- `test/features/home/presentation/home_screen_test.dart` - Added isActive: true to Task constructor helper
|
||||
- `test/features/tasks/presentation/task_list_screen_test.dart` - Added isActive: true to Task constructor helper
|
||||
|
||||
## Decisions Made
|
||||
- `isActive` column uses `BoolColumn.withDefault(const Constant(true))` so all existing rows become active after migration without explicit data backfill
|
||||
- Migration uses `from == 2` (not `from < 3`) for `addColumn` to avoid duplicate-column error when upgrading from v1 where `createTable` already includes the isActive column in the current schema definition
|
||||
- Migration test framework updated to only test paths that end at the current schema version (v3), since `AppDatabase.schemaVersion = 3` means all migrations go to v3
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Fixed Task constructor calls missing isActive parameter in test helpers**
|
||||
- **Found during:** Task 2 (running full test suite)
|
||||
- **Issue:** After adding `isActive` as a required field on the generated `Task` dataclass, two test files with manual `Task(...)` constructors (`home_screen_test.dart`, `task_list_screen_test.dart`) failed to compile
|
||||
- **Fix:** Added `isActive: true` to `_makeTask` helper functions in both files
|
||||
- **Files modified:** `test/features/home/presentation/home_screen_test.dart`, `test/features/tasks/presentation/task_list_screen_test.dart`
|
||||
- **Verification:** flutter test passes, all 144 tests pass
|
||||
- **Committed in:** `b2f14dc` (Task 2 commit)
|
||||
|
||||
**2. [Rule 1 - Bug] Fixed schemaVersion assertion in database_test.dart**
|
||||
- **Found during:** Task 2 (running full test suite)
|
||||
- **Issue:** `database_test.dart` had `expect(db.schemaVersion, equals(2))` which failed after bumping to v3
|
||||
- **Fix:** Updated assertion to `equals(3)` and renamed test to "has schemaVersion 3"
|
||||
- **Files modified:** `test/core/database/database_test.dart`
|
||||
- **Verification:** Test passes
|
||||
- **Committed in:** `b2f14dc` (Task 2 commit)
|
||||
|
||||
**3. [Rule 1 - Bug] Fixed Drift migration tests for v3 schema**
|
||||
- **Found during:** Task 2 (running full test suite)
|
||||
- **Issue:** Migration tests tested v1→v2 migration, but AppDatabase.schemaVersion=3 causes all migrations to end at v3. Also, the `from < 3` addColumn migration caused a duplicate-column error when migrating from v1 (since createTable already includes isActive)
|
||||
- **Fix:** (a) Generated schema_v3.dart snapshot, (b) Updated migration_test.dart to test v1→v3 and v2→v3, (c) Changed migration to `from == 2` instead of `from < 3`
|
||||
- **Files modified:** `test/drift/household_keeper/migration_test.dart`, `test/drift/household_keeper/generated/schema_v3.dart`, `test/drift/household_keeper/generated/schema.dart`, `drift_schemas/household_keeper/drift_schema_v3.json`
|
||||
- **Verification:** All 3 migration tests pass
|
||||
- **Committed in:** `b2f14dc` (Task 2 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 3 auto-fixed (all Rule 1 bugs caused directly by schema change)
|
||||
**Impact on plan:** All fixes necessary for correctness. No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
- The Drift migration testing framework requires schema snapshots for each version. Adding schema v3 required regenerating schema files and fixing the migration test to only test paths to the current version.
|
||||
|
||||
## User Setup Required
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
- `isActive` column and `softDeleteTask`/`getCompletionCount` methods are ready for use by the UI layer (task delete dialog in plan 08-02)
|
||||
- All active views (calendar, room task list, daily plan, room stats) now correctly exclude soft-deleted tasks
|
||||
- Hard delete (deleteTask) remains unchanged and still cascades to completions
|
||||
|
||||
---
|
||||
*Phase: 08-task-delete*
|
||||
*Completed: 2026-03-18*
|
||||
290
.planning/milestones/v1.2-phases/08-task-delete/08-02-PLAN.md
Normal file
290
.planning/milestones/v1.2-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>
|
||||
117
.planning/milestones/v1.2-phases/08-task-delete/08-02-SUMMARY.md
Normal file
117
.planning/milestones/v1.2-phases/08-task-delete/08-02-SUMMARY.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
phase: 08-task-delete
|
||||
plan: 02
|
||||
subsystem: ui
|
||||
tags: [flutter, riverpod, drift, material3, l10n]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 08-task-delete plan 01
|
||||
provides: softDeleteTask and getCompletionCount on TasksDao, isActive column migration
|
||||
|
||||
provides:
|
||||
- smartDeleteTask on TaskActions provider (hard delete if 0 completions, soft delete otherwise)
|
||||
- Red delete button in task edit form with Material 3 error color
|
||||
- Confirmation AlertDialog using existing German l10n strings
|
||||
- Full delete flow: button -> dialog -> smart delete -> navigate back
|
||||
|
||||
affects: [task-form, task-providers, any future task management UI]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- Smart delete pattern: check completion count to decide hard vs soft delete
|
||||
- Delete confirmation dialog matching room delete pattern with error-colored FilledButton
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- lib/features/tasks/presentation/task_providers.dart
|
||||
- lib/features/tasks/presentation/task_form_screen.dart
|
||||
|
||||
key-decisions:
|
||||
- "smartDeleteTask kept separate from deleteTask to preserve existing hard-delete path for cascade/other uses"
|
||||
- "Delete button placed after history section with divider, visible only in edit mode"
|
||||
|
||||
patterns-established:
|
||||
- "Delete confirmation pattern: showDialog<bool> returning true/false, error-colored FilledButton for confirm"
|
||||
- "Smart delete pattern: getCompletionCount -> conditional hard/soft delete invisible to user"
|
||||
|
||||
requirements-completed: [DEL-01, DEL-04]
|
||||
|
||||
# Metrics
|
||||
duration: 2min
|
||||
completed: 2026-03-18
|
||||
---
|
||||
|
||||
# Phase 8 Plan 02: Task Delete UI Summary
|
||||
|
||||
**Red delete button with confirmation dialog in task edit form: hard-deletes unused tasks, soft-deletes tasks with completion history**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 2 min
|
||||
- **Started:** 2026-03-18T19:59:37Z
|
||||
- **Completed:** 2026-03-18T20:02:05Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 2
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Added `smartDeleteTask` to `TaskActions` Riverpod notifier — checks completion count and routes to DAO `deleteTask` (hard) or `softDeleteTask` (soft)
|
||||
- Added red `FilledButton.icon` with error colorScheme at bottom of task edit form, separated from history section by a `Divider`
|
||||
- Added `_onDelete` confirmation dialog using existing `taskDeleteConfirmTitle`, `taskDeleteConfirmMessage`, `taskDeleteConfirmAction`, and `cancel` l10n strings
|
||||
- All 144 tests pass, `dart analyze --fatal-infos` clean
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Add smartDeleteTask to TaskActions provider** - `1b1b981` (feat)
|
||||
2. **Task 2: Add delete button and confirmation dialog to TaskFormScreen** - `6133c97` (feat)
|
||||
|
||||
**Plan metadata:** _(docs commit follows)_
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `lib/features/tasks/presentation/task_providers.dart` - Added `smartDeleteTask` method to `TaskActions` class
|
||||
- `lib/features/tasks/presentation/task_providers.g.dart` - Regenerated by build_runner (no functional changes)
|
||||
- `lib/features/tasks/presentation/task_form_screen.dart` - Added delete button in edit mode and `_onDelete` async method
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- `smartDeleteTask` kept separate from `deleteTask` to preserve the existing hard-delete path used for room cascade deletes and any other callers
|
||||
- Delete button placed after history ListTile, inside the `if (widget.isEditing)` block, so it never appears in create mode
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- Complete delete flow is working: task edit form -> red delete button -> confirmation dialog -> smart delete -> pop back to room task list
|
||||
- Soft-deleted tasks (isActive=false) are already filtered from all views (implemented in Plan 01)
|
||||
- Phase 08-task-delete is fully complete
|
||||
|
||||
---
|
||||
*Phase: 08-task-delete*
|
||||
*Completed: 2026-03-18*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: lib/features/tasks/presentation/task_providers.dart
|
||||
- FOUND: lib/features/tasks/presentation/task_form_screen.dart
|
||||
- FOUND: .planning/phases/08-task-delete/08-02-SUMMARY.md
|
||||
- FOUND: commit 1b1b981 (smartDeleteTask)
|
||||
- FOUND: commit 6133c97 (delete button and dialog)
|
||||
- FOUND: smartDeleteTask in task_providers.dart
|
||||
- FOUND: taskDeleteConfirmTitle in task_form_screen.dart
|
||||
@@ -0,0 +1,94 @@
|
||||
# Phase 8: Task Delete - Context
|
||||
|
||||
**Gathered:** 2026-03-18
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Add a delete action to tasks with smart behavior: hard delete (remove from DB) if the task has never been completed, soft delete (deactivate, hide from views) if the task has been completed at least once. Preserves completion history for future statistics. No UI to view or restore archived tasks in this phase.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Delete placement
|
||||
- Red delete button at the bottom of the task edit form, below the history section, separated by a divider
|
||||
- Edit mode only — no delete button in create mode (user can just back out)
|
||||
- No swipe-to-delete, no long-press context menu, no AppBar icon — form only
|
||||
- Deliberate action: user must open the task, scroll down, and tap delete
|
||||
|
||||
### Confirmation dialog
|
||||
- Single generic "Aufgabe löschen?" confirmation — same message for both hard and soft delete
|
||||
- User does not need to know the implementation difference (permanent vs deactivated)
|
||||
- Follow existing room delete dialog pattern: TextButton cancel + FilledButton with error color
|
||||
- Existing l10n strings (taskDeleteConfirmTitle, taskDeleteConfirmMessage, taskDeleteConfirmAction) already defined
|
||||
|
||||
### Delete behavior
|
||||
- Check task_completions count before deleting
|
||||
- 0 completions → hard delete: remove task and completions from DB (existing deleteTask DAO method)
|
||||
- 1+ completions → soft delete: set isActive = false on the task, task hidden from all active views
|
||||
- Need new `isActive` BoolColumn on Tasks table with default true + schema migration
|
||||
|
||||
### Post-delete navigation
|
||||
- Pop back to the room task list (same as save behavior)
|
||||
- Reactive providers will auto-update to reflect the deleted/deactivated task
|
||||
|
||||
### Archived task visibility
|
||||
- Soft-deleted tasks are completely hidden from all views — no toggle, no restore UI
|
||||
- Archived tasks preserved in DB purely for future statistics phase
|
||||
- No need to build any "show archived" UI in this phase
|
||||
|
||||
### Claude's Discretion
|
||||
- Migration version number and strategy
|
||||
- Exact button styling (consistent with Material 3 error patterns)
|
||||
- Whether to add a SnackBar confirmation after delete or just navigate back silently
|
||||
|
||||
</decisions>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- User explicitly wants simplicity: "just have a delete function keep it simple"
|
||||
- The smart hard/soft behavior is invisible to the user — they just see "delete"
|
||||
- Keep the flow dead simple: open task → scroll to bottom → tap delete → confirm → back to list
|
||||
|
||||
</specifics>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `TasksDao.deleteTask(taskId)`: Already implements hard delete with cascade (completions first, then task)
|
||||
- `TaskActionsNotifier.deleteTask(taskId)`: Provider method exists, calls DAO
|
||||
- Room delete confirmation dialog (`rooms_screen.dart:160-189`): AlertDialog pattern with error-colored FilledButton
|
||||
- German l10n strings already defined: taskDeleteConfirmTitle, taskDeleteConfirmMessage, taskDeleteConfirmAction
|
||||
|
||||
### Established Patterns
|
||||
- Confirmation dialogs: AlertDialog with TextButton cancel + error FilledButton
|
||||
- DAO transactions for cascade deletes
|
||||
- ConsumerStatefulWidget for screens with async callbacks
|
||||
- Schema migrations in database.dart with version tracking
|
||||
|
||||
### Integration Points
|
||||
- `task_form_screen.dart`: Add delete button after history ListTile (edit mode only)
|
||||
- `tasks_dao.dart`: Add softDeleteTask method (UPDATE isActive = false) alongside existing hard deleteTask
|
||||
- `calendar_dao.dart`: 6 queries need WHERE isActive = true filter
|
||||
- `tasks_dao.dart`: watchTasksInRoom needs WHERE isActive = true filter
|
||||
- `database.dart`: Add isActive BoolColumn to Tasks table + migration
|
||||
- All existing tasks must default to isActive = true in migration
|
||||
|
||||
</code_context>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
None — discussion stayed within phase scope
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 08-task-delete*
|
||||
*Context gathered: 2026-03-18*
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
phase: 08-task-delete
|
||||
verified: 2026-03-18T20:30:00Z
|
||||
status: passed
|
||||
score: 9/9 must-haves verified
|
||||
re_verification: false
|
||||
---
|
||||
|
||||
# Phase 8: Task Delete Verification Report
|
||||
|
||||
**Phase Goal:** Users can remove tasks they no longer need, with smart preservation of completion history for future statistics
|
||||
**Verified:** 2026-03-18T20:30:00Z
|
||||
**Status:** passed
|
||||
**Re-verification:** No — initial verification
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 1 | Active tasks appear in all views (calendar, room task lists, daily plan) | VERIFIED | All 4 DAOs filter `isActive=true`; 15 query sites confirmed across `tasks_dao.dart`, `calendar_dao.dart`, `daily_plan_dao.dart`, `rooms_dao.dart` |
|
||||
| 2 | Deactivated tasks are hidden from all views | VERIFIED | `isActive.equals(true)` present on all 6 CalendarDao queries, all 3 DailyPlanDao queries, `watchTasksInRoom`, `getOverdueTaskCount`, and `watchRoomWithStats` |
|
||||
| 3 | Hard delete removes task and completions from DB entirely | VERIFIED | `deleteTask` in `tasks_dao.dart` uses a transaction to delete completions then task; test "hard deleteTask still removes task and its completions" passes |
|
||||
| 4 | Soft delete sets isActive to false without removing data | VERIFIED | `softDeleteTask` updates `isActive: Value(false)` only; test "softDeleteTask sets isActive to false without removing the task" passes — row count stays 1, `isActive == false` |
|
||||
| 5 | Existing tasks default to active after migration | VERIFIED | `BoolColumn.withDefault(const Constant(true))` on Tasks table; `from == 2` migration block calls `m.addColumn(tasks, tasks.isActive)` which applies the default to existing rows |
|
||||
| 6 | User sees a red delete button at the bottom of the task edit form | VERIFIED | `FilledButton.icon` with `backgroundColor: theme.colorScheme.error` inside `if (widget.isEditing)` block in `task_form_screen.dart` lines 207-218 |
|
||||
| 7 | Tapping delete shows a confirmation dialog before any action | VERIFIED | `_onDelete()` calls `showDialog<bool>` with `AlertDialog` containing title `l10n.taskDeleteConfirmTitle`, message `l10n.taskDeleteConfirmMessage`, cancel TextButton, and error-colored confirm FilledButton |
|
||||
| 8 | Confirming delete routes to hard or soft delete based on completion history | VERIFIED | `smartDeleteTask` in `task_providers.dart` calls `getCompletionCount` and branches: `deleteTask` if 0, `softDeleteTask` if >0 |
|
||||
| 9 | After deletion the user is navigated back to the room task list | VERIFIED | `_onDelete()` calls `context.pop()` after awaiting `smartDeleteTask`; guarded by `if (mounted)` check |
|
||||
|
||||
**Score:** 9/9 truths verified
|
||||
|
||||
---
|
||||
|
||||
### Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `lib/core/database/database.dart` | `isActive` BoolColumn, schema v3, migration | VERIFIED | Line 38-39: `BoolColumn get isActive => boolean().withDefault(const Constant(true))();`; `schemaVersion => 3`; `from == 2` block calls `m.addColumn(tasks, tasks.isActive)` |
|
||||
| `lib/features/tasks/data/tasks_dao.dart` | `softDeleteTask`, `getCompletionCount`, `isActive` filter on queries | VERIFIED | Both methods present (lines 115-128); `watchTasksInRoom` and `getOverdueTaskCount` filter by `isActive.equals(true)` |
|
||||
| `lib/features/home/data/calendar_dao.dart` | `isActive=true` filter on all 6 queries | VERIFIED | All 6 methods confirmed: `watchTasksForDate` (l32), `getTaskCount` (l56), `watchTasksForDateInRoom` (l76), `watchOverdueTasks` (l105), `watchOverdueTasksInRoom` (l139), `getTaskCountInRoom` (l164) |
|
||||
| `lib/features/home/data/daily_plan_dao.dart` | `isActive=true` filter on all 3 queries | VERIFIED | `watchAllTasksWithRoomName` (l20), `getOverdueAndTodayTaskCount` (l44), `getOverdueTaskCount` (l57) — all confirmed |
|
||||
| `lib/features/rooms/data/rooms_dao.dart` | `isActive=true` filter in `watchRoomWithStats` task query | VERIFIED | Line 47-49: `t.roomId.equals(room.id) & t.isActive.equals(true)` |
|
||||
| `lib/features/tasks/presentation/task_providers.dart` | `smartDeleteTask` method using `getCompletionCount` | VERIFIED | Lines 94-102: method exists, branches on completion count to `deleteTask` or `softDeleteTask` |
|
||||
| `lib/features/tasks/presentation/task_form_screen.dart` | Delete button and confirmation dialog in edit mode; uses `taskDeleteConfirmTitle` | VERIFIED | Lines 204-218 (button), lines 471-507 (`_onDelete` method); `taskDeleteConfirmTitle` at line 476 |
|
||||
| `test/features/tasks/data/tasks_dao_test.dart` | Tests for `softDeleteTask`, `getCompletionCount`, `isActive` filtering | VERIFIED | 6 new tests present and all 13 tests in file pass |
|
||||
|
||||
---
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `lib/core/database/database.dart` | All DAOs | `BoolColumn get isActive` on Tasks table | WIRED | 59 occurrences of `isActive` across 6 DAO/schema files |
|
||||
| `lib/features/tasks/data/tasks_dao.dart` | `lib/features/tasks/presentation/task_providers.dart` | `softDeleteTask` and `getCompletionCount` | WIRED | `task_providers.dart` calls `db.tasksDao.getCompletionCount(taskId)` and `db.tasksDao.softDeleteTask(taskId)` at lines 96-100 |
|
||||
| `lib/features/tasks/presentation/task_form_screen.dart` | `lib/features/tasks/presentation/task_providers.dart` | `smartDeleteTask` call from `_onDelete` | WIRED | `ref.read(taskActionsProvider.notifier).smartDeleteTask(widget.taskId!)` at line 498 |
|
||||
| `lib/features/tasks/presentation/task_providers.dart` | `lib/features/tasks/data/tasks_dao.dart` | `getCompletionCount` then conditional `deleteTask` or `softDeleteTask` | WIRED | `smartDeleteTask` method at lines 94-102 confirmed complete — reads count, branches, calls DAO |
|
||||
|
||||
---
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
|-------------|-------------|-------------|--------|----------|
|
||||
| DEL-01 | 08-02-PLAN.md | User can delete a task from the task edit form via a clearly visible delete action | SATISFIED | Red `FilledButton.icon` with `Icons.delete_outline` at bottom of edit form, gated on `widget.isEditing` |
|
||||
| DEL-02 | 08-01-PLAN.md | Hard delete removes task with no completions from DB entirely | SATISFIED | `deleteTask` cascade + `smartDeleteTask` routes to it when `getCompletionCount == 0` |
|
||||
| DEL-03 | 08-01-PLAN.md | Deleting a task with completions deactivates it (soft delete) | SATISFIED | `softDeleteTask` sets `isActive=false`; all DAO queries filter by `isActive=true` so task disappears from views |
|
||||
| DEL-04 | 08-02-PLAN.md | User sees a confirmation before deleting/deactivating | SATISFIED | `_onDelete` shows `AlertDialog` with cancel and confirm actions; action only proceeds when `confirmed == true` |
|
||||
|
||||
All 4 requirements from REQUIREMENTS.md Phase 8 are SATISFIED. No orphaned requirements found.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Patterns Found
|
||||
|
||||
None. No TODOs, FIXMEs, placeholder returns, or stub implementations found in any modified files.
|
||||
|
||||
---
|
||||
|
||||
### Human Verification Required
|
||||
|
||||
#### 1. Delete button visual appearance
|
||||
|
||||
**Test:** Open the app, navigate to a room, tap any task to open the edit form, scroll to the bottom.
|
||||
**Expected:** A full-width red button labeled "Loschen" (with umlaut) appears below the history row, separated by a divider. Button does not appear when creating a new task.
|
||||
**Why human:** Visual layout, color rendering, and scroll behavior cannot be verified programmatically.
|
||||
|
||||
#### 2. Confirmation dialog flow
|
||||
|
||||
**Test:** Tap the delete button. Tap "Abbrechen" (cancel).
|
||||
**Expected:** Dialog dismisses, form remains open, task is unchanged.
|
||||
**Why human:** Dialog dismissal behavior and state preservation requires manual interaction.
|
||||
|
||||
#### 3. Smart delete — task with no completions (hard delete)
|
||||
|
||||
**Test:** Create a fresh task (never completed). Open it, tap delete, confirm.
|
||||
**Expected:** Task disappears from the room list immediately. Navigated back to room task list.
|
||||
**Why human:** End-to-end flow requires running app with real navigation and reactive provider updates.
|
||||
|
||||
#### 4. Smart delete — task with completions (soft delete)
|
||||
|
||||
**Test:** Complete a task at least once. Open it, tap delete, confirm.
|
||||
**Expected:** Task disappears from all views (room list, calendar, daily plan). Navigation returns to room. Task remains in DB (invisible to user but present for future statistics).
|
||||
**Why human:** Requires verifying absence from multiple views and confirming data is preserved in DB — combination of UI behavior and DB state inspection.
|
||||
|
||||
---
|
||||
|
||||
### Gaps Summary
|
||||
|
||||
No gaps. All must-haves verified.
|
||||
|
||||
---
|
||||
|
||||
## Test Results
|
||||
|
||||
- `flutter test test/features/tasks/data/tasks_dao_test.dart`: 13/13 passed (including all 6 new soft-delete tests)
|
||||
- `flutter test --reporter compact`: 144/144 passed
|
||||
- `dart analyze --fatal-infos`: No issues found
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-03-18T20:30:00Z_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
Reference in New Issue
Block a user