754 lines
21 KiB
Dart
754 lines
21 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
int _asInt(dynamic value, [int? fallback]) {
|
|
if (value is int) return value;
|
|
if (value is num) return value.toInt();
|
|
final parsed = int.tryParse(value?.toString() ?? '');
|
|
return parsed ?? fallback ?? 0;
|
|
}
|
|
|
|
double _asDouble(dynamic value, [double fallback = 0]) {
|
|
if (value is double) return value;
|
|
if (value is num) return value.toDouble();
|
|
final parsed = double.tryParse(value?.toString() ?? '');
|
|
return parsed ?? fallback;
|
|
}
|
|
|
|
String _asString(dynamic value, [String fallback = '']) {
|
|
final str = value?.toString();
|
|
return (str == null) ? fallback : str;
|
|
}
|
|
|
|
bool _asBool(dynamic value, [bool fallback = false]) {
|
|
if (value is bool) return value;
|
|
if (value is num) return value != 0;
|
|
final lower = value?.toString().toLowerCase();
|
|
if (lower == 'true' || lower == 'yes' || lower == '1') return true;
|
|
if (lower == 'false' || lower == 'no' || lower == '0') return false;
|
|
return fallback;
|
|
}
|
|
|
|
DateTime _asDateTime(dynamic value, [DateTime? fallback]) {
|
|
if (value is DateTime) return value;
|
|
final parsed = DateTime.tryParse(value?.toString() ?? '');
|
|
return parsed ?? (fallback ?? DateTime.fromMillisecondsSinceEpoch(0));
|
|
}
|
|
|
|
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> yearlyMileage;
|
|
final List<LocoSummary> topLocos;
|
|
final List<LeaderboardEntry> leaderboard;
|
|
final List<TripSummary> trips;
|
|
final int legCount;
|
|
final UserData? user;
|
|
|
|
HomepageStats({
|
|
required this.totalMileage,
|
|
required this.yearlyMileage,
|
|
required this.topLocos,
|
|
required this.leaderboard,
|
|
required this.trips,
|
|
required this.legCount,
|
|
this.user,
|
|
});
|
|
|
|
factory HomepageStats.fromJson(Map<String, dynamic> 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(),
|
|
legCount: _asInt(
|
|
json['leg_count'],
|
|
(json['trip_legs'] as List?)?.length ?? 0,
|
|
),
|
|
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<String, dynamic> json) => YearlyMileage(
|
|
year: json['year'] is int ? json['year'] as int : int.tryParse('${json['year']}'),
|
|
mileage: _asDouble(json['mileage']),
|
|
);
|
|
}
|
|
|
|
class Loco {
|
|
final int id;
|
|
final String type, number, locoClass;
|
|
final String? name, operator, notes, evn;
|
|
final bool powering;
|
|
|
|
Loco({
|
|
required this.id,
|
|
required this.type,
|
|
required this.number,
|
|
required this.name,
|
|
required this.locoClass,
|
|
required this.operator,
|
|
this.notes,
|
|
this.evn,
|
|
this.powering = true,
|
|
});
|
|
|
|
factory Loco.fromJson(Map<String, dynamic> 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'],
|
|
powering: _asBool(json['alloc_powering'] ?? json['powering'], true),
|
|
);
|
|
}
|
|
|
|
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<String, dynamic> 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<String, dynamic>? extra,
|
|
bool powering = true,
|
|
}) : extra = extra ?? const {},
|
|
super(
|
|
id: locoId,
|
|
type: locoType,
|
|
number: locoNumber,
|
|
name: locoName,
|
|
operator: locoOperator,
|
|
notes: locoNotes,
|
|
evn: locoEvn,
|
|
powering: powering,
|
|
);
|
|
|
|
factory LocoSummary.fromJson(Map<String, dynamic> 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<String, dynamic>.from(json),
|
|
powering: _asBool(json['alloc_powering'] ?? json['powering'], true),
|
|
);
|
|
}
|
|
|
|
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<String, dynamic> 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<LocoAttrVersion> fromGroupedJson(dynamic json) {
|
|
final List<LocoAttrVersion> items = [];
|
|
if (json is Map) {
|
|
json.forEach((key, value) {
|
|
if (value is List) {
|
|
for (final entry in value) {
|
|
if (entry is Map<String, dynamic>) {
|
|
final merged = Map<String, dynamic>.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 LocoChange {
|
|
final int locoId;
|
|
final String locoClass;
|
|
final String locoNumber;
|
|
final String locoName;
|
|
final String attrCode;
|
|
final String attrDisplay;
|
|
final String valueDisplay;
|
|
final DateTime? validFrom;
|
|
final DateTime? approvedAt;
|
|
final String approvedBy;
|
|
|
|
const LocoChange({
|
|
required this.locoId,
|
|
required this.locoClass,
|
|
required this.locoNumber,
|
|
required this.locoName,
|
|
required this.attrCode,
|
|
required this.attrDisplay,
|
|
required this.valueDisplay,
|
|
required this.validFrom,
|
|
required this.approvedAt,
|
|
required this.approvedBy,
|
|
});
|
|
|
|
factory LocoChange.fromJson(Map<String, dynamic> json) {
|
|
String _clean(dynamic value) {
|
|
final str = value?.toString().trim() ?? '';
|
|
if (str.isEmpty || str == '-' || str == '?') return '';
|
|
return str;
|
|
}
|
|
|
|
final valueLabel = json['value_norm'] ??
|
|
json['value_display'] ??
|
|
json['value_label'] ??
|
|
json['value_str'] ??
|
|
json['value_enum'] ??
|
|
json['value_norm'] ??
|
|
json['value'];
|
|
final approvedRaw = json['approved_at'] ?? json['approvedAt'];
|
|
final validFromRaw = json['valid_from'] ?? json['validFrom'];
|
|
return LocoChange(
|
|
locoId: _asInt(json['loco_id']),
|
|
locoClass: _clean(json['loco_class']),
|
|
locoNumber: _clean(json['loco_number']),
|
|
locoName: _clean(json['loco_name']),
|
|
attrCode: _asString(json['attr_code']),
|
|
attrDisplay: _clean(json['attr_display']),
|
|
valueDisplay: _clean(valueLabel),
|
|
validFrom: DateTime.tryParse(validFromRaw?.toString() ?? ''),
|
|
approvedAt: DateTime.tryParse(approvedRaw?.toString() ?? ''),
|
|
approvedBy: _clean(json['approved_by']),
|
|
);
|
|
}
|
|
|
|
String get locoLabel {
|
|
final parts = [locoClass, locoNumber]
|
|
.map((e) => e.trim())
|
|
.where((e) => e.isNotEmpty && e != '-')
|
|
.toList();
|
|
final label = parts.join(' ');
|
|
if (label.isEmpty) return locoName.isNotEmpty ? locoName : 'Loco $locoId';
|
|
return locoName.trim().isEmpty ? label : '$label — ${locoName.trim()}';
|
|
}
|
|
|
|
String get changeLabel =>
|
|
_cleanLabel(attrDisplay).isNotEmpty
|
|
? _cleanLabel(attrDisplay)
|
|
: _cleanLabel(attrCode).toUpperCase();
|
|
|
|
String get approvedDateLabel {
|
|
final date = approvedAt ?? validFrom;
|
|
if (date == null) return 'Pending date';
|
|
return DateFormat('yyyy-MM-dd').format(date);
|
|
}
|
|
|
|
String get valueLabel {
|
|
final value = _cleanLabel(valueDisplay);
|
|
if (value.isNotEmpty) return value;
|
|
return 'Unknown value';
|
|
}
|
|
|
|
String _cleanLabel(String raw) {
|
|
final trimmed = raw.trim();
|
|
if (trimmed.isEmpty) return '';
|
|
if (trimmed == '-' || trimmed == '?') return '';
|
|
return trimmed;
|
|
}
|
|
}
|
|
|
|
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<String, dynamic> json) =>
|
|
LeaderboardEntry(
|
|
userId: _asString(json['user_id']),
|
|
username: _asString(json['username']),
|
|
userFullName: _asString(json['user_full_name']),
|
|
mileage: _asDouble(json['mileage']),
|
|
);
|
|
}
|
|
|
|
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<String, dynamic> json) => TripSummary(
|
|
tripId: _asInt(json['trip_id']),
|
|
tripName: _asString(json['trip_name']),
|
|
tripMileage: _asDouble(json['trip_mileage']),
|
|
);
|
|
}
|
|
|
|
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<Loco> 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<String, dynamic> json) => Leg(
|
|
id: _asInt(json['leg_id']),
|
|
tripId: _asInt(json['leg_trip']),
|
|
start: _asString(json['leg_start']),
|
|
end: _asString(json['leg_end']),
|
|
beginTime: _asDateTime(json['leg_begin_time']),
|
|
timezone: _asInt(json['leg_timezone']),
|
|
network: _asString(json['leg_network']),
|
|
route: _asString(json['leg_route']),
|
|
mileage: _asDouble(json['leg_mileage']),
|
|
notes: _asString(json['leg_notes']),
|
|
headcode: _asString(json['leg_headcode']),
|
|
driving: _asInt(json['leg_driving']),
|
|
user: _asString(json['leg_user']),
|
|
locos: (json['locos'] is List ? (json['locos'] as List) : const [])
|
|
.whereType<Map>()
|
|
.map((e) => Loco.fromJson(Map<String, dynamic>.from(e)))
|
|
.toList(),
|
|
);
|
|
}
|
|
|
|
class RouteError {
|
|
final String error;
|
|
final String msg;
|
|
|
|
RouteError({required this.error, required this.msg});
|
|
|
|
factory RouteError.fromJson(Map<String, dynamic> json) {
|
|
return RouteError(error: json["error"], msg: json["msg"]);
|
|
}
|
|
}
|
|
|
|
class RouteResult {
|
|
final List<String> inputRoute;
|
|
final List<String> calculatedRoute;
|
|
final List<double> costs;
|
|
final double distance;
|
|
|
|
RouteResult({
|
|
required this.inputRoute,
|
|
required this.calculatedRoute,
|
|
required this.costs,
|
|
required this.distance,
|
|
});
|
|
|
|
factory RouteResult.fromJson(Map<String, dynamic> json) {
|
|
return RouteResult(
|
|
inputRoute: List<String>.from(json['input_route']),
|
|
calculatedRoute: List<String>.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<String, dynamic> json) => Station(
|
|
id: _asInt(json['id']),
|
|
name: _asString(json['name']),
|
|
network: _asString(json['network']),
|
|
country: _asString(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<Loco> 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<String, dynamic> 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<String, dynamic>))
|
|
.toList() ??
|
|
[],
|
|
);
|
|
}
|
|
|
|
class TripDetail {
|
|
final int id;
|
|
final String name;
|
|
final double mileage;
|
|
final int legCount;
|
|
final List<TripLeg> legs;
|
|
|
|
TripDetail({
|
|
required this.id,
|
|
required this.name,
|
|
required this.mileage,
|
|
required this.legCount,
|
|
required this.legs,
|
|
});
|
|
|
|
factory TripDetail.fromJson(Map<String, dynamic> 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<String, dynamic>))
|
|
.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<String, dynamic> json) {
|
|
final locoJson = json['loco'] is Map<String, dynamic>
|
|
? Map<String, dynamic>.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<String>? enumValues;
|
|
|
|
const EventField({
|
|
required this.name,
|
|
required this.display,
|
|
this.type,
|
|
this.enumValues,
|
|
});
|
|
|
|
factory EventField.fromJson(Map<String, dynamic> json) {
|
|
final enumList = json['enum'];
|
|
List<String>? 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,
|
|
);
|
|
}
|
|
}
|