371 lines
12 KiB
Dart
371 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import '../pocketbase_config.dart';
|
|
import '../services/client_repository.dart';
|
|
import '../services/reglages.dart';
|
|
import '../theme.dart';
|
|
import '../utils/format.dart';
|
|
|
|
/// Onglet 4 : réglages du programme de fidélité.
|
|
/// Le réglage clé est le ratio « combien d'euros pour 1 point ».
|
|
class ReglagesScreen extends StatefulWidget {
|
|
const ReglagesScreen({super.key});
|
|
|
|
@override
|
|
State<ReglagesScreen> createState() => _ReglagesScreenState();
|
|
}
|
|
|
|
class _ReglagesScreenState extends State<ReglagesScreen> {
|
|
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() {
|
|
super.initState();
|
|
_ratioCtrl = TextEditingController(
|
|
text: _formaterRatio(_reglages.eurosParPoint),
|
|
);
|
|
_nomCtrl = TextEditingController(text: _reglages.nomMagasin);
|
|
_verifierServeur();
|
|
}
|
|
|
|
Future<void> _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
|
|
void dispose() {
|
|
_ratioCtrl.dispose();
|
|
_nomCtrl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
String _formaterRatio(double v) {
|
|
return (v == v.roundToDouble() ? v.toInt().toString() : v.toString())
|
|
.replaceAll('.', ',');
|
|
}
|
|
|
|
Future<void> _enregistrerRatio() async {
|
|
final v = double.tryParse(_ratioCtrl.text.replaceAll(',', '.'));
|
|
if (v == null || v <= 0) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Valeur invalide (doit être > 0)')),
|
|
);
|
|
_ratioCtrl.text = _formaterRatio(_reglages.eurosParPoint);
|
|
return;
|
|
}
|
|
await _reglages.definirEurosParPoint(v);
|
|
_ratioCtrl.text = _formaterRatio(v);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Réglage enregistré')),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _enregistrerNom() async {
|
|
await _reglages.definirNomMagasin(_nomCtrl.text);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Réglages')),
|
|
body: AnimatedBuilder(
|
|
animation: _reglages,
|
|
builder: (context, _) {
|
|
return ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
_titre('Programme de fidélité'),
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Combien d\'euros dépensés pour gagner 1 point ?',
|
|
style: TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
const Text(
|
|
'1 point =',
|
|
style: TextStyle(
|
|
fontSize: 16, fontWeight: FontWeight.w600),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _ratioCtrl,
|
|
textAlign: TextAlign.center,
|
|
keyboardType: const TextInputType.numberWithOptions(
|
|
decimal: true),
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(
|
|
RegExp(r'[0-9.,]')),
|
|
],
|
|
onSubmitted: (_) => _enregistrerRatio(),
|
|
decoration: const InputDecoration(
|
|
suffixText: '€',
|
|
hintText: 'ex : 5',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: FilledButton(
|
|
onPressed: _enregistrerRatio,
|
|
child: const Text('Enregistrer'),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
_apercus(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
_titre('Magasin'),
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Nom affiché sur le QR code du client',
|
|
style: TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _nomCtrl,
|
|
textCapitalization: TextCapitalization.words,
|
|
onSubmitted: (_) => _enregistrerNom(),
|
|
onEditingComplete: _enregistrerNom,
|
|
decoration: const InputDecoration(
|
|
hintText: 'Ex : Boulangerie du Coin',
|
|
prefixIcon: Icon(Icons.storefront_outlined),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
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 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),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
if (_emailConnecte() != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: Text(
|
|
'Connecté en tant que ${_emailConnecte()}',
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 13, color: AppTheme.grisTexte),
|
|
),
|
|
),
|
|
OutlinedButton.icon(
|
|
onPressed: _deconnexion,
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.red.shade600,
|
|
minimumSize: const Size.fromHeight(52),
|
|
side: BorderSide(color: Colors.red.shade200),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14)),
|
|
),
|
|
icon: const Icon(Icons.logout),
|
|
label: const Text('Se déconnecter'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
String? _emailConnecte() {
|
|
final email = pb.authStore.record?.getStringValue('email');
|
|
return (email == null || email.isEmpty) ? null : email;
|
|
}
|
|
|
|
Future<void> _deconnexion() async {
|
|
final ok = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('Se déconnecter ?'),
|
|
content: const Text('Vous devrez ressaisir vos identifiants staff.'),
|
|
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) pb.authStore.clear();
|
|
}
|
|
|
|
Widget _apercus() {
|
|
const exemples = [10.0, 20.0, 25.0, 50.0];
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.accent.withValues(alpha: 0.07),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Aperçu',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppTheme.grisTexte)),
|
|
const SizedBox(height: 8),
|
|
for (final e in exemples)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('Facture de ${euro(e)}'),
|
|
Text('→ ${points(_reglages.pointsPour(e))}',
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w700, color: AppTheme.accent)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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),
|
|
child: Text(texte,
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppTheme.grisTexte)),
|
|
);
|
|
}
|
|
}
|