Files
2026-07-18 13:11:03 +02:00

310 lines
10 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<HomeClientScreen> createState() => _HomeClientScreenState();
}
class _HomeClientScreenState extends State<HomeClientScreen>
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<void> _deconnexion() async {
final ok = await showDialog<bool>(
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));
}