import 'dart:convert'; import 'package:drift/drift.dart'; import '../../../core/id.dart'; import '../../../domain/models/habit.dart'; import '../../../domain/rules/xor_protocol.dart'; import '../app_database.dart'; import '../tables/user_tables.dart'; part 'habit_dao.g.dart'; class HabitDraft { final String userId; final HabitType type; final String title; final String? protocolId; final String? breakProtocolId; final FrameLevel frameLevel; final String frameFramedText; final String? frameOriginalText; final String? anchorWhen; final String? anchorAfterWhat; final String? anchorWhere; final String startedAt; final String? phaseId; final List variants; const HabitDraft({ required this.userId, required this.type, required this.title, required this.frameLevel, required this.frameFramedText, required this.startedAt, this.protocolId, this.breakProtocolId, this.frameOriginalText, this.anchorWhen, this.anchorAfterWhat, this.anchorWhere, this.phaseId, this.variants = const [], }); } class VariantDraft { final String label; final String doseText; final List contextTags; final List conditionTags; final bool isMinimum; final int sortOrder; const VariantDraft({ required this.label, required this.doseText, this.contextTags = const [], this.conditionTags = const [], this.isMinimum = false, this.sortOrder = 0, }); } @DriftAccessor(tables: [Habits, HabitDoseVariants]) class HabitDao extends DatabaseAccessor with _$HabitDaoMixin { HabitDao(super.db); /// Insert habit + variants atomically. Future insertWithVariants(HabitDraft draft) async { assertXorProtocol( type: draft.type, protocolId: draft.protocolId, breakProtocolId: draft.breakProtocolId, ); final habitId = generateUlid('hb'); await transaction(() async { await into(habits).insert(HabitsCompanion.insert( id: habitId, userId: draft.userId, type: draft.type.dbValue, status: 'active', title: draft.title, protocolId: Value(draft.protocolId), breakProtocolId: Value(draft.breakProtocolId), frameLevel: draft.frameLevel.dbValue, frameFramedText: draft.frameFramedText, frameOriginalText: Value(draft.frameOriginalText), anchorWhen: Value(draft.anchorWhen), anchorAfterWhat: Value(draft.anchorAfterWhat), anchorWhere: Value(draft.anchorWhere), startedAt: draft.startedAt, phaseId: Value(draft.phaseId), )); for (final v in draft.variants) { await into(habitDoseVariants).insert(HabitDoseVariantsCompanion.insert( variantId: generateUlid('dv'), habitId: habitId, label: v.label, doseText: v.doseText, contextTagsJson: Value(jsonEncode(v.contextTags)), conditionTagsJson: Value(jsonEncode(v.conditionTags)), isMinimum: Value(v.isMinimum), sortOrder: Value(v.sortOrder), )); } }); return habitId; } Future countActive({ required String userId, required HabitType type, String? excludeHabitId, }) async { final query = select(habits) ..where((t) => t.userId.equals(userId)) ..where((t) => t.status.equals('active')) ..where((t) => t.type.equals(type.dbValue)); if (excludeHabitId != null) { query.where((t) => t.id.isNotValue(excludeHabitId)); } final rows = await query.get(); return rows.length; } Future> activeHabitsForUser(String userId) { return (select(habits) ..where((t) => t.userId.equals(userId)) ..where((t) => t.status.equals('active'))) .get(); } Future> variantsForHabit(String habitId) { return (select(habitDoseVariants) ..where((t) => t.habitId.equals(habitId)) ..orderBy([(t) => OrderingTerm.asc(t.sortOrder)])) .get(); } Future> variantsByIds(Set ids) { if (ids.isEmpty) return Future.value(const []); return (select(habitDoseVariants) ..where((t) => t.variantId.isIn(ids))) .get(); } }