Files
mobile/lib/features/booking/presentation/booking_modify_screen.dart
T

219 lines
8.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/theme/app_colors.dart';
import '../../../core/time/server_time.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<BookingModifyScreen> createState() => _BookingModifyScreenState();
}
class _BookingModifyScreenState extends ConsumerState<BookingModifyScreen> {
DateTime? _selectedDate;
TimeSlot? _selectedSlot;
bool _isSaving = false;
Future<void> _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
? '${ServerTime.format(booking.slotStart, pattern: 'EEEE dd MMM · HH:mm')} ${ServerTime.format(booking.slotEnd, pattern: 'HH:mm')}'
: '—',
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 = ServerTime.startOfToday().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(ServerTime.format(normalized, pattern: 'EEE dd/MM')),
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 =
'${ServerTime.format(slot.start, pattern: 'HH:mm')} ${ServerTime.format(slot.end, pattern: 'HH:mm')}';
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),
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'),
),
),
),
),
],
);
},
),
);
}
}