import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import '../models/movement_detail.dart'; import '../seed/default_rewards.dart'; import '../seed/default_rules.dart'; import 'tables.dart'; part 'app_database.g.dart'; @DriftDatabase(tables: [Enfants, Regles, Mouvements, Recompenses, Config]) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); AppDatabase.forTesting(super.executor); @override int get schemaVersion => 1; @override MigrationStrategy get migration => MigrationStrategy( onCreate: (m) async { await m.createAll(); }, onUpgrade: (m, from, to) async {}, ); /// Met à jour les packs par défaut si l'utilisateur les a déjà importés. Future initialize() async { if (await getConfig('regles_version') != null) { await _syncDefaultRules(); } if (await getConfig('recompenses_version') != null) { await _syncDefaultRewards(); } } Future importDefaultPack() async { await _syncDefaultRules(); await _syncDefaultRewards(); } Future _syncDefaultRules() async { final version = await getConfig('regles_version'); if (version == rulesVersion) return; await (update(regles)..where((r) => r.actif.equals(true))) .write(const ReglesCompanion(actif: Value(false))); for (final rule in defaultRules) { await into(regles).insert( ReglesCompanion.insert( libelle: rule.libelle, points: rule.points, icone: Value(rule.icone), ordre: Value(rule.ordre), famille: Value(rule.famille), familleOrdre: Value(rule.familleOrdre), ), ); } await setConfig('regles_version', rulesVersion); } Future _syncDefaultRewards() async { final version = await getConfig('recompenses_version'); if (version == rewardsVersion) return; await (update(recompenses)..where((r) => r.actif.equals(true))) .write(const RecompensesCompanion(actif: Value(false))); for (final reward in defaultRewards) { await into(recompenses).insert( RecompensesCompanion.insert( libelle: reward.libelle, coutPoints: reward.coutPoints, icone: Value(reward.icone), ordre: Value(reward.ordre), ), ); } await setConfig('recompenses_version', rewardsVersion); } Future getConfig(String key) async { final row = await (select(config)..where((c) => c.cle.equals(key))) .getSingleOrNull(); return row?.valeur; } Future setConfig(String key, String value) async { await into(config).insertOnConflictUpdate( ConfigCompanion.insert(cle: key, valeur: value), ); } Future isOnboardingComplete() async { return await getConfig('onboarding_complete') == 'true'; } Future completeOnboarding() async { await setConfig('onboarding_complete', 'true'); } Future> listChildren() { return (select(enfants)..orderBy([(e) => OrderingTerm.asc(e.ordre)])).get(); } Future countChildren() async { final count = await enfants.count().getSingle(); return count; } Future insertChild({ required String prenom, required String couleur, String? dateNaissance, }) { return into(enfants).insert( EnfantsCompanion.insert( prenom: prenom, couleur: Value(couleur), dateNaissance: Value(dateNaissance), ordre: const Value(1), ), ); } Future setPinHash(String hash) async { await setConfig('pin_hash', hash); } Future getPinHash() => getConfig('pin_hash'); Future> listActiveRules() { return (select(regles) ..where((r) => r.actif.equals(true)) ..orderBy([ (r) => OrderingTerm.asc(r.familleOrdre), (r) => OrderingTerm.asc(r.ordre), ])) .get(); } Future> listRecentMovements({int limit = 30}) async { final query = select(mouvements).join([ innerJoin(enfants, enfants.id.equalsExp(mouvements.enfantId)), leftOuterJoin(regles, regles.id.equalsExp(mouvements.regleId)), leftOuterJoin( recompenses, recompenses.id.equalsExp(mouvements.recompenseId), ), ]) ..where(mouvements.annule.equals(false)) ..orderBy([ OrderingTerm.desc(mouvements.creeLe), OrderingTerm.desc(mouvements.id), ]) ..limit(limit); final rows = await query.get(); return rows.map((row) { final movement = row.readTable(mouvements); final child = row.readTable(enfants); final rule = row.readTableOrNull(regles); final reward = row.readTableOrNull(recompenses); return MovementDetail( id: movement.id, childId: child.id, childName: child.prenom, delta: movement.delta, createdAt: movement.creeLe, note: movement.note, ruleLabel: rule?.libelle, ruleIcon: rule?.icone, rewardLabel: reward?.libelle, rewardIcon: reward?.icone, ); }).toList(); } /// Annule un mouvement : inverse le delta (plancher 0). Future cancelMovement(int movementId) async { return transaction(() async { final movement = await (select(mouvements) ..where((m) => m.id.equals(movementId) & m.annule.equals(false))) .getSingleOrNull(); if (movement == null) return null; final child = await (select(enfants) ..where((e) => e.id.equals(movement.enfantId))) .getSingleOrNull(); if (child == null) return null; final correction = -movement.delta; final newScore = (child.score + correction).clamp(0, 1 << 30); await (update(mouvements)..where((m) => m.id.equals(movementId))) .write(const MouvementsCompanion(annule: Value(true))); await (update(enfants)..where((e) => e.id.equals(child.id))) .write(EnfantsCompanion(score: Value(newScore))); return (select(enfants)..where((e) => e.id.equals(child.id))) .getSingleOrNull(); }); } /// Applique une règle : plancher score à 0, delta réel enregistré. Future applyRule(int childId, int ruleId, {String? note}) async { return transaction(() async { final child = await (select(enfants)..where((e) => e.id.equals(childId))) .getSingleOrNull(); final rule = await (select(regles) ..where((r) => r.id.equals(ruleId) & r.actif.equals(true))) .getSingleOrNull(); if (child == null || rule == null) return null; final newScore = (child.score + rule.points).clamp(0, 1 << 30); final realDelta = newScore - child.score; await into(mouvements).insert( MouvementsCompanion.insert( enfantId: childId, regleId: Value(ruleId), delta: realDelta, note: Value(note), ), ); await (update(enfants)..where((e) => e.id.equals(childId))) .write(EnfantsCompanion(score: Value(newScore))); return (select(enfants)..where((e) => e.id.equals(childId))) .getSingleOrNull(); }); } } LazyDatabase _openConnection() { return LazyDatabase(() async { final dir = await getApplicationDocumentsDirectory(); final file = File(p.join(dir.path, 'bonpoint.db')); return NativeDatabase.createInBackground(file); }); }