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 '../../booking/data/booking_repository.dart'; import '../data/machine_repository.dart'; import '../domain/machine_detail.dart'; /// Réservation de créneau pour une machine. class MachineBookingScreen extends ConsumerStatefulWidget { const MachineBookingScreen({super.key, required this.machineUuid}); final String machineUuid; @override ConsumerState createState() => _MachineBookingScreenState(); } class _MachineBookingScreenState extends ConsumerState { DateTime _selectedDate = DateTime.now(); TimeSlot? _selectedSlot; bool _isBooking = false; Future _confirmBooking(MachineDetail detail) async { final slot = _selectedSlot; if (slot == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Sélectionnez un créneau horaire')), ); return; } setState(() => _isBooking = true); try { await ref.read(bookingRepositoryProvider).createBooking( machineUuid: detail.machine.uuid, slotStart: slot.start, slotEnd: slot.end, ); ref.invalidate(bookingsProvider); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Créneau réservé 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(() => _isBooking = false); } } @override Widget build(BuildContext context) { final detailAsync = ref.watch(machineDetailProvider(widget.machineUuid)); return Scaffold( backgroundColor: AppColors.background, appBar: AppBar(title: const Text('Réserver un créneau')), body: detailAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (_, __) => Center(child: Text('Machine introuvable', style: Theme.of(context).textTheme.titleMedium)), data: (detail) { final machine = detail.machine; final availabilityAsync = ref.watch( machineAvailabilityProvider((uuid: machine.uuid, date: _selectedDate)), ); return Column( children: [ Expanded( child: ListView( padding: const EdgeInsets.all(16), children: [ Card( child: ListTile( leading: CircleAvatar( backgroundColor: AppColors.primary.withValues(alpha: 0.1), child: Icon( MachineStatusTheme.iconForType(machine.type), color: AppColors.primary, ), ), title: Text(machine.name), subtitle: Text( [ machine.typeLabel, if (detail.establishmentName != null) detail.establishmentName, ].join(' · '), ), ), ), const SizedBox(height: 20), Text('Choisir un 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('Créneaux disponibles', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 4), Text( 'Vous aurez 30 min pour démarrer après le début du créneau.', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 12), availabilityAsync.when( loading: () => const Center(child: Padding( padding: EdgeInsets.all(24), child: CircularProgressIndicator(), )), error: (_, __) => const Text('Impossible de charger les créneaux.'), data: (slots) { if (slots.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: 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: _isBooking ? null : () => _confirmBooking(detail), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), textStyle: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600), ), icon: _isBooking ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : const Icon(Icons.event_available_rounded, size: 24), label: Text(_isBooking ? 'Réservation…' : 'Confirmer la réservation'), ), ), ), ), ], ); }, ), ); } }