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 '../../machines/data/machine_repository.dart'; import '../../machines/domain/machine_detail.dart'; import '../data/booking_repository.dart'; import '../domain/booking.dart'; /// Modification du créneau d'une réservation existante. class BookingModifyScreen extends ConsumerStatefulWidget { const BookingModifyScreen({super.key, required this.bookingUuid}); final String bookingUuid; @override ConsumerState createState() => _BookingModifyScreenState(); } class _BookingModifyScreenState extends ConsumerState { DateTime? _selectedDate; TimeSlot? _selectedSlot; bool _isSaving = false; Future _confirmMove(Booking booking) async { final slot = _selectedSlot; if (slot == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Sélectionnez un nouveau créneau')), ); return; } setState(() => _isSaving = true); try { await ref.read(bookingRepositoryProvider).moveBooking( uuid: booking.uuid, slotStart: slot.start, slotEnd: slot.end, ); ref.invalidate(bookingsProvider); ref.invalidate(bookingDetailProvider(booking.uuid)); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Créneau modifié avec succès'), backgroundColor: AppColors.success, ), ); context.go(AppRoutes.bookings); } } on BookingException catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message), backgroundColor: AppColors.error), ); } } finally { if (mounted) setState(() => _isSaving = false); } } @override Widget build(BuildContext context) { final bookingAsync = ref.watch(bookingDetailProvider(widget.bookingUuid)); return Scaffold( backgroundColor: AppColors.background, appBar: AppBar(title: const Text('Modifier le créneau')), body: bookingAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (_, __) => const Center(child: Text('Réservation introuvable')), data: (booking) { if (!booking.canModify) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( 'Cette réservation ne peut plus être modifiée.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, ), ), ); } final selectedDate = _selectedDate ?? DateTime( booking.slotStart!.year, booking.slotStart!.month, booking.slotStart!.day, ); final availabilityAsync = ref.watch( machineAvailabilityProvider((uuid: booking.machineUuid, date: selectedDate)), ); return Column( children: [ Expanded( child: ListView( padding: const EdgeInsets.all(16), children: [ Card( child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(booking.machineName, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 4), Text( 'Créneau actuel', style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 12), ), Text( booking.slotStart != null && booking.slotEnd != null ? '${DateFormat('EEEE dd MMM · HH:mm', 'fr_FR').format(booking.slotStart!)} – ${DateFormat('HH:mm').format(booking.slotEnd!)}' : '—', style: const TextStyle(fontWeight: FontWeight.w600), ), ], ), ), ), const SizedBox(height: 20), Text('Nouveau jour', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 10), SizedBox( height: 44, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: 6, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, index) { final date = DateTime.now().add(Duration(days: index)); final normalized = DateTime(date.year, date.month, date.day); final isSelected = normalized.year == selectedDate.year && normalized.month == selectedDate.month && normalized.day == selectedDate.day; return ChoiceChip( label: Text(DateFormat('EEE dd/MM', 'fr_FR').format(normalized)), selected: isSelected, onSelected: (_) => setState(() { _selectedDate = normalized; _selectedSlot = null; }), selectedColor: AppColors.primary.withValues(alpha: 0.15), ); }, ), ), const SizedBox(height: 20), Text('Nouveau créneau', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 12), availabilityAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (_, __) => const Text('Impossible de charger les créneaux.'), data: (slots) { if (slots.isEmpty) { return Text( 'Aucun créneau disponible ce jour.', style: Theme.of(context).textTheme.bodyMedium, ); } return Wrap( spacing: 8, runSpacing: 8, children: slots.map((slot) { final label = '${DateFormat('HH:mm').format(slot.start)} – ${DateFormat('HH:mm').format(slot.end)}'; final isSelected = _selectedSlot?.start == slot.start; return FilterChip( label: Text(label), selected: isSelected, onSelected: (_) => setState(() => _selectedSlot = slot), selectedColor: AppColors.primary.withValues(alpha: 0.15), checkmarkColor: AppColors.primary, ); }).toList(), ); }, ), ], ), ), SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: SizedBox( width: double.infinity, height: 56, child: ElevatedButton.icon( onPressed: _isSaving ? null : () => _confirmMove(booking), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), textStyle: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600), ), icon: _isSaving ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : const Icon(Icons.check_rounded, size: 24), label: Text(_isSaving ? 'Enregistrement…' : 'Confirmer le changement'), ), ), ), ), ], ); }, ), ); } }