import "package:bus_running_record/models/operations/duty.dart"; import "package:bus_running_record/models/operations/scheduled_stop.dart"; import "package:bus_running_record/models/operations/stop.dart"; class Trip { final String scheduledTime; // "15:31" - default departure time final String tripNumber; // "112" final Duty duty; final String tripType; // "", "N", "R", "F" final bool isFinishing; final List scheduledStops; final String direction; // "outbound" or "inbound" String? actualDepartureTime; // "15:33" (nullable - user input) String? actualFleetNumber; // "33523" (nullable - user input) Trip({ required this.scheduledTime, required this.tripNumber, String? dutyNumber, String? busWorkNumber, Duty? duty, this.tripType = "", this.isFinishing = false, Map stationTimes = const {}, List stationOrder = const [], List? scheduledStops, this.direction = "outbound", this.actualDepartureTime, this.actualFleetNumber, }) : duty = duty ?? Duty( dutyNumber: dutyNumber ?? "", busWorkNumber: busWorkNumber ?? "", ), scheduledStops = scheduledStops ?? _buildScheduledStops(stationTimes, stationOrder); bool get isComplete => actualDepartureTime != null && actualFleetNumber != null; String get dutyNumber => duty.dutyNumber; String get busWorkNumber => duty.busWorkNumber; List get stationOrder => scheduledStops.map((stop) => stop.name).toList(growable: false); Map get stationTimes { final result = {}; for (final stop in scheduledStops) { final time = (stop.scheduledTime ?? "").trim(); if (time.isEmpty) continue; result[stop.name] = time; } return result; } Trip copyWith({ String? scheduledTime, String? tripNumber, String? dutyNumber, String? busWorkNumber, Duty? duty, String? tripType, bool? isFinishing, Map? stationTimes, List? stationOrder, List? scheduledStops, String? direction, String? actualDepartureTime, String? actualFleetNumber, }) { return Trip( scheduledTime: scheduledTime ?? this.scheduledTime, tripNumber: tripNumber ?? this.tripNumber, duty: duty ?? this.duty.copyWith( dutyNumber: dutyNumber, busWorkNumber: busWorkNumber, ), tripType: tripType ?? this.tripType, isFinishing: isFinishing ?? this.isFinishing, scheduledStops: scheduledStops ?? (stationTimes != null || stationOrder != null ? _buildScheduledStops( stationTimes ?? this.stationTimes, stationOrder ?? this.stationOrder, ) : this.scheduledStops), direction: direction ?? this.direction, actualDepartureTime: actualDepartureTime ?? this.actualDepartureTime, actualFleetNumber: actualFleetNumber ?? this.actualFleetNumber, ); } Trip withStopAliases( Map aliasesByRawName, { String aliasSource = "ai", }) { if (aliasesByRawName.isEmpty) return this; final aliasesByNormalizedName = {}; for (final entry in aliasesByRawName.entries) { aliasesByNormalizedName[Stop.normalizeName(entry.key)] = entry.value; } return copyWith( scheduledStops: scheduledStops .map( (stop) => stop.copyWith( alias: aliasesByNormalizedName[Stop.normalizeName(stop.name)], aliasSource: aliasesByNormalizedName[Stop.normalizeName(stop.name)] == null ? stop.aliasSource : aliasSource, ), ) .toList(growable: false), ); } Map toJson() { return { "scheduledTime": scheduledTime, "tripNumber": tripNumber, "duty": duty.toJson(), "dutyNumber": dutyNumber, "busWorkNumber": busWorkNumber, "tripType": tripType, "isFinishing": isFinishing, "scheduledStops": scheduledStops.map((stop) => stop.toJson()).toList(), "stationTimes": stationTimes, "stationOrder": stationOrder, "direction": direction, "actualDepartureTime": actualDepartureTime, "actualFleetNumber": actualFleetNumber, }; } factory Trip.fromJson(Map json) { final dutyJson = json["duty"]; final duty = dutyJson is Map ? Duty.fromJson(dutyJson) : Duty( dutyNumber: (json["dutyNumber"] ?? "").toString(), busWorkNumber: (json["busWorkNumber"] ?? json["runningNumber"] ?? "") .toString(), ); final scheduledStopsJson = json["scheduledStops"]; final scheduledStops = scheduledStopsJson is List ? scheduledStopsJson .whereType() .map( (stopJson) => ScheduledStop.fromJson(Map.from(stopJson)), ) .toList() : null; return Trip( scheduledTime: json["scheduledTime"] as String, tripNumber: json["tripNumber"] as String, duty: duty, tripType: json["tripType"] as String? ?? "", isFinishing: json["isFinishing"] as bool? ?? false, scheduledStops: scheduledStops, stationTimes: Map.from( json["stationTimes"] as Map? ?? {}, ), stationOrder: List.from(json["stationOrder"] as List? ?? []), direction: json["direction"] as String? ?? "outbound", actualDepartureTime: json["actualDepartureTime"] as String?, actualFleetNumber: json["actualFleetNumber"] as String?, ); } static List _buildScheduledStops( Map stationTimes, List stationOrder, ) { if (stationOrder.isEmpty) { var index = 0; return stationTimes.entries .map( (entry) => ScheduledStop( name: entry.key, sequence: ++index, scheduledTime: entry.value.trim().isEmpty ? null : entry.value, ), ) .toList(growable: false); } return List.generate(stationOrder.length, (i) { final stopName = stationOrder[i]; final time = (stationTimes[stopName] ?? "").trim(); return ScheduledStop( name: stopName, sequence: i + 1, scheduledTime: time.isEmpty ? null : time, ); }, growable: false); } }