Enhance API clients with auto-logout on unauthorized responses; improve navigation and schedule handling with better error management and UI updates
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
+28
-4
@@ -16,7 +16,31 @@ const AUTH_API = normalizeApiBaseUrl(RUNTIME.AUTH_API || import.meta.env.VITE_AU
|
|||||||
const TRANSIT_API = normalizeApiBaseUrl(RUNTIME.TRANSIT_API || import.meta.env.VITE_TRANSIT_API, 'http://localhost:5001')
|
const TRANSIT_API = normalizeApiBaseUrl(RUNTIME.TRANSIT_API || import.meta.env.VITE_TRANSIT_API, 'http://localhost:5001')
|
||||||
|
|
||||||
export const authClient = axios.create({ baseURL: AUTH_API })
|
export const authClient = axios.create({ baseURL: AUTH_API })
|
||||||
export const transitClient = (token) => axios.create({
|
|
||||||
baseURL: TRANSIT_API,
|
export const transitClient = (token) => {
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
const client = axios.create({
|
||||||
})
|
baseURL: TRANSIT_API,
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auto-logout on unauthorized responses to avoid delayed failures
|
||||||
|
client.interceptors.response.use(
|
||||||
|
response => response,
|
||||||
|
error => {
|
||||||
|
try {
|
||||||
|
const status = error?.response?.status
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
// reload to let App show the Login screen
|
||||||
|
if (typeof window !== 'undefined') window.location.reload()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,27 @@ export default function FleetManager({ token, user }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const overlay = {
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: '2rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
maxWidth: '400px',
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadBuses()
|
loadBuses()
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
@@ -424,18 +424,34 @@ export default function ScheduleMaker({ token, user }) {
|
|||||||
<td style={{ flex: 1 }}>{rs.name}</td>
|
<td style={{ flex: 1 }}>{rs.name}</td>
|
||||||
<td style={{ textAlign: 'right', color: '#666' }}>{formatTime(departureDate.toISOString())}</td>
|
<td style={{ textAlign: 'right', color: '#666' }}>{formatTime(departureDate.toISOString())}</td>
|
||||||
<td style={{ paddingLeft: '0.5rem' }}>
|
<td style={{ paddingLeft: '0.5rem' }}>
|
||||||
<input
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
type="number"
|
<input
|
||||||
value={rs.estimatedArrivalMinutes ?? 0}
|
type="range"
|
||||||
onChange={e => {
|
min="0"
|
||||||
const val = e.target.value === '' ? '' : Number(e.target.value)
|
max="240"
|
||||||
setRouteStopsByLine(prev => ({
|
value={Number(rs.estimatedArrivalMinutes ?? 0)}
|
||||||
...prev,
|
onChange={e => {
|
||||||
[lineId]: prev[lineId].map(item => item.id === rs.id ? { ...item, estimatedArrivalMinutes: val } : item)
|
const val = Number(e.target.value)
|
||||||
}))
|
setRouteStopsByLine(prev => ({
|
||||||
}}
|
...prev,
|
||||||
style={{ width: '5rem' }}
|
[lineId]: prev[lineId].map(item => item.id === rs.id ? { ...item, estimatedArrivalMinutes: val } : item)
|
||||||
/>
|
}))
|
||||||
|
}}
|
||||||
|
style={{ width: '8rem' }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={Number(rs.estimatedArrivalMinutes ?? 0)}
|
||||||
|
onChange={e => {
|
||||||
|
const val = e.target.value === '' ? 0 : Number(e.target.value)
|
||||||
|
setRouteStopsByLine(prev => ({
|
||||||
|
...prev,
|
||||||
|
[lineId]: prev[lineId].map(item => item.id === rs.id ? { ...item, estimatedArrivalMinutes: val } : item)
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
style={{ width: '4.5rem' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button type="button" onClick={async () => {
|
<button type="button" onClick={async () => {
|
||||||
const newVal = Number((routeStopsByLine[lineId].find(i => i.id === rs.id)?.estimatedArrivalMinutes) || 0)
|
const newVal = Number((routeStopsByLine[lineId].find(i => i.id === rs.id)?.estimatedArrivalMinutes) || 0)
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -73,8 +73,6 @@ class _NavigationScreenState extends State<NavigationScreen> {
|
|||||||
// Defensive: filter out invalid stops (missing/NaN coordinates) and deduplicate
|
// Defensive: filter out invalid stops (missing/NaN coordinates) and deduplicate
|
||||||
if (_stops != null) {
|
if (_stops != null) {
|
||||||
_stops = _stops!
|
_stops = _stops!
|
||||||
.where((s) => s != null)
|
|
||||||
.where((s) => s.latitude != null && s.longitude != null)
|
|
||||||
.where((s) => s.latitude.isFinite && s.longitude.isFinite)
|
.where((s) => s.latitude.isFinite && s.longitude.isFinite)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -107,7 +105,7 @@ class _NavigationScreenState extends State<NavigationScreen> {
|
|||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
_fitMapToRoute(optimized.route);
|
WidgetsBinding.instance.addPostFrameCallback((_) => _fitMapToRoute(optimized.route));
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -157,12 +155,12 @@ class _NavigationScreenState extends State<NavigationScreen> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentRoute = navRoute;
|
_currentRoute = navRoute;
|
||||||
_alternativeRoutes = (alternatives ?? []).take(2).toList();
|
_alternativeRoutes = alternatives.take(2).toList();
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fit map to route bounds
|
// Fit map to route bounds after first frame so MapController is attached
|
||||||
_fitMapToRoute(navRoute);
|
WidgetsBinding.instance.addPostFrameCallback((_) => _fitMapToRoute(navRoute));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -265,7 +263,7 @@ class _NavigationScreenState extends State<NavigationScreen> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (navRoute != null) {
|
if (navRoute != null) {
|
||||||
_fitMapToRoute(navRoute);
|
WidgetsBinding.instance.addPostFrameCallback((_) => _fitMapToRoute(navRoute));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -776,7 +774,7 @@ class _NavigationScreenState extends State<NavigationScreen> {
|
|||||||
)
|
)
|
||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
itemCount: _stops!.length,
|
itemCount: _stops!.length,
|
||||||
separatorBuilder: (_, __) => const Divider(),
|
separatorBuilder: (context, index) => const Divider(),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final s = _stops![index];
|
final s = _stops![index];
|
||||||
final color = _stopColor(index);
|
final color = _stopColor(index);
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> {
|
|||||||
// 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();
|
||||||
|
|
||||||
final routeObj = sched.Route(
|
final routeObj = sched.Route(
|
||||||
id: (svc.id is num) ? (svc.id as num).toInt() : int.tryParse(svc.id.toString()) ?? 0,
|
id: svc.id,
|
||||||
busLine: sched.BusLine(
|
busLine: sched.BusLine(
|
||||||
id: svc.busLineId,
|
id: svc.busLineId,
|
||||||
lineNumber: svc.lineNumber,
|
lineNumber: svc.lineNumber,
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ class NavigationService {
|
|||||||
required NavigationPoint end,
|
required NavigationPoint end,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
bool validCoord(double lat, double lon) {
|
||||||
|
return lat.isFinite && lon.isFinite && lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
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})');
|
||||||
|
}
|
||||||
// OSRM format: /route/v1/car/lon1,lat1;lon2,lat2
|
// OSRM format: /route/v1/car/lon1,lat1;lon2,lat2
|
||||||
final coordinates =
|
final coordinates =
|
||||||
'${start.longitude},${start.latitude};${end.longitude},${end.latitude}';
|
'${start.longitude},${start.latitude};${end.longitude},${end.latitude}';
|
||||||
@@ -67,6 +74,16 @@ class NavigationService {
|
|||||||
start: start,
|
start: start,
|
||||||
end: end,
|
end: end,
|
||||||
);
|
);
|
||||||
|
} else if (response.statusCode == 400) {
|
||||||
|
// Bad request: OSRM couldn't parse coordinates. Fall back to a simple straight-line route.
|
||||||
|
try {
|
||||||
|
final distanceMeters = calculateDistance(start, end);
|
||||||
|
final durationSec = (distanceMeters / 11.111111).toInt(); // assume ~40 km/h -> 11.11 m/s
|
||||||
|
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);
|
||||||
|
} catch (e) {
|
||||||
|
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}');
|
||||||
}
|
}
|
||||||
@@ -140,6 +157,14 @@ 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) {
|
||||||
|
if (response.statusCode == 400) {
|
||||||
|
// fallback: return simple ordered stops route
|
||||||
|
final distanceMeters = _sumDistancesBetween(stops);
|
||||||
|
final durationSec = (distanceMeters / 11.111111).toInt();
|
||||||
|
final leg = RouteLeg.fromPoints(points: stops, distance: distanceMeters, duration: durationSec, summary: 'Fallback optimized trip');
|
||||||
|
final route = NavigationRoute(start: stops.first, end: stops.last, legs: [leg], totalDistance: distanceMeters, totalDuration: durationSec);
|
||||||
|
return OptimizedRouteResult(route: route, orderedStops: stops);
|
||||||
|
}
|
||||||
throw Exception('Failed to get optimized route: ${response.statusCode}');
|
throw Exception('Failed to get optimized route: ${response.statusCode}');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +280,12 @@ class NavigationService {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
|
} else if (response.statusCode == 400) {
|
||||||
|
// fallback: single straight-line route
|
||||||
|
final distanceMeters = calculateDistance(start, end);
|
||||||
|
final durationSec = (distanceMeters / 11.111111).toInt();
|
||||||
|
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)];
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to get routes: ${response.statusCode}');
|
throw Exception('Failed to get routes: ${response.statusCode}');
|
||||||
}
|
}
|
||||||
@@ -263,6 +294,14 @@ class NavigationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _sumDistancesBetween(List<NavigationPoint> points) {
|
||||||
|
var total = 0.0;
|
||||||
|
for (var i = 0; i < points.length - 1; i++) {
|
||||||
|
total += calculateDistance(points[i], points[i + 1]);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
/// Calculate straight-line distance between two points
|
/// Calculate straight-line distance between two points
|
||||||
double calculateDistance(NavigationPoint p1, NavigationPoint p2) {
|
double calculateDistance(NavigationPoint p1, NavigationPoint p2) {
|
||||||
const distance = Distance();
|
const distance = Distance();
|
||||||
|
|||||||
Reference in New Issue
Block a user