Implement schedule status update functionality for driver workflows; enhance navigation button behavior to reflect status changes
This commit is contained in:
@@ -112,7 +112,12 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
await _scheduleService.updateScheduleStatus(svc.id, 'in_progress');
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
// Close the sheet and start navigation with provided stops
|
// Close the sheet and start navigation with provided stops
|
||||||
Navigator.of(sheetContext).pop();
|
Navigator.of(sheetContext).pop();
|
||||||
|
|
||||||
@@ -133,7 +138,7 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> {
|
|||||||
),
|
),
|
||||||
startTime: svc.startTime,
|
startTime: svc.startTime,
|
||||||
endTime: svc.endTime,
|
endTime: svc.endTime,
|
||||||
status: svc.status,
|
status: 'in_progress',
|
||||||
);
|
);
|
||||||
|
|
||||||
final navStops = stops
|
final navStops = stops
|
||||||
@@ -149,6 +154,12 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> {
|
|||||||
builder: (_) => NavigationScreen(route: routeObj, initialStops: navStops),
|
builder: (_) => NavigationScreen(route: routeObj, initialStops: navStops),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Status bijwerken mislukt: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.navigation),
|
icon: const Icon(Icons.navigation),
|
||||||
label: const Text('Start route'),
|
label: const Text('Start route'),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import '../models/navigation.dart';
|
|||||||
/// Navigation Service using OpenStreetMap
|
/// Navigation Service using OpenStreetMap
|
||||||
class NavigationService {
|
class NavigationService {
|
||||||
// OSRM (OpenStreetMap Routing Machine) - free public service
|
// OSRM (OpenStreetMap Routing Machine) - free public service
|
||||||
static const String _osrmBaseUrl = 'https://router.project-osrm.org/route/v1/car';
|
static const String _osrmBaseUrl = 'https://router.project-osrm.org/route/v1/driving';
|
||||||
|
|
||||||
/// Get current device location
|
/// Get current device location
|
||||||
Future<NavigationPoint?> getCurrentLocation() async {
|
Future<NavigationPoint?> getCurrentLocation() async {
|
||||||
@@ -45,7 +45,7 @@ class NavigationService {
|
|||||||
if (!validCoord(start.latitude, start.longitude) || !validCoord(end.latitude, end.longitude)) {
|
if (!validCoord(start.latitude, start.longitude) || !validCoord(end.latitude, end.longitude)) {
|
||||||
throw Exception('Invalid coordinates for routing: start=(${start.latitude},${start.longitude}) end=(${end.latitude},${end.longitude})');
|
throw Exception('Invalid coordinates for routing: start=(${start.latitude},${start.longitude}) end=(${end.latitude},${end.longitude})');
|
||||||
}
|
}
|
||||||
// OSRM format: /route/v1/car/lon1,lat1;lon2,lat2
|
// OSRM format: /route/v1/driving/lon1,lat1;lon2,lat2
|
||||||
final coordinates =
|
final coordinates =
|
||||||
'${start.longitude},${start.latitude};${end.longitude},${end.latitude}';
|
'${start.longitude},${start.latitude};${end.longitude},${end.latitude}';
|
||||||
final url =
|
final url =
|
||||||
@@ -152,7 +152,7 @@ class NavigationService {
|
|||||||
.map((stop) => '${stop.longitude},${stop.latitude}')
|
.map((stop) => '${stop.longitude},${stop.latitude}')
|
||||||
.join(';');
|
.join(';');
|
||||||
final url =
|
final url =
|
||||||
'https://router.project-osrm.org/trip/v1/car/$coordinates?overview=full&steps=true&geometries=geojson&source=first&destination=last&roundtrip=false';
|
'https://router.project-osrm.org/trip/v1/driving/$coordinates?overview=full&steps=true&geometries=geojson&source=first&destination=last&roundtrip=false';
|
||||||
|
|
||||||
final response = await http.get(Uri.parse(url));
|
final response = await http.get(Uri.parse(url));
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,29 @@ class ScheduleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update schedule status for driver-facing schedule workflows.
|
||||||
|
Future<void> updateScheduleStatus(int scheduleId, String status) async {
|
||||||
|
try {
|
||||||
|
final headers = await _authService.getAuthHeaders();
|
||||||
|
|
||||||
|
final response = await http.patch(
|
||||||
|
Uri.parse('$_apiBaseUrl/driver/schedules/$scheduleId/status'),
|
||||||
|
headers: headers,
|
||||||
|
body: jsonEncode({'status': status}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
|
await _authService.logout();
|
||||||
|
throw UnauthorizedException('Session expired - Please login again');
|
||||||
|
} else if (response.statusCode != 200) {
|
||||||
|
throw Exception('Failed to update schedule status: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e is UnauthorizedException) rethrow;
|
||||||
|
throw Exception('Schedule status update error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get stops for a bus line
|
/// Get stops for a bus line
|
||||||
Future<List<NavigationPoint>> getStops(int busLineId) async {
|
Future<List<NavigationPoint>> getStops(int busLineId) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../models/schedule.dart' as schedule;
|
import '../models/schedule.dart' as schedule;
|
||||||
import '../screens/navigation_screen.dart';
|
import '../screens/navigation_screen.dart';
|
||||||
|
import '../services/schedule_service.dart';
|
||||||
|
|
||||||
/// Route card widget showing route details
|
/// Route card widget showing route details
|
||||||
class RouteCard extends StatelessWidget {
|
class RouteCard extends StatelessWidget {
|
||||||
@@ -56,7 +57,14 @@ class RouteCard extends StatelessWidget {
|
|||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
final scheduleService = ScheduleService();
|
||||||
|
await scheduleService.updateRouteStatus(route.id, 'in_progress');
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore status update errors here; navigation should still open.
|
||||||
|
}
|
||||||
|
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => NavigationScreen(route: route),
|
builder: (context) => NavigationScreen(route: route),
|
||||||
|
|||||||
+35
-2
@@ -1523,6 +1523,39 @@ app.patch('/routes/:routeId/status', authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// DRIVER - Update schedule status
|
||||||
|
// ============================================
|
||||||
|
app.patch('/driver/schedules/:scheduleId/status', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { scheduleId } = req.params;
|
||||||
|
const { status } = req.body;
|
||||||
|
|
||||||
|
const validStatuses = ['planned', 'in_progress', 'completed', 'cancelled', 'active'];
|
||||||
|
if (!validStatuses.includes(status)) {
|
||||||
|
return res.status(400).json({ error: 'invalid_status' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedStatus = status === 'active' ? 'in_progress' : status;
|
||||||
|
|
||||||
|
const result = await db.query(`
|
||||||
|
UPDATE transit.schedules
|
||||||
|
SET status = $1, updated_at = NOW()
|
||||||
|
WHERE id = $2
|
||||||
|
RETURNING *
|
||||||
|
`, [normalizedStatus, scheduleId]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'schedule_not_found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ schedule: result.rows[0] });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// ADMIN - Get drivers for scheduling
|
// ADMIN - Get drivers for scheduling
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -2142,7 +2175,7 @@ app.post('/driver/schedules/:scheduleId/stops/:stopOrder/passed', authMiddleware
|
|||||||
schedule.status = 'completed';
|
schedule.status = 'completed';
|
||||||
schedule.endTime = actualPassedAt.toISOString();
|
schedule.endTime = actualPassedAt.toISOString();
|
||||||
} else if (schedule.status === 'planned') {
|
} else if (schedule.status === 'planned') {
|
||||||
schedule.status = 'active';
|
schedule.status = 'in_progress';
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
@@ -2233,7 +2266,7 @@ app.post('/driver/schedules/:scheduleId/stops/:stopOrder/passed', authMiddleware
|
|||||||
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);
|
||||||
const isCompleted = totalStops > 0 && passedStops >= totalStops;
|
const isCompleted = totalStops > 0 && passedStops >= totalStops;
|
||||||
const nextStatus = isCompleted ? 'completed' : 'active';
|
const nextStatus = isCompleted ? 'completed' : 'in_progress';
|
||||||
|
|
||||||
await db.query(`
|
await db.query(`
|
||||||
UPDATE transit.schedules
|
UPDATE transit.schedules
|
||||||
|
|||||||
Reference in New Issue
Block a user