Initialise l'app Flutter mobile : BDD Drift, onboarding sans compte et tableau de bord.
Phase 1 du MVP : schéma SQLite porté depuis le prototype web, seed des règles/récompenses, flux d'onboarding en 4 étapes et écran d'accueil minimal. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
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 '../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');
|
||||
|
||||
/// 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);
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Enfants extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get prenom => text()();
|
||||
TextColumn get dateNaissance => text().nullable()();
|
||||
IntColumn get score => integer().withDefault(const Constant(0))();
|
||||
TextColumn get couleur => text().withDefault(const Constant('#6C63FF'))();
|
||||
IntColumn get ordre => integer().withDefault(const Constant(0))();
|
||||
|
||||
@override
|
||||
List<Set<Column<Object>>>? get uniqueKeys => [
|
||||
{prenom},
|
||||
];
|
||||
}
|
||||
|
||||
class Regles extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get libelle => text()();
|
||||
IntColumn get points => integer()();
|
||||
TextColumn get icone => text().withDefault(const Constant('⭐'))();
|
||||
BoolColumn get actif => boolean().withDefault(const Constant(true))();
|
||||
IntColumn get ordre => integer().withDefault(const Constant(0))();
|
||||
TextColumn get famille => text().withDefault(const Constant('📋 Autre'))();
|
||||
IntColumn get familleOrdre => integer().withDefault(const Constant(99))();
|
||||
}
|
||||
|
||||
class Mouvements extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get enfantId => integer().references(Enfants, #id)();
|
||||
IntColumn get regleId => integer().nullable().references(Regles, #id)();
|
||||
IntColumn get recompenseId => integer().nullable().references(Recompenses, #id)();
|
||||
IntColumn get delta => integer()();
|
||||
TextColumn get note => text().nullable()();
|
||||
DateTimeColumn get creeLe => dateTime().withDefault(currentDateAndTime)();
|
||||
BoolColumn get annule => boolean().withDefault(const Constant(false))();
|
||||
}
|
||||
|
||||
class Recompenses extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get libelle => text()();
|
||||
IntColumn get coutPoints => integer()();
|
||||
TextColumn get icone => text().withDefault(const Constant('🎁'))();
|
||||
BoolColumn get actif => boolean().withDefault(const Constant(true))();
|
||||
IntColumn get ordre => integer().withDefault(const Constant(0))();
|
||||
}
|
||||
|
||||
class Config extends Table {
|
||||
TextColumn get cle => text()();
|
||||
TextColumn get valeur => text()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>>? get primaryKey => {cle};
|
||||
}
|
||||
Reference in New Issue
Block a user