1459 lines
55 KiB
Dart
1459 lines
55 KiB
Dart
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<MotorcycleStats> 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<MotorcycleStats> 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<Color>(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) {
|
||
return AlertDialog(
|
||
title: const Text('Log New Service'),
|
||
content: SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
DropdownButtonFormField<String>(
|
||
initialValue: selectedType,
|
||
decoration: const InputDecoration(labelText: 'Service Type'),
|
||
items: serviceTypes.map((t) => DropdownMenuItem(value: t, child: Text(t))).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<String>(
|
||
initialValue: selectedType,
|
||
decoration: const InputDecoration(labelText: 'Service Type'),
|
||
items: serviceTypes.map((t) => DropdownMenuItem(value: t, child: Text(t))).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);
|
||
}
|
||
|