Update readme's, branches and actions
Build and Push Docker Images / detect-changes (push) Successful in 10s
Build and Push Docker Images / build-auth-api (push) Successful in 58s
Build and Push Docker Images / build-employee-api (push) Successful in 58s
Build and Push Docker Images / build-transit-api (push) Successful in 59s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m8s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m14s
Build and Push Docker Images / deploy-test-node (push) Successful in 5s

This commit is contained in:
Sven laptop
2026-09-17 12:04:44 +02:00
parent fdbdb98b21
commit 9d1a37e248
158 changed files with 428 additions and 176 deletions
View File
+211
View File
@@ -0,0 +1,211 @@
import 'package:latlong2/latlong.dart';
/// Navigation route point
class NavigationPoint {
final double latitude;
final double longitude;
final String? name;
NavigationPoint({
required this.latitude,
required this.longitude,
this.name,
});
LatLng toLatLng() => LatLng(latitude, longitude);
factory NavigationPoint.fromLatLng(LatLng latlng, {String? name}) {
return NavigationPoint(
latitude: latlng.latitude,
longitude: latlng.longitude,
name: name,
);
}
Map<String, dynamic> toJson() {
return {
'latitude': latitude,
'longitude': longitude,
'name': name,
};
}
factory NavigationPoint.fromJson(Map<String, dynamic> json) {
return NavigationPoint(
latitude: json['latitude'] as double,
longitude: json['longitude'] as double,
name: json['name'] as String?,
);
}
}
/// Route leg with distance and duration
class RouteLeg {
final List<NavigationPoint> points;
final double distance; // in meters
final int duration; // in seconds
final String summary;
RouteLeg({
required this.points,
required this.distance,
required this.duration,
required this.summary,
});
String getDistanceKm() => (distance / 1000).toStringAsFixed(1);
String getDurationMinutes() => (duration ~/ 60).toString();
factory RouteLeg.fromCoordinates(
List<dynamic> coordinates, {
double distance = 0.0,
int duration = 0,
String summary = 'Route',
}) {
return RouteLeg(
points: coordinates
.map((coord) {
if (coord is List && coord.length >= 2) {
return NavigationPoint(
latitude: (coord[1] as num).toDouble(),
longitude: (coord[0] as num).toDouble(),
);
}
return null;
})
.whereType<NavigationPoint>()
.toList(),
distance: distance,
duration: duration,
summary: summary,
);
}
factory RouteLeg.fromPoints({
required List<NavigationPoint> points,
required double distance,
required int duration,
String summary = 'Route',
}) {
return RouteLeg(
points: points,
distance: distance,
duration: duration,
summary: summary,
);
}
factory RouteLeg.fromJson(Map<String, dynamic> json) {
final geometry = json['geometry'] as Map<String, dynamic>?;
final coordinates = geometry?['coordinates'] as List? ?? [];
return RouteLeg(
points: coordinates
.map((coord) {
if (coord is List && coord.length >= 2) {
return NavigationPoint(
latitude: (coord[1] as num).toDouble(),
longitude: (coord[0] as num).toDouble(),
);
}
return null;
})
.whereType<NavigationPoint>()
.toList(),
distance: (json['distance'] as num?)?.toDouble() ?? 0.0,
duration: (json['duration'] as num?)?.toInt() ?? 0,
summary: (json['summary'] as String?) ?? 'Route',
);
}
}
/// Complete navigation route from start to end
class NavigationRoute {
final NavigationPoint start;
final NavigationPoint end;
final List<RouteLeg> legs;
final double totalDistance; // in meters
final int totalDuration; // in seconds
NavigationRoute({
required this.start,
required this.end,
required this.legs,
required this.totalDistance,
required this.totalDuration,
});
String getTotalDistanceKm() => (totalDistance / 1000).toStringAsFixed(1);
String getTotalDurationMinutes() => (totalDuration ~/ 60).toString();
String getTotalDurationHM() {
final hours = totalDuration ~/ 3600;
final minutes = (totalDuration % 3600) ~/ 60;
if (hours > 0) {
return '${hours}h ${minutes}m';
}
return '${minutes}m';
}
List<NavigationPoint> getAllPoints() {
final allPoints = <NavigationPoint>[];
for (final leg in legs) {
allPoints.addAll(leg.points);
}
return allPoints;
}
factory NavigationRoute.fromJson(Map<String, dynamic> json) {
final routes = json['routes'] as List?;
if (routes == null || routes.isEmpty) {
throw Exception('No route found in response');
}
final routeData = routes[0] as Map<String, dynamic>?;
if (routeData == null) {
throw Exception('Invalid route data');
}
return NavigationRoute.fromOsrmRouteData(
routeData,
start: NavigationPoint(latitude: 0, longitude: 0),
end: NavigationPoint(latitude: 0, longitude: 0),
);
}
factory NavigationRoute.fromOsrmRouteData(
Map<String, dynamic> routeData, {
required NavigationPoint start,
required NavigationPoint end,
}) {
final geometry = routeData['geometry'] as Map<String, dynamic>?;
final coordinates = geometry?['coordinates'] as List? ?? [];
final distance = (routeData['distance'] as num?)?.toDouble() ?? 0.0;
final duration = (routeData['duration'] as num?)?.toInt() ?? 0;
final leg = RouteLeg.fromCoordinates(
coordinates,
distance: distance,
duration: duration,
summary: (routeData['summary'] as String?) ?? 'Route',
);
return NavigationRoute(
start: start,
end: end,
legs: [leg],
totalDistance: distance,
totalDuration: duration,
);
}
}
/// Result of an optimized trip calculation with reordered stops.
class OptimizedRouteResult {
final NavigationRoute route;
final List<NavigationPoint> orderedStops;
OptimizedRouteResult({
required this.route,
required this.orderedStops,
});
}
+222
View File
@@ -0,0 +1,222 @@
import 'dart:convert';
/// Bus Line model
class BusLine {
final int id;
final int lineNumber;
final String startStop;
final String endStop;
final int estimatedDuration; // in minutes
final String? description;
BusLine({
required this.id,
required this.lineNumber,
required this.startStop,
required this.endStop,
required this.estimatedDuration,
this.description,
});
factory BusLine.fromJson(Map<String, dynamic> json) {
return BusLine(
id: json['id'] as int,
lineNumber: json['lineNumber'] as int,
startStop: json['startStop'] as String,
endStop: json['endStop'] as String,
estimatedDuration: json['estimatedDuration'] as int,
description: json['description'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'lineNumber': lineNumber,
'startStop': startStop,
'endStop': endStop,
'estimatedDuration': estimatedDuration,
'description': description,
};
}
}
/// Bus type model
class BusType {
final int id;
final String name;
final int seatCapacity;
final String licensePlate;
BusType({
required this.id,
required this.name,
required this.seatCapacity,
required this.licensePlate,
});
factory BusType.fromJson(Map<String, dynamic> json) {
return BusType(
id: json['id'] as int,
name: json['name'] as String,
seatCapacity: json['seatCapacity'] as int,
licensePlate: json['licensePlate'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'seatCapacity': seatCapacity,
'licensePlate': licensePlate,
};
}
}
/// Route model for daily schedule
class Route {
final int id;
final BusLine busLine;
final BusType busType;
final DateTime startTime;
final DateTime? endTime;
final String status; // scheduled, in_progress, completed, cancelled
Route({
required this.id,
required this.busLine,
required this.busType,
required this.startTime,
this.endTime,
required this.status,
});
factory Route.fromJson(Map<String, dynamic> json) {
return Route(
id: json['id'] as int,
busLine: BusLine.fromJson(json['busLine'] as Map<String, dynamic>),
busType: BusType.fromJson(json['busType'] as Map<String, dynamic>),
startTime: DateTime.parse(json['startTime'] as String),
endTime:
json['endTime'] != null ? DateTime.parse(json['endTime'] as String) : null,
status: json['status'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'busLine': busLine.toJson(),
'busType': busType.toJson(),
'startTime': startTime.toIso8601String(),
'endTime': endTime?.toIso8601String(),
'status': status,
};
}
}
/// Daily schedule model
class DailySchedule {
final List<Route> routes;
final DateTime date;
DailySchedule({
required this.routes,
required this.date,
});
factory DailySchedule.fromJson(Map<String, dynamic> json) {
return DailySchedule(
routes: (json['routes'] as List)
.map((r) => Route.fromJson(r as Map<String, dynamic>))
.toList(),
date: DateTime.parse(json['date'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'routes': routes.map((r) => r.toJson()).toList(),
'date': date.toIso8601String(),
};
}
}
/// Stop model for the new schedule system.
class ScheduleStop {
final int id;
final int order;
final String name;
final double latitude;
final double longitude;
final int estimatedArrivalMinutes;
ScheduleStop({
required this.id,
required this.order,
required this.name,
required this.latitude,
required this.longitude,
required this.estimatedArrivalMinutes,
});
factory ScheduleStop.fromJson(Map<String, dynamic> json) {
return ScheduleStop(
id: (json['id'] as num).toInt(),
order: (json['order'] as num).toInt(),
name: (json['name'] ?? json['stopName'] ?? '') as String,
latitude: (json['latitude'] as num).toDouble(),
longitude: (json['longitude'] as num).toDouble(),
estimatedArrivalMinutes: (json['estimatedArrivalMinutes'] as num?)?.toInt() ?? 0,
);
}
}
/// Schedule model for the new public schedule API.
class ServiceSchedule {
final int id;
final int busLineId;
final int busId;
final DateTime startTime;
final DateTime endTime;
final String status;
final int lineNumber;
final String busName;
final List<int> weekdays;
ServiceSchedule({
required this.id,
required this.busLineId,
required this.busId,
required this.startTime,
required this.endTime,
required this.status,
required this.lineNumber,
required this.busName,
required this.weekdays,
});
factory ServiceSchedule.fromJson(Map<String, dynamic> json) {
final rawWeekdays = json['weekdays'];
final weekdays = rawWeekdays is List
? rawWeekdays.map((day) => (day as num).toInt()).toList()
: rawWeekdays is String
? (jsonDecode(rawWeekdays) as List).map((day) => (day as num).toInt()).toList()
: <int>[];
return ServiceSchedule(
id: (json['id'] as num).toInt(),
busLineId: (json['busLineId'] as num).toInt(),
busId: (json['busId'] as num).toInt(),
startTime: DateTime.parse(json['startTime'] as String),
endTime: DateTime.parse(json['endTime'] as String),
status: (json['status'] ?? 'planned') as String,
lineNumber: (json['lineNumber'] as num).toInt(),
busName: (json['busName'] ?? '') as String,
weekdays: weekdays,
);
}
bool get isRecurring => weekdays.isNotEmpty;
}
+85
View File
@@ -0,0 +1,85 @@
/// User model
class User {
final int id;
final int userCode;
final String? job;
User({
required this.id,
required this.userCode,
this.job,
});
factory User.fromJson(Map<String, dynamic> json) {
// userCode may be provided as 'userCode' or 'usercode' and may be int or string
final dynamic rawUserCode = json['userCode'] ?? json['usercode'] ?? json['user_code'];
int parsedUserCode;
if (rawUserCode is int) {
parsedUserCode = rawUserCode;
} else if (rawUserCode is String) {
parsedUserCode = int.tryParse(rawUserCode) ?? (throw FormatException('Invalid userCode'));
} else {
// If the backend doesn't return a userCode, treat as unknown (-1)
parsedUserCode = -1;
}
return User(
id: json['id'] as int,
userCode: parsedUserCode,
job: json['job'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'userCode': userCode,
'job': job,
};
}
}
/// Login response model
class AuthResponse {
final String token;
final User? user;
AuthResponse({
required this.token,
this.user,
});
factory AuthResponse.fromJson(Map<String, dynamic> json) {
final dynamic rawToken = json['token'] ?? json['Token'] ?? json['authToken'];
final token = rawToken?.toString();
if (token == null || token.isEmpty) throw FormatException('Missing token in auth response');
// Determine if a user object exists and appears valid
Map<String, dynamic>? userMap;
if (json.containsKey('user') && json['user'] is Map<String, dynamic>) {
userMap = json['user'] as Map<String, dynamic>;
} else {
// maybe user fields are top-level (id, userCode, job)
final hasUserFields = json.containsKey('id') || json.containsKey('userCode') || json.containsKey('usercode') || json.containsKey('user_code');
if (hasUserFields) {
// treat whole json as user map
userMap = json.cast<String, dynamic>();
}
}
User? parsedUser;
if (userMap != null) {
try {
parsedUser = User.fromJson(userMap);
} catch (e) {
// If parsing user fails, ignore user but keep token
parsedUser = null;
}
}
return AuthResponse(
token: token,
user: parsedUser,
);
}
}