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 '../providers/dashboard_providers.dart'; import '../../../core/services/reservation_service.dart'; import '../../../widgets/status_badge.dart'; import '../../../widgets/error_view.dart'; class ReservationDetailScreen extends ConsumerWidget { final String id; const ReservationDetailScreen({super.key, required this.id}); @override Widget build(BuildContext context, WidgetRef ref) { final async = ref.watch(reservationDetailProvider(id)); final paymentsAsync = ref.watch(reservationPaymentsProvider(id)); final inspectionsAsync = ref.watch(inspectionsProvider(id)); return async.when( loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())), error: (e, _) => Scaffold( appBar: AppBar(), body: ErrorView( message: 'Could not load reservation', onRetry: () => ref.invalidate(reservationDetailProvider(id)), ), ), data: (res) { final fmt = DateFormat('MMM d, yyyy'); final canConfirm = res.status == 'DRAFT'; final canCancel = res.status == 'DRAFT' || res.status == 'CONFIRMED'; final canCheckin = res.status == 'CONFIRMED'; final canCheckout = res.status == 'ACTIVE'; final canClose = res.status == 'COMPLETED'; final canRecordPayment = (res.status != 'CANCELLED') && res.paymentStatus != 'PAID'; return Scaffold( appBar: AppBar( title: Text('Reservation #${id.substring(0, 8)}'), actions: [ IconButton( icon: const Icon(Icons.description_outlined), tooltip: 'View Contract', onPressed: () => context.push( '/dashboard/reservations/$id/contract'), ), StatusBadge(status: res.status), const SizedBox(width: 8), ], ), body: ListView( padding: const EdgeInsets.all(16), children: [ _Section( title: 'Vehicle', child: res.vehicle != null ? _InfoRow(label: 'Vehicle', value: res.vehicle!.displayName) : const Text('N/A'), ), _Section( title: 'Customer', child: res.customer != null ? Column( children: [ _InfoRow( label: 'Name', value: res.customer!.fullName), if (res.customer!.email != null) _InfoRow( label: 'Email', value: res.customer!.email!), if (res.customer!.phone != null) _InfoRow( label: 'Phone', value: res.customer!.phone!), ], ) : const Text('Walk-in customer'), ), _Section( title: 'Dates', child: Column( children: [ _InfoRow(label: 'Start', value: fmt.format(res.startDate)), _InfoRow(label: 'End', value: fmt.format(res.endDate)), _InfoRow(label: 'Duration', value: '${res.days} days'), ], ), ), _Section( title: 'Payment', child: Column( children: [ _InfoRow( label: 'Total', value: '\$${(res.totalAmount / 100).toStringAsFixed(2)}'), Row( children: [ const Text('Status: ', style: TextStyle(color: Color(0xFF6B7280))), const SizedBox(width: 4), StatusBadge(status: res.paymentStatus), ], ), ], ), ), paymentsAsync.when( loading: () => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(), data: (payments) { if (payments.isEmpty) return const SizedBox.shrink(); return _Section( title: 'Payment History', child: Column( children: payments .map((p) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( p.paymentMethod, style: const TextStyle( fontWeight: FontWeight.w500, fontSize: 13), ), Text( DateFormat('MMM d, yyyy') .format(p.createdAt), style: const TextStyle( fontSize: 11, color: Color(0xFF9CA3AF)), ), ], ), ), Text( '\$${(p.amount / 100).toStringAsFixed(2)}', style: const TextStyle( fontWeight: FontWeight.w600, color: Color(0xFF0E9F6E), ), ), ], ), )) .toList(), ), ); }, ), // Inspections inspectionsAsync.when( loading: () => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(), data: (inspections) { final checkin = inspections .where((i) => i.type == 'CHECKIN') .firstOrNull; final checkout = inspections .where((i) => i.type == 'CHECKOUT') .firstOrNull; final canEditCheckin = res.status == 'CONFIRMED' || res.status == 'ACTIVE'; final canEditCheckout = res.status == 'ACTIVE' || res.status == 'COMPLETED'; if (!canEditCheckin && !canEditCheckout && inspections.isEmpty) { return const SizedBox.shrink(); } return _Section( title: 'Inspections', child: Column( children: [ _InspectionCard( label: 'Departure', inspection: checkin, canEdit: canEditCheckin, onEdit: () => _showInspectionSheet( context, ref, 'CHECKIN', checkin), ), const SizedBox(height: 10), _InspectionCard( label: 'Return', inspection: checkout, canEdit: canEditCheckout, onEdit: () => _showInspectionSheet( context, ref, 'CHECKOUT', checkout), ), ], ), ); }, ), const SizedBox(height: 16), if (canRecordPayment) _ActionButton( label: 'Record Payment', color: const Color(0xFF0E9F6E), icon: Icons.payments_outlined, onTap: () => _showPaymentDialog(context, ref), ), if (canConfirm) _ActionButton( label: 'Confirm Reservation', color: const Color(0xFF0E9F6E), icon: Icons.check_circle_outline, onTap: () async { await ref .read(reservationServiceProvider) .confirmReservation(id); ref.invalidate(reservationDetailProvider(id)); }, ), if (canCheckin) _ActionButton( label: 'Check In', color: const Color(0xFF1A56DB), icon: Icons.login, onTap: () => _showMileageDialog(context, ref, 'checkin'), ), if (canCheckout) _ActionButton( label: 'Check Out', color: const Color(0xFF5521B5), icon: Icons.logout, onTap: () => _showMileageDialog(context, ref, 'checkout'), ), if (canClose) _ActionButton( label: 'Close Reservation', color: const Color(0xFF5521B5), icon: Icons.lock_outline, onTap: () async { final confirmed = await showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Close Reservation'), content: const Text( 'Mark this reservation as closed? This finalises the rental.'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), ElevatedButton( onPressed: () => Navigator.pop(context, true), child: const Text('Close'), ), ], ), ); if (confirmed == true && context.mounted) { await ref .read(reservationServiceProvider) .closeReservation(id); ref.invalidate(reservationDetailProvider(id)); } }, ), if (canCancel) _ActionButton( label: 'Cancel Reservation', color: const Color(0xFFE02424), icon: Icons.cancel_outlined, onTap: () => _showCancelDialog(context, ref), ), ], ), ); }, ); } void _showPaymentDialog(BuildContext context, WidgetRef ref) { final amountCtrl = TextEditingController(); String method = 'CASH'; String type = 'CHARGE'; const methods = ['CASH', 'CHECK', 'BANK_TRANSFER', 'CARD']; const types = ['CHARGE', 'DEPOSIT']; showDialog( context: context, builder: (_) => StatefulBuilder( builder: (ctx, setState) => AlertDialog( title: const Text('Record Payment'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: amountCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'Amount (\$)'), ), const SizedBox(height: 12), InputDecorator( decoration: const InputDecoration(labelText: 'Method'), child: DropdownButton( value: method, isExpanded: true, underline: const SizedBox(), items: methods .map((m) => DropdownMenuItem(value: m, child: Text(m))) .toList(), onChanged: (v) => setState(() => method = v!), ), ), const SizedBox(height: 12), InputDecorator( decoration: const InputDecoration(labelText: 'Type'), child: DropdownButton( value: type, isExpanded: true, underline: const SizedBox(), items: types .map((t) => DropdownMenuItem(value: t, child: Text(t))) .toList(), onChanged: (v) => setState(() => type = v!), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Cancel')), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0E9F6E)), onPressed: () async { final dollars = double.tryParse(amountCtrl.text); if (dollars == null || dollars <= 0) return; Navigator.pop(ctx); try { await ref.read(paymentServiceProvider).recordManualPayment( id, { 'amount': (dollars * 100).round(), 'currency': 'USD', 'type': type, 'paymentMethod': method, }, ); ref.invalidate(reservationDetailProvider(id)); } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Payment failed: $e'))); } } }, child: const Text('Record'), ), ], ), ), ); } void _showMileageDialog( BuildContext context, WidgetRef ref, String type) { final ctrl = TextEditingController(); showDialog( context: context, builder: (_) => AlertDialog( title: Text(type == 'checkin' ? 'Check In' : 'Check Out'), content: TextField( controller: ctrl, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Current mileage (km)', ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel')), ElevatedButton( onPressed: () async { final mileage = int.tryParse(ctrl.text); if (mileage == null) return; Navigator.pop(context); try { if (type == 'checkin') { await ref .read(reservationServiceProvider) .checkin(id, mileage); } else { await ref .read(reservationServiceProvider) .checkout(id, mileage); } ref.invalidate(reservationDetailProvider(id)); } catch (_) {} }, child: const Text('Confirm'), ), ], ), ); } void _showInspectionSheet( BuildContext context, WidgetRef ref, String type, ReservationInspection? existing, ) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => _InspectionSheet( reservationId: id, type: type, existing: existing, onSaved: () => ref.invalidate(inspectionsProvider(id)), ), ); } void _showCancelDialog(BuildContext context, WidgetRef ref) { final ctrl = TextEditingController(); showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Cancel Reservation'), content: TextField( controller: ctrl, decoration: const InputDecoration(labelText: 'Reason'), maxLines: 3, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Back')), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFE02424)), onPressed: () async { Navigator.pop(context); await ref .read(reservationServiceProvider) .cancelReservation(id, ctrl.text); ref.invalidate(reservationDetailProvider(id)); }, child: const Text('Cancel Reservation'), ), ], ), ); } } class _Section extends StatelessWidget { final String title; final Widget child; const _Section({required this.title, required this.child}); @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.only(bottom: 12), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle( fontWeight: FontWeight.bold, color: Color(0xFF6B7280), fontSize: 12)), const SizedBox(height: 10), child, ], ), ), ); } } class _InfoRow extends StatelessWidget { final String label; final String value; const _InfoRow({required this.label, required this.value}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Row( children: [ Text('$label: ', style: const TextStyle(color: Color(0xFF6B7280))), Expanded( child: Text(value, style: const TextStyle(fontWeight: FontWeight.w500)), ), ], ), ); } } class _ActionButton extends StatelessWidget { final String label; final Color color; final IconData icon; final VoidCallback onTap; const _ActionButton({ required this.label, required this.color, required this.icon, required this.onTap, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: color, minimumSize: const Size.fromHeight(48), ), icon: Icon(icon), label: Text(label), onPressed: onTap, ), ); } } // ── Fuel level helpers ──────────────────────────────────────────────────────── const _fuelLevels = [ 'FULL', 'SEVEN_EIGHTHS', 'THREE_QUARTERS', 'FIVE_EIGHTHS', 'HALF', 'THREE_EIGHTHS', 'QUARTER', 'ONE_EIGHTH', 'EMPTY', ]; const _fuelLabels = { 'FULL': 'Full', 'SEVEN_EIGHTHS': '7/8', 'THREE_QUARTERS': '3/4', 'FIVE_EIGHTHS': '5/8', 'HALF': '1/2', 'THREE_EIGHTHS': '3/8', 'QUARTER': '1/4', 'ONE_EIGHTH': '1/8', 'EMPTY': 'Empty', }; double _fuelFraction(String level) { const fractions = { 'FULL': 1.0, 'SEVEN_EIGHTHS': 0.875, 'THREE_QUARTERS': 0.75, 'FIVE_EIGHTHS': 0.625, 'HALF': 0.5, 'THREE_EIGHTHS': 0.375, 'QUARTER': 0.25, 'ONE_EIGHTH': 0.125, 'EMPTY': 0.0, }; return fractions[level] ?? 0; } // ── InspectionCard ──────────────────────────────────────────────────────────── class _InspectionCard extends StatelessWidget { final String label; final ReservationInspection? inspection; final bool canEdit; final VoidCallback onEdit; const _InspectionCard({ required this.label, required this.inspection, required this.canEdit, required this.onEdit, }); @override Widget build(BuildContext context) { final i = inspection; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFF9FAFB), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 13, color: Color(0xFF374151))), const SizedBox(height: 6), if (i == null) const Text('Not recorded', style: TextStyle( fontSize: 12, color: Color(0xFF9CA3AF))) else ...[ if (i.fuelLevel != null) _FuelBar(level: i.fuelLevel!), if (i.mileage != null) ...[ const SizedBox(height: 4), Text('${i.mileage} km', style: const TextStyle( fontSize: 12, color: Color(0xFF6B7280))), ], if (i.generalCondition != null && i.generalCondition!.isNotEmpty) ...[ const SizedBox(height: 4), Text(i.generalCondition!, style: const TextStyle( fontSize: 12, color: Color(0xFF6B7280))), ], if (i.employeeNotes != null && i.employeeNotes!.isNotEmpty) ...[ const SizedBox(height: 4), Text(i.employeeNotes!, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 11, color: Color(0xFF9CA3AF), fontStyle: FontStyle.italic)), ], ], ], ), ), if (canEdit) IconButton( icon: Icon( i == null ? Icons.add_circle_outline : Icons.edit_outlined, size: 20, color: const Color(0xFF1A56DB), ), onPressed: onEdit, ), ], ), ); } } class _FuelBar extends StatelessWidget { final String level; const _FuelBar({required this.level}); @override Widget build(BuildContext context) { final fraction = _fuelFraction(level); final label = _fuelLabels[level] ?? level; final color = fraction > 0.5 ? const Color(0xFF057A55) : fraction > 0.25 ? const Color(0xFFFF6B00) : const Color(0xFFE02424); return Row( children: [ Expanded( child: ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: fraction, minHeight: 8, backgroundColor: const Color(0xFFE5E7EB), valueColor: AlwaysStoppedAnimation(color), ), ), ), const SizedBox(width: 8), Text(label, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: color)), ], ); } } // ── InspectionSheet ─────────────────────────────────────────────────────────── class _InspectionSheet extends ConsumerStatefulWidget { final String reservationId; final String type; // CHECKIN | CHECKOUT final ReservationInspection? existing; final VoidCallback onSaved; const _InspectionSheet({ required this.reservationId, required this.type, this.existing, required this.onSaved, }); @override ConsumerState<_InspectionSheet> createState() => _InspectionSheetState(); } class _InspectionSheetState extends ConsumerState<_InspectionSheet> { late String _fuelLevel; final _mileageCtrl = TextEditingController(); final _conditionCtrl = TextEditingController(); final _notesCtrl = TextEditingController(); bool _saving = false; String? _error; @override void initState() { super.initState(); final e = widget.existing; _fuelLevel = e?.fuelLevel ?? 'FULL'; if (e?.mileage != null) _mileageCtrl.text = '${e!.mileage}'; _conditionCtrl.text = e?.generalCondition ?? ''; _notesCtrl.text = e?.employeeNotes ?? ''; } @override void dispose() { _mileageCtrl.dispose(); _conditionCtrl.dispose(); _notesCtrl.dispose(); super.dispose(); } Future _save() async { setState(() { _saving = true; _error = null; }); try { await ref.read(reservationServiceProvider).upsertInspection( widget.reservationId, widget.type, { 'fuelLevel': _fuelLevel, if (_mileageCtrl.text.isNotEmpty) 'mileage': int.parse(_mileageCtrl.text), if (_conditionCtrl.text.trim().isNotEmpty) 'generalCondition': _conditionCtrl.text.trim(), if (_notesCtrl.text.trim().isNotEmpty) 'employeeNotes': _notesCtrl.text.trim(), }, ); widget.onSaved(); if (mounted) Navigator.of(context).pop(); } catch (e) { setState(() { _saving = false; _error = 'Failed to save. Please try again.'; }); } } @override Widget build(BuildContext context) { final title = widget.type == 'CHECKIN' ? 'Departure Inspection' : 'Return Inspection'; return Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), // Fuel level const Text('Fuel Level', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF374151))), const SizedBox(height: 8), _FuelBar(level: _fuelLevel), const SizedBox(height: 8), SizedBox( height: 36, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _fuelLevels.length, separatorBuilder: (_, _) => const SizedBox(width: 6), itemBuilder: (_, i) { final level = _fuelLevels[i]; final selected = level == _fuelLevel; return GestureDetector( onTap: () => setState(() => _fuelLevel = level), child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6), decoration: BoxDecoration( color: selected ? const Color(0xFF1A56DB) : const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(6), ), child: Text( _fuelLabels[level] ?? level, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: selected ? Colors.white : const Color(0xFF374151), ), ), ), ); }, ), ), const SizedBox(height: 14), // Mileage TextField( controller: _mileageCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'Mileage (km)'), ), const SizedBox(height: 12), // General condition TextField( controller: _conditionCtrl, decoration: const InputDecoration( labelText: 'General Condition (optional)'), ), const SizedBox(height: 12), // Notes TextField( controller: _notesCtrl, maxLines: 3, decoration: const InputDecoration( labelText: 'Notes (optional)', alignLabelWithHint: true, ), ), if (_error != null) ...[ const SizedBox(height: 10), Text(_error!, style: const TextStyle(color: Color(0xFF9B1C1C))), ], const SizedBox(height: 20), ElevatedButton( onPressed: _saving ? null : _save, style: ElevatedButton.styleFrom( minimumSize: const Size.fromHeight(48)), child: _saving ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white)), ) : Text(widget.existing == null ? 'Record Inspection' : 'Update Inspection'), ), ], ), ), ); } }