import 'package:flutter/foundation.dart'; import '../models/client.dart'; import '../models/mouvement.dart'; import '../models/recompense.dart'; import '../supabase_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 à Supabase. /// Le solde de points est tenu à jour côté base (trigger) à chaque mouvement ; /// on reflète le delta en mémoire pour un affichage immédiat. class ClientRepository extends ChangeNotifier { ClientRepository._(); static final ClientRepository instance = ClientRepository._(); final List _clients = []; bool _charge = false; bool _enCours = false; String? _erreur; List get clients => List.unmodifiable(_clients); bool get charge => _charge; bool get enCours => _enCours; String? get erreur => _erreur; Future charger() async { _enCours = true; _erreur = null; notifyListeners(); try { final rows = await supabase.from('clients').select().order('nom'); _clients ..clear() ..addAll((rows as List).map((e) => Client.fromMap(e))); _charge = true; } catch (e) { _erreur = e.toString(); } finally { _enCours = false; notifyListeners(); } } 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; } /// Filtre par nom, téléphone ou code (recherche insensible à la casse). List 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 » (sans compte de connexion). Le code unique /// et le QR sont générés côté base. Future creer({ required String nom, String? prenom, String? telephone, }) async { final row = await supabase .from('clients') .insert({ 'nom': nom.trim(), 'prenom': _ouNull(prenom), 'telephone': _ouNull(telephone), }) .select() .single(); final cree = Client.fromMap(row); _clients.add(cree); _trier(); notifyListeners(); return cree; } /// Modifie nom / prénom / téléphone (pas le solde : il évolue via facture). Future modifier(Client c) async { await supabase.from('clients').update({ 'nom': c.nom.trim(), 'prenom': _ouNull(c.prenom), 'telephone': _ouNull(c.telephone), }).eq('id', c.id); 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(); } /// Renvoie null si la chaîne est vide/espaces, sinon la valeur nettoyée. String? _ouNull(String? v) => (v == null || v.trim().isEmpty) ? null : v.trim(); Future supprimer(String id) async { await supabase.from('clients').delete().eq('id', id); _clients.removeWhere((e) => e.id == id); notifyListeners(); } /// Ajoute une facture : crédite les points correspondant au montant dépensé. /// Retourne le nombre de points gagnés. Future ajouterFacture({ required String clientId, required double montantEuros, }) async { final gagnes = Reglages.instance.pointsPour(montantEuros); await supabase.from('mouvements').insert({ 'client_id': clientId, 'type': TypeMouvement.facture, 'montant_euros': montantEuros, 'points': gagnes, 'libelle': 'Facture ${euro(montantEuros)}', }); _majSoldeMemoire(clientId, gagnes); return gagnes; } /// Utilise une récompense : débite son coût. Lève [SoldeInsuffisant] si besoin. Future 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 supabase.from('mouvements').insert({ 'client_id': clientId, 'type': TypeMouvement.recompense, 'points': -recompense.coutPoints, 'libelle': recompense.nom, }); _majSoldeMemoire(clientId, -recompense.coutPoints); } /// Correction manuelle du solde (ajout ou retrait de points). Future ajuster({ required String clientId, required double points, required String libelle, }) async { await supabase.from('mouvements').insert({ 'client_id': clientId, 'type': TypeMouvement.ajustement, 'points': points, 'libelle': libelle, }); _majSoldeMemoire(clientId, points); } /// Historique d'un client, du plus récent au plus ancien. Future> mouvements(String clientId) async { final rows = await supabase .from('mouvements') .select() .eq('client_id', clientId) .order('created_at', ascending: false); return (rows as List).map((e) => Mouvement.fromMap(e)).toList(); } /// Corrige le montant d'une facture existante : recalcule les points au ratio /// courant et réajuste le solde du client (via le trigger côté base). Future modifierFacture({ required Mouvement mouvement, required double nouveauMontant, }) async { final nouveauxPoints = Reglages.instance.pointsPour(nouveauMontant); await supabase.from('mouvements').update({ 'montant_euros': nouveauMontant, 'points': nouveauxPoints, 'libelle': 'Facture ${euro(nouveauMontant)}', }).eq('id', mouvement.id); _majSoldeMemoire(mouvement.clientId, nouveauxPoints - mouvement.points); } /// Supprime un mouvement (facture, récompense ou ajustement). Le solde du /// client est réajusté automatiquement (trigger côté base). Future supprimerMouvement(Mouvement mouvement) async { await supabase.from('mouvements').delete().eq('id', mouvement.id); _majSoldeMemoire(mouvement.clientId, -mouvement.points); } void _majSoldeMemoire(String clientId, double delta) { final i = _clients.indexWhere((c) => c.id == clientId); if (i != -1) { _clients[i] = _clients[i].copyWith(points: _clients[i].points + delta); } notifyListeners(); } void _trier() => _clients .sort((a, b) => a.nom.toLowerCase().compareTo(b.nom.toLowerCase())); }