Enhance navigation and error handling; implement global navigator and messenger keys, update schedule progress retrieval, and improve error messaging in various services
This commit is contained in:
@@ -3,6 +3,7 @@ import 'screens/login_screen.dart';
|
|||||||
import 'screens/schedule_overview_screen.dart';
|
import 'screens/schedule_overview_screen.dart';
|
||||||
import 'services/auth_service.dart';
|
import 'services/auth_service.dart';
|
||||||
import 'theme/app_theme.dart';
|
import 'theme/app_theme.dart';
|
||||||
|
import 'services/app_navigator.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MainApp());
|
runApp(const MainApp());
|
||||||
@@ -15,6 +16,8 @@ class MainApp extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Bus Terminal',
|
title: 'Bus Terminal',
|
||||||
|
navigatorKey: navigatorKey,
|
||||||
|
scaffoldMessengerKey: messengerKey,
|
||||||
theme: AppTheme.lightTheme(),
|
theme: AppTheme.lightTheme(),
|
||||||
darkTheme: AppTheme.darkTheme(),
|
darkTheme: AppTheme.darkTheme(),
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class User {
|
|||||||
} else if (rawUserCode is String) {
|
} else if (rawUserCode is String) {
|
||||||
parsedUserCode = int.tryParse(rawUserCode) ?? (throw FormatException('Invalid userCode'));
|
parsedUserCode = int.tryParse(rawUserCode) ?? (throw FormatException('Invalid userCode'));
|
||||||
} else {
|
} else {
|
||||||
throw FormatException('Missing userCode');
|
// If the backend doesn't return a userCode, treat as unknown (-1)
|
||||||
|
parsedUserCode = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return User(
|
return User(
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> with Ti
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_scheduleFuture = _scheduleService.getSchedules(_selectedDate);
|
_scheduleFuture = _scheduleService.getSchedules(_selectedDate);
|
||||||
_tabController = TabController(length: 2, vsync: this);
|
_tabController = TabController(length: 3, vsync: this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -125,7 +125,11 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> with Ti
|
|||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
try {
|
||||||
await _scheduleService.updateScheduleStatus(svc.id, 'in_progress');
|
await _scheduleService.updateScheduleStatus(
|
||||||
|
svc.id,
|
||||||
|
'in_progress',
|
||||||
|
date: svc.startTime,
|
||||||
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(sheetContext).pop();
|
Navigator.of(sheetContext).pop();
|
||||||
|
|
||||||
@@ -157,11 +161,18 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> with Ti
|
|||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
Navigator.of(context).push(
|
try {
|
||||||
|
await Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => NavigationScreen(route: routeObj, initialStops: navStops),
|
builder: (_) => NavigationScreen(route: routeObj, initialStops: navStops),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} catch (navErr) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Navigatiefout: ${navErr.toString()}')),
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -227,6 +238,7 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> with Ti
|
|||||||
tabs: const [
|
tabs: const [
|
||||||
Tab(icon: Icon(Icons.calendar_today), text: 'Dagschema'),
|
Tab(icon: Icon(Icons.calendar_today), text: 'Dagschema'),
|
||||||
Tab(icon: Icon(Icons.repeat), text: 'Vaste diensten'),
|
Tab(icon: Icon(Icons.repeat), text: 'Vaste diensten'),
|
||||||
|
Tab(icon: Icon(Icons.timeline), text: 'Voortgang'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -249,12 +261,131 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> with Ti
|
|||||||
scheduleFuture: _scheduleFuture,
|
scheduleFuture: _scheduleFuture,
|
||||||
),
|
),
|
||||||
const RecurringSchedulesScreen(),
|
const RecurringSchedulesScreen(),
|
||||||
|
_ProgressTab(
|
||||||
|
scheduleService: _scheduleService,
|
||||||
|
selectedDate: _selectedDate,
|
||||||
|
scheduleFuture: _scheduleFuture,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ProgressTab extends StatelessWidget {
|
||||||
|
final ScheduleService scheduleService;
|
||||||
|
final DateTime selectedDate;
|
||||||
|
final Future<List<sched.ServiceSchedule>> scheduleFuture;
|
||||||
|
|
||||||
|
const _ProgressTab({
|
||||||
|
required this.scheduleService,
|
||||||
|
required this.selectedDate,
|
||||||
|
required this.scheduleFuture,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final dateStr = '${selectedDate.year}-${selectedDate.month.toString().padLeft(2, '0')}-${selectedDate.day.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('Voortgang voor $dateStr', style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
// refresh handled by parent via reselecting date; show a simple feedback
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Ververs de datum in Dagschema-tab om andere datum te laden')));
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.info_outline),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: FutureBuilder<List<sched.ServiceSchedule>>(
|
||||||
|
future: scheduleFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
||||||
|
if (snapshot.hasError) return Center(child: Text('Fout bij laden: ${snapshot.error}'));
|
||||||
|
|
||||||
|
final schedules = snapshot.data ?? [];
|
||||||
|
if (schedules.isEmpty) return Center(child: Text('Geen diensten op $dateStr'));
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
itemCount: schedules.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final svc = schedules[index];
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text('Lijn ${svc.lineNumber} — ${svc.busName}'),
|
||||||
|
subtitle: Text('${_formatTime(svc.startTime)}'),
|
||||||
|
trailing: TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
final progress = await scheduleService.getScheduleProgress(svc.id, selectedDate);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
await showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (ctx) {
|
||||||
|
final stops = (progress['stops'] as List<dynamic>?) ?? [];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Voortgang — Lijn ${svc.lineNumber}', style: Theme.of(ctx).textTheme.headlineSmall),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text('Status: ${progress['schedule']?['status'] ?? 'onbekend'}'),
|
||||||
|
Text('Driver state: ${progress['driverState'] ?? 'onbekend'}'),
|
||||||
|
Text('Passed: ${progress['passedStopCount'] ?? 0} / ${progress['totalStopCount'] ?? 0}'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text('Haltes:'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: stops.length,
|
||||||
|
itemBuilder: (c, i) {
|
||||||
|
final s = stops[i] as Map<String, dynamic>;
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text('${s['stopOrder']}. ${s['stopName']}'),
|
||||||
|
subtitle: Text('Scheduled: ${s['scheduledPassedAt'] ?? '-'}\nActual: ${s['actualPassedAt'] ?? '-'}'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Kon voortgang niet laden: $e')));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Bekijk voortgang'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _DailyScheduleTab extends StatelessWidget {
|
class _DailyScheduleTab extends StatelessWidget {
|
||||||
final ScheduleService scheduleService;
|
final ScheduleService scheduleService;
|
||||||
final DateTime selectedDate;
|
final DateTime selectedDate;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'auth_service.dart';
|
||||||
|
|
||||||
|
/// Global navigator and messenger keys so services can navigate/logout
|
||||||
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
final GlobalKey<ScaffoldMessengerState> messengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||||
|
|
||||||
|
/// Handle expired authentication: clear stored token and navigate to login.
|
||||||
|
Future<void> handleAuthExpired({String? message}) async {
|
||||||
|
try {
|
||||||
|
await AuthService().logout();
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Show a message if possible
|
||||||
|
try {
|
||||||
|
final messenger = messengerKey.currentState;
|
||||||
|
if (messenger != null && message != null) {
|
||||||
|
messenger.showSnackBar(SnackBar(content: Text(message)));
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Navigate to login, removing all previous routes
|
||||||
|
try {
|
||||||
|
final nav = navigatorKey.currentState;
|
||||||
|
if (nav != null) {
|
||||||
|
nav.pushNamedAndRemoveUntil('/login', (r) => false);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import '../models/user.dart';
|
|||||||
|
|
||||||
/// Authentication Service
|
/// Authentication Service
|
||||||
class AuthService {
|
class AuthService {
|
||||||
static const String _apiBaseUrl = 'http://192.168.2.66:5000';
|
static const String _apiBaseUrl = 'http://api.transit.local:5000';
|
||||||
static const String _tokenKey = 'auth_token';
|
static const String _tokenKey = 'auth_token';
|
||||||
|
|
||||||
late SharedPreferences _prefs;
|
late SharedPreferences _prefs;
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
/// Custom exception for authentication errors
|
/// Custom exception for authentication errors
|
||||||
|
import 'app_navigator.dart';
|
||||||
|
|
||||||
class UnauthorizedException implements Exception {
|
class UnauthorizedException implements Exception {
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
UnauthorizedException(this.message);
|
UnauthorizedException(this.message) {
|
||||||
|
// Trigger centralized logout/navigation asynchronously
|
||||||
|
try {
|
||||||
|
handleAuthExpired(message: message);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => message;
|
String toString() => message;
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class NavigationService {
|
|||||||
throw Exception('Failed to get route (400) and fallback failed: ${e.toString()}');
|
throw Exception('Failed to get route (400) and fallback failed: ${e.toString()}');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to get route: ${response.statusCode}');
|
throw Exception('Failed to get route: ${response.statusCode} -- response body: ${response.body}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Navigation error: $e');
|
throw Exception('Navigation error: $e');
|
||||||
@@ -110,7 +110,25 @@ class NavigationService {
|
|||||||
final response = await http.get(Uri.parse(url));
|
final response = await http.get(Uri.parse(url));
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('Failed to get route: ${response.statusCode}');
|
if (response.statusCode == 400) {
|
||||||
|
final distanceMeters = _sumDistancesBetween(stops);
|
||||||
|
final durationSec = (distanceMeters / 11.111111).toInt();
|
||||||
|
final leg = RouteLeg.fromPoints(
|
||||||
|
points: stops,
|
||||||
|
distance: distanceMeters,
|
||||||
|
duration: durationSec,
|
||||||
|
summary: 'Direct fallback',
|
||||||
|
);
|
||||||
|
return NavigationRoute(
|
||||||
|
start: stops.first,
|
||||||
|
end: stops.last,
|
||||||
|
legs: [leg],
|
||||||
|
totalDistance: distanceMeters,
|
||||||
|
totalDuration: durationSec,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('Failed to get route: ${response.statusCode} -- response body: ${response.body}');
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = jsonDecode(response.body) as Map<String, dynamic>?;
|
final data = jsonDecode(response.body) as Map<String, dynamic>?;
|
||||||
@@ -165,7 +183,7 @@ class NavigationService {
|
|||||||
final route = NavigationRoute(start: stops.first, end: stops.last, legs: [leg], totalDistance: distanceMeters, totalDuration: durationSec);
|
final route = NavigationRoute(start: stops.first, end: stops.last, legs: [leg], totalDistance: distanceMeters, totalDuration: durationSec);
|
||||||
return OptimizedRouteResult(route: route, orderedStops: stops);
|
return OptimizedRouteResult(route: route, orderedStops: stops);
|
||||||
}
|
}
|
||||||
throw Exception('Failed to get optimized route: ${response.statusCode}');
|
throw Exception('Failed to get optimized route: ${response.statusCode} -- response body: ${response.body}');
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = jsonDecode(response.body) as Map<String, dynamic>?;
|
final data = jsonDecode(response.body) as Map<String, dynamic>?;
|
||||||
@@ -287,7 +305,7 @@ class NavigationService {
|
|||||||
final leg = RouteLeg.fromPoints(points: [start, end], distance: distanceMeters, duration: durationSec, summary: 'Direct');
|
final leg = RouteLeg.fromPoints(points: [start, end], distance: distanceMeters, duration: durationSec, summary: 'Direct');
|
||||||
return [NavigationRoute(start: start, end: end, legs: [leg], totalDistance: distanceMeters, totalDuration: durationSec)];
|
return [NavigationRoute(start: start, end: end, legs: [leg], totalDistance: distanceMeters, totalDuration: durationSec)];
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to get routes: ${response.statusCode}');
|
throw Exception('Failed to get routes: ${response.statusCode} -- response body: ${response.body}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Navigation error: $e');
|
throw Exception('Navigation error: $e');
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import 'exceptions.dart';
|
|||||||
|
|
||||||
/// Schedule/Route Service
|
/// Schedule/Route Service
|
||||||
class ScheduleService {
|
class ScheduleService {
|
||||||
// TODO: Implement API connector - configure base URL from env/config
|
static const String _apiBaseUrl = 'https://transit.xtransit.testinstance.nl';
|
||||||
// Transit API runs on port 5001
|
|
||||||
static const String _apiBaseUrl = 'http://192.168.2.66:5001';
|
|
||||||
final _authService = AuthService();
|
final _authService = AuthService();
|
||||||
|
|
||||||
/// Get public schedule overview for a specific date.
|
/// Get public schedule overview for a specific date.
|
||||||
@@ -141,12 +139,28 @@ class ScheduleService {
|
|||||||
return getDailySchedule(DateTime.now());
|
return getDailySchedule(DateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get per-date progress for a schedule occurrence (public endpoint)
|
||||||
|
Future<Map<String, dynamic>> getScheduleProgress(int scheduleId, DateTime date) async {
|
||||||
|
try {
|
||||||
|
final dateStr = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||||
|
final uri = Uri.parse('$_apiBaseUrl/public/schedules/$scheduleId/progress').replace(queryParameters: {'date': dateStr});
|
||||||
|
final response = await http.get(uri);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('Failed to load progress: ${response.statusCode}');
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Schedule progress error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get route details
|
/// Get route details
|
||||||
Future<Route> getRouteDetails(int routeId) async {
|
Future<Route> getRouteDetails(int routeId) async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authService.getAuthHeaders();
|
final headers = await _authService.getAuthHeaders();
|
||||||
|
|
||||||
// Transit API exposes route details at /routes/:routeId
|
|
||||||
final response = await http.get(
|
final response = await http.get(
|
||||||
Uri.parse('$_apiBaseUrl/routes/$routeId'),
|
Uri.parse('$_apiBaseUrl/routes/$routeId'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
@@ -156,10 +170,10 @@ class ScheduleService {
|
|||||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
return Route.fromJson(data);
|
return Route.fromJson(data);
|
||||||
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
await _authService.logout();
|
|
||||||
throw UnauthorizedException('Session expired - Please login again');
|
throw UnauthorizedException('Session expired - Please login again');
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to load route: ${response.statusCode}');
|
final body = response.body;
|
||||||
|
throw Exception('Failed to load route: ${response.statusCode} -- response body: $body');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is UnauthorizedException) rethrow;
|
if (e is UnauthorizedException) rethrow;
|
||||||
@@ -180,7 +194,6 @@ class ScheduleService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 401 || response.statusCode == 403) {
|
if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
await _authService.logout();
|
|
||||||
throw UnauthorizedException('Session expired - Please login again');
|
throw UnauthorizedException('Session expired - Please login again');
|
||||||
} else if (response.statusCode != 200) {
|
} else if (response.statusCode != 200) {
|
||||||
throw Exception('Failed to update status: ${response.statusCode}');
|
throw Exception('Failed to update status: ${response.statusCode}');
|
||||||
@@ -192,18 +205,23 @@ class ScheduleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update schedule status for driver-facing schedule workflows.
|
/// Update schedule status for driver-facing schedule workflows.
|
||||||
Future<void> updateScheduleStatus(int scheduleId, String status) async {
|
Future<void> updateScheduleStatus(int scheduleId, String status, {DateTime? date}) async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authService.getAuthHeaders();
|
final headers = await _authService.getAuthHeaders();
|
||||||
|
|
||||||
|
final body = <String, dynamic>{'status': status};
|
||||||
|
if (date != null) {
|
||||||
|
final dateStr = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||||
|
body['date'] = dateStr;
|
||||||
|
}
|
||||||
|
|
||||||
final response = await http.patch(
|
final response = await http.patch(
|
||||||
Uri.parse('$_apiBaseUrl/driver/schedules/$scheduleId/status'),
|
Uri.parse('$_apiBaseUrl/driver/schedules/$scheduleId/status'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: jsonEncode({'status': status}),
|
body: jsonEncode(body),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 401 || response.statusCode == 403) {
|
if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
await _authService.logout();
|
|
||||||
throw UnauthorizedException('Session expired - Please login again');
|
throw UnauthorizedException('Session expired - Please login again');
|
||||||
} else if (response.statusCode != 200) {
|
} else if (response.statusCode != 200) {
|
||||||
throw Exception('Failed to update schedule status: ${response.statusCode}');
|
throw Exception('Failed to update schedule status: ${response.statusCode}');
|
||||||
@@ -226,9 +244,8 @@ class ScheduleService {
|
|||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
final stops = (data['stops'] as List?)
|
final raw = data['stops'] as List? ?? [];
|
||||||
?.map((s) => s as Map<String, dynamic>)
|
final stops = raw.map((s) => s as Map<String, dynamic>).map((s) {
|
||||||
.map((s) {
|
|
||||||
final lat = (s['latitude'] ?? s['lat']) as num?;
|
final lat = (s['latitude'] ?? s['lat']) as num?;
|
||||||
final lon = (s['longitude'] ?? s['lon']) as num?;
|
final lon = (s['longitude'] ?? s['lon']) as num?;
|
||||||
final name = (s['name'] ?? s['stop_name'] ?? s['stopName']) as String?;
|
final name = (s['name'] ?? s['stop_name'] ?? s['stopName']) as String?;
|
||||||
@@ -238,15 +255,14 @@ class ScheduleService {
|
|||||||
longitude: lon.toDouble(),
|
longitude: lon.toDouble(),
|
||||||
name: name,
|
name: name,
|
||||||
);
|
);
|
||||||
})
|
}).whereType<NavigationPoint>().toList();
|
||||||
.whereType<NavigationPoint>()
|
|
||||||
.toList() ?? [];
|
|
||||||
return stops;
|
return stops;
|
||||||
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
await _authService.logout();
|
|
||||||
throw UnauthorizedException('Session expired - Please login again');
|
throw UnauthorizedException('Session expired - Please login again');
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to load stops: ${response.statusCode}');
|
final body = response.body;
|
||||||
|
throw Exception('Failed to load stops: ${response.statusCode} -- response body: $body');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is UnauthorizedException) rethrow;
|
if (e is UnauthorizedException) rethrow;
|
||||||
@@ -263,12 +279,13 @@ class ScheduleService {
|
|||||||
try {
|
try {
|
||||||
final headers = await _authService.getAuthHeaders();
|
final headers = await _authService.getAuthHeaders();
|
||||||
|
|
||||||
|
final body = <String, dynamic>{};
|
||||||
|
if (actualPassedAt != null) body['actualPassedAt'] = actualPassedAt.toIso8601String();
|
||||||
|
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse('$_apiBaseUrl/driver/schedules/$scheduleId/stops/$stopOrder/passed'),
|
Uri.parse('$_apiBaseUrl/driver/schedules/$scheduleId/stops/$stopOrder/passed'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: jsonEncode({
|
body: jsonEncode(body),
|
||||||
if (actualPassedAt != null) 'actualPassedAt': actualPassedAt.toIso8601String(),
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 401 || response.statusCode == 403) {
|
if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
|
|||||||
@@ -67,11 +67,18 @@ class RouteCard extends StatelessWidget {
|
|||||||
// Ignore status update errors here; navigation should still open.
|
// Ignore status update errors here; navigation should still open.
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.of(context).push(
|
try {
|
||||||
|
await Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => NavigationScreen(route: route),
|
builder: (context) => NavigationScreen(route: route),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Surface navigation errors to the user so they can report/log.
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Navigatiefout: ${e.toString()}')),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
splashFactory: InkSparkle.splashFactory,
|
splashFactory: InkSparkle.splashFactory,
|
||||||
|
|||||||
@@ -96,10 +96,16 @@ Alle endpoints vereisen een `Authorization: Bearer {token}` header.
|
|||||||
- Haalt een datum-gebonden overzicht op van geplande diensten
|
- Haalt een datum-gebonden overzicht op van geplande diensten
|
||||||
- Herhaalde diensten worden naar de geselecteerde datum gematerialiseerd via `weekdays`
|
- Herhaalde diensten worden naar de geselecteerde datum gematerialiseerd via `weekdays`
|
||||||
- Incidentele diensten worden alleen meegenomen op hun exacte datum
|
- Incidentele diensten worden alleen meegenomen op hun exacte datum
|
||||||
|
- Filters: `driverId`/`chauffeurId`, `stopId`/`busStopId`, `lineId`, `status`
|
||||||
- Response: `{ "date": "2026-05-06", "summary": {...}, "schedules": [...] }`
|
- Response: `{ "date": "2026-05-06", "summary": {...}, "schedules": [...] }`
|
||||||
|
|
||||||
De legacy endpoint **GET** `/schedules` blijft beschikbaar voor bestaande clients.
|
De legacy endpoint **GET** `/schedules` blijft beschikbaar voor bestaande clients.
|
||||||
|
|
||||||
|
Voorbeeld:
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&busStopId=3"
|
||||||
|
```
|
||||||
|
|
||||||
### Navigation
|
### Navigation
|
||||||
|
|
||||||
**POST** `/navigation/route`
|
**POST** `/navigation/route`
|
||||||
|
|||||||
+158
-10
@@ -324,10 +324,10 @@ function parseScheduleFilters(query, options = {}) {
|
|||||||
date: query.date ? String(query.date).trim() : null,
|
date: query.date ? String(query.date).trim() : null,
|
||||||
fromDate: query.fromDate ? String(query.fromDate).trim() : null,
|
fromDate: query.fromDate ? String(query.fromDate).trim() : null,
|
||||||
toDate: query.toDate ? String(query.toDate).trim() : null,
|
toDate: query.toDate ? String(query.toDate).trim() : null,
|
||||||
driverId: toNumberOrNull(query.driverId ?? query.driver_id),
|
driverId: toNumberOrNull(query.driverId ?? query.driver_id ?? query.chauffeurId ?? query.chauffeur_id),
|
||||||
busId: toNumberOrNull(query.busId ?? query.bus_id),
|
busId: toNumberOrNull(query.busId ?? query.bus_id),
|
||||||
busLineId: toNumberOrNull(query.busLineId ?? query.lineId ?? query.bus_line_id),
|
busLineId: toNumberOrNull(query.busLineId ?? query.lineId ?? query.bus_line_id),
|
||||||
stopId: toNumberOrNull(query.stopId ?? query.stop_id),
|
stopId: toNumberOrNull(query.stopId ?? query.stop_id ?? query.busStopId ?? query.bus_stop_id),
|
||||||
statuses,
|
statuses,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -429,8 +429,9 @@ function materializeScheduleOverviewRow(row, dateKey, routeStopsByLine = new Map
|
|||||||
id: Number(row.id),
|
id: Number(row.id),
|
||||||
busLineId,
|
busLineId,
|
||||||
busId: Number(row.busId ?? row.bus_id),
|
busId: Number(row.busId ?? row.bus_id),
|
||||||
driverId: row.driverId !== null && row.driverId !== undefined ? Number(row.driverId) : null,
|
// Hide driver information in public schedule materialization for privacy
|
||||||
driverName: row.driverName ?? row.driver_name ?? null,
|
driverId: null,
|
||||||
|
driverName: null,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
weekdays,
|
weekdays,
|
||||||
@@ -1923,6 +1924,91 @@ app.patch('/driver/schedules/:scheduleId/status', authMiddleware, async (req, re
|
|||||||
|
|
||||||
const normalizedStatus = status === 'active' ? 'in_progress' : status;
|
const normalizedStatus = status === 'active' ? 'in_progress' : status;
|
||||||
|
|
||||||
|
// Fetch the target schedule row to detect recurring templates
|
||||||
|
const existing = await db.query(`SELECT * FROM transit.schedules WHERE id = $1`, [scheduleId]);
|
||||||
|
if (existing.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'schedule_not_found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleRow = existing.rows[0];
|
||||||
|
const weekdays = parseWeekdaysValue(scheduleRow.weekdays);
|
||||||
|
|
||||||
|
// Allow updating a specific occurrence of a recurring template by passing { date: 'YYYY-MM-DD' }
|
||||||
|
const requestedDate = req.body.date || null; // optional date key
|
||||||
|
|
||||||
|
if (weekdays && weekdays.length > 0) {
|
||||||
|
// This is a recurring template. If a date was provided, or the template occurs today, operate on an instance.
|
||||||
|
let targetDateKey = requestedDate;
|
||||||
|
if (!targetDateKey) {
|
||||||
|
const todayKey = new Date().toISOString().slice(0, 10);
|
||||||
|
if (scheduleOccursOnDate(scheduleRow, todayKey)) {
|
||||||
|
targetDateKey = todayKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetDateKey) {
|
||||||
|
// Build occurrence start/end based on template times
|
||||||
|
const templateStart = new Date(scheduleRow.start_time ?? scheduleRow.startTime);
|
||||||
|
const templateEnd = new Date(scheduleRow.end_time ?? scheduleRow.endTime);
|
||||||
|
const occurrenceStart = new Date(`${targetDateKey}T00:00:00`);
|
||||||
|
occurrenceStart.setHours(
|
||||||
|
templateStart.getHours(),
|
||||||
|
templateStart.getMinutes(),
|
||||||
|
templateStart.getSeconds(),
|
||||||
|
templateStart.getMilliseconds()
|
||||||
|
);
|
||||||
|
|
||||||
|
let occurrenceEnd = null;
|
||||||
|
if (Number.isFinite(templateEnd.getTime()) && Number.isFinite(templateStart.getTime())) {
|
||||||
|
const duration = templateEnd.getTime() - templateStart.getTime();
|
||||||
|
occurrenceEnd = new Date(occurrenceStart.getTime() + duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a concrete instance already exists for this template/date
|
||||||
|
const existingInstance = await db.query(
|
||||||
|
`SELECT * FROM transit.schedules WHERE template_id = $1 AND DATE(start_time) = $2`,
|
||||||
|
[scheduleId, targetDateKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingInstance.rows.length > 0) {
|
||||||
|
const upd = await db.query(`
|
||||||
|
UPDATE transit.schedules SET status = $1, updated_at = NOW()
|
||||||
|
WHERE id = $2
|
||||||
|
RETURNING *
|
||||||
|
`, [normalizedStatus, existingInstance.rows[0].id]);
|
||||||
|
return res.json({ schedule: upd.rows[0] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a new instance derived from template
|
||||||
|
const hasTemplateIdCol = await db.query(`
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema='transit' AND table_name='schedules' AND column_name='template_id'
|
||||||
|
`);
|
||||||
|
|
||||||
|
let insertResult;
|
||||||
|
if (hasTemplateIdCol.rows.length > 0) {
|
||||||
|
insertResult = await db.query(`
|
||||||
|
WITH next_id AS (SELECT COALESCE(MAX(id), 0) + 1 AS id FROM transit.schedules)
|
||||||
|
INSERT INTO transit.schedules (id, bus_line_id, bus_id, driver_id, start_time, end_time, status, template_id)
|
||||||
|
SELECT next_id.id, $1, $2, $3, $4, $5, $6, $7
|
||||||
|
FROM next_id
|
||||||
|
RETURNING *
|
||||||
|
`, [scheduleRow.bus_line_id, scheduleRow.bus_id, scheduleRow.driver_id, occurrenceStart.toISOString(), occurrenceEnd ? occurrenceEnd.toISOString() : null, normalizedStatus, scheduleId]);
|
||||||
|
} else {
|
||||||
|
insertResult = await db.query(`
|
||||||
|
WITH next_id AS (SELECT COALESCE(MAX(id), 0) + 1 AS id FROM transit.schedules)
|
||||||
|
INSERT INTO transit.schedules (id, bus_line_id, bus_id, driver_id, start_time, end_time, status)
|
||||||
|
SELECT next_id.id, $1, $2, $3, $4, $5, $6
|
||||||
|
FROM next_id
|
||||||
|
RETURNING *
|
||||||
|
`, [scheduleRow.bus_line_id, scheduleRow.bus_id, scheduleRow.driver_id, occurrenceStart.toISOString(), occurrenceEnd ? occurrenceEnd.toISOString() : null, normalizedStatus]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({ schedule: insertResult.rows[0] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: update the schedule row directly (one-time or when no occurrence date is inferred)
|
||||||
const result = await db.query(`
|
const result = await db.query(`
|
||||||
UPDATE transit.schedules
|
UPDATE transit.schedules
|
||||||
SET status = $1, updated_at = NOW()
|
SET status = $1, updated_at = NOW()
|
||||||
@@ -2566,7 +2652,7 @@ app.get('/schedules', async (req, res) => {
|
|||||||
status: s.status,
|
status: s.status,
|
||||||
lineNumber: busLine?.lineNumber || 0,
|
lineNumber: busLine?.lineNumber || 0,
|
||||||
busName: bus?.name || 'Unknown Bus',
|
busName: bus?.name || 'Unknown Bus',
|
||||||
driverId: s.driverId || null,
|
driverId: null,
|
||||||
estimatedDurationMinutes: busLine?.estimatedDurationMinutes || 60,
|
estimatedDurationMinutes: busLine?.estimatedDurationMinutes || 60,
|
||||||
};
|
};
|
||||||
})).filter(row => scheduleMatchesFilters(row, filters, routeStopsByLine));
|
})).filter(row => scheduleMatchesFilters(row, filters, routeStopsByLine));
|
||||||
@@ -2648,7 +2734,8 @@ app.get('/schedules', async (req, res) => {
|
|||||||
id: Number(row.id),
|
id: Number(row.id),
|
||||||
busLineId: Number(row.bus_line_id),
|
busLineId: Number(row.bus_line_id),
|
||||||
busId: Number(row.bus_id),
|
busId: Number(row.bus_id),
|
||||||
driverId: row.driver_id !== null && row.driver_id !== undefined ? Number(row.driver_id) : null,
|
// Hide driver from public API responses
|
||||||
|
driverId: null,
|
||||||
startTime: row.start_time,
|
startTime: row.start_time,
|
||||||
endTime: row.end_time,
|
endTime: row.end_time,
|
||||||
weekdays: row.weekdays ? (typeof row.weekdays === 'string' ? JSON.parse(row.weekdays) : row.weekdays) : [],
|
weekdays: row.weekdays ? (typeof row.weekdays === 'string' ? JSON.parse(row.weekdays) : row.weekdays) : [],
|
||||||
@@ -3279,7 +3366,68 @@ app.post('/driver/schedules/:scheduleId/stops/:stopOrder/passed', authMiddleware
|
|||||||
|
|
||||||
const routeStop = routeStopResult.rows[0];
|
const routeStop = routeStopResult.rows[0];
|
||||||
// Determine occurrence date (for recurring templates callers can pass ?date=YYYY-MM-DD or body.date)
|
// Determine occurrence date (for recurring templates callers can pass ?date=YYYY-MM-DD or body.date)
|
||||||
const occurrenceDateKey = req.query.date || req.body.date || toDateKey(schedule.start_time);
|
let occurrenceDateKey = req.query.date || req.body.date || toDateKey(schedule.start_time);
|
||||||
|
|
||||||
|
// If this schedule is a recurring template, map to or create a concrete instance for the occurrence date
|
||||||
|
const weekdays = parseWeekdaysValue(schedule.weekdays);
|
||||||
|
let instanceScheduleId = scheduleId;
|
||||||
|
if (weekdays && weekdays.length > 0) {
|
||||||
|
// If no date provided but template occurs today, use today
|
||||||
|
if (!occurrenceDateKey) {
|
||||||
|
const todayKey = new Date().toISOString().slice(0, 10);
|
||||||
|
if (scheduleOccursOnDate(schedule, todayKey)) occurrenceDateKey = todayKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (occurrenceDateKey) {
|
||||||
|
const existingInstance = await db.query(
|
||||||
|
`SELECT id FROM transit.schedules WHERE template_id = $1 AND DATE(start_time) = $2 LIMIT 1`,
|
||||||
|
[scheduleId, occurrenceDateKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingInstance.rows.length > 0) {
|
||||||
|
instanceScheduleId = existingInstance.rows[0].id;
|
||||||
|
} else {
|
||||||
|
// Create instance from template
|
||||||
|
const templateStart = new Date(schedule.start_time);
|
||||||
|
const templateEnd = schedule.end_time ? new Date(schedule.end_time) : null;
|
||||||
|
const occurrenceStart = new Date(`${occurrenceDateKey}T00:00:00`);
|
||||||
|
occurrenceStart.setHours(templateStart.getHours(), templateStart.getMinutes(), templateStart.getSeconds(), templateStart.getMilliseconds());
|
||||||
|
|
||||||
|
let occurrenceEnd = null;
|
||||||
|
if (templateEnd && Number.isFinite(templateEnd.getTime()) && Number.isFinite(templateStart.getTime())) {
|
||||||
|
const duration = templateEnd.getTime() - templateStart.getTime();
|
||||||
|
occurrenceEnd = new Date(occurrenceStart.getTime() + duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasTemplateIdCol = await db.query(`
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema='transit' AND table_name='schedules' AND column_name='template_id'
|
||||||
|
`);
|
||||||
|
|
||||||
|
let insertRes;
|
||||||
|
if (hasTemplateIdCol.rows.length > 0) {
|
||||||
|
insertRes = await db.query(`
|
||||||
|
WITH next_id AS (SELECT COALESCE(MAX(id), 0) + 1 AS id FROM transit.schedules)
|
||||||
|
INSERT INTO transit.schedules (id, bus_line_id, bus_id, driver_id, start_time, end_time, status, template_id)
|
||||||
|
SELECT next_id.id, $1, $2, $3, $4, $5, 'planned', $6
|
||||||
|
FROM next_id
|
||||||
|
RETURNING id
|
||||||
|
`, [schedule.bus_line_id, schedule.bus_id, schedule.driver_id, occurrenceStart.toISOString(), occurrenceEnd ? occurrenceEnd.toISOString() : null, scheduleId]);
|
||||||
|
} else {
|
||||||
|
insertRes = await db.query(`
|
||||||
|
WITH next_id AS (SELECT COALESCE(MAX(id), 0) + 1 AS id FROM transit.schedules)
|
||||||
|
INSERT INTO transit.schedules (id, bus_line_id, bus_id, driver_id, start_time, end_time, status)
|
||||||
|
SELECT next_id.id, $1, $2, $3, $4, $5, 'planned'
|
||||||
|
FROM next_id
|
||||||
|
RETURNING id
|
||||||
|
`, [schedule.bus_line_id, schedule.bus_id, schedule.driver_id, occurrenceStart.toISOString(), occurrenceEnd ? occurrenceEnd.toISOString() : null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceScheduleId = insertRes.rows[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const templateStart = new Date(schedule.start_time);
|
const templateStart = new Date(schedule.start_time);
|
||||||
const scheduledPassedAt = new Date(`${occurrenceDateKey}T00:00:00`);
|
const scheduledPassedAt = new Date(`${occurrenceDateKey}T00:00:00`);
|
||||||
scheduledPassedAt.setHours(templateStart.getHours(), templateStart.getMinutes(), templateStart.getSeconds(), templateStart.getMilliseconds());
|
scheduledPassedAt.setHours(templateStart.getHours(), templateStart.getMinutes(), templateStart.getSeconds(), templateStart.getMilliseconds());
|
||||||
@@ -3305,7 +3453,7 @@ app.post('/driver/schedules/:scheduleId/stops/:stopOrder/passed', authMiddleware
|
|||||||
marked_by_user_id = EXCLUDED.marked_by_user_id,
|
marked_by_user_id = EXCLUDED.marked_by_user_id,
|
||||||
created_at = NOW()
|
created_at = NOW()
|
||||||
`, [
|
`, [
|
||||||
scheduleId,
|
instanceScheduleId,
|
||||||
Number(schedule.bus_line_id),
|
Number(schedule.bus_line_id),
|
||||||
Number(routeStop.stop_id),
|
Number(routeStop.stop_id),
|
||||||
stopOrder,
|
stopOrder,
|
||||||
@@ -3321,7 +3469,7 @@ app.post('/driver/schedules/:scheduleId/stops/:stopOrder/passed', authMiddleware
|
|||||||
SELECT
|
SELECT
|
||||||
(SELECT COUNT(*)::int FROM transit.route_stops WHERE bus_line_id = $1) AS total_stops,
|
(SELECT COUNT(*)::int FROM transit.route_stops WHERE bus_line_id = $1) AS total_stops,
|
||||||
(SELECT COUNT(*)::int FROM transit.schedule_stop_progress WHERE schedule_id = $2 AND scheduled_passed_date = $3) AS passed_stops
|
(SELECT COUNT(*)::int FROM transit.schedule_stop_progress WHERE schedule_id = $2 AND scheduled_passed_date = $3) AS passed_stops
|
||||||
`, [schedule.bus_line_id, scheduleId, occurrenceDateKey]);
|
`, [schedule.bus_line_id, instanceScheduleId, occurrenceDateKey]);
|
||||||
|
|
||||||
const totalStops = Number(totalsResult.rows[0]?.total_stops || 0);
|
const totalStops = Number(totalsResult.rows[0]?.total_stops || 0);
|
||||||
const passedStops = Number(totalsResult.rows[0]?.passed_stops || 0);
|
const passedStops = Number(totalsResult.rows[0]?.passed_stops || 0);
|
||||||
@@ -3334,7 +3482,7 @@ app.post('/driver/schedules/:scheduleId/stops/:stopOrder/passed', authMiddleware
|
|||||||
status = $2,
|
status = $2,
|
||||||
end_time = CASE WHEN $2 = 'completed' THEN $3 ELSE end_time END
|
end_time = CASE WHEN $2 = 'completed' THEN $3 ELSE end_time END
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`, [scheduleId, nextStatus, actualPassedAt.toISOString()]);
|
`, [instanceScheduleId, nextStatus, actualPassedAt.toISOString()]);
|
||||||
|
|
||||||
// Calculate delay live
|
// Calculate delay live
|
||||||
const delayMinutes = Math.round((actualPassedAt.getTime() - scheduledPassedAt.getTime()) / 60000);
|
const delayMinutes = Math.round((actualPassedAt.getTime() - scheduledPassedAt.getTime()) / 60000);
|
||||||
|
|||||||
Reference in New Issue
Block a user