import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import '../providers/dashboard_providers.dart'; import '../../../core/constants/app_constants.dart'; import '../../../core/services/vehicle_service.dart'; import '../../../widgets/status_badge.dart'; import '../../../widgets/error_view.dart'; import 'vehicle_form_screen.dart'; class VehicleDetailScreen extends ConsumerWidget { final String id; const VehicleDetailScreen({super.key, required this.id}); String _resolveUrl(String photo) { if (!photo.startsWith('http')) { return '${AppConstants.baseUrl.replaceAll('/api/v1', '')}$photo'; } if (Platform.isAndroid) { return photo.replaceFirst('localhost', '10.0.2.2'); } return photo; } @override Widget build(BuildContext context, WidgetRef ref) { final vehicleAsync = ref.watch(vehicleDetailProvider(id)); return vehicleAsync.when( loading: () => const Scaffold( body: Center(child: CircularProgressIndicator()), ), error: (e, _) => Scaffold( appBar: AppBar(), body: ErrorView( message: 'Could not load vehicle', onRetry: () => ref.invalidate(vehicleDetailProvider(id)), ), ), data: (vehicle) { return DefaultTabController( length: 3, child: Scaffold( body: NestedScrollView( headerSliverBuilder: (context, _) => [ SliverAppBar( expandedHeight: 240, pinned: true, actions: [ IconButton( icon: const Icon(Icons.add_photo_alternate_outlined), onPressed: () async { final result = await showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => VehiclePhotosSheet( vehicleId: id, currentPhotos: vehicle.photos .map((p) => _resolveUrl(p)) .toList(), ), ); if (result == true) { ref.invalidate(vehicleDetailProvider(id)); } }, ), IconButton( icon: const Icon(Icons.edit_outlined), onPressed: () async { final result = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => VehicleFormScreen(vehicle: vehicle)), ); if (result == true) { ref.invalidate(vehicleDetailProvider(id)); } }, ), PopupMenuButton( onSelected: (v) async { if (v == 'delete') { final confirmed = await showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Delete Vehicle'), content: Text( 'Delete ${vehicle.displayName}? This cannot be undone.'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFE02424)), onPressed: () => Navigator.pop(context, true), child: const Text('Delete'), ), ], ), ); if (confirmed == true && context.mounted) { await ref .read(vehicleServiceProvider) .deleteVehicle(id); if (context.mounted) Navigator.of(context).pop(); } } }, itemBuilder: (_) => const [ PopupMenuItem( value: 'delete', child: Text('Delete', style: TextStyle(color: Color(0xFFE02424))), ), ], ), ], flexibleSpace: FlexibleSpaceBar( background: vehicle.primaryPhoto != null ? CachedNetworkImage( imageUrl: _resolveUrl(vehicle.primaryPhoto!), fit: BoxFit.cover, errorWidget: (context, url, _) => _placeholder(), ) : _placeholder(), ), ), SliverPersistentHeader( pinned: true, delegate: _TabBarDelegate( TabBar( tabs: const [ Tab(text: 'Details'), Tab(text: 'Calendar'), Tab(text: 'Maintenance'), ], ), ), ), ], body: TabBarView( children: [ _DetailsTab( vehicle: vehicle, onPublishChanged: (v) async { await ref .read(vehicleServiceProvider) .togglePublish(id, v); ref.invalidate(vehicleDetailProvider(id)); }, ), _CalendarTab(vehicleId: id), _MaintenanceTab(vehicleId: id), ], ), ), ), ); }, ); } Widget _placeholder() => Container( color: const Color(0xFFF3F4F6), child: const Icon(Icons.directions_car, size: 72, color: Color(0xFFD1D5DB)), ); } class _TabBarDelegate extends SliverPersistentHeaderDelegate { final TabBar tabBar; _TabBarDelegate(this.tabBar); @override double get minExtent => tabBar.preferredSize.height; @override double get maxExtent => tabBar.preferredSize.height; @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) => Container( color: Theme.of(context).scaffoldBackgroundColor, child: tabBar, ); @override bool shouldRebuild(_TabBarDelegate old) => false; } class _DetailsTab extends StatelessWidget { final dynamic vehicle; final ValueChanged onPublishChanged; const _DetailsTab({ required this.vehicle, required this.onPublishChanged, }); @override Widget build(BuildContext context) { return ListView( padding: const EdgeInsets.all(20), children: [ Row( children: [ Expanded( child: Text( vehicle.displayName, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Color(0xFF111928), ), ), ), StatusBadge(status: vehicle.status), ], ), const SizedBox(height: 8), Text( vehicle.licensePlate, style: const TextStyle(fontSize: 15, color: Color(0xFF6B7280)), ), const SizedBox(height: 20), _InfoGrid(vehicle: vehicle), const SizedBox(height: 20), const Text( 'Pricing', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF111928), ), ), const SizedBox(height: 12), Card( child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ const Icon(Icons.attach_money, color: Color(0xFF1A56DB)), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '\$${(vehicle.dailyRate / 100).toStringAsFixed(2)}/day', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF1A56DB), ), ), const Text( 'Daily rate', style: TextStyle( fontSize: 12, color: Color(0xFF6B7280)), ), ], ), const Spacer(), Switch( value: vehicle.isPublished, onChanged: onPublishChanged, ), const Text( 'Published', style: TextStyle( fontSize: 12, color: Color(0xFF6B7280)), ), ], ), ), ), ], ); } } class _MaintenanceTab extends ConsumerWidget { final String vehicleId; const _MaintenanceTab({required this.vehicleId}); @override Widget build(BuildContext context, WidgetRef ref) { final async = ref.watch(maintenanceProvider(vehicleId)); return async.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => ErrorView( message: 'Could not load maintenance logs', onRetry: () => ref.invalidate(maintenanceProvider(vehicleId)), ), data: (logs) => Stack( children: [ logs.isEmpty ? const Center( child: Text('No maintenance records', style: TextStyle(color: Color(0xFF9CA3AF))), ) : ListView.separated( padding: const EdgeInsets.all(16), itemCount: logs.length, separatorBuilder: (_, _) => const SizedBox(height: 8), itemBuilder: (_, i) => _MaintenanceCard(log: logs[i]), ), Positioned( bottom: 16, right: 16, child: FloatingActionButton.small( onPressed: () => _showAddMaintenance(context, ref, vehicleId), child: const Icon(Icons.add), ), ), ], ), ); } void _showAddMaintenance( BuildContext context, WidgetRef ref, String vehicleId) { final typeCtrl = TextEditingController(); final descCtrl = TextEditingController(); final costCtrl = TextEditingController(); showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => 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: [ const Text('Add Maintenance Log', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), TextField( controller: typeCtrl, decoration: const InputDecoration(labelText: 'Type (e.g. Oil Change)')), const SizedBox(height: 12), TextField( controller: descCtrl, decoration: const InputDecoration(labelText: 'Description'), maxLines: 2), const SizedBox(height: 12), TextField( controller: costCtrl, decoration: const InputDecoration(labelText: 'Cost (\$)'), keyboardType: TextInputType.number), const SizedBox(height: 20), ElevatedButton( onPressed: () async { if (typeCtrl.text.isEmpty) return; await ref.read(vehicleServiceProvider).addMaintenance( vehicleId, { 'type': typeCtrl.text.trim(), if (descCtrl.text.isNotEmpty) 'description': descCtrl.text.trim(), if (costCtrl.text.isNotEmpty) 'cost': ((double.tryParse(costCtrl.text) ?? 0) * 100) .round(), 'performedAt': DateTime.now().toIso8601String(), }, ); ref.invalidate(maintenanceProvider(vehicleId)); if (context.mounted) Navigator.of(context).pop(); }, style: ElevatedButton.styleFrom( minimumSize: const Size.fromHeight(48)), child: const Text('Add Log'), ), ], ), ), ), ); } } class _MaintenanceCard extends StatelessWidget { final dynamic log; const _MaintenanceCard({required this.log}); @override Widget build(BuildContext context) { final fmt = DateFormat('MMM d, yyyy'); return Card( child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( log.type as String, style: const TextStyle(fontWeight: FontWeight.w600), ), ), if (log.cost != null) Text( '\$${((log.cost as int) / 100).toStringAsFixed(2)}', style: const TextStyle( fontWeight: FontWeight.w600, color: Color(0xFF1A56DB)), ), ], ), if (log.description != null) ...[ const SizedBox(height: 4), Text( log.description as String, style: const TextStyle( fontSize: 13, color: Color(0xFF6B7280)), ), ], const SizedBox(height: 6), Text( fmt.format(log.performedAt as DateTime), style: const TextStyle(fontSize: 12, color: Color(0xFF9CA3AF)), ), ], ), ), ); } } class _InfoGrid extends StatelessWidget { final dynamic vehicle; const _InfoGrid({required this.vehicle}); @override Widget build(BuildContext context) { final items = [ ('Category', vehicle.category), if (vehicle.seats != null) ('Seats', '${vehicle.seats}'), if (vehicle.transmission != null) ('Transmission', vehicle.transmission!), if (vehicle.fuelType != null) ('Fuel', vehicle.fuelType!), if (vehicle.mileage != null) ('Mileage', '${vehicle.mileage} km'), ]; return Wrap( spacing: 12, runSpacing: 12, children: items .map((item) => Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10), decoration: BoxDecoration( color: const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.$1, style: const TextStyle( fontSize: 11, color: Color(0xFF9CA3AF)), ), const SizedBox(height: 2), Text( item.$2, style: const TextStyle( fontWeight: FontWeight.w600, color: Color(0xFF111928), ), ), ], ), )) .toList(), ); } } // ─── Calendar Tab ───────────────────────────────────────────────────────────── class _CalendarTab extends ConsumerStatefulWidget { final String vehicleId; const _CalendarTab({required this.vehicleId}); @override ConsumerState<_CalendarTab> createState() => _CalendarTabState(); } class _CalendarTabState extends ConsumerState<_CalendarTab> with AutomaticKeepAliveClientMixin { late int _year; late int _month; DateTime? _selectedDay; static const _eventColors = { 'RESERVATION': Color(0xFF1A56DB), 'MAINTENANCE': Color(0xFFFF6B00), 'BLOCK': Color(0xFFE02424), }; static const _eventBg = { 'RESERVATION': Color(0xFFEBF5FF), 'MAINTENANCE': Color(0xFFFFF3E0), 'BLOCK': Color(0xFFFDE8E8), }; @override bool get wantKeepAlive => true; @override void initState() { super.initState(); final now = DateTime.now(); _year = now.year; _month = now.month; } void _prevMonth() => setState(() { if (_month == 1) { _month = 12; _year--; } else { _month--; } _selectedDay = null; }); void _nextMonth() => setState(() { if (_month == 12) { _month = 1; _year++; } else { _month++; } _selectedDay = null; }); bool _isSameDay(DateTime a, DateTime b) => a.year == b.year && a.month == b.month && a.day == b.day; List _eventsForDay( DateTime day, List events) { final d = DateTime(day.year, day.month, day.day); return events.where((e) { final s = DateTime(e.startDate.year, e.startDate.month, e.startDate.day); final en = DateTime(e.endDate.year, e.endDate.month, e.endDate.day); return !d.isBefore(s) && !d.isAfter(en); }).toList(); } @override Widget build(BuildContext context) { super.build(context); final key = (vehicleId: widget.vehicleId, year: _year, month: _month); final eventsAsync = ref.watch(calendarProvider(key)); return Stack( children: [ eventsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => ErrorView( message: 'Could not load calendar', onRetry: () => ref.invalidate(calendarProvider(key)), ), data: (events) => ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 80), children: [ // Month navigation Row( children: [ IconButton( icon: const Icon(Icons.chevron_left), onPressed: _prevMonth, ), Expanded( child: Text( DateFormat('MMMM yyyy').format(DateTime(_year, _month)), textAlign: TextAlign.center, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF111928), ), ), ), IconButton( icon: const Icon(Icons.chevron_right), onPressed: _nextMonth, ), ], ), const SizedBox(height: 4), // Day-of-week headers Row( children: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] .map((d) => Expanded( child: Center( child: Text(d, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF9CA3AF), )), ), )) .toList(), ), const SizedBox(height: 4), // Calendar grid _buildGrid(events), const SizedBox(height: 12), // Legend Row( mainAxisAlignment: MainAxisAlignment.center, children: _eventColors.entries.expand((entry) => [ Container( width: 8, height: 8, decoration: BoxDecoration( color: entry.value, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( entry.key[0] + entry.key.substring(1).toLowerCase(), style: const TextStyle( fontSize: 11, color: Color(0xFF6B7280)), ), const SizedBox(width: 14), ]).toList(), ), const SizedBox(height: 16), // Selected-day events if (_selectedDay != null) ...[ Text( DateFormat('EEEE, MMMM d').format(_selectedDay!), style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: Color(0xFF374151), ), ), const SizedBox(height: 10), ..._eventsForDay(_selectedDay!, events).map((e) => _EventTile( event: e, color: _eventColors[e.type] ?? Colors.grey, bg: _eventBg[e.type] ?? const Color(0xFFF3F4F6), onDelete: e.type == 'BLOCK' ? () async { await ref .read(vehicleServiceProvider) .deleteCalendarBlock( widget.vehicleId, e.id); ref.invalidate(calendarProvider(key)); } : null, )), if (_eventsForDay(_selectedDay!, events).isEmpty) const Text( 'No events on this day', style: TextStyle(color: Color(0xFF9CA3AF)), ), ], ], ), ), Positioned( bottom: 16, right: 16, child: FloatingActionButton.small( heroTag: 'cal_add', tooltip: 'Block dates', onPressed: () => _showAddBlock(context), child: const Icon(Icons.block), ), ), ], ); } Widget _buildGrid(List events) { final daysInMonth = DateTime(_year, _month + 1, 0).day; final firstDay = DateTime(_year, _month, 1); final startOffset = (firstDay.weekday - 1) % 7; final cells = [ for (int i = 0; i < startOffset; i++) const SizedBox(), for (int day = 1; day <= daysInMonth; day++) _buildDayCell(day, events), ]; while (cells.length % 7 != 0) { cells.add(const SizedBox()); } return GridView.count( crossAxisCount: 7, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: cells, ); } Widget _buildDayCell(int day, List events) { final date = DateTime(_year, _month, day); final dayEvents = _eventsForDay(date, events); final isSelected = _selectedDay != null && _isSameDay(_selectedDay!, date); final isToday = _isSameDay(DateTime.now(), date); return GestureDetector( onTap: () => setState( () => _selectedDay = isSelected ? null : date), child: Container( margin: const EdgeInsets.all(2), decoration: BoxDecoration( color: isSelected ? const Color(0xFF1A56DB) : isToday ? const Color(0xFFEBF5FF) : null, borderRadius: BorderRadius.circular(6), border: isToday && !isSelected ? Border.all(color: const Color(0xFF1A56DB)) : null, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '$day', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: isSelected ? Colors.white : isToday ? const Color(0xFF1A56DB) : const Color(0xFF374151), ), ), if (dayEvents.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 2), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: dayEvents .take(3) .map((e) => Container( width: 4, height: 4, margin: const EdgeInsets.symmetric(horizontal: 1), decoration: BoxDecoration( color: isSelected ? Colors.white70 : (_eventColors[e.type] ?? Colors.grey), shape: BoxShape.circle, ), )) .toList(), ), ), ], ), ), ); } void _showAddBlock(BuildContext context) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => _AddBlockSheet( vehicleId: widget.vehicleId, initialStart: _selectedDay ?? DateTime.now(), onSaved: () { final key = (vehicleId: widget.vehicleId, year: _year, month: _month); ref.invalidate(calendarProvider(key)); }, ), ); } } class _EventTile extends StatelessWidget { final CalendarEvent event; final Color color; final Color bg; final VoidCallback? onDelete; const _EventTile({ required this.event, required this.color, required this.bg, this.onDelete, }); @override Widget build(BuildContext context) { final fmt = DateFormat('MMM d'); return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withValues(alpha: 0.3)), ), child: Row( children: [ Container( width: 4, height: 36, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( event.label, style: TextStyle( fontWeight: FontWeight.w600, color: color, fontSize: 13, ), ), Text( '${fmt.format(event.startDate)} → ${fmt.format(event.endDate)}', style: const TextStyle( fontSize: 11, color: Color(0xFF6B7280)), ), if (event.status != null) Text( event.status!, style: const TextStyle( fontSize: 11, color: Color(0xFF9CA3AF)), ), ], ), ), if (onDelete != null) IconButton( icon: const Icon(Icons.delete_outline, size: 18), color: const Color(0xFFE02424), onPressed: onDelete, ), ], ), ); } } class _AddBlockSheet extends ConsumerStatefulWidget { final String vehicleId; final DateTime initialStart; final VoidCallback onSaved; const _AddBlockSheet({ required this.vehicleId, required this.initialStart, required this.onSaved, }); @override ConsumerState<_AddBlockSheet> createState() => _AddBlockSheetState(); } class _AddBlockSheetState extends ConsumerState<_AddBlockSheet> { late DateTime _start; late DateTime _end; final _reasonCtrl = TextEditingController(); String _blockType = 'MANUAL'; bool _saving = false; String? _error; @override void initState() { super.initState(); _start = widget.initialStart; _end = widget.initialStart.add(const Duration(days: 1)); } @override void dispose() { _reasonCtrl.dispose(); super.dispose(); } Future _save() async { setState(() { _saving = true; _error = null; }); try { await ref.read(vehicleServiceProvider).createCalendarBlock( widget.vehicleId, { 'startDate': '${_start.toIso8601String().substring(0, 10)}T00:00:00.000Z', 'endDate': '${_end.toIso8601String().substring(0, 10)}T23:59:59.000Z', 'type': _blockType, if (_reasonCtrl.text.trim().isNotEmpty) 'reason': _reasonCtrl.text.trim(), }, ); widget.onSaved(); if (mounted) Navigator.of(context).pop(); } catch (e) { setState(() { _saving = false; _error = 'Failed to create block. Please try again.'; }); } } @override Widget build(BuildContext context) { final fmt = DateFormat('MMM d, yyyy'); final far = DateTime.now().add(const Duration(days: 365 * 2)); final near = DateTime.now().subtract(const Duration(days: 365)); 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: [ const Text('Block Dates', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), Row( children: [ Expanded( child: OutlinedButton.icon( icon: const Icon(Icons.calendar_today, size: 14), label: Text(fmt.format(_start)), onPressed: () async { final d = await showDatePicker( context: context, initialDate: _start, firstDate: near, lastDate: far, ); if (d != null) { setState(() { _start = d; if (_end.isBefore(_start)) _end = _start; }); } }, ), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: Text('→'), ), Expanded( child: OutlinedButton.icon( icon: const Icon(Icons.calendar_today, size: 14), label: Text(fmt.format(_end)), onPressed: () async { final d = await showDatePicker( context: context, initialDate: _end, firstDate: _start, lastDate: far, ); if (d != null) setState(() => _end = d); }, ), ), ], ), const SizedBox(height: 12), InputDecorator( decoration: const InputDecoration(labelText: 'Block Type'), child: DropdownButton( value: _blockType, isExpanded: true, underline: const SizedBox(), items: const [ DropdownMenuItem( value: 'MANUAL', child: Text('Manual Block')), DropdownMenuItem( value: 'MAINTENANCE', child: Text('Maintenance')), ], onChanged: (v) => setState(() => _blockType = v!), ), ), const SizedBox(height: 12), TextField( controller: _reasonCtrl, decoration: const InputDecoration(labelText: 'Reason (optional)'), ), 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)), ) : const Text('Block Dates'), ), ], ), ), ); } }