264 lines
8.1 KiB
Dart
264 lines
8.1 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import '../config.dart';
|
|
import '../models/client.dart';
|
|
import '../models/mouvement.dart';
|
|
import '../models/recompense.dart';
|
|
import '../pocketbase_config.dart';
|
|
import '../utils/format.dart';
|
|
import 'reglages.dart';
|
|
|
|
/// Erreur levée quand un client n'a pas assez de points pour une récompense.
|
|
class SoldeInsuffisant implements Exception {
|
|
final double manquant;
|
|
SoldeInsuffisant(this.manquant);
|
|
@override
|
|
String toString() => 'Solde insuffisant (${points(manquant)} manquants)';
|
|
}
|
|
|
|
/// Source de vérité des clients (côté staff), adossée à PocketBase.
|
|
/// Comme PocketBase n'a pas de trigger, le solde de points est maintenu par
|
|
/// l'app : à chaque mouvement, on crée l'enregistrement ET on met à jour le
|
|
/// solde du client. Seul le staff (la tablette) écrit → pas de concurrence.
|
|
class ClientRepository extends ChangeNotifier {
|
|
ClientRepository._();
|
|
static final ClientRepository instance = ClientRepository._();
|
|
|
|
final List<Client> _clients = [];
|
|
bool _charge = false;
|
|
bool _enCours = false;
|
|
bool _abonne = false;
|
|
String? _erreur;
|
|
|
|
List<Client> get clients => List.unmodifiable(_clients);
|
|
bool get charge => _charge;
|
|
bool get enCours => _enCours;
|
|
bool get tempsReelActif => _abonne;
|
|
String? get erreur => _erreur;
|
|
|
|
Future<void> charger() async {
|
|
_enCours = true;
|
|
_erreur = null;
|
|
notifyListeners();
|
|
try {
|
|
final rows = await pb.collection('clients').getFullList(sort: 'nom');
|
|
_clients
|
|
..clear()
|
|
..addAll(rows.map((r) => Client.fromMap(r.toJson())));
|
|
_charge = true;
|
|
await _sabonner();
|
|
} catch (e) {
|
|
_erreur = e.toString();
|
|
} finally {
|
|
_enCours = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Abonnement temps réel : la liste et les soldes se synchronisent en direct
|
|
/// entre tous les appareils (autre tablette, app client…).
|
|
Future<void> _sabonner() async {
|
|
if (_abonne) return;
|
|
_abonne = true;
|
|
try {
|
|
await pb.collection('clients').subscribe('*', (e) {
|
|
final rec = e.record;
|
|
if (rec == null) return;
|
|
if (e.action == 'delete') {
|
|
_clients.removeWhere((x) => x.id == rec.id);
|
|
} else {
|
|
final c = Client.fromMap(rec.toJson());
|
|
final i = _clients.indexWhere((x) => x.id == c.id);
|
|
if (i == -1) {
|
|
_clients.add(c);
|
|
} else {
|
|
_clients[i] = c;
|
|
}
|
|
_trier();
|
|
}
|
|
notifyListeners();
|
|
});
|
|
} catch (_) {
|
|
_abonne = false; // on réessaiera au prochain chargement
|
|
}
|
|
}
|
|
|
|
Client? parCode(String code) {
|
|
for (final c in _clients) {
|
|
if (c.code == code) return c;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Client? parId(String id) {
|
|
for (final c in _clients) {
|
|
if (c.id == id) return c;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
List<Client> rechercher(String requete) {
|
|
final q = requete.trim().toLowerCase();
|
|
if (q.isEmpty) return clients;
|
|
return _clients
|
|
.where((c) =>
|
|
c.nom.toLowerCase().contains(q) ||
|
|
(c.prenom?.toLowerCase().contains(q) ?? false) ||
|
|
(c.telephone?.toLowerCase().contains(q) ?? false) ||
|
|
c.code.toLowerCase().contains(q))
|
|
.toList();
|
|
}
|
|
|
|
/// Crée un client « au comptoir » avec un code unique généré par l'app.
|
|
Future<Client> creer({
|
|
required String nom,
|
|
String? prenom,
|
|
String? telephone,
|
|
}) async {
|
|
// Quelques essais en cas de collision (peu probable) sur le code.
|
|
Object? derniereErreur;
|
|
for (var essai = 0; essai < 8; essai++) {
|
|
try {
|
|
final rec = await pb.collection('clients').create(body: {
|
|
'code': _genererCode(),
|
|
'nom': nom.trim(),
|
|
'prenom': _ouNull(prenom),
|
|
'telephone': _ouNull(telephone),
|
|
'points': 0,
|
|
});
|
|
final cree = Client.fromMap(rec.toJson());
|
|
_clients.add(cree);
|
|
_trier();
|
|
notifyListeners();
|
|
return cree;
|
|
} catch (e) {
|
|
derniereErreur = e; // probable conflit d'index unique → on régénère
|
|
}
|
|
}
|
|
throw Exception('Création du client impossible : $derniereErreur');
|
|
}
|
|
|
|
Future<void> modifier(Client c) async {
|
|
await pb.collection('clients').update(c.id, body: {
|
|
'nom': c.nom.trim(),
|
|
'prenom': _ouNull(c.prenom),
|
|
'telephone': _ouNull(c.telephone),
|
|
});
|
|
final i = _clients.indexWhere((e) => e.id == c.id);
|
|
if (i != -1) {
|
|
_clients[i] =
|
|
_clients[i].copyWith(nom: c.nom, prenom: c.prenom, telephone: c.telephone);
|
|
}
|
|
_trier();
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> supprimer(String id) async {
|
|
await pb.collection('clients').delete(id);
|
|
_clients.removeWhere((e) => e.id == id);
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Ajoute une facture : crédite les points correspondant au montant dépensé.
|
|
Future<double> ajouterFacture({
|
|
required String clientId,
|
|
required double montantEuros,
|
|
}) async {
|
|
final gagnes = Reglages.instance.pointsPour(montantEuros);
|
|
await pb.collection('mouvements').create(body: {
|
|
'client': clientId,
|
|
'type': TypeMouvement.facture,
|
|
'montant_euros': montantEuros,
|
|
'points': gagnes,
|
|
'libelle': 'Facture ${euro(montantEuros)}',
|
|
});
|
|
await _ecrireSolde(clientId, gagnes);
|
|
return gagnes;
|
|
}
|
|
|
|
Future<void> utiliserRecompense({
|
|
required String clientId,
|
|
required Recompense recompense,
|
|
}) async {
|
|
final client = parId(clientId);
|
|
if (client == null) throw Exception('Client introuvable');
|
|
if (client.points < recompense.coutPoints) {
|
|
throw SoldeInsuffisant(recompense.coutPoints - client.points);
|
|
}
|
|
await pb.collection('mouvements').create(body: {
|
|
'client': clientId,
|
|
'type': TypeMouvement.recompense,
|
|
'points': -recompense.coutPoints,
|
|
'libelle': recompense.nom,
|
|
});
|
|
await _ecrireSolde(clientId, -recompense.coutPoints);
|
|
}
|
|
|
|
Future<void> ajuster({
|
|
required String clientId,
|
|
required double points,
|
|
required String libelle,
|
|
}) async {
|
|
await pb.collection('mouvements').create(body: {
|
|
'client': clientId,
|
|
'type': TypeMouvement.ajustement,
|
|
'points': points,
|
|
'libelle': libelle,
|
|
});
|
|
await _ecrireSolde(clientId, points);
|
|
}
|
|
|
|
/// Corrige le montant d'une facture (recalcule les points au ratio courant).
|
|
Future<void> modifierFacture({
|
|
required Mouvement mouvement,
|
|
required double nouveauMontant,
|
|
}) async {
|
|
final nouveauxPoints = Reglages.instance.pointsPour(nouveauMontant);
|
|
await pb.collection('mouvements').update(mouvement.id, body: {
|
|
'montant_euros': nouveauMontant,
|
|
'points': nouveauxPoints,
|
|
'libelle': 'Facture ${euro(nouveauMontant)}',
|
|
});
|
|
await _ecrireSolde(mouvement.clientId, nouveauxPoints - mouvement.points);
|
|
}
|
|
|
|
Future<void> supprimerMouvement(Mouvement mouvement) async {
|
|
await pb.collection('mouvements').delete(mouvement.id);
|
|
await _ecrireSolde(mouvement.clientId, -mouvement.points);
|
|
}
|
|
|
|
Future<List<Mouvement>> mouvements(String clientId) async {
|
|
final rows = await pb.collection('mouvements').getFullList(
|
|
filter: 'client = "$clientId"',
|
|
sort: '-created',
|
|
);
|
|
return rows.map((r) => Mouvement.fromMap(r.toJson())).toList();
|
|
}
|
|
|
|
/// Applique un delta au solde du client : met à jour la base ET la mémoire.
|
|
Future<void> _ecrireSolde(String clientId, double delta) async {
|
|
final i = _clients.indexWhere((c) => c.id == clientId);
|
|
final actuel = i != -1 ? _clients[i].points : 0.0;
|
|
final nouveau = actuel + delta;
|
|
await pb.collection('clients').update(clientId, body: {'points': nouveau});
|
|
if (i != -1) _clients[i] = _clients[i].copyWith(points: nouveau);
|
|
notifyListeners();
|
|
}
|
|
|
|
String _genererCode() {
|
|
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // sans I, O, 0, 1
|
|
final rnd = Random();
|
|
final suffixe =
|
|
List.generate(6, (_) => alphabet[rnd.nextInt(alphabet.length)]).join();
|
|
return '${Config.prefixeCode}$suffixe';
|
|
}
|
|
|
|
String? _ouNull(String? v) =>
|
|
(v == null || v.trim().isEmpty) ? null : v.trim();
|
|
|
|
void _trier() => _clients
|
|
.sort((a, b) => a.nom.toLowerCase().compareTo(b.nom.toLowerCase()));
|
|
}
|