import 'package:flutter/material.dart'; import 'package:qr_flutter/qr_flutter.dart'; import '../models/mouvement.dart'; import '../models/recompense.dart'; import '../pocketbase_config.dart'; import '../services/session_client.dart'; import '../theme.dart'; import '../utils/format.dart'; /// Écran principal du client : sa carte de fidélité (points + QR), les /// récompenses qu'il peut viser, et son historique. class HomeClientScreen extends StatefulWidget { const HomeClientScreen({super.key}); @override State createState() => _HomeClientScreenState(); } class _HomeClientScreenState extends State with WidgetsBindingObserver { final _session = SessionClient.instance; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { // Rafraîchit le solde au retour dans l'app (après un passage en caisse). if (state == AppLifecycleState.resumed) _session.charger(); } Future _deconnexion() async { final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Se déconnecter ?'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Annuler'), ), FilledButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Déconnexion'), ), ], ), ); if (ok == true) { _session.vider(); pb.authStore.clear(); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_session.nomMagasin.isEmpty ? 'Ma fidélité' : _session.nomMagasin), actions: [ IconButton( tooltip: 'Rafraîchir', icon: const Icon(Icons.refresh), onPressed: () => _session.charger(), ), IconButton( tooltip: 'Se déconnecter', icon: const Icon(Icons.logout), onPressed: _deconnexion, ), ], ), body: AnimatedBuilder( animation: _session, builder: (context, _) { final client = _session.monClient; if (client == null) { return const Center(child: CircularProgressIndicator()); } return RefreshIndicator( onRefresh: () => _session.charger(), child: ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 32), children: [ _carteFidelite(client.nomComplet, client.code, client.points), const SizedBox(height: 24), if (_session.recompenses.isNotEmpty) ...[ _titre('Mes récompenses'), const SizedBox(height: 8), ..._session.recompenses.map( (r) => _ligneRecompense(r, client.points), ), const SizedBox(height: 24), ], _titre('Historique'), const SizedBox(height: 8), _historique(), ], ), ); }, ), ); } Widget _carteFidelite(String nom, String code, double pts) { return Card( child: Padding( padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), child: Column( children: [ Text(nom, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ const Icon(Icons.stars_rounded, color: AppTheme.accent, size: 34), const SizedBox(width: 8), Text( pointsNombre(pts), style: const TextStyle( fontSize: 48, fontWeight: FontWeight.w800, color: AppTheme.accent), ), const SizedBox(width: 6), const Padding( padding: EdgeInsets.only(bottom: 8), child: Text('pts', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: AppTheme.accent)), ), ], ), const SizedBox(height: 20), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFE0E0E4)), ), child: QrImageView( data: code, version: QrVersions.auto, size: 200, eyeStyle: const QrEyeStyle( eyeShape: QrEyeShape.square, color: AppTheme.noir, ), dataModuleStyle: const QrDataModuleStyle( dataModuleShape: QrDataModuleShape.square, color: AppTheme.noir, ), ), ), const SizedBox(height: 12), Text(code, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, letterSpacing: 2)), const SizedBox(height: 6), const Text( 'Présentez ce code en caisse pour cumuler vos points.', textAlign: TextAlign.center, style: TextStyle(fontSize: 12, color: AppTheme.grisTexte), ), ], ), ), ); } Widget _ligneRecompense(Recompense r, double solde) { final atteignable = solde >= r.coutPoints; final progression = r.coutPoints <= 0 ? 1.0 : (solde / r.coutPoints).clamp(0.0, 1.0); return Padding( padding: const EdgeInsets.only(bottom: 10), child: Card( child: Padding( padding: const EdgeInsets.all(14), child: Row( children: [ CircleAvatar( backgroundColor: atteignable ? AppTheme.accent.withValues(alpha: 0.14) : Colors.grey.withValues(alpha: 0.12), child: Icon(Icons.card_giftcard, color: atteignable ? AppTheme.accent : Colors.grey), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(r.nom, style: const TextStyle(fontWeight: FontWeight.w700)), const SizedBox(height: 6), ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: progression, minHeight: 6, backgroundColor: Colors.grey.withValues(alpha: 0.18), color: AppTheme.accent, ), ), const SizedBox(height: 6), Text( atteignable ? 'Disponible ! (${points(r.coutPoints)})' : 'Encore ${points(r.coutPoints - solde)} • coût ${points(r.coutPoints)}', style: TextStyle( fontSize: 12, color: atteignable ? AppTheme.accent : AppTheme.grisTexte, fontWeight: atteignable ? FontWeight.w700 : FontWeight.w500, ), ), ], ), ), ], ), ), ), ); } Widget _historique() { final mvts = _session.mouvements; if (mvts.isEmpty) { return const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Center( child: Text('Aucun mouvement pour l\'instant.', style: TextStyle(color: AppTheme.grisTexte)), ), ); } return Column(children: mvts.map(_ligneMouvement).toList()); } Widget _ligneMouvement(Mouvement m) { final gain = m.estGain; final couleur = gain ? AppTheme.accent : Colors.red.shade600; final icone = switch (m.type) { TypeMouvement.facture => Icons.receipt_long, TypeMouvement.recompense => Icons.card_giftcard, _ => Icons.tune, }; return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ CircleAvatar( radius: 20, backgroundColor: couleur.withValues(alpha: 0.12), child: Icon(icone, size: 20, color: couleur), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(m.libelle, style: const TextStyle(fontWeight: FontWeight.w600)), const SizedBox(height: 2), Text(dateHeure(m.date), style: const TextStyle( fontSize: 12, color: AppTheme.grisTexte)), ], ), ), Text( '${gain ? '+' : '−'}${points(m.points.abs())}', style: TextStyle(fontWeight: FontWeight.w700, color: couleur), ), ], ), ); } Widget _titre(String texte) => Text(texte, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)); }