import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; 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'; /// Écran machine — infos + actions principales. class MachineActionScreen extends ConsumerStatefulWidget { const MachineActionScreen({super.key, required this.machineUuid}); final String machineUuid; @override ConsumerState createState() => _MachineActionScreenState(); } 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( SnackBar( content: Text('${detail.machine.name} — cycle démarré !'), backgroundColor: AppColors.success, ), ); context.go(AppRoutes.washes); } } on WashStartException catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message), backgroundColor: AppColors.error), ); } } finally { if (mounted) setState(() => _isStarting = false); } } @override Widget build(BuildContext context) { final detailAsync = ref.watch(machineDetailProvider(widget.machineUuid)); final currency = NumberFormat.currency(locale: 'fr_FR', symbol: '€'); return Scaffold( backgroundColor: AppColors.background, appBar: AppBar(title: const Text('Machine')), body: detailAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => EmptyState( icon: Icons.error_outline, title: 'Machine introuvable', subtitle: error is DioException ? 'Impossible de charger les informations.' : '$error', actionLabel: 'Retour', onAction: () => context.pop(), ), data: (detail) => _MachineActionBody( detail: detail, currency: currency, isStarting: _isStarting, onStart: () => _confirmAndStartWash(detail), onReserve: () => context.push(AppRoutes.machineBooking(widget.machineUuid)), ), ), ); } } class _MachineActionBody extends StatelessWidget { const _MachineActionBody({ required this.detail, required this.currency, required this.isStarting, required this.onStart, required this.onReserve, }); final MachineDetail detail; final NumberFormat currency; final bool isStarting; final VoidCallback onStart; final VoidCallback onReserve; @override Widget build(BuildContext context) { final machine = detail.machine; final statusColor = MachineStatusTheme.color(machine.status); final canStart = MachineStatusTheme.canStart(machine.status); return ListView( padding: const EdgeInsets.all(16), children: [ Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: AppColors.gradientAccent, borderRadius: BorderRadius.circular(16), ), child: Row( children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(14), ), child: Icon( MachineStatusTheme.iconForType(machine.type), color: Colors.white, size: 28, ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( machine.name, style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.w700, ), ), Text( machine.typeLabel, style: TextStyle(color: Colors.white.withValues(alpha: 0.9)), ), if (detail.establishmentName != null) Text( detail.establishmentName!, style: TextStyle( color: Colors.white.withValues(alpha: 0.75), fontSize: 12, ), ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), child: Text( MachineStatusTheme.label(machine.status), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 12), ), ), ], ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: _InfoTile( icon: Icons.euro, label: 'Prix du cycle', value: currency.format(detail.price), color: AppColors.primary, ), ), const SizedBox(width: 10), Expanded( child: _InfoTile( icon: Icons.schedule, label: 'Durée estimée', value: '~${detail.estimatedDurationMinutes} min', color: AppColors.secondary, ), ), ], ), if (!canStart) ...[ const SizedBox(height: 16), Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Text( machine.status == 'running' ? 'Machine en cours d\'utilisation — vous pouvez réserver un créneau ultérieur.' : 'Machine indisponible pour le moment — réservez un créneau.', textAlign: TextAlign.center, style: TextStyle(color: statusColor, fontWeight: FontWeight.w500), ), ), ], const SizedBox(height: 28), SizedBox( width: double.infinity, height: 60, child: ElevatedButton.icon( onPressed: canStart && !isStarting ? onStart : null, icon: isStarting ? const SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white), ) : const Icon(Icons.play_arrow_rounded, size: 28), label: Text(isStarting ? 'Démarrage…' : 'Commencer un lavage'), ), ), const SizedBox(height: 14), SizedBox( width: double.infinity, height: 56, child: ElevatedButton.icon( onPressed: onReserve, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, ), icon: const Icon(Icons.event_available_outlined, size: 24), label: const Text('Réserver un créneau'), ), ), const SizedBox(height: 24), ], ); } } class _InfoTile extends StatelessWidget { const _InfoTile({ required this.icon, required this.label, required this.value, required this.color, }); final IconData icon; final String label; final String value; final Color color; @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: color, size: 22), const SizedBox(height: 8), Text(label, style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 12)), const SizedBox(height: 2), Text(value, style: Theme.of(context).textTheme.titleMedium), ], ), ), ); } }