import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class DestinationObject { const DestinationObject( this.label, this.icon, this.selectedIcon, this.pageWidget, ); final String label; final Widget icon; final Widget selectedIcon; final Widget pageWidget; } class UserData { const UserData(this.username, this.fullName, this.userId, this.email); final String userId; final String username; final String fullName; final String email; } class AuthenticatedUserData extends UserData { const AuthenticatedUserData({ required String userId, required String username, required String fullName, required String email, required this.accessToken, }) : super(username, fullName, userId, email); final String accessToken; } class HomepageStats { final double totalMileage; final List yearlyMileage; final List topLocos; final List leaderboard; final List trips; final UserData? user; HomepageStats({ required this.totalMileage, required this.yearlyMileage, required this.topLocos, required this.leaderboard, required this.trips, this.user, }); factory HomepageStats.fromJson(Map json) { final userData = json['user_data']; final mileageData = json['milage_data']; final totalMileage = mileageData is Map && mileageData['mileage'] != null ? (mileageData['mileage'] as num).toDouble() : 0.0; return HomepageStats( totalMileage: totalMileage, yearlyMileage: (json['yearly_mileage'] as List? ?? []) .map((e) => YearlyMileage.fromJson(e)) .toList(), topLocos: (json['top_locos'] as List? ?? []) .map((e) => LocoSummary.fromJson(e)) .toList(), leaderboard: (json['leaderboard_data'] as List? ?? []) .map((e) => LeaderboardEntry.fromJson(e)) .toList(), trips: (json['trip_data'] as List? ?? []) .map((e) => TripSummary.fromJson(e)) .toList(), user: userData == null ? null : UserData( userData['username'] ?? '', userData['full_name'] ?? '', userData['user_id'] ?? '', userData['email'] ?? '', ), ); } } class YearlyMileage { final int? year; final double mileage; YearlyMileage({this.year, required this.mileage}); factory YearlyMileage.fromJson(Map json) => YearlyMileage( year: json['year'], mileage: (json['mileage'] as num).toDouble(), ); } class Loco { final int id; final String type, number, locoClass; final String? name, operator, notes, evn; Loco({ required this.id, required this.type, required this.number, required this.name, required this.locoClass, required this.operator, this.notes, this.evn, }); factory Loco.fromJson(Map json) => Loco( id: json['loco_id'], type: json['type'], number: json['number'], name: json['name'] ?? "", locoClass: json['class'], operator: json['operator'], notes: json['notes'], evn: json['evn'], ); } class LocoSummary extends Loco { final double? mileage; final int? journeys; final int? trips; final String? status; final String? domain; final String? owner; final String? livery; final String? location; final Map extra; LocoSummary({ required int locoId, required String locoType, required String locoNumber, required String locoName, required super.locoClass, required String locoOperator, String? locoNotes, String? locoEvn, this.mileage, this.journeys, this.trips, this.status, this.domain, this.owner, this.livery, this.location, Map? extra, }) : extra = extra ?? const {}, super( id: locoId, type: locoType, number: locoNumber, name: locoName, operator: locoOperator, notes: locoNotes, evn: locoEvn, ); factory LocoSummary.fromJson(Map json) => LocoSummary( locoId: json['loco_id'] ?? json['id'] ?? 0, locoType: json['type'] ?? json['loco_type'] ?? '', locoNumber: json['number'] ?? json['loco_number'] ?? '', locoName: json['name'] ?? json['loco_name'] ?? "", locoClass: json['class'] ?? json['loco_class'] ?? '', locoOperator: json['operator'] ?? json['loco_operator'] ?? '', locoNotes: json['notes'], locoEvn: json['evn'] ?? json['loco_evn'], mileage: ((json['loco_mileage'] ?? json['mileage']) as num?)?.toDouble() ?? 0, journeys: (json['loco_journeys'] ?? json['journeys'] ?? 0) is num ? (json['loco_journeys'] ?? json['journeys'] ?? 0).toInt() : 0, trips: (json['loco_trips'] ?? json['trips']) is num ? (json['loco_trips'] ?? json['trips']).toInt() : null, status: json['status'] ?? json['loco_status'], domain: json['domain'], owner: json['owner'] ?? json['loco_owner'], livery: json['livery'], location: json['location'], extra: Map.from(json), ); } class LocoAttrVersion { final String attrCode; final int? versionId; final int locoId; final int? attrTypeId; final String? valueStr; final int? valueInt; final DateTime? valueDate; final bool? valueBool; final String? valueEnum; final DateTime? validFrom; final DateTime? validTo; final DateTime? txnFrom; final DateTime? txnTo; final String? suggestedBy; final String? approvedBy; final DateTime? approvedAt; final int? sourceEventId; final String? precisionLevel; final String? maskedValidFrom; final dynamic valueNorm; const LocoAttrVersion({ required this.attrCode, required this.locoId, this.versionId, this.attrTypeId, this.valueStr, this.valueInt, this.valueDate, this.valueBool, this.valueEnum, this.validFrom, this.validTo, this.txnFrom, this.txnTo, this.suggestedBy, this.approvedBy, this.approvedAt, this.sourceEventId, this.precisionLevel, this.maskedValidFrom, this.valueNorm, }); factory LocoAttrVersion.fromJson(Map json) { return LocoAttrVersion( attrCode: json['attr_code']?.toString() ?? '', locoId: (json['loco_id'] as num?)?.toInt() ?? 0, versionId: (json['loco_attr_v_id'] as num?)?.toInt(), attrTypeId: (json['attr_type_id'] as num?)?.toInt(), valueStr: json['value_str']?.toString(), valueInt: (json['value_int'] as num?)?.toInt(), valueDate: _parseDate(json['value_date']), valueBool: _parseBool(json['value_bool']), valueEnum: json['value_enum']?.toString(), validFrom: _parseDate(json['valid_from']), validTo: _parseDate(json['valid_to']), txnFrom: _parseDate(json['txn_from']), txnTo: _parseDate(json['txn_to']), suggestedBy: json['suggested_by']?.toString(), approvedBy: json['approved_by']?.toString(), approvedAt: _parseDate(json['approved_at']), sourceEventId: (json['source_event_id'] as num?)?.toInt(), precisionLevel: json['precision_level']?.toString(), maskedValidFrom: json['masked_valid_from']?.toString(), valueNorm: json['value_norm'], ); } static DateTime? _parseDate(dynamic value) { if (value == null) return null; if (value is DateTime) return value; return DateTime.tryParse(value.toString()); } static bool? _parseBool(dynamic value) { if (value == null) return null; if (value is bool) return value; if (value is num) return value != 0; final str = value.toString().toLowerCase(); if (['true', '1', 'yes'].contains(str)) return true; if (['false', '0', 'no'].contains(str)) return false; return null; } static List fromGroupedJson(dynamic json) { final List items = []; if (json is Map) { json.forEach((key, value) { if (value is List) { for (final entry in value) { if (entry is Map) { final merged = Map.from(entry); merged.putIfAbsent('attr_code', () => key); items.add(LocoAttrVersion.fromJson(merged)); } } } }); } items.sort( (a, b) { final aDate = a.validFrom ?? a.txnFrom ?? DateTime.fromMillisecondsSinceEpoch(0); final bDate = b.validFrom ?? b.txnFrom ?? DateTime.fromMillisecondsSinceEpoch(0); final dateCompare = aDate.compareTo(bDate); if (dateCompare != 0) return dateCompare; return a.attrCode.compareTo(b.attrCode); }, ); return items; } String get valueLabel { if (valueStr != null && valueStr!.isNotEmpty) return valueStr!; if (valueEnum != null && valueEnum!.isNotEmpty) return valueEnum!; if (valueInt != null) return valueInt!.toString(); if (valueBool != null) return valueBool! ? 'Yes' : 'No'; if (valueDate != null) return DateFormat('yyyy-MM-dd').format(valueDate!); if (valueNorm != null && valueNorm.toString().isNotEmpty) { return valueNorm.toString(); } return '—'; } String get validityRange { final start = maskedValidFrom ?? _formatDate(validFrom) ?? 'Unknown'; final end = _formatDate(validTo, fallback: 'Present') ?? 'Present'; return '$start → $end'; } String? _formatDate(DateTime? value, {String? fallback}) { if (value == null) return fallback; return DateFormat('yyyy-MM-dd').format(value); } } class LeaderboardEntry { final String userId, username, userFullName; final double mileage; LeaderboardEntry({ required this.userId, required this.username, required this.userFullName, required this.mileage, }); factory LeaderboardEntry.fromJson(Map json) => LeaderboardEntry( userId: json['user_id'], username: json['username'], userFullName: json['user_full_name'], mileage: (json['mileage'] as num).toDouble(), ); } class TripSummary { final int tripId; final String tripName; final double tripMileage; TripSummary({ required this.tripId, required this.tripName, required this.tripMileage, }); factory TripSummary.fromJson(Map json) => TripSummary( tripId: json['trip_id'], tripName: json['trip_name'], tripMileage: (json['trip_mileage'] as num).toDouble(), ); } class Leg { final int id, tripId, timezone, driving; final String start, end, route, network, notes, headcode, user; final DateTime beginTime; final double mileage; final List locos; Leg({ required this.id, required this.tripId, required this.start, required this.end, required this.beginTime, required this.timezone, required this.network, required this.route, required this.mileage, required this.notes, required this.headcode, required this.driving, required this.user, required this.locos, }); factory Leg.fromJson(Map json) => Leg( id: json['leg_id'], tripId: json['leg_trip'] ?? 0, start: json['leg_start'], end: json['leg_end'], beginTime: DateTime.parse(json['leg_begin_time']), timezone: (json['leg_timezone'] as num).toInt(), network: json['leg_network'] ?? "", route: json['leg_route'], mileage: (json['leg_mileage'] as num).toDouble(), notes: json['leg_notes'] ?? "", headcode: json['leg_headcode'] ?? "", driving: json['leg_driving'], user: json['leg_user'], locos: (json['locos'] as List) .map((e) => Loco.fromJson(e as Map)) .toList(), ); } class RouteError { final String error; final String msg; RouteError({required this.error, required this.msg}); factory RouteError.fromJson(Map json) { return RouteError(error: json["error"], msg: json["msg"]); } } class RouteResult { final List inputRoute; final List calculatedRoute; final List costs; final double distance; RouteResult({ required this.inputRoute, required this.calculatedRoute, required this.costs, required this.distance, }); factory RouteResult.fromJson(Map json) { return RouteResult( inputRoute: List.from(json['input_route']), calculatedRoute: List.from(json['calculated_route']), costs: (json['costs'] as List).map((e) => (e as num).toDouble()).toList(), distance: (json['distance'] as num).toDouble(), ); } } class Station { final int id; final String name; final String network; final String country; Station({ required this.id, required this.name, required this.network, required this.country, }); factory Station.fromJson(Map json) => Station( id: json['id'], name: json['name'], network: json['network'], country: json['country'], ); } class TripLeg { final int? id; final String start; final String end; final DateTime? beginTime; final String? network; final String? route; final double? mileage; final String? notes; final List locos; TripLeg({ required this.id, required this.start, required this.end, required this.beginTime, required this.network, required this.route, required this.mileage, required this.notes, required this.locos, }); factory TripLeg.fromJson(Map json) => TripLeg( id: json['leg_id'], start: json['leg_start'] ?? '', end: json['leg_end'] ?? '', beginTime: json['leg_begin_time'] != null && json['leg_begin_time'] is String ? DateTime.tryParse(json['leg_begin_time']) : (json['leg_begin_time'] is DateTime ? json['leg_begin_time'] : null), network: json['leg_network'], route: json['leg_route'], mileage: (json['leg_mileage'] as num?)?.toDouble(), notes: json['leg_notes'], locos: (json['locos'] as List?) ?.map((e) => Loco.fromJson(e as Map)) .toList() ?? [], ); } class TripDetail { final int id; final String name; final double mileage; final int legCount; final List legs; TripDetail({ required this.id, required this.name, required this.mileage, required this.legCount, required this.legs, }); factory TripDetail.fromJson(Map json) => TripDetail( id: json['trip_id'] ?? json['id'] ?? 0, name: json['trip_name'] ?? '', mileage: (json['trip_mileage'] as num?)?.toDouble() ?? 0, legCount: json['leg_count'] ?? ((json['trip_legs'] as List?)?.length ?? 0), legs: (json['trip_legs'] as List?) ?.map((e) => TripLeg.fromJson(e as Map)) .toList() ?? [], ); } class TripLocoStat { final String locoClass; final String number; final String? name; final bool won; TripLocoStat({ required this.locoClass, required this.number, required this.won, this.name, }); factory TripLocoStat.fromJson(Map json) { final locoJson = json['loco'] is Map ? Map.from(json['loco'] as Map) : null; final locoClass = json['loco_class'] ?? json['class'] ?? locoJson?['class'] ?? locoJson?['loco_class'] ?? ''; final locoNumber = json['loco_number'] ?? json['number'] ?? locoJson?['number'] ?? locoJson?['loco_number'] ?? ''; final locoName = json['loco_name'] ?? json['name'] ?? locoJson?['name'] ?? locoJson?['loco_name']; final wonValue = json['won'] ?? json['result'] ?? json['winner']; final won = _parseWonFlag(wonValue); return TripLocoStat( locoClass: locoClass, number: locoNumber, name: locoName, won: won, ); } static bool _parseWonFlag(dynamic value) { if (value == null) return false; if (value is bool) return value; if (value is num) return value != 0; final normalized = value.toString().toLowerCase(); return ['1', 'true', 'win', 'won', 'yes', 'w'].contains(normalized); } } class EventField { final String name; final String display; final String? type; final List? enumValues; const EventField({ required this.name, required this.display, this.type, this.enumValues, }); factory EventField.fromJson(Map json) { final enumList = json['enum']; List? enumValues; if (enumList is List) { enumValues = enumList.map((e) => e.toString()).toList(); } final baseName = json['name']?.toString() ?? json['field']?.toString() ?? ''; final display = json['field']?.toString() ?? baseName; return EventField( name: baseName, display: display, type: json['type']?.toString(), enumValues: enumValues, ); } }