import 'dart:convert'; 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 '../../database/database.dart'; import '../../models/motorcycle_models.dart'; import '../../models/maintenance_models.dart'; import '../../core/responsive/responsive_layout.dart'; import '../../core/theme/theme_extensions.dart'; import 'widgets/motorcycle_photo.dart'; import 'garage_providers.dart'; class MotorcycleDetailScreen extends ConsumerWidget { final int motorcycleId; const MotorcycleDetailScreen({super.key, required this.motorcycleId}); @override Widget build(BuildContext context, WidgetRef ref) { final bikesAsync = ref.watch(garageMotorcyclesStreamProvider); final statsAsync = ref.watch(motorcyclePerformanceStatsProvider(motorcycleId)); final colors = context.motorColors; final theme = Theme.of(context); return bikesAsync.when( loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())), error: (err, _) => Scaffold(body: Center(child: Text('Error: $err'))), data: (bikes) { final bike = bikes.firstWhere( (b) => b.id == motorcycleId, orElse: () => GarageMotorcycle( id: 0, year: 2024, makeId: '', makeName: 'Unknown', modelId: '', modelName: 'Unknown', isDefault: false, dateAdded: DateTime.now(), lastUpdated: DateTime.now(), ), ); if (bike.id == 0) { return Scaffold( appBar: AppBar(), body: const Center(child: Text('Motorcycle not found')), ); } MotorcycleSpecification? spec; if (bike.specificationJson != null) { try { spec = MotorcycleSpecification.fromJson(json.decode(bike.specificationJson!)); } catch (_) {} } return Scaffold( appBar: AppBar( title: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( '${bike.year} ${bike.makeName} ${bike.modelName}', style: const TextStyle(fontWeight: FontWeight.bold), ), ), actions: [ IconButton( icon: const Icon(Icons.edit), tooltip: 'Edit Nickname', onPressed: () => _showEditNicknameDialog(context, ref, bike), ), IconButton( icon: Icon(Icons.delete_outline, color: colors.raceRed), tooltip: 'Delete Motorcycle', onPressed: () => _confirmDelete(context, ref, bike, colors), ), ], ), body: ResponsiveLayout( builder: (context, responsive) { final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape); if (isWide) { return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 4, child: _buildHeaderCard(context, ref, bike, spec, colors, theme), ), const SizedBox(width: 16), Expanded( flex: 6, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildMyPerformanceSection(context, statsAsync, spec, colors, theme), const SizedBox(height: 20), _buildMaintenanceSection(context, ref, bike, statsAsync, colors, theme), const SizedBox(height: 20), _buildSpecificationSheet(context, spec, colors, theme), ], ), ), ], ), ); } return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildHeaderCard(context, ref, bike, spec, colors, theme), const SizedBox(height: 20), _buildMyPerformanceSection(context, statsAsync, spec, colors, theme), const SizedBox(height: 20), _buildMaintenanceSection(context, ref, bike, statsAsync, colors, theme), const SizedBox(height: 20), _buildSpecificationSheet(context, spec, colors, theme), const SizedBox(height: 24), ], ), ); }, ), ); }, ); } Widget _buildHeaderCard( BuildContext context, WidgetRef ref, GarageMotorcycle bike, MotorcycleSpecification? spec, MotorSignalThemeColors colors, ThemeData theme, ) { return Card( elevation: 2, color: colors.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide( color: bike.isDefault ? colors.cyberCyan : colors.outlineVariant, width: bike.isDefault ? 1.8 : 1.0, ), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MotorcyclePhoto( year: bike.year, make: bike.makeName, model: bike.modelName, height: 190, borderRadius: BorderRadius.circular(12), showAttribution: true, fit: BoxFit.cover, ), const SizedBox(height: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${bike.year}', style: TextStyle( fontSize: 14, color: colors.cyberCyan, fontWeight: FontWeight.bold, fontFamily: 'JetBrains Mono', ), ), Text( '${bike.makeName} ${bike.modelName}'.toUpperCase(), style: TextStyle( fontSize: 20, fontWeight: FontWeight.w900, fontFamily: 'Sora', color: theme.colorScheme.onSurface, ), ), if (bike.nickname != null && bike.nickname!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( '"${bike.nickname}"', style: TextStyle( fontSize: 15, fontStyle: FontStyle.italic, color: colors.warningAmber, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: 16), Divider(color: colors.outlineVariant, height: 1), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildHeaderStat( 'DISPLACEMENT', spec?.displacementCc != null ? '${spec!.displacementCc!.toStringAsFixed(0)} cc' : '—', theme, ), _buildHeaderStat( 'POWER', spec?.horsepower != null ? '${spec!.horsepower!.toStringAsFixed(0)} HP' : '—', theme, ), _buildHeaderStat( 'TORQUE', spec?.torqueLbFt != null ? '${spec!.torqueLbFt!.toStringAsFixed(1)} lb-ft' : '—', theme, ), ], ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: bike.isDefault ? Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: colors.cyberCyan.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), border: Border.all(color: colors.cyberCyan), ), child: Center( child: Text( 'CURRENT MOTORCYCLE', style: TextStyle( fontFamily: 'Sora', fontWeight: FontWeight.bold, letterSpacing: 1.2, color: colors.cyberCyan, ), ), ), ) : OutlinedButton( style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), side: BorderSide(color: colors.cyberCyan), foregroundColor: colors.cyberCyan, ), onPressed: () { ref.read(garageControllerProvider).setDefaultMotorcycle(bike.id); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Set ${bike.makeName} ${bike.modelName} as current motorcycle.', ), ), ); }, child: const Text( 'SET AS CURRENT MOTORCYCLE', style: TextStyle( fontFamily: 'Sora', fontWeight: FontWeight.bold, letterSpacing: 1.2, ), ), ), ), ], ), ), ); } Widget _buildHeaderStat(String label, String value, ThemeData theme) { return Expanded( child: Column( children: [ FittedBox( fit: BoxFit.scaleDown, child: Text( label, style: TextStyle( fontSize: 9.5, color: theme.colorScheme.onSurface.withValues(alpha: 0.5), letterSpacing: 1.0, fontWeight: FontWeight.w700, ), ), ), const SizedBox(height: 4), FittedBox( fit: BoxFit.scaleDown, child: Text( value, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, fontFamily: 'JetBrains Mono', color: theme.colorScheme.onSurface, ), ), ), ], ), ); } Widget _buildMyPerformanceSection( BuildContext context, AsyncValue statsAsync, MotorcycleSpecification? spec, MotorSignalThemeColors colors, ThemeData theme, ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'MY PERFORMANCE', style: TextStyle( fontFamily: 'Sora', fontSize: 16, fontWeight: FontWeight.bold, color: colors.cyberCyan, letterSpacing: 0.8, ), ), const SizedBox(height: 12), statsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) => Text('Error loading stats: $err'), data: (stats) { return Column( children: [ Row( children: [ Expanded(child: _buildPerfTile('TOTAL RIDES', '${stats.rideCount}', colors, theme)), const SizedBox(width: 12), Expanded( child: _buildPerfTile( 'TOTAL DISTANCE', '${stats.totalDistanceMiles.toStringAsFixed(1)} MI', colors, theme, ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildPerfTile( 'BEST TOP SPEED', '${stats.topSpeedMph.toStringAsFixed(1)} MPH', colors, theme, published: spec?.claimedTopSpeedMph != null ? '~${spec!.claimedTopSpeedMph!.toStringAsFixed(0)} MPH' : null, ), ), const SizedBox(width: 12), Expanded( child: _buildPerfTile( 'BEST 0–60', stats.best0to60Ms != null ? '${(stats.best0to60Ms! / 1000.0).toStringAsFixed(2)} s' : '—', colors, theme, published: spec?.claimedZeroToSixtySec != null ? '~${spec!.claimedZeroToSixtySec!.toStringAsFixed(1)} s' : null, ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildPerfTile( 'MAX LEAN', '${stats.maxLean.toStringAsFixed(1)}°', colors, theme, ), ), const SizedBox(width: 12), Expanded( child: _buildPerfTile( 'MAX ACCEL G', '${stats.maxAccelG.toStringAsFixed(2)} G', colors, theme, ), ), ], ), ], ); }, ), ], ); } Widget _buildPerfTile( String label, String value, MotorSignalThemeColors colors, ThemeData theme, { String? published, }) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: colors.surfaceContainer, borderRadius: BorderRadius.circular(12), border: Border.all(color: colors.outlineVariant, width: 1.2), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 10, color: theme.colorScheme.onSurface.withValues(alpha: 0.5), fontWeight: FontWeight.w700, letterSpacing: 0.8, ), ), const SizedBox(height: 4), FittedBox( fit: BoxFit.scaleDown, child: Text( value, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, fontFamily: 'JetBrains Mono', color: theme.colorScheme.onSurface, ), ), ), if (published != null) ...[ const SizedBox(height: 4), Text( 'PUBLISHED: $published', style: TextStyle( fontSize: 9.5, color: colors.cyberCyan, fontFamily: 'JetBrains Mono', ), ), ], ], ), ); } Widget _buildSpecificationSheet( BuildContext context, MotorcycleSpecification? spec, MotorSignalThemeColors colors, ThemeData theme, ) { if (spec == null) { return Card( color: colors.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: colors.outlineVariant), ), child: const Padding( padding: EdgeInsets.all(16.0), child: Text( 'Detailed specifications unavailable for this motorcycle.', style: TextStyle(color: Colors.white54), ), ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'SPECIFICATIONS', style: TextStyle( fontFamily: 'Sora', fontSize: 16, fontWeight: FontWeight.bold, color: colors.cyberCyan, letterSpacing: 0.8, ), ), const SizedBox(height: 12), _buildSpecGroup('ENGINE', [ _SpecRow('Displacement', spec.displacementCc != null ? '${spec.displacementCc!.toStringAsFixed(0)} cc' : null), _SpecRow('Configuration', spec.engineConfiguration), _SpecRow('Cylinders', spec.cylinderCount?.toString()), _SpecRow('Horsepower', spec.horsepower != null ? '${spec.horsepower!.toStringAsFixed(0)} HP @ ${spec.horsepowerRpm ?? "—"} RPM' : null), _SpecRow('Torque', spec.torqueLbFt != null ? '${spec.torqueLbFt!.toStringAsFixed(1)} lb-ft @ ${spec.torqueRpm ?? "—"} RPM' : null), _SpecRow('Redline', spec.redlineRpm != null ? '${spec.redlineRpm} RPM' : null), _SpecRow('Compression Ratio', spec.compressionRatio), _SpecRow('Cooling System', spec.coolingSystem), _SpecRow('Fuel System', spec.fuelSystem), ], colors, theme), _buildSpecGroup('PERFORMANCE (PUBLISHED)', [ _SpecRow('Claimed Top Speed', spec.claimedTopSpeedMph != null ? '~${spec.claimedTopSpeedMph!.toStringAsFixed(0)} MPH' : null), _SpecRow('Claimed 0–60 MPH', spec.claimedZeroToSixtySec != null ? '~${spec.claimedZeroToSixtySec!.toStringAsFixed(1)} sec' : null), _SpecRow('Claimed 1/4 Mile', spec.claimedQuarterMileSec != null ? '~${spec.claimedQuarterMileSec!.toStringAsFixed(1)} sec' : null), ], colors, theme), _buildSpecGroup('WEIGHT & DIMENSIONS', [ _SpecRow('Wet Weight', spec.wetWeightLbs != null ? '${spec.wetWeightLbs!.toStringAsFixed(0)} lbs' : null), _SpecRow('Dry Weight', spec.dryWeightLbs != null ? '${spec.dryWeightLbs!.toStringAsFixed(0)} lbs' : null), _SpecRow('Seat Height', spec.seatHeightInches != null ? '${spec.seatHeightInches!.toStringAsFixed(1)} in' : null), _SpecRow('Wheelbase', spec.wheelbaseInches != null ? '${spec.wheelbaseInches!.toStringAsFixed(1)} in' : null), ], colors, theme), _buildSpecGroup('FUEL', [ _SpecRow('Tank Capacity', spec.fuelCapacityGallons != null ? '${spec.fuelCapacityGallons!.toStringAsFixed(1)} gal' : null), _SpecRow('Fuel Economy', spec.fuelEconomyMpg != null ? '${spec.fuelEconomyMpg!.toStringAsFixed(0)} MPG' : null), _SpecRow('Estimated Range', spec.estimatedRangeMiles != null ? '~${spec.estimatedRangeMiles!.toStringAsFixed(0)} miles' : null), ], colors, theme), _buildSpecGroup('CHASSIS & BRAKES', [ _SpecRow('Frame Type', spec.frameType), _SpecRow('Front Suspension', spec.frontSuspension), _SpecRow('Rear Suspension', spec.rearSuspension), _SpecRow('Front Brake', spec.frontBrake), _SpecRow('Rear Brake', spec.rearBrake), _SpecRow('ABS System', spec.absType), _SpecRow('Front Tire', spec.frontTireSize), _SpecRow('Rear Tire', spec.rearTireSize), ], colors, theme), _buildSpecGroup('TRANSMISSION', [ _SpecRow('Transmission Type', spec.transmissionType), _SpecRow('Gears', spec.gearCount?.toString()), _SpecRow('Final Drive', spec.finalDrive), ], colors, theme), if (spec.source != null) ...[ const SizedBox(height: 12), Text( 'Specifications Source: ${spec.source}', style: TextStyle( fontSize: 11, color: theme.colorScheme.onSurface.withValues(alpha: 0.4), fontFamily: 'Inter', ), ), ], ], ); } Widget _buildSpecGroup( String title, List<_SpecRow> rows, MotorSignalThemeColors colors, ThemeData theme, ) { final validRows = rows.where((r) => r.value != null && r.value!.isNotEmpty).toList(); if (validRows.isEmpty) return const SizedBox.shrink(); return Card( elevation: 1, color: colors.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: colors.outlineVariant), ), margin: const EdgeInsets.only(bottom: 12), child: Padding( padding: const EdgeInsets.all(14.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontFamily: 'Sora', fontWeight: FontWeight.bold, color: colors.cyberCyan, fontSize: 12, letterSpacing: 0.8, ), ), const SizedBox(height: 8), ...validRows.map( (r) => Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( r.label, style: TextStyle( fontSize: 12, color: theme.colorScheme.onSurface.withValues(alpha: 0.6), ), ), const SizedBox(width: 8), Expanded( child: Text( r.value!, textAlign: TextAlign.right, style: TextStyle( fontWeight: FontWeight.bold, fontFamily: 'JetBrains Mono', fontSize: 12, color: theme.colorScheme.onSurface, ), ), ), ], ), ), ), ], ), ), ); } void _showEditNicknameDialog(BuildContext context, WidgetRef ref, GarageMotorcycle bike) { final controller = TextEditingController(text: bike.nickname ?? ''); showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Edit Nickname'), content: TextField( controller: controller, decoration: const InputDecoration(hintText: 'Enter nickname'), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), ElevatedButton( onPressed: () { ref.read(garageControllerProvider).updateNickname( bike.id, controller.text.trim().isNotEmpty ? controller.text.trim() : null, ); Navigator.pop(context); }, child: const Text('Save'), ), ], ); }, ); } void _confirmDelete( BuildContext context, WidgetRef ref, GarageMotorcycle bike, MotorSignalThemeColors colors, ) { showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Delete Motorcycle?'), content: Text( 'Are you sure you want to remove ${bike.makeName} ${bike.modelName} from your Garage? Historical rides associated with this motorcycle will be preserved.', ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: colors.raceRed), onPressed: () { ref.read(garageControllerProvider).deleteMotorcycle(bike.id); Navigator.pop(context); context.pop(); }, child: const Text('Delete', style: TextStyle(color: Colors.white)), ), ], ); }, ); } Widget _buildMaintenanceSection( BuildContext context, WidgetRef ref, GarageMotorcycle bike, AsyncValue statsAsync, MotorSignalThemeColors colors, ThemeData theme, ) { final stats = statsAsync.value; final totalRiddenMiles = stats?.totalDistanceMiles ?? 0.0; final maintenanceData = MaintenanceData.fromSpecificationJson(bike.specificationJson); final currentOdometer = maintenanceData.baseOdometer + totalRiddenMiles; final pills = maintenanceData.calculatePills(totalRiddenMiles); final records = maintenanceData.serviceRecords; return Card( elevation: 2, color: colors.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide(color: colors.outlineVariant, width: 1.0), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Section Header Wrap( alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, runSpacing: 8, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'MAINTENANCE DUE TRACKING', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, fontFamily: 'JetBrains Mono', color: colors.cyberCyan, letterSpacing: 1.0, ), ), const SizedBox(height: 2), InkWell( onTap: () => _showSetOdometerDialog(context, ref, bike, maintenanceData.baseOdometer, colors), borderRadius: BorderRadius.circular(4), child: Padding( padding: const EdgeInsets.symmetric(vertical: 2.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( 'Odo: ${currentOdometer.toStringAsFixed(0)} mi', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, fontFamily: 'JetBrains Mono', color: theme.colorScheme.onSurface, ), ), const SizedBox(width: 4), Icon(Icons.edit_note, size: 16, color: colors.cyberCyan), ], ), ), ), ], ), ElevatedButton.icon( onPressed: () => _showLogServiceDialog(context, ref, bike, currentOdometer, colors, theme), icon: const Icon(Icons.add_circle_outline, size: 16), label: const Text('Log Service'), style: ElevatedButton.styleFrom( backgroundColor: colors.cyberCyan.withValues(alpha: 0.15), foregroundColor: colors.cyberCyan, side: BorderSide(color: colors.cyberCyan.withValues(alpha: 0.4)), elevation: 0, textStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, fontFamily: 'JetBrains Mono', ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), ), ], ), const SizedBox(height: 16), // Countdown Pills Grid if (pills.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.surfaceContainerHigh.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(10), border: Border.all( color: colors.outlineVariant.withValues(alpha: 0.3), ), ), child: Row( children: [ Icon(Icons.info_outline, size: 18, color: colors.cyberCyan), const SizedBox(width: 10), Expanded( child: Text( 'Set your odometer or log your first service above to activate maintenance countdowns.', style: TextStyle( fontSize: 11.5, fontFamily: 'Inter', color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), ), ), ], ), ) else LayoutBuilder( builder: (context, constraints) { final width = constraints.maxWidth; final itemWidth = width >= 480 ? (width - 20) / 3 : (width >= 280 ? (width - 10) / 2 : width); return Wrap( spacing: 10, runSpacing: 10, children: pills .map( (pill) => SizedBox( width: itemWidth, child: _buildMaintenancePill(context, ref, bike, pill, currentOdometer, colors, theme), ), ) .toList(), ); }, ), const SizedBox(height: 20), Divider(color: colors.outlineVariant.withValues(alpha: 0.4), height: 1), const SizedBox(height: 16), // Service History Header Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'SERVICE HISTORY (${records.length})', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, fontFamily: 'JetBrains Mono', color: colors.outlineVariant, letterSpacing: 0.5, ), ), ], ), const SizedBox(height: 10), if (records.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(10), border: Border.all(color: colors.outlineVariant.withValues(alpha: 0.3)), ), child: Column( children: [ Icon(Icons.build_circle_outlined, size: 32, color: colors.outlineVariant), const SizedBox(height: 8), Text( 'No maintenance logged yet', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 4), Text( 'Tap "+ Log Service" to record oil changes, chain service, and tire wear.', textAlign: TextAlign.center, style: TextStyle( fontSize: 11, color: theme.colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], ), ) else ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: records.length, separatorBuilder: (c, i) => const SizedBox(height: 8), itemBuilder: (context, index) { final record = records[index]; final dateFormatted = DateFormat('MMM d, yyyy').format(record.date); return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(10), border: Border.all(color: colors.outlineVariant.withValues(alpha: 0.4)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colors.cyberCyan.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(Icons.build, size: 18, color: colors.cyberCyan), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( record.serviceType, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, ), ), ), if (record.cost != null && record.cost! > 0) Text( '\$${record.cost!.toStringAsFixed(2)}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, fontFamily: 'JetBrains Mono', color: colors.cyberCyan, ), ), ], ), const SizedBox(height: 3), Row( children: [ Text( dateFormatted, style: TextStyle( fontSize: 11, fontFamily: 'JetBrains Mono', color: theme.colorScheme.onSurface.withValues(alpha: 0.6), ), ), Text( ' • ${record.odometer.toStringAsFixed(0)} mi', style: TextStyle( fontSize: 11, fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface.withValues(alpha: 0.8), ), ), ], ), if (record.notes != null && record.notes!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( record.notes!, style: TextStyle( fontSize: 11, fontStyle: FontStyle.italic, color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), ), ), ], ), ), IconButton( icon: Icon(Icons.edit_outlined, size: 16, color: colors.cyberCyan), tooltip: 'Edit Service Record', padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () => _showEditServiceRecordDialog(context, ref, bike, record, colors, theme), ), const SizedBox(width: 8), IconButton( icon: Icon(Icons.delete_outline, size: 16, color: colors.outlineVariant), tooltip: 'Delete Service Record', padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () => _confirmDeleteServiceRecord(context, ref, bike, record.id, colors), ), ], ), ); }, ), ], ), ), ); } Widget _buildMaintenancePill( BuildContext context, WidgetRef ref, GarageMotorcycle bike, MaintenanceItemStatus pill, double currentOdometer, MotorSignalThemeColors colors, ThemeData theme, ) { final badgeColor = pill.getBadgeColor(context); final isUrgent = pill.urgency == MaintenanceUrgency.overdue || pill.urgency == MaintenanceUrgency.dueSoon; return InkWell( borderRadius: BorderRadius.circular(12), onTap: () => _showLogServiceForPill(context, ref, bike, pill, currentOdometer, colors, theme), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), decoration: BoxDecoration( color: badgeColor.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(12), border: Border.all( color: badgeColor.withValues(alpha: isUrgent ? 0.8 : 0.35), width: isUrgent ? 1.5 : 1.0, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(pill.icon, size: 15, color: badgeColor), const SizedBox(width: 5), Expanded( child: Text( pill.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, fontFamily: 'JetBrains Mono', color: theme.colorScheme.onSurface, ), ), ), Icon(Icons.add_task, size: 13, color: badgeColor.withValues(alpha: 0.7)), ], ), const SizedBox(height: 6), Text( pill.label, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w800, fontFamily: 'JetBrains Mono', color: badgeColor, ), ), const SizedBox(height: 6), ClipRRect( borderRadius: BorderRadius.circular(2), child: LinearProgressIndicator( value: (1.0 - (pill.milesRemaining / pill.intervalMiles)).clamp(0.0, 1.0), backgroundColor: colors.outlineVariant.withValues(alpha: 0.2), valueColor: AlwaysStoppedAnimation(badgeColor), minHeight: 3, ), ), ], ), ), ); } void _showLogServiceForPill( BuildContext context, WidgetRef ref, GarageMotorcycle bike, MaintenanceItemStatus pill, double currentOdometer, MotorSignalThemeColors colors, ThemeData theme, ) { String serviceType = 'General Maintenance'; switch (pill.key) { case 'oil': serviceType = 'Engine Oil & Filter'; break; case 'chain': serviceType = 'Chain Clean & Lube'; break; case 'tire': serviceType = 'Tire Replacement (Rear)'; break; case 'brake': serviceType = 'Brake Fluid Flush'; break; } _showLogServiceDialog( context, ref, bike, currentOdometer, colors, theme, initialServiceType: serviceType, ); } void _showSetOdometerDialog( BuildContext context, WidgetRef ref, GarageMotorcycle bike, double currentBaseOdo, MotorSignalThemeColors colors, ) { final controller = TextEditingController(text: currentBaseOdo.round().toString()); showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Update Base Odometer'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Enter the odometer reading when this bike was added to your garage. Ridden telemetry miles are added automatically.', style: TextStyle(fontSize: 12), ), const SizedBox(height: 12), TextField( controller: controller, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Base Odometer (miles)', suffixText: 'mi', ), ), ], ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), ElevatedButton( onPressed: () { final odo = double.tryParse(controller.text.trim()) ?? currentBaseOdo; ref.read(garageControllerProvider).updateBaseOdometer(bike.id, odo); Navigator.pop(context); }, child: const Text('Save'), ), ], ); }, ); } void _showLogServiceDialog( BuildContext context, WidgetRef ref, GarageMotorcycle bike, double currentOdometer, MotorSignalThemeColors colors, ThemeData theme, { String? initialServiceType, }) { final serviceTypes = [ 'Engine Oil & Filter', 'Chain Clean & Lube', 'Tire Replacement (Rear)', 'Tire Replacement (Front)', 'Tire Replacement (Set)', 'Brake Fluid Flush', 'Brake Pads Replacement', 'Air Filter Replacement', 'Spark Plugs', 'Valve Clearance Check', 'General Maintenance', ]; String selectedType = (initialServiceType != null && serviceTypes.contains(initialServiceType)) ? initialServiceType : serviceTypes[0]; final odoController = TextEditingController(text: currentOdometer.round().toString()); final notesController = TextEditingController(); final costController = TextEditingController(); DateTime serviceDate = DateTime.now(); showDialog( context: context, builder: (context) { return StatefulBuilder( builder: (context, setState) { final size = MediaQuery.sizeOf(context); final viewInsets = MediaQuery.viewInsetsOf(context); return AlertDialog( scrollable: true, constraints: BoxConstraints( maxWidth: 560, maxHeight: size.height - viewInsets.bottom - 48, ), title: const Text('Log New Service'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ DropdownButtonFormField( initialValue: selectedType, isExpanded: true, decoration: const InputDecoration(labelText: 'Service Type'), items: serviceTypes .map((t) => DropdownMenuItem( value: t, child: Text(t, maxLines: 1, overflow: TextOverflow.ellipsis), )) .toList(), onChanged: (val) { if (val != null) setState(() => selectedType = val); }, ), const SizedBox(height: 12), TextField( controller: odoController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Odometer at Service', suffixText: 'mi', ), ), const SizedBox(height: 12), ListTile( contentPadding: EdgeInsets.zero, title: const Text('Service Date', style: TextStyle(fontSize: 12)), subtitle: Text(DateFormat('MMMM d, yyyy').format(serviceDate)), trailing: const Icon(Icons.calendar_today, size: 18), onTap: () async { final picked = await showDatePicker( context: context, initialDate: serviceDate, firstDate: DateTime(2000), lastDate: DateTime.now().add(const Duration(days: 1)), ); if (picked != null) { setState(() => serviceDate = picked); } }, ), const SizedBox(height: 8), TextField( controller: costController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration( labelText: 'Cost (Optional)', prefixText: '\$ ', ), ), const SizedBox(height: 12), TextField( controller: notesController, decoration: const InputDecoration( labelText: 'Notes (e.g. Brand, weight, shop)', hintText: 'e.g. Motul 300V 10W-40, OEM filter', ), ), ], ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), ElevatedButton( onPressed: () { final odo = double.tryParse(odoController.text.trim()) ?? currentOdometer; final cost = double.tryParse(costController.text.trim()); final record = ServiceRecord( id: 'srv_${DateTime.now().millisecondsSinceEpoch}', date: serviceDate, odometer: odo, serviceType: selectedType, notes: notesController.text.trim().isNotEmpty ? notesController.text.trim() : null, cost: cost, ); ref.read(garageControllerProvider).addServiceRecord(bike.id, record); Navigator.pop(context); }, child: const Text('Save Record'), ), ], ); }, ); }, ); } void _showEditServiceRecordDialog( BuildContext context, WidgetRef ref, GarageMotorcycle bike, ServiceRecord record, MotorSignalThemeColors colors, ThemeData theme, ) { final serviceTypes = [ 'Engine Oil & Filter', 'Chain Clean & Lube', 'Tire Replacement (Rear)', 'Tire Replacement (Front)', 'Tire Replacement (Set)', 'Brake Fluid Flush', 'Brake Pads Replacement', 'Air Filter Replacement', 'Spark Plugs', 'Valve Clearance Check', 'General Maintenance', ]; String selectedType = serviceTypes.contains(record.serviceType) ? record.serviceType : serviceTypes.last; final odoController = TextEditingController(text: record.odometer.round().toString()); final notesController = TextEditingController(text: record.notes ?? ''); final costController = TextEditingController(text: record.cost != null ? record.cost!.toStringAsFixed(2) : ''); DateTime serviceDate = record.date; showDialog( context: context, builder: (context) { return StatefulBuilder( builder: (context, setState) { return AlertDialog( title: const Text('Edit Service Record'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ DropdownButtonFormField( initialValue: selectedType, isExpanded: true, decoration: const InputDecoration(labelText: 'Service Type'), items: serviceTypes .map((t) => DropdownMenuItem( value: t, child: Text(t, maxLines: 1, overflow: TextOverflow.ellipsis), )) .toList(), onChanged: (val) { if (val != null) setState(() => selectedType = val); }, ), const SizedBox(height: 12), TextField( controller: odoController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Odometer at Service', suffixText: 'mi', ), ), const SizedBox(height: 12), ListTile( contentPadding: EdgeInsets.zero, title: const Text('Service Date', style: TextStyle(fontSize: 12)), subtitle: Text(DateFormat('MMMM d, yyyy').format(serviceDate)), trailing: const Icon(Icons.calendar_today, size: 18), onTap: () async { final picked = await showDatePicker( context: context, initialDate: serviceDate, firstDate: DateTime(2000), lastDate: DateTime.now().add(const Duration(days: 1)), ); if (picked != null) { setState(() => serviceDate = picked); } }, ), const SizedBox(height: 8), TextField( controller: costController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration( labelText: 'Cost (Optional)', prefixText: '\$ ', ), ), const SizedBox(height: 12), TextField( controller: notesController, decoration: const InputDecoration( labelText: 'Notes (e.g. Brand, weight, shop)', hintText: 'e.g. Motul 300V 10W-40, OEM filter', ), ), ], ), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), ElevatedButton( onPressed: () { final odo = double.tryParse(odoController.text.trim()) ?? record.odometer; final cost = double.tryParse(costController.text.trim()); final updated = ServiceRecord( id: record.id, date: serviceDate, odometer: odo, serviceType: selectedType, notes: notesController.text.trim().isNotEmpty ? notesController.text.trim() : null, cost: cost, ); ref.read(garageControllerProvider).updateServiceRecord(bike.id, updated); Navigator.pop(context); }, child: const Text('Save Changes'), ), ], ); }, ); }, ); } void _confirmDeleteServiceRecord( BuildContext context, WidgetRef ref, GarageMotorcycle bike, String recordId, MotorSignalThemeColors colors, ) { showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Delete Service Record?'), content: const Text('Are you sure you want to remove this maintenance entry?'), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: colors.raceRed), onPressed: () { ref.read(garageControllerProvider).deleteServiceRecord(bike.id, recordId); Navigator.pop(context); }, child: const Text('Delete', style: TextStyle(color: Colors.white)), ), ], ); }, ); } } class _SpecRow { final String label; final String? value; _SpecRow(this.label, this.value); }