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
This commit is contained in:
2026-03-18 20:49:45 +01:00
parent a2cef91d7e
commit 4b51f5fa04
3 changed files with 98 additions and 6 deletions

View File

@@ -35,6 +35,8 @@ class Tasks extends Table {
DateTimeColumn get nextDueDate => dateTime()(); DateTimeColumn get nextDueDate => dateTime()();
DateTimeColumn get createdAt => DateTimeColumn get createdAt =>
dateTime().clientDefault(() => DateTime.now())(); dateTime().clientDefault(() => DateTime.now())();
BoolColumn get isActive =>
boolean().withDefault(const Constant(true))();
} }
/// TaskCompletions table: records when a task was completed. /// TaskCompletions table: records when a task was completed.
@@ -53,7 +55,7 @@ class AppDatabase extends _$AppDatabase {
: super(executor ?? _openConnection()); : super(executor ?? _openConnection());
@override @override
int get schemaVersion => 2; int get schemaVersion => 3;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@@ -67,6 +69,9 @@ class AppDatabase extends _$AppDatabase {
await m.createTable(tasks); await m.createTable(tasks);
await m.createTable(taskCompletions); await m.createTable(taskCompletions);
} }
if (from < 3) {
await m.addColumn(tasks, tasks.isActive);
}
}, },
beforeOpen: (details) async { beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON'); await customStatement('PRAGMA foreign_keys = ON');

View File

@@ -470,6 +470,21 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> {
requiredDuringInsert: false, requiredDuringInsert: false,
clientDefault: () => DateTime.now(), clientDefault: () => DateTime.now(),
); );
static const VerificationMeta _isActiveMeta = const VerificationMeta(
'isActive',
);
@override
late final GeneratedColumn<bool> isActive = GeneratedColumn<bool>(
'is_active',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("is_active" IN (0, 1))',
),
defaultValue: const Constant(true),
);
@override @override
List<GeneratedColumn> get $columns => [ List<GeneratedColumn> get $columns => [
id, id,
@@ -482,6 +497,7 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> {
effortLevel, effortLevel,
nextDueDate, nextDueDate,
createdAt, createdAt,
isActive,
]; ];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@@ -555,6 +571,12 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> {
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
); );
} }
if (data.containsKey('is_active')) {
context.handle(
_isActiveMeta,
isActive.isAcceptableOrUnknown(data['is_active']!, _isActiveMeta),
);
}
return context; return context;
} }
@@ -608,6 +630,10 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> {
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}created_at'], data['${effectivePrefix}created_at'],
)!, )!,
isActive: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}is_active'],
)!,
); );
} }
@@ -633,6 +659,7 @@ class Task extends DataClass implements Insertable<Task> {
final EffortLevel effortLevel; final EffortLevel effortLevel;
final DateTime nextDueDate; final DateTime nextDueDate;
final DateTime createdAt; final DateTime createdAt;
final bool isActive;
const Task({ const Task({
required this.id, required this.id,
required this.roomId, required this.roomId,
@@ -644,6 +671,7 @@ class Task extends DataClass implements Insertable<Task> {
required this.effortLevel, required this.effortLevel,
required this.nextDueDate, required this.nextDueDate,
required this.createdAt, required this.createdAt,
required this.isActive,
}); });
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
@@ -670,6 +698,7 @@ class Task extends DataClass implements Insertable<Task> {
} }
map['next_due_date'] = Variable<DateTime>(nextDueDate); map['next_due_date'] = Variable<DateTime>(nextDueDate);
map['created_at'] = Variable<DateTime>(createdAt); map['created_at'] = Variable<DateTime>(createdAt);
map['is_active'] = Variable<bool>(isActive);
return map; return map;
} }
@@ -689,6 +718,7 @@ class Task extends DataClass implements Insertable<Task> {
effortLevel: Value(effortLevel), effortLevel: Value(effortLevel),
nextDueDate: Value(nextDueDate), nextDueDate: Value(nextDueDate),
createdAt: Value(createdAt), createdAt: Value(createdAt),
isActive: Value(isActive),
); );
} }
@@ -712,6 +742,7 @@ class Task extends DataClass implements Insertable<Task> {
), ),
nextDueDate: serializer.fromJson<DateTime>(json['nextDueDate']), nextDueDate: serializer.fromJson<DateTime>(json['nextDueDate']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']), createdAt: serializer.fromJson<DateTime>(json['createdAt']),
isActive: serializer.fromJson<bool>(json['isActive']),
); );
} }
@override @override
@@ -732,6 +763,7 @@ class Task extends DataClass implements Insertable<Task> {
), ),
'nextDueDate': serializer.toJson<DateTime>(nextDueDate), 'nextDueDate': serializer.toJson<DateTime>(nextDueDate),
'createdAt': serializer.toJson<DateTime>(createdAt), 'createdAt': serializer.toJson<DateTime>(createdAt),
'isActive': serializer.toJson<bool>(isActive),
}; };
} }
@@ -746,6 +778,7 @@ class Task extends DataClass implements Insertable<Task> {
EffortLevel? effortLevel, EffortLevel? effortLevel,
DateTime? nextDueDate, DateTime? nextDueDate,
DateTime? createdAt, DateTime? createdAt,
bool? isActive,
}) => Task( }) => Task(
id: id ?? this.id, id: id ?? this.id,
roomId: roomId ?? this.roomId, roomId: roomId ?? this.roomId,
@@ -757,6 +790,7 @@ class Task extends DataClass implements Insertable<Task> {
effortLevel: effortLevel ?? this.effortLevel, effortLevel: effortLevel ?? this.effortLevel,
nextDueDate: nextDueDate ?? this.nextDueDate, nextDueDate: nextDueDate ?? this.nextDueDate,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
isActive: isActive ?? this.isActive,
); );
Task copyWithCompanion(TasksCompanion data) { Task copyWithCompanion(TasksCompanion data) {
return Task( return Task(
@@ -780,6 +814,7 @@ class Task extends DataClass implements Insertable<Task> {
? data.nextDueDate.value ? data.nextDueDate.value
: this.nextDueDate, : this.nextDueDate,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, 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<Task> {
..write('anchorDay: $anchorDay, ') ..write('anchorDay: $anchorDay, ')
..write('effortLevel: $effortLevel, ') ..write('effortLevel: $effortLevel, ')
..write('nextDueDate: $nextDueDate, ') ..write('nextDueDate: $nextDueDate, ')
..write('createdAt: $createdAt') ..write('createdAt: $createdAt, ')
..write('isActive: $isActive')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@@ -812,6 +848,7 @@ class Task extends DataClass implements Insertable<Task> {
effortLevel, effortLevel,
nextDueDate, nextDueDate,
createdAt, createdAt,
isActive,
); );
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@@ -826,7 +863,8 @@ class Task extends DataClass implements Insertable<Task> {
other.anchorDay == this.anchorDay && other.anchorDay == this.anchorDay &&
other.effortLevel == this.effortLevel && other.effortLevel == this.effortLevel &&
other.nextDueDate == this.nextDueDate && other.nextDueDate == this.nextDueDate &&
other.createdAt == this.createdAt); other.createdAt == this.createdAt &&
other.isActive == this.isActive);
} }
class TasksCompanion extends UpdateCompanion<Task> { class TasksCompanion extends UpdateCompanion<Task> {
@@ -840,6 +878,7 @@ class TasksCompanion extends UpdateCompanion<Task> {
final Value<EffortLevel> effortLevel; final Value<EffortLevel> effortLevel;
final Value<DateTime> nextDueDate; final Value<DateTime> nextDueDate;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
final Value<bool> isActive;
const TasksCompanion({ const TasksCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
this.roomId = const Value.absent(), this.roomId = const Value.absent(),
@@ -851,6 +890,7 @@ class TasksCompanion extends UpdateCompanion<Task> {
this.effortLevel = const Value.absent(), this.effortLevel = const Value.absent(),
this.nextDueDate = const Value.absent(), this.nextDueDate = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.isActive = const Value.absent(),
}); });
TasksCompanion.insert({ TasksCompanion.insert({
this.id = const Value.absent(), this.id = const Value.absent(),
@@ -863,6 +903,7 @@ class TasksCompanion extends UpdateCompanion<Task> {
required EffortLevel effortLevel, required EffortLevel effortLevel,
required DateTime nextDueDate, required DateTime nextDueDate,
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.isActive = const Value.absent(),
}) : roomId = Value(roomId), }) : roomId = Value(roomId),
name = Value(name), name = Value(name),
intervalType = Value(intervalType), intervalType = Value(intervalType),
@@ -879,6 +920,7 @@ class TasksCompanion extends UpdateCompanion<Task> {
Expression<int>? effortLevel, Expression<int>? effortLevel,
Expression<DateTime>? nextDueDate, Expression<DateTime>? nextDueDate,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
Expression<bool>? isActive,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
if (id != null) 'id': id, if (id != null) 'id': id,
@@ -891,6 +933,7 @@ class TasksCompanion extends UpdateCompanion<Task> {
if (effortLevel != null) 'effort_level': effortLevel, if (effortLevel != null) 'effort_level': effortLevel,
if (nextDueDate != null) 'next_due_date': nextDueDate, if (nextDueDate != null) 'next_due_date': nextDueDate,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
if (isActive != null) 'is_active': isActive,
}); });
} }
@@ -905,6 +948,7 @@ class TasksCompanion extends UpdateCompanion<Task> {
Value<EffortLevel>? effortLevel, Value<EffortLevel>? effortLevel,
Value<DateTime>? nextDueDate, Value<DateTime>? nextDueDate,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
Value<bool>? isActive,
}) { }) {
return TasksCompanion( return TasksCompanion(
id: id ?? this.id, id: id ?? this.id,
@@ -917,6 +961,7 @@ class TasksCompanion extends UpdateCompanion<Task> {
effortLevel: effortLevel ?? this.effortLevel, effortLevel: effortLevel ?? this.effortLevel,
nextDueDate: nextDueDate ?? this.nextDueDate, nextDueDate: nextDueDate ?? this.nextDueDate,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
isActive: isActive ?? this.isActive,
); );
} }
@@ -957,6 +1002,9 @@ class TasksCompanion extends UpdateCompanion<Task> {
if (createdAt.present) { if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value); map['created_at'] = Variable<DateTime>(createdAt.value);
} }
if (isActive.present) {
map['is_active'] = Variable<bool>(isActive.value);
}
return map; return map;
} }
@@ -972,7 +1020,8 @@ class TasksCompanion extends UpdateCompanion<Task> {
..write('anchorDay: $anchorDay, ') ..write('anchorDay: $anchorDay, ')
..write('effortLevel: $effortLevel, ') ..write('effortLevel: $effortLevel, ')
..write('nextDueDate: $nextDueDate, ') ..write('nextDueDate: $nextDueDate, ')
..write('createdAt: $createdAt') ..write('createdAt: $createdAt, ')
..write('isActive: $isActive')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@@ -1556,6 +1605,7 @@ typedef $$TasksTableCreateCompanionBuilder =
required EffortLevel effortLevel, required EffortLevel effortLevel,
required DateTime nextDueDate, required DateTime nextDueDate,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<bool> isActive,
}); });
typedef $$TasksTableUpdateCompanionBuilder = typedef $$TasksTableUpdateCompanionBuilder =
TasksCompanion Function({ TasksCompanion Function({
@@ -1569,6 +1619,7 @@ typedef $$TasksTableUpdateCompanionBuilder =
Value<EffortLevel> effortLevel, Value<EffortLevel> effortLevel,
Value<DateTime> nextDueDate, Value<DateTime> nextDueDate,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<bool> isActive,
}); });
final class $$TasksTableReferences final class $$TasksTableReferences
@@ -1668,6 +1719,11 @@ class $$TasksTableFilterComposer extends Composer<_$AppDatabase, $TasksTable> {
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<bool> get isActive => $composableBuilder(
column: $table.isActive,
builder: (column) => ColumnFilters(column),
);
$$RoomsTableFilterComposer get roomId { $$RoomsTableFilterComposer get roomId {
final $$RoomsTableFilterComposer composer = $composerBuilder( final $$RoomsTableFilterComposer composer = $composerBuilder(
composer: this, composer: this,
@@ -1771,6 +1827,11 @@ class $$TasksTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<bool> get isActive => $composableBuilder(
column: $table.isActive,
builder: (column) => ColumnOrderings(column),
);
$$RoomsTableOrderingComposer get roomId { $$RoomsTableOrderingComposer get roomId {
final $$RoomsTableOrderingComposer composer = $composerBuilder( final $$RoomsTableOrderingComposer composer = $composerBuilder(
composer: this, composer: this,
@@ -1843,6 +1904,9 @@ class $$TasksTableAnnotationComposer
GeneratedColumn<DateTime> get createdAt => GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column); $composableBuilder(column: $table.createdAt, builder: (column) => column);
GeneratedColumn<bool> get isActive =>
$composableBuilder(column: $table.isActive, builder: (column) => column);
$$RoomsTableAnnotationComposer get roomId { $$RoomsTableAnnotationComposer get roomId {
final $$RoomsTableAnnotationComposer composer = $composerBuilder( final $$RoomsTableAnnotationComposer composer = $composerBuilder(
composer: this, composer: this,
@@ -1930,6 +1994,7 @@ class $$TasksTableTableManager
Value<EffortLevel> effortLevel = const Value.absent(), Value<EffortLevel> effortLevel = const Value.absent(),
Value<DateTime> nextDueDate = const Value.absent(), Value<DateTime> nextDueDate = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<bool> isActive = const Value.absent(),
}) => TasksCompanion( }) => TasksCompanion(
id: id, id: id,
roomId: roomId, roomId: roomId,
@@ -1941,6 +2006,7 @@ class $$TasksTableTableManager
effortLevel: effortLevel, effortLevel: effortLevel,
nextDueDate: nextDueDate, nextDueDate: nextDueDate,
createdAt: createdAt, createdAt: createdAt,
isActive: isActive,
), ),
createCompanionCallback: createCompanionCallback:
({ ({
@@ -1954,6 +2020,7 @@ class $$TasksTableTableManager
required EffortLevel effortLevel, required EffortLevel effortLevel,
required DateTime nextDueDate, required DateTime nextDueDate,
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<bool> isActive = const Value.absent(),
}) => TasksCompanion.insert( }) => TasksCompanion.insert(
id: id, id: id,
roomId: roomId, roomId: roomId,
@@ -1965,6 +2032,7 @@ class $$TasksTableTableManager
effortLevel: effortLevel, effortLevel: effortLevel,
nextDueDate: nextDueDate, nextDueDate: nextDueDate,
createdAt: createdAt, createdAt: createdAt,
isActive: isActive,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
.map( .map(

View File

@@ -10,9 +10,10 @@ class TasksDao extends DatabaseAccessor<AppDatabase> with _$TasksDaoMixin {
TasksDao(super.attachedDatabase); TasksDao(super.attachedDatabase);
/// Watch tasks in a room sorted by nextDueDate ascending. /// Watch tasks in a room sorted by nextDueDate ascending.
/// Only returns active tasks (isActive = true).
Stream<List<Task>> watchTasksInRoom(int roomId) { Stream<List<Task>> watchTasksInRoom(int roomId) {
return (select(tasks) 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)])) ..orderBy([(t) => OrderingTerm.asc(t.nextDueDate)]))
.watch(); .watch();
} }
@@ -90,12 +91,13 @@ class TasksDao extends DatabaseAccessor<AppDatabase> with _$TasksDaoMixin {
} }
/// Count overdue tasks in a room (nextDueDate before today). /// Count overdue tasks in a room (nextDueDate before today).
/// Only counts active tasks (isActive = true).
Future<int> getOverdueTaskCount(int roomId, {DateTime? today}) async { Future<int> getOverdueTaskCount(int roomId, {DateTime? today}) async {
final now = today ?? DateTime.now(); final now = today ?? DateTime.now();
final todayDateOnly = DateTime(now.year, now.month, now.day); final todayDateOnly = DateTime(now.year, now.month, now.day);
final taskList = await (select(tasks) final taskList = await (select(tasks)
..where((t) => t.roomId.equals(roomId))) ..where((t) => t.roomId.equals(roomId) & t.isActive.equals(true)))
.get(); .get();
return taskList.where((task) { return taskList.where((task) {
@@ -107,4 +109,21 @@ class TasksDao extends DatabaseAccessor<AppDatabase> with _$TasksDaoMixin {
return dueDate.isBefore(todayDateOnly); return dueDate.isBefore(todayDateOnly);
}).length; }).length;
} }
/// Soft-delete a task by setting isActive to false.
/// The task and its completions remain in the database.
Future<void> softDeleteTask(int taskId) {
return (update(tasks)..where((t) => t.id.equals(taskId)))
.write(const TasksCompanion(isActive: Value(false)));
}
/// Count completions for a task.
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;
}
} }