From 4b51f5fa045fcc5eae28fdd78aee3fa19127a16d Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 18 Mar 2026 20:49:45 +0100 Subject: [PATCH] feat(08-01): add isActive column, migration v3, softDeleteTask and getCompletionCount - Add isActive BoolColumn (default true) to Tasks table - Bump schema version from 2 to 3 with addColumn migration - Filter watchTasksInRoom to isActive=true only - Filter getOverdueTaskCount to isActive=true only - Add softDeleteTask(taskId) - sets isActive=false without removing data - Add getCompletionCount(taskId) - counts TaskCompletions for a task --- lib/core/database/database.dart | 7 ++- lib/core/database/database.g.dart | 74 ++++++++++++++++++++++++-- lib/features/tasks/data/tasks_dao.dart | 23 +++++++- 3 files changed, 98 insertions(+), 6 deletions(-) diff --git a/lib/core/database/database.dart b/lib/core/database/database.dart index 4d6708a..bb356de 100644 --- a/lib/core/database/database.dart +++ b/lib/core/database/database.dart @@ -35,6 +35,8 @@ class Tasks extends Table { DateTimeColumn get nextDueDate => dateTime()(); DateTimeColumn get createdAt => dateTime().clientDefault(() => DateTime.now())(); + BoolColumn get isActive => + boolean().withDefault(const Constant(true))(); } /// TaskCompletions table: records when a task was completed. @@ -53,7 +55,7 @@ class AppDatabase extends _$AppDatabase { : super(executor ?? _openConnection()); @override - int get schemaVersion => 2; + int get schemaVersion => 3; @override MigrationStrategy get migration { @@ -67,6 +69,9 @@ class AppDatabase extends _$AppDatabase { await m.createTable(tasks); await m.createTable(taskCompletions); } + if (from < 3) { + await m.addColumn(tasks, tasks.isActive); + } }, beforeOpen: (details) async { await customStatement('PRAGMA foreign_keys = ON'); diff --git a/lib/core/database/database.g.dart b/lib/core/database/database.g.dart index adae5b6..fe2ecf3 100644 --- a/lib/core/database/database.g.dart +++ b/lib/core/database/database.g.dart @@ -470,6 +470,21 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> { requiredDuringInsert: false, clientDefault: () => DateTime.now(), ); + static const VerificationMeta _isActiveMeta = const VerificationMeta( + 'isActive', + ); + @override + late final GeneratedColumn isActive = GeneratedColumn( + 'is_active', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_active" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); @override List get $columns => [ id, @@ -482,6 +497,7 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> { effortLevel, nextDueDate, createdAt, + isActive, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -555,6 +571,12 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> { createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), ); } + if (data.containsKey('is_active')) { + context.handle( + _isActiveMeta, + isActive.isAcceptableOrUnknown(data['is_active']!, _isActiveMeta), + ); + } return context; } @@ -608,6 +630,10 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> { DriftSqlType.dateTime, data['${effectivePrefix}created_at'], )!, + isActive: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_active'], + )!, ); } @@ -633,6 +659,7 @@ class Task extends DataClass implements Insertable { final EffortLevel effortLevel; final DateTime nextDueDate; final DateTime createdAt; + final bool isActive; const Task({ required this.id, required this.roomId, @@ -644,6 +671,7 @@ class Task extends DataClass implements Insertable { required this.effortLevel, required this.nextDueDate, required this.createdAt, + required this.isActive, }); @override Map toColumns(bool nullToAbsent) { @@ -670,6 +698,7 @@ class Task extends DataClass implements Insertable { } map['next_due_date'] = Variable(nextDueDate); map['created_at'] = Variable(createdAt); + map['is_active'] = Variable(isActive); return map; } @@ -689,6 +718,7 @@ class Task extends DataClass implements Insertable { effortLevel: Value(effortLevel), nextDueDate: Value(nextDueDate), createdAt: Value(createdAt), + isActive: Value(isActive), ); } @@ -712,6 +742,7 @@ class Task extends DataClass implements Insertable { ), nextDueDate: serializer.fromJson(json['nextDueDate']), createdAt: serializer.fromJson(json['createdAt']), + isActive: serializer.fromJson(json['isActive']), ); } @override @@ -732,6 +763,7 @@ class Task extends DataClass implements Insertable { ), 'nextDueDate': serializer.toJson(nextDueDate), 'createdAt': serializer.toJson(createdAt), + 'isActive': serializer.toJson(isActive), }; } @@ -746,6 +778,7 @@ class Task extends DataClass implements Insertable { EffortLevel? effortLevel, DateTime? nextDueDate, DateTime? createdAt, + bool? isActive, }) => Task( id: id ?? this.id, roomId: roomId ?? this.roomId, @@ -757,6 +790,7 @@ class Task extends DataClass implements Insertable { effortLevel: effortLevel ?? this.effortLevel, nextDueDate: nextDueDate ?? this.nextDueDate, createdAt: createdAt ?? this.createdAt, + isActive: isActive ?? this.isActive, ); Task copyWithCompanion(TasksCompanion data) { return Task( @@ -780,6 +814,7 @@ class Task extends DataClass implements Insertable { ? data.nextDueDate.value : this.nextDueDate, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + isActive: data.isActive.present ? data.isActive.value : this.isActive, ); } @@ -795,7 +830,8 @@ class Task extends DataClass implements Insertable { ..write('anchorDay: $anchorDay, ') ..write('effortLevel: $effortLevel, ') ..write('nextDueDate: $nextDueDate, ') - ..write('createdAt: $createdAt') + ..write('createdAt: $createdAt, ') + ..write('isActive: $isActive') ..write(')')) .toString(); } @@ -812,6 +848,7 @@ class Task extends DataClass implements Insertable { effortLevel, nextDueDate, createdAt, + isActive, ); @override bool operator ==(Object other) => @@ -826,7 +863,8 @@ class Task extends DataClass implements Insertable { other.anchorDay == this.anchorDay && other.effortLevel == this.effortLevel && other.nextDueDate == this.nextDueDate && - other.createdAt == this.createdAt); + other.createdAt == this.createdAt && + other.isActive == this.isActive); } class TasksCompanion extends UpdateCompanion { @@ -840,6 +878,7 @@ class TasksCompanion extends UpdateCompanion { final Value effortLevel; final Value nextDueDate; final Value createdAt; + final Value isActive; const TasksCompanion({ this.id = const Value.absent(), this.roomId = const Value.absent(), @@ -851,6 +890,7 @@ class TasksCompanion extends UpdateCompanion { this.effortLevel = const Value.absent(), this.nextDueDate = const Value.absent(), this.createdAt = const Value.absent(), + this.isActive = const Value.absent(), }); TasksCompanion.insert({ this.id = const Value.absent(), @@ -863,6 +903,7 @@ class TasksCompanion extends UpdateCompanion { required EffortLevel effortLevel, required DateTime nextDueDate, this.createdAt = const Value.absent(), + this.isActive = const Value.absent(), }) : roomId = Value(roomId), name = Value(name), intervalType = Value(intervalType), @@ -879,6 +920,7 @@ class TasksCompanion extends UpdateCompanion { Expression? effortLevel, Expression? nextDueDate, Expression? createdAt, + Expression? isActive, }) { return RawValuesInsertable({ if (id != null) 'id': id, @@ -891,6 +933,7 @@ class TasksCompanion extends UpdateCompanion { if (effortLevel != null) 'effort_level': effortLevel, if (nextDueDate != null) 'next_due_date': nextDueDate, if (createdAt != null) 'created_at': createdAt, + if (isActive != null) 'is_active': isActive, }); } @@ -905,6 +948,7 @@ class TasksCompanion extends UpdateCompanion { Value? effortLevel, Value? nextDueDate, Value? createdAt, + Value? isActive, }) { return TasksCompanion( id: id ?? this.id, @@ -917,6 +961,7 @@ class TasksCompanion extends UpdateCompanion { effortLevel: effortLevel ?? this.effortLevel, nextDueDate: nextDueDate ?? this.nextDueDate, createdAt: createdAt ?? this.createdAt, + isActive: isActive ?? this.isActive, ); } @@ -957,6 +1002,9 @@ class TasksCompanion extends UpdateCompanion { if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } + if (isActive.present) { + map['is_active'] = Variable(isActive.value); + } return map; } @@ -972,7 +1020,8 @@ class TasksCompanion extends UpdateCompanion { ..write('anchorDay: $anchorDay, ') ..write('effortLevel: $effortLevel, ') ..write('nextDueDate: $nextDueDate, ') - ..write('createdAt: $createdAt') + ..write('createdAt: $createdAt, ') + ..write('isActive: $isActive') ..write(')')) .toString(); } @@ -1556,6 +1605,7 @@ typedef $$TasksTableCreateCompanionBuilder = required EffortLevel effortLevel, required DateTime nextDueDate, Value createdAt, + Value isActive, }); typedef $$TasksTableUpdateCompanionBuilder = TasksCompanion Function({ @@ -1569,6 +1619,7 @@ typedef $$TasksTableUpdateCompanionBuilder = Value effortLevel, Value nextDueDate, Value createdAt, + Value isActive, }); final class $$TasksTableReferences @@ -1668,6 +1719,11 @@ class $$TasksTableFilterComposer extends Composer<_$AppDatabase, $TasksTable> { builder: (column) => ColumnFilters(column), ); + ColumnFilters get isActive => $composableBuilder( + column: $table.isActive, + builder: (column) => ColumnFilters(column), + ); + $$RoomsTableFilterComposer get roomId { final $$RoomsTableFilterComposer composer = $composerBuilder( composer: this, @@ -1771,6 +1827,11 @@ class $$TasksTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get isActive => $composableBuilder( + column: $table.isActive, + builder: (column) => ColumnOrderings(column), + ); + $$RoomsTableOrderingComposer get roomId { final $$RoomsTableOrderingComposer composer = $composerBuilder( composer: this, @@ -1843,6 +1904,9 @@ class $$TasksTableAnnotationComposer GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get isActive => + $composableBuilder(column: $table.isActive, builder: (column) => column); + $$RoomsTableAnnotationComposer get roomId { final $$RoomsTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -1930,6 +1994,7 @@ class $$TasksTableTableManager Value effortLevel = const Value.absent(), Value nextDueDate = const Value.absent(), Value createdAt = const Value.absent(), + Value isActive = const Value.absent(), }) => TasksCompanion( id: id, roomId: roomId, @@ -1941,6 +2006,7 @@ class $$TasksTableTableManager effortLevel: effortLevel, nextDueDate: nextDueDate, createdAt: createdAt, + isActive: isActive, ), createCompanionCallback: ({ @@ -1954,6 +2020,7 @@ class $$TasksTableTableManager required EffortLevel effortLevel, required DateTime nextDueDate, Value createdAt = const Value.absent(), + Value isActive = const Value.absent(), }) => TasksCompanion.insert( id: id, roomId: roomId, @@ -1965,6 +2032,7 @@ class $$TasksTableTableManager effortLevel: effortLevel, nextDueDate: nextDueDate, createdAt: createdAt, + isActive: isActive, ), withReferenceMapper: (p0) => p0 .map( diff --git a/lib/features/tasks/data/tasks_dao.dart b/lib/features/tasks/data/tasks_dao.dart index 8f8d23d..c9d96c1 100644 --- a/lib/features/tasks/data/tasks_dao.dart +++ b/lib/features/tasks/data/tasks_dao.dart @@ -10,9 +10,10 @@ class TasksDao extends DatabaseAccessor with _$TasksDaoMixin { TasksDao(super.attachedDatabase); /// Watch tasks in a room sorted by nextDueDate ascending. + /// Only returns active tasks (isActive = true). Stream> watchTasksInRoom(int roomId) { return (select(tasks) - ..where((t) => t.roomId.equals(roomId)) + ..where((t) => t.roomId.equals(roomId) & t.isActive.equals(true)) ..orderBy([(t) => OrderingTerm.asc(t.nextDueDate)])) .watch(); } @@ -90,12 +91,13 @@ class TasksDao extends DatabaseAccessor with _$TasksDaoMixin { } /// Count overdue tasks in a room (nextDueDate before today). + /// Only counts active tasks (isActive = true). Future getOverdueTaskCount(int roomId, {DateTime? today}) async { final now = today ?? DateTime.now(); final todayDateOnly = DateTime(now.year, now.month, now.day); final taskList = await (select(tasks) - ..where((t) => t.roomId.equals(roomId))) + ..where((t) => t.roomId.equals(roomId) & t.isActive.equals(true))) .get(); return taskList.where((task) { @@ -107,4 +109,21 @@ class TasksDao extends DatabaseAccessor with _$TasksDaoMixin { return dueDate.isBefore(todayDateOnly); }).length; } + + /// Soft-delete a task by setting isActive to false. + /// The task and its completions remain in the database. + Future softDeleteTask(int taskId) { + return (update(tasks)..where((t) => t.id.equals(taskId))) + .write(const TasksCompanion(isActive: Value(false))); + } + + /// Count completions for a task. + 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; + } }