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:
@@ -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');
|
||||
|
||||
@@ -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<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
|
||||
List<GeneratedColumn> 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<Task> {
|
||||
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<Task> {
|
||||
required this.effortLevel,
|
||||
required this.nextDueDate,
|
||||
required this.createdAt,
|
||||
required this.isActive,
|
||||
});
|
||||
@override
|
||||
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['created_at'] = Variable<DateTime>(createdAt);
|
||||
map['is_active'] = Variable<bool>(isActive);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -689,6 +718,7 @@ class Task extends DataClass implements Insertable<Task> {
|
||||
effortLevel: Value(effortLevel),
|
||||
nextDueDate: Value(nextDueDate),
|
||||
createdAt: Value(createdAt),
|
||||
isActive: Value(isActive),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -712,6 +742,7 @@ class Task extends DataClass implements Insertable<Task> {
|
||||
),
|
||||
nextDueDate: serializer.fromJson<DateTime>(json['nextDueDate']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
isActive: serializer.fromJson<bool>(json['isActive']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -732,6 +763,7 @@ class Task extends DataClass implements Insertable<Task> {
|
||||
),
|
||||
'nextDueDate': serializer.toJson<DateTime>(nextDueDate),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'isActive': serializer.toJson<bool>(isActive),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -746,6 +778,7 @@ class Task extends DataClass implements Insertable<Task> {
|
||||
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<Task> {
|
||||
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<Task> {
|
||||
? 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<Task> {
|
||||
..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<Task> {
|
||||
effortLevel,
|
||||
nextDueDate,
|
||||
createdAt,
|
||||
isActive,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -826,7 +863,8 @@ class Task extends DataClass implements Insertable<Task> {
|
||||
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<Task> {
|
||||
@@ -840,6 +878,7 @@ class TasksCompanion extends UpdateCompanion<Task> {
|
||||
final Value<EffortLevel> effortLevel;
|
||||
final Value<DateTime> nextDueDate;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<bool> isActive;
|
||||
const TasksCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.roomId = const Value.absent(),
|
||||
@@ -851,6 +890,7 @@ class TasksCompanion extends UpdateCompanion<Task> {
|
||||
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<Task> {
|
||||
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<Task> {
|
||||
Expression<int>? effortLevel,
|
||||
Expression<DateTime>? nextDueDate,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<bool>? isActive,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
@@ -891,6 +933,7 @@ class TasksCompanion extends UpdateCompanion<Task> {
|
||||
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<Task> {
|
||||
Value<EffortLevel>? effortLevel,
|
||||
Value<DateTime>? nextDueDate,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<bool>? isActive,
|
||||
}) {
|
||||
return TasksCompanion(
|
||||
id: id ?? this.id,
|
||||
@@ -917,6 +961,7 @@ class TasksCompanion extends UpdateCompanion<Task> {
|
||||
effortLevel: effortLevel ?? this.effortLevel,
|
||||
nextDueDate: nextDueDate ?? this.nextDueDate,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
isActive: isActive ?? this.isActive,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -957,6 +1002,9 @@ class TasksCompanion extends UpdateCompanion<Task> {
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
if (isActive.present) {
|
||||
map['is_active'] = Variable<bool>(isActive.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -972,7 +1020,8 @@ class TasksCompanion extends UpdateCompanion<Task> {
|
||||
..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<DateTime> createdAt,
|
||||
Value<bool> isActive,
|
||||
});
|
||||
typedef $$TasksTableUpdateCompanionBuilder =
|
||||
TasksCompanion Function({
|
||||
@@ -1569,6 +1619,7 @@ typedef $$TasksTableUpdateCompanionBuilder =
|
||||
Value<EffortLevel> effortLevel,
|
||||
Value<DateTime> nextDueDate,
|
||||
Value<DateTime> createdAt,
|
||||
Value<bool> isActive,
|
||||
});
|
||||
|
||||
final class $$TasksTableReferences
|
||||
@@ -1668,6 +1719,11 @@ class $$TasksTableFilterComposer extends Composer<_$AppDatabase, $TasksTable> {
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<bool> 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<bool> 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<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<bool> 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> effortLevel = const Value.absent(),
|
||||
Value<DateTime> nextDueDate = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<bool> 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<DateTime> createdAt = const Value.absent(),
|
||||
Value<bool> 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(
|
||||
|
||||
@@ -10,9 +10,10 @@ class TasksDao extends DatabaseAccessor<AppDatabase> with _$TasksDaoMixin {
|
||||
TasksDao(super.attachedDatabase);
|
||||
|
||||
/// Watch tasks in a room sorted by nextDueDate ascending.
|
||||
/// Only returns active tasks (isActive = true).
|
||||
Stream<List<Task>> 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<AppDatabase> with _$TasksDaoMixin {
|
||||
}
|
||||
|
||||
/// Count overdue tasks in a room (nextDueDate before today).
|
||||
/// Only counts active tasks (isActive = true).
|
||||
Future<int> 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<AppDatabase> 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<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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user