import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/router/app_router.dart'; import '../../../core/time/server_time.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/widgets/empty_state.dart'; import '../../../core/widgets/screen_header.dart'; import '../data/booking_repository.dart'; import '../domain/booking.dart'; /// Écran listant les réservations avec annulation et modification. class BookingsScreen extends ConsumerWidget { const BookingsScreen({super.key}); Future _cancelBooking(BuildContext context, WidgetRef ref, Booking booking) async { final slotLabel = booking.slotStart != null ? ServerTime.format(booking.slotStart, pattern: 'EEEE dd MMM à HH:mm') : 'ce créneau'; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Annuler la réservation ?'), content: Text( 'Le créneau du $slotLabel sera libéré.\n\n' 'Annulation gratuite plus de 2 h avant le créneau, sinon des frais peuvent s\'appliquer.', ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Non')), TextButton( onPressed: () => Navigator.pop(ctx, true), style: TextButton.styleFrom(foregroundColor: AppColors.error), child: const Text('Oui, annuler'), ), ], ), ); if (confirmed != true || !context.mounted) return; try { await ref.read(bookingRepositoryProvider).cancelBooking(booking.uuid); ref.invalidate(bookingsProvider); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Réservation annulée'), backgroundColor: AppColors.success, ), ); } } on BookingException catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message), backgroundColor: AppColors.error), ); } } } @override Widget build(BuildContext context, WidgetRef ref) { final bookingsAsync = ref.watch(bookingsProvider); return bookingsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => EmptyState( icon: Icons.event_busy_outlined, title: 'Erreur', subtitle: 'Impossible de charger vos réservations.', actionLabel: 'Réessayer', onAction: () => ref.invalidate(bookingsProvider), ), data: (bookings) { if (bookings.isEmpty) { return EmptyState( icon: Icons.event_outlined, title: 'Aucune réservation', subtitle: 'Réservez un créneau jusqu\'à 6 jours\nà l\'avance depuis une laverie.', actionLabel: 'Voir les laveries', onAction: () => context.go(AppRoutes.home), ); } final upcoming = bookings .where((b) => b.status != 'cancelled' && b.status != 'completed' && b.status != 'no_show') .toList(); final past = bookings .where((b) => b.status == 'cancelled' || b.status == 'completed' || b.status == 'no_show') .toList(); return RefreshIndicator( onRefresh: () async => ref.invalidate(bookingsProvider), child: ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 88), children: [ const ScreenHeader( title: 'Mes créneaux', subtitle: '30 min pour démarrer après notification', ), if (upcoming.isNotEmpty) ...[ Text('À venir', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), ...upcoming.map( (b) => _BookingCard( booking: b, highlight: true, onModify: b.canModify ? () => context.push(AppRoutes.bookingModify(b.uuid)) : null, onCancel: b.canCancel ? () => _cancelBooking(context, ref, b) : null, ), ), const SizedBox(height: 16), ], if (past.isNotEmpty) ...[ Text('Passées', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), ...past.map((b) => _BookingCard(booking: b, highlight: false)), ], ], ), ); }, ); } } class _BookingCard extends StatelessWidget { const _BookingCard({ required this.booking, required this.highlight, this.onModify, this.onCancel, }); final Booking booking; final bool highlight; final VoidCallback? onModify; final VoidCallback? onCancel; @override Widget build(BuildContext context) { final slotText = booking.slotStart != null ? ServerTime.format(booking.slotStart, pattern: 'EEEE dd MMM · HH:mm') : 'Créneau à confirmer'; return Card( margin: const EdgeInsets.only(bottom: 8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: highlight ? BorderSide(color: AppColors.machineReserved.withValues(alpha: 0.4)) : BorderSide.none, ), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: (highlight ? AppColors.machineReserved : AppColors.textSecondary) .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.event, color: highlight ? AppColors.machineReserved : AppColors.textSecondary, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(booking.machineName, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 2), Text(slotText, style: Theme.of(context).textTheme.bodyMedium), ], ), ), StatusBadge.fromStatus(booking.status), ], ), if (onModify != null || onCancel != null) ...[ const SizedBox(height: 12), Row( children: [ if (onModify != null) Expanded( child: OutlinedButton.icon( onPressed: onModify, icon: const Icon(Icons.edit_calendar_outlined, size: 18), label: const Text('Modifier'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.primary, padding: const EdgeInsets.symmetric(vertical: 10), ), ), ), if (onModify != null && onCancel != null) const SizedBox(width: 8), if (onCancel != null) Expanded( child: OutlinedButton.icon( onPressed: onCancel, icon: const Icon(Icons.cancel_outlined, size: 18), label: const Text('Annuler'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.error, padding: const EdgeInsets.symmetric(vertical: 10), ), ), ), ], ), ], ], ), ), ); } }