Integration fonctionnalités V1 ( resas + gestion machines
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/api/api_client.dart';
|
||||
import '../../../core/api/api_endpoints.dart';
|
||||
import '../../../core/api/api_response.dart';
|
||||
import '../../../core/auth/auth_provider.dart';
|
||||
import '../domain/booking.dart';
|
||||
|
||||
/// Dépôt de données pour les réservations.
|
||||
class BookingRepository {
|
||||
BookingRepository(this._apiClient);
|
||||
|
||||
final ApiClient _apiClient;
|
||||
|
||||
Future<List<Booking>> fetchBookings() async {
|
||||
final response = await _apiClient.get(ApiEndpoints.bookings);
|
||||
final list = ApiResponse.list(response.data, 'bookings');
|
||||
return list.map((json) => Booking.fromJson(json as Map<String, dynamic>)).toList();
|
||||
}
|
||||
|
||||
Future<Booking> fetchBooking(String uuid) async {
|
||||
final response = await _apiClient.get(ApiEndpoints.booking(uuid));
|
||||
final json = ApiResponse.object(response.data, 'booking');
|
||||
return Booking.fromJson(json);
|
||||
}
|
||||
|
||||
Future<Booking> createBooking({
|
||||
required String machineUuid,
|
||||
required DateTime slotStart,
|
||||
required DateTime slotEnd,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
ApiEndpoints.bookings,
|
||||
data: {
|
||||
'machine_uuid': machineUuid,
|
||||
'slot_start': slotStart.toUtc().toIso8601String(),
|
||||
'slot_end': slotEnd.toUtc().toIso8601String(),
|
||||
},
|
||||
);
|
||||
final json = ApiResponse.object(response.data, 'booking');
|
||||
return Booking.fromJson(json);
|
||||
} on DioException catch (error) {
|
||||
throw BookingException(ApiResponse.errorMessage(error, fallback: 'Impossible de réserver'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<Booking> cancelBooking(String uuid) async {
|
||||
try {
|
||||
final response = await _apiClient.patch(ApiEndpoints.bookingCancel(uuid));
|
||||
final json = ApiResponse.object(response.data, 'booking');
|
||||
return Booking.fromJson(json);
|
||||
} on DioException catch (error) {
|
||||
throw BookingException(ApiResponse.errorMessage(error, fallback: 'Impossible d\'annuler'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<Booking> moveBooking({
|
||||
required String uuid,
|
||||
required DateTime slotStart,
|
||||
required DateTime slotEnd,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.patch(
|
||||
ApiEndpoints.bookingMove(uuid),
|
||||
data: {
|
||||
'slot_start': slotStart.toUtc().toIso8601String(),
|
||||
'slot_end': slotEnd.toUtc().toIso8601String(),
|
||||
},
|
||||
);
|
||||
final json = ApiResponse.object(response.data, 'booking');
|
||||
return Booking.fromJson(json);
|
||||
} on DioException catch (error) {
|
||||
throw BookingException(ApiResponse.errorMessage(error, fallback: 'Impossible de modifier'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BookingException implements Exception {
|
||||
BookingException(this.message);
|
||||
final String message;
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
final bookingRepositoryProvider = Provider<BookingRepository>((ref) {
|
||||
return BookingRepository(ref.watch(apiClientProvider));
|
||||
});
|
||||
|
||||
final bookingsProvider = FutureProvider<List<Booking>>((ref) async {
|
||||
final token = ref.watch(authProvider.select((state) => state.accessToken));
|
||||
if (token == null || token.isEmpty) {
|
||||
throw StateError('Non authentifié');
|
||||
}
|
||||
return ref.watch(bookingRepositoryProvider).fetchBookings();
|
||||
});
|
||||
|
||||
final bookingDetailProvider = FutureProvider.family<Booking, String>((ref, uuid) async {
|
||||
return ref.watch(bookingRepositoryProvider).fetchBooking(uuid);
|
||||
});
|
||||
@@ -18,6 +18,13 @@ class Booking {
|
||||
final String status;
|
||||
final double bookingFee;
|
||||
|
||||
bool get canCancel =>
|
||||
(status == 'confirmed' || status == 'pending') &&
|
||||
slotStart != null &&
|
||||
slotStart!.isAfter(DateTime.now());
|
||||
|
||||
bool get canModify => canCancel;
|
||||
|
||||
factory Booking.fromJson(Map<String, dynamic> json) {
|
||||
final machine = json['machine'] as Map<String, dynamic>?;
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
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<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
|
||||
? '${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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +1,228 @@
|
||||
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/api/api_client.dart';
|
||||
import '../../../core/api/api_endpoints.dart';
|
||||
import '../../../core/router/app_router.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';
|
||||
|
||||
/// Fournisseur des réservations de l'utilisateur connecté.
|
||||
final bookingsProvider = FutureProvider<List<Booking>>((ref) async {
|
||||
final response = await ref.watch(apiClientProvider).get(ApiEndpoints.bookings);
|
||||
final data = response.data;
|
||||
|
||||
List<dynamic> list;
|
||||
if (data is List<dynamic>) {
|
||||
list = data;
|
||||
} else if (data is Map<String, dynamic> && data['data'] is List<dynamic>) {
|
||||
list = data['data'] as List<dynamic>;
|
||||
} else {
|
||||
list = [];
|
||||
}
|
||||
|
||||
return list
|
||||
.map((json) => Booking.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
/// Écran listant les réservations de l'utilisateur.
|
||||
/// Écran listant les réservations avec annulation et modification.
|
||||
class BookingsScreen extends ConsumerWidget {
|
||||
const BookingsScreen({super.key});
|
||||
|
||||
Future<void> _cancelBooking(BuildContext context, WidgetRef ref, Booking booking) async {
|
||||
final slotLabel = booking.slotStart != null
|
||||
? DateFormat('EEEE dd MMM à HH:mm', 'fr_FR').format(booking.slotStart!)
|
||||
: 'ce créneau';
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
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 Scaffold(
|
||||
appBar: AppBar(title: const Text('Mes réservations')),
|
||||
body: bookingsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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: [
|
||||
Text('Erreur : $error'),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.invalidate(bookingsProvider),
|
||||
child: const Text('Réessayer'),
|
||||
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)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (bookings) {
|
||||
if (bookings.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('Aucune réservation.\nRéservez un créneau depuis une laverie.'),
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => ref.invalidate(bookingsProvider),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: bookings.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final booking = bookings[index];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.event),
|
||||
title: Text(booking.machineName),
|
||||
subtitle: Text(
|
||||
booking.slotStart != null
|
||||
? DateFormat('dd/MM/yyyy HH:mm').format(booking.slotStart!)
|
||||
: 'Créneau à confirmer',
|
||||
),
|
||||
trailing: Chip(label: Text(booking.status)),
|
||||
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
|
||||
? DateFormat('EEEE dd MMM · HH:mm', 'fr_FR').format(booking.slotStart!)
|
||||
: '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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user