From e483fd9070bdec52f9c99cfec56b49767ef3f305 Mon Sep 17 00:00:00 2001 From: Mathew Date: Sat, 18 Jul 2026 11:51:32 +0200 Subject: [PATCH] (Feat) Migration PocketBase --- .claude/CLAUDE.md | 140 +++++++++++++ lib/main.dart | 13 +- lib/models/client.dart | 2 +- lib/models/mouvement.dart | 8 +- lib/pocketbase_config.dart | 35 ++++ lib/screens/auth_gate.dart | 48 +---- lib/screens/client_detail_screen.dart | 23 ++ lib/screens/reglages_screen.dart | 110 +++++++++- lib/screens/staff_login_screen.dart | 24 ++- lib/services/client_repository.dart | 166 +++++++++------ lib/services/recompense_repository.dart | 58 +++-- lib/services/reglages.dart | 55 +++-- lib/supabase_config.dart | 22 -- pubspec.lock | 268 +----------------------- pubspec.yaml | 3 +- 15 files changed, 522 insertions(+), 453 deletions(-) create mode 100644 .claude/CLAUDE.md create mode 100644 lib/pocketbase_config.dart delete mode 100644 lib/supabase_config.dart diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..abfe504 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,140 @@ +# CLAUDE.md — App Fidélité (tablette magasin) + +Guide pour travailler sur ce projet. À lire avant toute modification. + +## Vue d'ensemble + +Application **Flutter** de **programme de fidélité** pour un magasin, sur +**tablette Android en portrait** (caisse). Chaque client a une carte = un **QR +code** ; le staff scanne, ajoute des **factures** (montant € → **points**), et le +client échange ses points contre des **récompenses**. + +Fait partie d'un écosystème de **deux apps** partageant le même backend : + +- **`app_fideliter`** (CE repo) : app **tablette / staff**. +- **`app_fideliter_client`** (repo séparé) : app **client** (compte, points, QR). + +App sœur `gestion_prix_produit` (autre projet) : même stack et mêmes conventions, +reprises ici. Thème identique mais **accent vert émeraude** (au lieu du bleu) pour +distinguer les apps sur une même tablette. + +## Stack technique + +- **Flutter** (Material 3), Dart. Cible : Android tablette, portrait verrouillé. +- **PocketBase** comme backend (auto-hébergé sur un **Raspberry Pi**) : + auth + base SQLite + API REST + **temps réel (SSE)**. +- Packages clés : + - `pocketbase` — client backend (auth, CRUD, réaltime) + - `shared_preferences` — persistance de la session (rester connecté) + - `mobile_scanner` — scan du QR client (caméra ; scanette externe à venir) + - `qr_flutter` — affichage/génération du QR d'un client + - `intl` — formatage € et dates (locale `fr_FR`) +- Icône via `flutter_launcher_icons` (source : `assets/icon/`). + +## Backend PocketBase + +- **URL serveur** : `https://db.tailb756e1.ts.net` (définie dans + [`lib/pocketbase_config.dart`](../lib/pocketbase_config.dart)). Le Raspberry est + exposé **publiquement en HTTPS via Tailscale Funnel** (gratuit) → accessible de + partout (4G, autre WiFi…), pas seulement sur le LAN. En local, le serveur reste + aussi joignable en `http://192.168.1.32:8090`. Tailscale est installé **sur le + Pi uniquement** ; les clients n'installent que l'app. Le Pi doit rester allumé + + connecté à Internet. +- **Compte staff** : `mathew.simon2004@gmail.com` (mot de passe défini au setup). + Un compte est « staff » si son champ `is_staff = true` dans la collection `users`. +- **Superuser** (admin PocketBase) : géré via l'UI `http://192.168.1.32:8090/_/`. + +### Collections + +| Collection | Type | Champs principaux | Accès (règles) | +|---|---|---|---| +| `users` | auth | `is_staff` (bool) + email/password | chacun voit son compte ; `is_staff` non modifiable par l'utilisateur | +| `clients` | base | `user` (relation, option.), `code` (unique), `nom`, `prenom`, `telephone`, `points` | staff = tout ; client = sa fiche uniquement | +| `mouvements` | base | `client` (relation), `type` (facture/recompense/ajustement), `montant_euros`, `points`, `libelle`, `created` | staff écrit ; client lit les siens (anti-triche) | +| `recompenses` | base | `nom`, `cout_points`, `actif` | tout connecté lit ; staff modifie | +| `reglages` | base | `euros_par_point`, `nom_magasin` (1 seule ligne) | tout connecté lit ; staff modifie | + +### Points de vigilance backend + +- **Pas de trigger PocketBase** : le solde `clients.points` est maintenu **par + l'app** (`ClientRepository._ecrireSolde`), qui crée le mouvement PUIS met à jour + le solde. Comme seul le staff écrit, pas de souci de concurrence. +- **Code client** (`FID-XXXXXX`) généré **côté app** (`ClientRepository._genererCode`), + unicité garantie par un index unique + réessais. +- Créer/modifier des collections : voir l'admin UI, ou l'API superuser + (`POST /api/collections/_superusers/auth-with-password` puis `/api/collections`). + +## Architecture de l'app + +- **Singletons** `.instance` pour les services/repositories. +- **Repositories** `ChangeNotifier` = source de vérité, écoutés via `AnimatedBuilder`. +- **Temps réel** : chaque repository s'abonne (`pb.collection(...).subscribe`) et + met à jour sa liste en mémoire → l'UI se synchronise entre tous les appareils. +- **Auth** : `AuthGate` écoute `pb.authStore.onChange`. Session persistée via + `AsyncAuthStore` (SharedPreferences) → on reste connecté. + +### Structure + +```text +lib/ +├── main.dart # init (locale fr, PocketBase) + thème +├── config.dart # constantes (ratio défaut, préfixe code) +├── pocketbase_config.dart # URL serveur, client `pb`, helper `estStaff` +├── theme.dart # thème noir & blanc + accent vert émeraude +├── models/ # client, mouvement, recompense (fromMap PocketBase) +├── services/ +│ ├── reglages.dart # ratio €/point + nom magasin (+ temps réel) +│ ├── client_repository.dart # clients + factures + récompenses + points (+ temps réel) +│ └── recompense_repository.dart# catalogue (+ temps réel) +├── screens/ +│ ├── auth_gate.dart # login staff ↔ app selon la session +│ ├── staff_login_screen.dart +│ ├── home_shell.dart # 4 onglets +│ ├── clients_screen.dart # liste + recherche + création +│ ├── client_edit_screen.dart # Nom / Prénom / Téléphone +│ ├── client_detail_screen.dart # solde, facture, récompense, historique (édit/suppr, temps réel) +│ ├── scan_screen.dart # scan QR → fiche client (_ouvrirParCode) +│ ├── recompenses_screen.dart +│ └── reglages_screen.dart # ratio, nom magasin, état serveur, déconnexion +├── widgets/ # pastille_points, qr_client +└── utils/format.dart # euro(), points(), dateHeure() (fr_FR) +``` + +## Règles métier + +- **Points gagnés = montant facture ÷ `euros_par_point`** (fractionnaires, ex. + 12 € à 10 €/pt = 1,2 pt). +- Changer le ratio n'affecte **que les nouvelles factures** (pas de recalcul + rétroactif — volontaire). +- Éditer une facture recalcule ses points au **ratio courant** et réajuste le solde. +- Récompenses : gagner **et** dépenser des points (débit à l'utilisation). + +## Conventions de code + +- **Tout en français** : noms de variables, méthodes, commentaires, libellés UI. +- Reprendre le style existant (singletons, `ChangeNotifier`, `copyWith`, `fromMap`). +- Modèles : `fromMap(record.toJson())` où `record` est un `RecordModel` PocketBase. + Les relations sont des **id** (string) ; `created` est une date ISO (string). + +## Commandes + +```bash +flutter pub get +flutter analyze # doit rester à 0 problème +flutter test +flutter build apk --release +# Installer en gardant les données (session conservée) : +adb -s install -r build/app/outputs/flutter-apk/app-release.apk +adb devices # lister les appareils branchés +``` + +Icône : `dart run flutter_launcher_icons` après modif de `assets/icon/`. + +## Roadmap / à faire + +- **Scanette USB externe** : se comporte comme un clavier → brancher un champ + caché qui appelle `scan_screen.dart` → `_ouvrirParCode` (déjà factorisé). +- **Accès externe** au serveur (hors LAN) : IP/domaine public + **HTTPS** (puis + retirer `usesCleartextTraffic`). +- Impression du QR (carte physique), bonus de bienvenue / paliers, statistiques. +- App client (`app_fideliter_client`) : à aligner sur ce backend PocketBase. diff --git a/lib/main.dart b/lib/main.dart index a44b79f..ccf31ea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/date_symbol_data_local.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import 'pocketbase_config.dart'; import 'screens/auth_gate.dart'; -import 'supabase_config.dart'; import 'theme.dart'; Future main() async { @@ -19,14 +18,8 @@ Future main() async { DeviceOrientation.portraitDown, ]); - // Connexion au backend Supabase (partagé avec l'app client). - if (SupabaseConfig.estConfigure) { - await Supabase.initialize( - url: SupabaseConfig.url, - // Accepte la clé « anon public » (ou « publishable ») du projet. - publishableKey: SupabaseConfig.anonKey, - ); - } + // Connexion au backend PocketBase (Raspberry Pi), avec session persistée. + await initPocketBase(); runApp(const MonApp()); } diff --git a/lib/models/client.dart b/lib/models/client.dart index bec8f6a..52dd2d6 100644 --- a/lib/models/client.dart +++ b/lib/models/client.dart @@ -1,7 +1,7 @@ /// Un client fidélité. Le [code] est ce qui est encodé dans son QR code : /// scanner le QR permet de retrouver le compte. class Client { - final String id; // uuid Supabase + final String id; // id PocketBase /// Code unique du compte (encodé dans le QR), ex : « FID-3F9K2A ». final String code; diff --git a/lib/models/mouvement.dart b/lib/models/mouvement.dart index 55e044c..f44181e 100644 --- a/lib/models/mouvement.dart +++ b/lib/models/mouvement.dart @@ -41,12 +41,14 @@ class Mouvement { factory Mouvement.fromMap(Map map) { return Mouvement( id: map['id'].toString(), - clientId: map['client_id'].toString(), - type: (map['type'] as String?) ?? TypeMouvement.ajustement, + clientId: (map['client'] ?? '').toString(), + type: (map['type'] as String?)?.isNotEmpty == true + ? map['type'] as String + : TypeMouvement.ajustement, montantEuros: (map['montant_euros'] as num?)?.toDouble(), points: (map['points'] as num?)?.toDouble() ?? 0, libelle: (map['libelle'] as String?) ?? '', - date: DateTime.tryParse(map['created_at']?.toString() ?? '')?.toLocal() ?? + date: DateTime.tryParse(map['created']?.toString() ?? '')?.toLocal() ?? DateTime.fromMillisecondsSinceEpoch(0), ); } diff --git a/lib/pocketbase_config.dart b/lib/pocketbase_config.dart new file mode 100644 index 0000000..3b28a51 --- /dev/null +++ b/lib/pocketbase_config.dart @@ -0,0 +1,35 @@ +import 'package:pocketbase/pocketbase.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Connexion au backend PocketBase (auto-hébergé sur le Raspberry Pi). +class PocketBaseConfig { + PocketBaseConfig._(); + + /// URL du serveur PocketBase, exposé publiquement en HTTPS via Tailscale + /// Funnel (sur le Raspberry). Accessible de partout (4G, autre WiFi…). + /// Accès local direct (dans le bar) possible via http://192.168.1.32:8090 + /// mais on garde l'URL publique partout pour que ça marche en tout lieu. + static const String url = 'https://db.tailb756e1.ts.net'; + + static const _cleAuth = 'pb_auth'; +} + +/// Client PocketBase global (initialisé par [initPocketBase]). +late final PocketBase pb; + +/// Initialise le client PocketBase avec une session persistée (SharedPreferences), +/// pour rester connecté d'une ouverture de l'app à l'autre. +Future initPocketBase() async { + final prefs = await SharedPreferences.getInstance(); + final store = AsyncAuthStore( + save: (data) async => prefs.setString(PocketBaseConfig._cleAuth, data), + initial: prefs.getString(PocketBaseConfig._cleAuth), + clear: () async => prefs.remove(PocketBaseConfig._cleAuth), + ); + pb = PocketBase(PocketBaseConfig.url, authStore: store); +} + +/// Vrai si l'utilisateur connecté fait partie du personnel du magasin. +bool get estStaff => + pb.authStore.isValid && + (pb.authStore.record?.getBoolValue('is_staff') ?? false); diff --git a/lib/screens/auth_gate.dart b/lib/screens/auth_gate.dart index 50454b9..d097bf8 100644 --- a/lib/screens/auth_gate.dart +++ b/lib/screens/auth_gate.dart @@ -1,35 +1,30 @@ import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import '../pocketbase_config.dart'; import '../services/client_repository.dart'; import '../services/recompense_repository.dart'; import '../services/reglages.dart'; -import '../supabase_config.dart'; import 'home_shell.dart'; import 'staff_login_screen.dart'; /// Aiguille entre l'écran de connexion (staff non connecté) et l'app. -/// Écoute l'état d'authentification Supabase en continu. +/// Réagit aux changements de session PocketBase. class AuthGate extends StatelessWidget { const AuthGate({super.key}); @override Widget build(BuildContext context) { - if (!SupabaseConfig.estConfigure) return const _EcranNonConfigure(); - - return StreamBuilder( - stream: supabase.auth.onAuthStateChange, + return StreamBuilder( + stream: pb.authStore.onChange, builder: (context, snapshot) { - final session = supabase.auth.currentSession; - if (session == null) return const StaffLoginScreen(); + if (!estStaff) return const StaffLoginScreen(); return const _AppChargee(); }, ); } } -/// Charge les données (réglages, clients, récompenses) après connexion, puis -/// affiche l'app. Recharge si le compte connecté change. +/// Charge les données (réglages, clients, récompenses) après connexion. class _AppChargee extends StatefulWidget { const _AppChargee(); @@ -67,34 +62,3 @@ class _AppChargeeState extends State<_AppChargee> { ); } } - -class _EcranNonConfigure extends StatelessWidget { - const _EcranNonConfigure(); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - mainAxisSize: MainAxisSize.min, - children: const [ - Icon(Icons.cloud_off, size: 56, color: Colors.grey), - SizedBox(height: 16), - Text('Supabase non configuré', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), - SizedBox(height: 8), - Text( - 'Renseignez l\'URL et la clé anon dans lib/supabase_config.dart.\n' - 'Voir supabase/SETUP.md', - textAlign: TextAlign.center, - style: TextStyle(color: Color(0xFF6B6B72)), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/screens/client_detail_screen.dart b/lib/screens/client_detail_screen.dart index f92c9a5..24095aa 100644 --- a/lib/screens/client_detail_screen.dart +++ b/lib/screens/client_detail_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import '../models/client.dart'; import '../models/mouvement.dart'; import '../models/recompense.dart'; +import '../pocketbase_config.dart'; import '../services/client_repository.dart'; import '../services/recompense_repository.dart'; import '../services/reglages.dart'; @@ -26,11 +27,33 @@ class _ClientDetailScreenState extends State { final _repo = ClientRepository.instance; List _mouvements = []; bool _chargeMouvements = true; + Future Function()? _desabonner; @override void initState() { super.initState(); _rechargerMouvements(); + _sabonnerMouvements(); + } + + @override + void dispose() { + _desabonner?.call(); + super.dispose(); + } + + /// Historique en temps réel : si un autre appareil ajoute/retire un mouvement + /// pour ce client, la liste se met à jour toute seule. + Future _sabonnerMouvements() async { + try { + _desabonner = await pb.collection('mouvements').subscribe('*', (e) { + final rec = e.record; + if (rec != null && rec.data['client'] != widget.clientId) return; + _rechargerMouvements(); + }); + } catch (_) { + // Temps réel indisponible : on garde le rafraîchissement manuel. + } } Future _rechargerMouvements() async { diff --git a/lib/screens/reglages_screen.dart b/lib/screens/reglages_screen.dart index 7319875..5f6054f 100644 --- a/lib/screens/reglages_screen.dart +++ b/lib/screens/reglages_screen.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import '../pocketbase_config.dart'; +import '../services/client_repository.dart'; import '../services/reglages.dart'; -import '../supabase_config.dart'; import '../theme.dart'; import '../utils/format.dart'; @@ -19,6 +20,8 @@ class _ReglagesScreenState extends State { final _reglages = Reglages.instance; late final TextEditingController _ratioCtrl; late final TextEditingController _nomCtrl; + bool? _serveurOk; // null = en cours de vérification + bool _verifEnCours = false; @override void initState() { @@ -27,6 +30,24 @@ class _ReglagesScreenState extends State { text: _formaterRatio(_reglages.eurosParPoint), ); _nomCtrl = TextEditingController(text: _reglages.nomMagasin); + _verifierServeur(); + } + + Future _verifierServeur() async { + setState(() => _verifEnCours = true); + bool ok; + try { + await pb.health.check(); + ok = true; + } catch (_) { + ok = false; + } + if (mounted) { + setState(() { + _serveurOk = ok; + _verifEnCours = false; + }); + } } @override @@ -155,12 +176,15 @@ class _ReglagesScreenState extends State { ), ), const SizedBox(height: 24), + _titre('Serveur'), + _carteServeur(), + const SizedBox(height: 24), _titre('À propos'), const Card( child: Padding( padding: EdgeInsets.all(16), child: Text( - 'Données partagées via Supabase avec l\'app client. ' + 'Données hébergées sur le serveur PocketBase du magasin. ' 'Le scan se fait avec la caméra ; la scanette externe sera ' 'branchée dans une prochaine version.', style: TextStyle(color: AppTheme.grisTexte, height: 1.4), @@ -197,7 +221,10 @@ class _ReglagesScreenState extends State { ); } - String? _emailConnecte() => supabase.auth.currentUser?.email; + String? _emailConnecte() { + final email = pb.authStore.record?.getStringValue('email'); + return (email == null || email.isEmpty) ? null : email; + } Future _deconnexion() async { final ok = await showDialog( @@ -217,7 +244,7 @@ class _ReglagesScreenState extends State { ], ), ); - if (ok == true) await supabase.auth.signOut(); + if (ok == true) pb.authStore.clear(); } Widget _apercus() { @@ -255,6 +282,81 @@ class _ReglagesScreenState extends State { ); } + Widget _carteServeur() { + final syncOk = ClientRepository.instance.tempsReelActif; + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.dns_outlined, + size: 18, color: AppTheme.grisTexte), + const SizedBox(width: 8), + Expanded( + child: Text( + PocketBaseConfig.url, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + ], + ), + const Divider(height: 22), + Row( + children: [ + Expanded( + child: _verifEnCours + ? _ligneStatut(null, 'Vérification…') + : _ligneStatut(_serveurOk, 'Connecté au serveur', + koTexte: 'Serveur injoignable'), + ), + if (_verifEnCours) + const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + IconButton( + tooltip: 'Revérifier', + icon: const Icon(Icons.refresh), + onPressed: _verifierServeur, + ), + ], + ), + const SizedBox(height: 8), + _ligneStatut(syncOk, 'Synchronisation temps réel active', + koTexte: 'Temps réel inactif'), + ], + ), + ), + ); + } + + Widget _ligneStatut(bool? ok, String texte, {String? koTexte}) { + final couleur = ok == null + ? AppTheme.grisTexte + : (ok ? Colors.green.shade600 : Colors.red.shade600); + return Row( + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration(color: couleur, shape: BoxShape.circle), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + (ok == false && koTexte != null) ? koTexte : texte, + style: TextStyle(color: couleur, fontWeight: FontWeight.w600), + ), + ), + ], + ); + } + Widget _titre(String texte) { return Padding( padding: const EdgeInsets.fromLTRB(4, 0, 4, 10), diff --git a/lib/screens/staff_login_screen.dart b/lib/screens/staff_login_screen.dart index ea737ef..9fcb77f 100644 --- a/lib/screens/staff_login_screen.dart +++ b/lib/screens/staff_login_screen.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:pocketbase/pocketbase.dart'; -import '../supabase_config.dart'; +import '../pocketbase_config.dart'; import '../theme.dart'; /// Connexion du personnel du magasin (staff). Les comptes staff sont créés dans -/// Supabase (voir SETUP.md) — pas d'inscription libre ici. +/// PocketBase (champ is_staff = true) — pas d'inscription libre ici. class StaffLoginScreen extends StatefulWidget { const StaffLoginScreen({super.key}); @@ -35,22 +35,24 @@ class _StaffLoginScreenState extends State { _erreur = null; }); try { - await supabase.auth.signInWithPassword( - email: _emailCtrl.text.trim(), - password: _mdpCtrl.text, - ); + await pb.collection('users').authWithPassword( + _emailCtrl.text.trim(), + _mdpCtrl.text, + ); // Vérifie que ce compte est bien autorisé (staff). - final estStaff = await supabase.rpc('est_staff') as bool? ?? false; if (!estStaff) { - await supabase.auth.signOut(); + pb.authStore.clear(); if (mounted) { setState(() => _erreur = 'Ce compte n\'est pas autorisé pour la tablette (staff).'); } } // Si staff : l'AuthGate bascule automatiquement vers l'app. - } on AuthException catch (e) { - if (mounted) setState(() => _erreur = e.message); + } on ClientException catch (e) { + if (mounted) { + setState(() => _erreur = 'Identifiants incorrects'); + } + debugPrint('Login error: ${e.response}'); } catch (e) { if (mounted) setState(() => _erreur = 'Erreur : $e'); } finally { diff --git a/lib/services/client_repository.dart b/lib/services/client_repository.dart index 8337806..649b21d 100644 --- a/lib/services/client_repository.dart +++ b/lib/services/client_repository.dart @@ -1,9 +1,12 @@ +import 'dart:math'; + import 'package:flutter/foundation.dart'; +import '../config.dart'; import '../models/client.dart'; import '../models/mouvement.dart'; import '../models/recompense.dart'; -import '../supabase_config.dart'; +import '../pocketbase_config.dart'; import '../utils/format.dart'; import 'reglages.dart'; @@ -15,9 +18,10 @@ class SoldeInsuffisant implements Exception { 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. +/// 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._(); @@ -25,11 +29,13 @@ class ClientRepository extends ChangeNotifier { final List _clients = []; bool _charge = false; bool _enCours = false; + bool _abonne = false; String? _erreur; List get clients => List.unmodifiable(_clients); bool get charge => _charge; bool get enCours => _enCours; + bool get tempsReelActif => _abonne; String? get erreur => _erreur; Future charger() async { @@ -37,11 +43,12 @@ class ClientRepository extends ChangeNotifier { _erreur = null; notifyListeners(); try { - final rows = await supabase.from('clients').select().order('nom'); + final rows = await pb.collection('clients').getFullList(sort: 'nom'); _clients ..clear() - ..addAll((rows as List).map((e) => Client.fromMap(e))); + ..addAll(rows.map((r) => Client.fromMap(r.toJson()))); _charge = true; + await _sabonner(); } catch (e) { _erreur = e.toString(); } finally { @@ -50,6 +57,34 @@ class ClientRepository extends ChangeNotifier { } } + /// Abonnement temps réel : la liste et les soldes se synchronisent en direct + /// entre tous les appareils (autre tablette, app client…). + Future _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; @@ -64,7 +99,6 @@ class ClientRepository extends ChangeNotifier { 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; @@ -77,36 +111,41 @@ class ClientRepository extends ChangeNotifier { .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. + /// Crée un client « au comptoir » avec un code unique généré par l'app. Future creer({ required String nom, String? prenom, String? telephone, }) async { - final row = await supabase - .from('clients') - .insert({ + // 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), - }) - .select() - .single(); - final cree = Client.fromMap(row); - _clients.add(cree); - _trier(); - notifyListeners(); - return cree; + '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'); } - /// Modifie nom / prénom / téléphone (pas le solde : il évolue via facture). Future modifier(Client c) async { - await supabase.from('clients').update({ + await pb.collection('clients').update(c.id, body: { '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] = @@ -116,35 +155,29 @@ class ClientRepository extends ChangeNotifier { 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); + 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é. - /// 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, + await pb.collection('mouvements').create(body: { + 'client': clientId, 'type': TypeMouvement.facture, 'montant_euros': montantEuros, 'points': gagnes, 'libelle': 'Facture ${euro(montantEuros)}', }); - _majSoldeMemoire(clientId, gagnes); + await _ecrireSolde(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, @@ -154,70 +187,77 @@ class ClientRepository extends ChangeNotifier { if (client.points < recompense.coutPoints) { throw SoldeInsuffisant(recompense.coutPoints - client.points); } - await supabase.from('mouvements').insert({ - 'client_id': clientId, + await pb.collection('mouvements').create(body: { + 'client': clientId, 'type': TypeMouvement.recompense, 'points': -recompense.coutPoints, 'libelle': recompense.nom, }); - _majSoldeMemoire(clientId, -recompense.coutPoints); + await _ecrireSolde(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, + await pb.collection('mouvements').create(body: { + 'client': clientId, 'type': TypeMouvement.ajustement, 'points': points, 'libelle': libelle, }); - _majSoldeMemoire(clientId, points); + await _ecrireSolde(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). + /// Corrige le montant d'une facture (recalcule les points au ratio courant). Future modifierFacture({ required Mouvement mouvement, required double nouveauMontant, }) async { final nouveauxPoints = Reglages.instance.pointsPour(nouveauMontant); - await supabase.from('mouvements').update({ + await pb.collection('mouvements').update(mouvement.id, body: { 'montant_euros': nouveauMontant, 'points': nouveauxPoints, 'libelle': 'Facture ${euro(nouveauMontant)}', - }).eq('id', mouvement.id); - _majSoldeMemoire(mouvement.clientId, nouveauxPoints - mouvement.points); + }); + await _ecrireSolde(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); + await pb.collection('mouvements').delete(mouvement.id); + await _ecrireSolde(mouvement.clientId, -mouvement.points); } - void _majSoldeMemoire(String clientId, double delta) { + Future> 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 _ecrireSolde(String clientId, double delta) async { final i = _clients.indexWhere((c) => c.id == clientId); - if (i != -1) { - _clients[i] = _clients[i].copyWith(points: _clients[i].points + delta); - } + 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())); } diff --git a/lib/services/recompense_repository.dart b/lib/services/recompense_repository.dart index 9deffce..18861f8 100644 --- a/lib/services/recompense_repository.dart +++ b/lib/services/recompense_repository.dart @@ -1,19 +1,19 @@ import 'package:flutter/foundation.dart'; import '../models/recompense.dart'; -import '../supabase_config.dart'; +import '../pocketbase_config.dart'; -/// Source de vérité du catalogue de récompenses (table `recompenses` Supabase). +/// Source de vérité du catalogue de récompenses (collection `recompenses`). class RecompenseRepository extends ChangeNotifier { RecompenseRepository._(); static final RecompenseRepository instance = RecompenseRepository._(); final List _recompenses = []; bool _charge = false; + bool _abonne = false; List get recompenses => List.unmodifiable(_recompenses); - /// Uniquement les récompenses proposées (actives), triées par coût croissant. List get actives { final l = _recompenses.where((r) => r.actif).toList() ..sort((a, b) => a.coutPoints.compareTo(b.coutPoints)); @@ -25,25 +25,53 @@ class RecompenseRepository extends ChangeNotifier { Future charger() async { try { final rows = - await supabase.from('recompenses').select().order('cout_points'); + await pb.collection('recompenses').getFullList(sort: 'cout_points'); _recompenses ..clear() - ..addAll((rows as List).map((e) => Recompense.fromMap(e))); + ..addAll(rows.map((r) => Recompense.fromMap(r.toJson()))); _charge = true; notifyListeners(); + await _sabonner(); } catch (_) { - // Ignore si non connecté ; sera rechargé après connexion. + // Ignoré si non connecté ; rechargé après connexion. + } + } + + /// Abonnement temps réel au catalogue (synchronisé entre appareils). + Future _sabonner() async { + if (_abonne) return; + _abonne = true; + try { + await pb.collection('recompenses').subscribe('*', (e) { + final rec = e.record; + if (rec == null) return; + if (e.action == 'delete') { + _recompenses.removeWhere((x) => x.id == rec.id); + } else { + final r = Recompense.fromMap(rec.toJson()); + final i = _recompenses.indexWhere((x) => x.id == r.id); + if (i == -1) { + _recompenses.add(r); + } else { + _recompenses[i] = r; + } + _trier(); + } + notifyListeners(); + }); + } catch (_) { + _abonne = false; } } Future creer( {required String nom, required double coutPoints}) async { - final row = await supabase - .from('recompenses') - .insert({'nom': nom.trim(), 'cout_points': coutPoints, 'actif': true}) - .select() - .single(); - final cree = Recompense.fromMap(row); + final rec = await pb.collection('recompenses').create(body: { + 'nom': nom.trim(), + 'cout_points': coutPoints, + 'actif': true, + }); + final cree = Recompense.fromMap(rec.toJson()); _recompenses.add(cree); _trier(); notifyListeners(); @@ -51,11 +79,11 @@ class RecompenseRepository extends ChangeNotifier { } Future modifier(Recompense r) async { - await supabase.from('recompenses').update({ + await pb.collection('recompenses').update(r.id, body: { 'nom': r.nom.trim(), 'cout_points': r.coutPoints, 'actif': r.actif, - }).eq('id', r.id); + }); final i = _recompenses.indexWhere((e) => e.id == r.id); if (i != -1) _recompenses[i] = r; _trier(); @@ -63,7 +91,7 @@ class RecompenseRepository extends ChangeNotifier { } Future supprimer(String id) async { - await supabase.from('recompenses').delete().eq('id', id); + await pb.collection('recompenses').delete(id); _recompenses.removeWhere((e) => e.id == id); notifyListeners(); } diff --git a/lib/services/reglages.dart b/lib/services/reglages.dart index 69d2f40..5549b60 100644 --- a/lib/services/reglages.dart +++ b/lib/services/reglages.dart @@ -1,55 +1,72 @@ import 'package:flutter/foundation.dart'; import '../config.dart'; -import '../supabase_config.dart'; +import '../pocketbase_config.dart'; -/// Réglages globaux du magasin (ligne unique `reglages` id=1 sur Supabase). -/// Étend [ChangeNotifier] pour que les écrans se rafraîchissent au changement. +/// Réglages globaux du magasin (unique enregistrement de la collection +/// `reglages` sur PocketBase). [ChangeNotifier] pour rafraîchir les écrans. class Reglages extends ChangeNotifier { Reglages._(); static final Reglages instance = Reglages._(); + String? _recordId; + bool _abonne = false; double _eurosParPoint = Config.eurosParPointParDefaut; String _nomMagasin = ''; - /// Nombre d'euros à dépenser pour gagner 1 point. double get eurosParPoint => _eurosParPoint; - - /// Nom du magasin, affiché sur la carte / le QR du client. String get nomMagasin => _nomMagasin; Future charger() async { try { - final row = await supabase - .from('reglages') - .select('euros_par_point, nom_magasin') - .eq('id', 1) - .maybeSingle(); - if (row != null) { - _eurosParPoint = (row['euros_par_point'] as num?)?.toDouble() ?? + final rows = await pb.collection('reglages').getFullList(); + if (rows.isNotEmpty) { + final r = rows.first; + _recordId = r.id; + _eurosParPoint = (r.toJson()['euros_par_point'] as num?)?.toDouble() ?? Config.eurosParPointParDefaut; - _nomMagasin = (row['nom_magasin'] as String?) ?? ''; + _nomMagasin = (r.toJson()['nom_magasin'] as String?) ?? ''; notifyListeners(); + await _sabonner(); } } catch (_) { - // Réglages par défaut si non connecté / hors-ligne. + // Valeurs par défaut si non connecté / serveur injoignable. + } + } + + /// Abonnement temps réel : un changement de ratio/nom se propage aux appareils. + Future _sabonner() async { + if (_abonne) return; + _abonne = true; + try { + await pb.collection('reglages').subscribe('*', (e) { + final rec = e.record; + if (rec == null) return; + _recordId = rec.id; + _eurosParPoint = (rec.toJson()['euros_par_point'] as num?)?.toDouble() ?? + _eurosParPoint; + _nomMagasin = (rec.toJson()['nom_magasin'] as String?) ?? _nomMagasin; + notifyListeners(); + }); + } catch (_) { + _abonne = false; } } - /// Points gagnés pour un montant donné, selon le ratio courant. double pointsPour(double euros) => _eurosParPoint > 0 ? euros / _eurosParPoint : 0; Future definirEurosParPoint(double valeur) async { - if (valeur <= 0) return; - await supabase.from('reglages').update({'euros_par_point': valeur}).eq('id', 1); + if (valeur <= 0 || _recordId == null) return; + await pb.collection('reglages').update(_recordId!, body: {'euros_par_point': valeur}); _eurosParPoint = valeur; notifyListeners(); } Future definirNomMagasin(String valeur) async { + if (_recordId == null) return; final v = valeur.trim(); - await supabase.from('reglages').update({'nom_magasin': v}).eq('id', 1); + await pb.collection('reglages').update(_recordId!, body: {'nom_magasin': v}); _nomMagasin = v; notifyListeners(); } diff --git a/lib/supabase_config.dart b/lib/supabase_config.dart deleted file mode 100644 index e0473c6..0000000 --- a/lib/supabase_config.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:supabase_flutter/supabase_flutter.dart'; - -/// Configuration de connexion au backend Supabase (partagé avec l'app client). -/// -/// ⚠️ À remplir avec les valeurs de VOTRE projet Supabase : -/// Project Settings → API → « Project URL » et « anon public ». -/// Voir le guide : supabase/SETUP.md -/// -/// La clé « anon » est publique et peut vivre dans l'app : la sécurité est -/// assurée par les règles RLS définies dans supabase/schema.sql. -class SupabaseConfig { - SupabaseConfig._(); - - static const String url = 'https://zfhcnzbggpdvgvosrkgp.supabase.co'; - static const String anonKey = 'sb_publishable_4R3-_q1Zy_usliTff01Ilg_jhFlIdPH'; - - static bool get estConfigure => - !url.contains('VOTRE-PROJET') && !anonKey.contains('VOTRE_CLE'); -} - -/// Raccourci vers le client Supabase (une fois [Supabase.initialize] appelé). -SupabaseClient get supabase => Supabase.instance.client; diff --git a/pubspec.lock b/pubspec.lock index 9e27147..dad121d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,38 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - app_links: - dependency: transitive - description: - name: app_links - sha256: "9d3c82f634c7f5b5c752f7ee46b67724246043f5e1d5fc1b433dd5b38d780dbe" - url: "https://pub.dev" - source: hosted - version: "7.2.0" - app_links_linux: - dependency: transitive - description: - name: app_links_linux - sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.dev" - source: hosted - version: "1.0.3" - app_links_platform_interface: - dependency: transitive - description: - name: app_links_platform_interface - sha256: "78a18580eecac98108d1eef52a7db668bc317714f5205e616973363326efe333" - url: "https://pub.dev" - source: hosted - version: "2.0.3" - app_links_web: - dependency: transitive - description: - name: app_links_web - sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.dev" - source: hosted - version: "1.0.4" archive: dependency: transitive description: @@ -105,22 +73,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -129,14 +81,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" - dart_jsonwebtoken: - dependency: transitive - description: - name: dart_jsonwebtoken - sha256: ad84e60181696513d04d5f2078e0bbc20365b911f46f647797317414bdc88fbe - url: "https://pub.dev" - source: hosted - version: "3.4.1" fake_async: dependency: transitive description: @@ -192,30 +136,6 @@ packages: description: flutter source: sdk version: "0.0.0" - functions_client: - dependency: transitive - description: - name: functions_client - sha256: "5b17b8dcf5ae1cd6a6428e7a4ca8b569477297a7f158955b5407dda23baeaa5b" - url: "https://pub.dev" - source: hosted - version: "2.6.3" - gotrue: - dependency: transitive - description: - name: gotrue - sha256: "54e8abe49c8596234c503f4b64d07022c7913a6475c7a85c1f1eacc1a93af166" - url: "https://pub.dev" - source: hosted - version: "2.24.0" - gtk: - dependency: transitive - description: - name: gtk - sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" - url: "https://pub.dev" - source: hosted - version: "2.2.0" http: dependency: transitive description: @@ -256,14 +176,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.12.0" - jwt_decode: - dependency: transitive - description: - name: jwt_decode - sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb - url: "https://pub.dev" - source: hosted - version: "0.3.1" leak_tracker: dependency: transitive description: @@ -296,14 +208,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" matcher: dependency: transitive description: @@ -328,14 +232,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" mobile_scanner: dependency: "direct main" description: @@ -344,14 +240,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.2.0" - passkeys_platform_interface: - dependency: transitive - description: - name: passkeys_platform_interface - sha256: "9610bd136b3382500390912ddd8517ee99505228b1af7b507b9c907f7e99a47d" - url: "https://pub.dev" - source: hosted - version: "2.8.0" path: dependency: transitive description: @@ -408,14 +296,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive + pocketbase: + dependency: "direct main" description: - name: pointycastle - sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + name: pocketbase + sha256: f70353ea9c583fabe4165880e054504c5036c428f59c0fc588d64bb19218df05 url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "0.22.0" posix: dependency: transitive description: @@ -424,14 +312,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" - postgrest: - dependency: transitive - description: - name: postgrest - sha256: "801f6659cffbb6372d38cb4deb0cd892cb0cb38d75e0686ff24f4a724e9f4999" - url: "https://pub.dev" - source: hosted - version: "2.7.4" qr: dependency: transitive description: @@ -448,32 +328,8 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - realtime_client: - dependency: transitive - description: - name: realtime_client - sha256: "3261ba98af41fccfdbb6c66299b5bd6871907e98bb41bce9d55e0fb91287ed85" - url: "https://pub.dev" - source: hosted - version: "2.9.1" - retry: - dependency: transitive - description: - name: retry - sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" - url: "https://pub.dev" - source: hosted - version: "3.1.2" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf @@ -549,14 +405,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" - storage_client: - dependency: transitive - description: - name: storage_client - sha256: "2cc5432c312c4a08687bcce795aff377d006e2892097c7f88d57512885bb4b10" - url: "https://pub.dev" - source: hosted - version: "2.5.9" stream_channel: dependency: transitive description: @@ -573,22 +421,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - supabase: - dependency: transitive - description: - name: supabase - sha256: d425df9330ce5f215a0ee708b084899d89031e8a8f3e096f46c38ea37df9fd7d - url: "https://pub.dev" - source: hosted - version: "2.13.3" - supabase_flutter: - dependency: "direct main" - description: - name: supabase_flutter - sha256: "262c7eda45997a308ac0311be82fe8e9134526a18125fe5e0cf0a58e9cd16cb6" - url: "https://pub.dev" - source: hosted - version: "2.15.3" term_glyph: dependency: transitive description: @@ -613,70 +445,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - url_launcher: - dependency: transitive - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 - url: "https://pub.dev" - source: hosted - version: "6.3.32" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" - source: hosted - version: "3.2.5" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" - url: "https://pub.dev" - source: hosted - version: "2.4.3" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" - source: hosted - version: "3.1.5" vector_math: dependency: transitive description: @@ -701,22 +469,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" - source: hosted - version: "3.0.3" xdg_directories: dependency: transitive description: @@ -741,14 +493,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" - yet_another_json_isolate: - dependency: transitive - description: - name: yet_another_json_isolate - sha256: eaa26beb5990b25a49d942374fd5a0c5aa67a837e03b14b4c26134aaa1ed01a9 - url: "https://pub.dev" - source: hosted - version: "2.1.1" sdks: dart: ">=3.12.2 <4.0.0" flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 20c0bbd..9845ddb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,8 @@ dependencies: cupertino_icons: ^1.0.8 mobile_scanner: ^7.2.0 # scan du QR code client (caméra en attendant la scanette) qr_flutter: ^4.1.0 # génération du QR code d'un client (affichage / impression) - supabase_flutter: ^2.8.0 # backend : auth staff + base de données partagée + pocketbase: ^0.22.0 # backend : auth staff + base de données (Raspberry Pi) + shared_preferences: ^2.5.5 # persistance de la session (rester connecté) intl: ^0.20.3 # formatage € et dates (locale fr_FR) dev_dependencies: