MotorSignal-App/lib/models/maintenance_models.dart

278 lines
8.2 KiB
Dart

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<String, dynamic> 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<String, dynamic> 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<ServiceRecord> serviceRecords;
final Map<String, int> 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<String, dynamic>) return const MaintenanceData();
final baseOdo = (decoded['baseOdometer'] as num?)?.toDouble() ?? 0.0;
final rawRecords = decoded['serviceRecords'];
final List<ServiceRecord> records = [];
if (rawRecords is List) {
for (final r in rawRecords) {
if (r is Map<String, dynamic>) {
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<String, int> 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.
/// Returns empty list if no odometer is set and no service records exist yet,
/// avoiding fake synthetic countdowns.
List<MaintenanceItemStatus> calculatePills(double totalRiddenMiles) {
if (baseOdometer == 0.0 && serviceRecords.isEmpty) {
return const [];
}
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<ServiceRecord>? serviceRecords,
Map<String, int>? customIntervals,
}) {
Map<String, dynamic> map = {};
if (originalSpecJson != null && originalSpecJson.trim().isNotEmpty) {
try {
final decoded = json.decode(originalSpecJson);
if (decoded is Map<String, dynamic>) {
map = Map<String, dynamic>.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);
}
}