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>
This commit is contained in:
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -137,6 +138,83 @@ class AppDatabase extends _$AppDatabase {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
class MovementDetail {
|
||||
const MovementDetail({
|
||||
required this.id,
|
||||
required this.childId,
|
||||
required this.childName,
|
||||
required this.delta,
|
||||
required this.createdAt,
|
||||
this.note,
|
||||
this.ruleLabel,
|
||||
this.ruleIcon,
|
||||
this.rewardLabel,
|
||||
this.rewardIcon,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final int childId;
|
||||
final String childName;
|
||||
final int delta;
|
||||
final DateTime createdAt;
|
||||
final String? note;
|
||||
final String? ruleLabel;
|
||||
final String? ruleIcon;
|
||||
final String? rewardLabel;
|
||||
final String? rewardIcon;
|
||||
|
||||
String get label =>
|
||||
rewardLabel ?? ruleLabel ?? note ?? 'Mouvement';
|
||||
|
||||
String get icon => rewardIcon ?? ruleIcon ?? '📋';
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:bcrypt/bcrypt.dart';
|
||||
|
||||
import '../database/app_database.dart';
|
||||
import '../models/movement_detail.dart';
|
||||
|
||||
class ParentRepository {
|
||||
ParentRepository(this._db);
|
||||
|
||||
final AppDatabase _db;
|
||||
|
||||
Future<bool> verifyPin(String pin) async {
|
||||
final hash = await _db.getPinHash();
|
||||
if (hash == null) return false;
|
||||
return BCrypt.checkpw(pin, hash);
|
||||
}
|
||||
|
||||
Future<List<Regle>> listRules() => _db.listActiveRules();
|
||||
|
||||
Future<List<MovementDetail>> listMovements({int limit = 30}) {
|
||||
return _db.listRecentMovements(limit: limit);
|
||||
}
|
||||
|
||||
Future<Enfant?> applyRule(int childId, int ruleId) {
|
||||
return _db.applyRule(childId, ruleId);
|
||||
}
|
||||
|
||||
Future<Enfant?> cancelMovement(int movementId) {
|
||||
return _db.cancelMovement(movementId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user