bonpoint/mobile/lib/data/database/app_database.dart
Julien Martin b8faf3b4df Ajoute l'espace parent : PIN, grille de règles, historique et annulation.
Phase 2 du MVP Flutter : vérification PIN locale, application des règles par famille, suivi des mouvements et correction avec plancher à 0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 12:24:42 +02:00

257 lines
7.4 KiB
Dart

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<void> initialize() async {
if (await getConfig('regles_version') != null) {
await _syncDefaultRules();
}
if (await getConfig('recompenses_version') != null) {
await _syncDefaultRewards();
}
}
Future<void> importDefaultPack() async {
await _syncDefaultRules();
await _syncDefaultRewards();
}
Future<void> _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<void> _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<String?> getConfig(String key) async {
final row = await (select(config)..where((c) => c.cle.equals(key)))
.getSingleOrNull();
return row?.valeur;
}
Future<void> setConfig(String key, String value) async {
await into(config).insertOnConflictUpdate(
ConfigCompanion.insert(cle: key, valeur: value),
);
}
Future<bool> isOnboardingComplete() async {
return await getConfig('onboarding_complete') == 'true';
}
Future<void> completeOnboarding() async {
await setConfig('onboarding_complete', 'true');
}
Future<List<Enfant>> listChildren() {
return (select(enfants)..orderBy([(e) => OrderingTerm.asc(e.ordre)])).get();
}
Future<int> countChildren() async {
final count = await enfants.count().getSingle();
return count;
}
Future<int> 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<void> setPinHash(String hash) async {
await setConfig('pin_hash', hash);
}
Future<String?> getPinHash() => getConfig('pin_hash');
Future<List<Regle>> listActiveRules() {
return (select(regles)
..where((r) => r.actif.equals(true))
..orderBy([
(r) => OrderingTerm.asc(r.familleOrdre),
(r) => OrderingTerm.asc(r.ordre),
]))
.get();
}
Future<List<MovementDetail>> 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<Enfant?> 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<Enfant?> 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);
});
}