From cef740de26b5607aceec99e96d66aee97941797b Mon Sep 17 00:00:00 2001 From: bastien Date: Thu, 9 Jul 2026 23:08:28 +0200 Subject: [PATCH] Test validation lancement lavage --- .../wash_start_confirmation_dialog.dart | 160 ++++++++++++++++++ .../presentation/machine_action_screen.dart | 39 ++++- 2 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 lib/core/widgets/wash_start_confirmation_dialog.dart diff --git a/lib/core/widgets/wash_start_confirmation_dialog.dart b/lib/core/widgets/wash_start_confirmation_dialog.dart new file mode 100644 index 0000000..1bbac40 --- /dev/null +++ b/lib/core/widgets/wash_start_confirmation_dialog.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; + +import '../router/app_router.dart'; +import '../theme/app_colors.dart'; + +/// Fenêtre de confirmation avant le lancement d'un lavage. +class WashStartConfirmationDialog { + static Future show( + BuildContext context, { + required String machineName, + required double currentBalance, + required double price, + required NumberFormat currency, + }) async { + final remainingBalance = currentBalance - price; + final hasEnoughBalance = remainingBalance >= 0; + + final result = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + title: const Text('Confirmer le lavage'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + machineName, + style: Theme.of(dialogContext).textTheme.bodyMedium?.copyWith( + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.background, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.divider), + ), + child: Column( + children: [ + _AmountRow( + label: 'Solde actuel', + amount: currency.format(currentBalance), + emphasized: true, + ), + const SizedBox(height: 12), + _AmountRow( + label: 'Prix du cycle', + amount: '− ${currency.format(price)}', + color: AppColors.textSecondary, + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Divider(height: 1), + ), + _AmountRow( + label: 'Solde restant', + amount: currency.format(remainingBalance), + emphasized: true, + color: hasEnoughBalance ? AppColors.success : AppColors.error, + ), + ], + ), + ), + if (!hasEnoughBalance) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.error.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + const Icon(Icons.warning_amber_rounded, color: AppColors.error, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Solde insuffisant. Rechargez votre portefeuille pour continuer.', + style: Theme.of(dialogContext).textTheme.bodySmall?.copyWith( + color: AppColors.error, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ], + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext, false), + child: const Text('Annuler'), + ), + if (!hasEnoughBalance) + TextButton( + onPressed: () { + Navigator.pop(dialogContext, false); + dialogContext.push(AppRoutes.walletTopUp); + }, + child: const Text('Recharger'), + ) + else + ElevatedButton( + onPressed: () => Navigator.pop(dialogContext, true), + child: const Text('Démarrer le lavage'), + ), + ], + ), + ); + + return result ?? false; + } +} + +class _AmountRow extends StatelessWidget { + const _AmountRow({ + required this.label, + required this.amount, + this.emphasized = false, + this.color, + }); + + final String label; + final String amount; + final bool emphasized; + final Color? color; + + @override + Widget build(BuildContext context) { + final amountColor = color ?? AppColors.textPrimary; + + return Row( + children: [ + Expanded( + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: emphasized ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + Text( + amount, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: amountColor, + fontWeight: emphasized ? FontWeight.w700 : FontWeight.w600, + fontSize: emphasized ? 18 : 16, + ), + ), + ], + ); + } +} diff --git a/lib/features/machines/presentation/machine_action_screen.dart b/lib/features/machines/presentation/machine_action_screen.dart index 4993bf1..933e091 100644 --- a/lib/features/machines/presentation/machine_action_screen.dart +++ b/lib/features/machines/presentation/machine_action_screen.dart @@ -8,6 +8,8 @@ import '../../../core/router/app_router.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/machine_status_theme.dart'; import '../../../core/widgets/empty_state.dart'; +import '../../../core/widgets/wash_start_confirmation_dialog.dart'; +import '../../wallet/data/wallet_repository.dart'; import '../../wash/data/wash_repository.dart'; import '../data/machine_repository.dart'; import '../domain/machine_detail.dart'; @@ -25,11 +27,46 @@ class MachineActionScreen extends ConsumerStatefulWidget { class _MachineActionScreenState extends ConsumerState { bool _isStarting = false; + Future _confirmAndStartWash(MachineDetail detail) async { + final currency = NumberFormat.currency(locale: 'fr_FR', symbol: '€'); + + double currentBalance; + try { + final wallet = await ref.read(walletProvider.future); + currentBalance = wallet.currentBalance; + } catch (_) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Impossible de charger votre solde'), + backgroundColor: AppColors.error, + ), + ); + } + return; + } + + if (!mounted) return; + + final confirmed = await WashStartConfirmationDialog.show( + context, + machineName: detail.machine.name, + currentBalance: currentBalance, + price: detail.price, + currency: currency, + ); + + if (confirmed && mounted) { + await _startWash(detail); + } + } + Future _startWash(MachineDetail detail) async { setState(() => _isStarting = true); try { await ref.read(washRepositoryProvider).startWashFromMachine(detail.machine.uuid); ref.invalidate(washesProvider); + ref.invalidate(walletProvider); ref.invalidate(machineDetailProvider(widget.machineUuid)); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -74,7 +111,7 @@ class _MachineActionScreenState extends ConsumerState { detail: detail, currency: currency, isStarting: _isStarting, - onStart: () => _startWash(detail), + onStart: () => _confirmAndStartWash(detail), onReserve: () => context.push(AppRoutes.machineBooking(widget.machineUuid)), ), ),