import 'dart:convert'; import 'package:flutter/material.dart'; /// Individual service log entry for a motorcycle in the rider's garage. class ServiceRecord { final String id; final DateTime date; final double odometer; final String serviceType; final String? notes; final double? cost; const ServiceRecord({ required this.id, required this.date, required this.odometer, required this.serviceType, this.notes, this.cost, }); Map toJson() => { 'id': id, 'date': date.toIso8601String(), 'odometer': odometer, 'serviceType': serviceType, if (notes != null && notes!.isNotEmpty) 'notes': notes, if (cost != null) 'cost': cost, }; factory ServiceRecord.fromJson(Map json) { return ServiceRecord( id: json['id']?.toString() ?? 'srv_${DateTime.now().millisecondsSinceEpoch}', date: json['date'] != null ? DateTime.tryParse(json['date'].toString()) ?? DateTime.now() : DateTime.now(), odometer: (json['odometer'] as num?)?.toDouble() ?? 0.0, serviceType: json['serviceType']?.toString() ?? 'General Maintenance', notes: json['notes']?.toString(), cost: (json['cost'] as num?)?.toDouble(), ); } } enum MaintenanceUrgency { ok, dueSoon, overdue, } /// Dynamic countdown status for a specific scheduled maintenance item. class MaintenanceItemStatus { final String key; final String title; final IconData icon; final int intervalMiles; final double milesRemaining; final double lastServicedOdometer; final MaintenanceUrgency urgency; const MaintenanceItemStatus({ required this.key, required this.title, required this.icon, required this.intervalMiles, required this.milesRemaining, required this.lastServicedOdometer, required this.urgency, }); String get label { if (milesRemaining <= 0) { return 'OVERDUE (${(-milesRemaining).round()} mi)'; } else if (urgency == MaintenanceUrgency.dueSoon) { return 'Due Soon (${milesRemaining.round()} mi)'; } else { return '${milesRemaining.round()} mi remaining'; } } Color getBadgeColor(BuildContext context) { switch (urgency) { case MaintenanceUrgency.overdue: return const Color(0xFFEF4444); // Red case MaintenanceUrgency.dueSoon: return const Color(0xFFF59E0B); // Amber case MaintenanceUrgency.ok: return const Color(0xFF10B981); // Emerald } } } /// Helper to parse and calculate maintenance intervals and logs from specificationJson. class MaintenanceData { final double baseOdometer; final List serviceRecords; final Map customIntervals; const MaintenanceData({ this.baseOdometer = 0.0, this.serviceRecords = const [], this.customIntervals = const {}, }); static const int defaultOilInterval = 3000; static const int defaultChainInterval = 600; static const int defaultTireInterval = 4000; static const int defaultBrakeInterval = 6000; static MaintenanceData fromSpecificationJson(String? specJson) { if (specJson == null || specJson.trim().isEmpty) { return const MaintenanceData(); } try { final decoded = json.decode(specJson); if (decoded is! Map) return const MaintenanceData(); final baseOdo = (decoded['baseOdometer'] as num?)?.toDouble() ?? 0.0; final rawRecords = decoded['serviceRecords']; final List records = []; if (rawRecords is List) { for (final r in rawRecords) { if (r is Map) { records.add(ServiceRecord.fromJson(r)); } } } // Sort service records descending by date and odometer records.sort((a, b) => b.odometer.compareTo(a.odometer)); final rawIntervals = decoded['customIntervals']; final Map intervals = {}; if (rawIntervals is Map) { for (final entry in rawIntervals.entries) { if (entry.value is num) { intervals[entry.key.toString()] = (entry.value as num).toInt(); } } } return MaintenanceData( baseOdometer: baseOdo, serviceRecords: records, customIntervals: intervals, ); } catch (_) { return const MaintenanceData(); } } /// Calculates real-time countdown pills for all standard maintenance categories. List calculatePills(double totalRiddenMiles) { final currentOdometer = baseOdometer + totalRiddenMiles; final oilInterval = customIntervals['oil'] ?? defaultOilInterval; final chainInterval = customIntervals['chain'] ?? defaultChainInterval; final tireInterval = customIntervals['tire'] ?? defaultTireInterval; final brakeInterval = customIntervals['brake'] ?? defaultBrakeInterval; return [ _calculateItem( key: 'oil', title: 'Engine Oil', icon: Icons.oil_barrel, interval: oilInterval, currentOdometer: currentOdometer, typeMatch: (t) => t.toLowerCase().contains('oil'), dueSoonThreshold: 350, ), _calculateItem( key: 'chain', title: 'Chain Service', icon: Icons.link, interval: chainInterval, currentOdometer: currentOdometer, typeMatch: (t) => t.toLowerCase().contains('chain'), dueSoonThreshold: 80, ), _calculateItem( key: 'tire', title: 'Tire Wear', icon: Icons.tire_repair, interval: tireInterval, currentOdometer: currentOdometer, typeMatch: (t) => t.toLowerCase().contains('tire') || t.toLowerCase().contains('tyre'), dueSoonThreshold: 450, ), _calculateItem( key: 'brake', title: 'Brake Fluid/Pads', icon: Icons.speed, interval: brakeInterval, currentOdometer: currentOdometer, typeMatch: (t) => t.toLowerCase().contains('brake'), dueSoonThreshold: 600, ), ]; } MaintenanceItemStatus _calculateItem({ required String key, required String title, required IconData icon, required int interval, required double currentOdometer, required bool Function(String) typeMatch, required int dueSoonThreshold, }) { // Find the latest service record for this category ServiceRecord? lastService; for (final r in serviceRecords) { if (typeMatch(r.serviceType)) { if (lastService == null || r.odometer > lastService.odometer) { lastService = r; } } } final lastServicedOdo = lastService?.odometer ?? baseOdometer; final milesSinceService = currentOdometer - lastServicedOdo; final milesRemaining = interval - milesSinceService; final urgency = milesRemaining <= 0 ? MaintenanceUrgency.overdue : (milesRemaining <= dueSoonThreshold ? MaintenanceUrgency.dueSoon : MaintenanceUrgency.ok); return MaintenanceItemStatus( key: key, title: title, icon: icon, intervalMiles: interval, milesRemaining: milesRemaining, lastServicedOdometer: lastServicedOdo, urgency: urgency, ); } /// Injects or updates maintenance records and base odometer into the existing specification JSON string. static String updateSpecJsonWithMaintenance({ required String? originalSpecJson, double? baseOdometer, List? serviceRecords, Map? customIntervals, }) { Map map = {}; if (originalSpecJson != null && originalSpecJson.trim().isNotEmpty) { try { final decoded = json.decode(originalSpecJson); if (decoded is Map) { map = Map.from(decoded); } } catch (_) {} } if (baseOdometer != null) { map['baseOdometer'] = baseOdometer; } if (serviceRecords != null) { map['serviceRecords'] = serviceRecords.map((s) => s.toJson()).toList(); } if (customIntervals != null) { map['customIntervals'] = customIntervals; } return json.encode(map); } }