chore: fix gps

This commit is contained in:
2026-02-21 15:22:17 +01:00
parent bf4df84ffe
commit 7c8ea006f6
9 changed files with 383 additions and 563 deletions
-13
View File
@@ -1,13 +0,0 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.
> What do the files contain?
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
- "settings.json": contains the server configuration that is used to serve the application manifest.
> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
-3
View File
@@ -1,3 +0,0 @@
{
"devices": []
}
+1 -1
View File
@@ -1,5 +1,5 @@
node_modules node_modules
.expo .expo/
dist dist
build build
*.log *.log
+2 -2
View File
@@ -10,7 +10,7 @@
"splash": { "splash": {
"image": "./assets/splash-icon.png", "image": "./assets/splash-icon.png",
"resizeMode": "contain", "resizeMode": "contain",
"backgroundColor": "#000" "backgroundColor": "#000000"
}, },
"ios": { "ios": {
"supportsTablet": true, "supportsTablet": true,
@@ -20,7 +20,7 @@
"package": "com.uberstup.adminpanel", "package": "com.uberstup.adminpanel",
"adaptiveIcon": { "adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png", "foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#000" "backgroundColor": "#000000"
}, },
"edgeToEdgeEnabled": true, "edgeToEdgeEnabled": true,
"permissions": [ "permissions": [
+1 -1
View File
@@ -84,7 +84,7 @@ export async function geocodeAddress(
address: string, address: string,
): Promise<LatLng | null> { ): Promise<LatLng | null> {
try { try {
const url = `https://api.tomtom.com/search/2/geocode/${encodeURIComponent(address)}.json?key=${TOMTOM_API_KEY}&countrySet=FR&limit=1`; const url = `https://api.tomtom.com/search/2/geocode/${encodeURIComponent(address)}.json?key=${TOMTOM_API_KEY}&limit=1`;
const { data } = await axios.get(url); const { data } = await axios.get(url);
if (data.results && data.results.length > 0) { if (data.results && data.results.length > 0) {
const pos = data.results[0].position; const pos = data.results[0].position;
+154 -87
View File
@@ -7,6 +7,7 @@ import React, {
} from "react"; } from "react";
import { StyleSheet } from "react-native"; import { StyleSheet } from "react-native";
import { WebView } from "react-native-webview"; import { WebView } from "react-native-webview";
import { geocodeAddress, calculateRoute } from "../api/tomtom";
const TOMTOM_API_KEY = "MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB"; const TOMTOM_API_KEY = "MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB";
@@ -20,29 +21,25 @@ export interface TomTomMarker {
isSelected?: boolean; isSelected?: boolean;
} }
export interface TomTomRoute {
coordinates: { latitude: number; longitude: number }[];
color?: string;
}
export interface TomTomDestination {
latitude: number;
longitude: number;
color?: string;
}
export interface TomTomMapRef { export interface TomTomMapRef {
fitAllMarkers: () => void; fitAllMarkers: () => void;
fitToCoordinates: ( fitToCoordinates: (
coords: { latitude: number; longitude: number }[], coords: { latitude: number; longitude: number }[],
) => void; ) => void;
calcRoute: (
origin: { latitude: number; longitude: number },
destination: { latitude: number; longitude: number },
) => void;
calcRouteFromAddress: (
origin: { latitude: number; longitude: number },
address: string,
) => void;
clearRoute: () => void;
} }
interface TomTomMapProps { interface TomTomMapProps {
style?: any; style?: any;
markers?: TomTomMarker[]; markers?: TomTomMarker[];
route?: TomTomRoute | null;
destination?: TomTomDestination | null;
initialCenter?: { latitude: number; longitude: number }; initialCenter?: { latitude: number; longitude: number };
initialZoom?: number; initialZoom?: number;
onMarkerPress?: (markerId: string) => void; onMarkerPress?: (markerId: string) => void;
@@ -53,10 +50,8 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
{ {
style, style,
markers = [], markers = [],
route,
destination,
initialCenter, initialCenter,
initialZoom = 12, initialZoom = 14,
onMarkerPress, onMarkerPress,
}, },
ref, ref,
@@ -69,7 +64,7 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
const json = JSON.stringify(msg); const json = JSON.stringify(msg);
if (isReady.current) { if (isReady.current) {
webViewRef.current?.injectJavaScript( webViewRef.current?.injectJavaScript(
`handleMessage(${json}); true;`, `(function(){ try { handleMessage(${json}); } catch(e) {} })(); true;`,
); );
} else { } else {
pendingMessages.current.push(json); pendingMessages.current.push(json);
@@ -82,6 +77,53 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
fitAllMarkers: () => sendMessage({ type: "fitAll" }), fitAllMarkers: () => sendMessage({ type: "fitAll" }),
fitToCoordinates: (coords) => fitToCoordinates: (coords) =>
sendMessage({ type: "fitCoords", coords }), sendMessage({ type: "fitCoords", coords }),
clearRoute: () => sendMessage({ type: "clearRoute" }),
calcRoute: (origin, dest) => {
calculateRoute(origin, dest).then((result) => {
if (result) {
sendMessage({
type: "drawRoute",
coordinates: result.route.coordinates,
destination: dest,
});
} else {
// Fallback: ligne droite
sendMessage({
type: "drawRoute",
coordinates: [origin, dest],
destination: dest,
});
}
}).catch(() => {
sendMessage({
type: "drawRoute",
coordinates: [origin, dest],
destination: dest,
});
});
},
calcRouteFromAddress: async (origin, address) => {
try {
const dest = await geocodeAddress(address);
if (!dest) return;
const result = await calculateRoute(origin, dest);
if (result) {
sendMessage({
type: "drawRoute",
coordinates: result.route.coordinates,
destination: dest,
});
} else {
sendMessage({
type: "drawRoute",
coordinates: [origin, dest],
destination: dest,
});
}
} catch {
/* silent */
}
},
}), }),
[sendMessage], [sendMessage],
); );
@@ -90,30 +132,25 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
sendMessage({ type: "updateMarkers", markers }); sendMessage({ type: "updateMarkers", markers });
}, [markers, sendMessage]); }, [markers, sendMessage]);
useEffect(() => {
sendMessage({
type: "updateRoute",
route: route || null,
destination: destination || null,
});
}, [route, destination, sendMessage]);
const onMessage = useCallback( const onMessage = useCallback(
(event: any) => { (event: any) => {
try { try {
const data = JSON.parse(event.nativeEvent.data); const data = JSON.parse(event.nativeEvent.data);
if (data.type === "ready") { if (data.type === "ready") {
isReady.current = true; isReady.current = true;
// flush pending // Rejouer les messages en attente
for (const msg of pendingMessages.current) { const pending = pendingMessages.current.slice();
pendingMessages.current = [];
for (const msg of pending) {
webViewRef.current?.injectJavaScript( webViewRef.current?.injectJavaScript(
`handleMessage(${msg}); true;`, `(function(){ try { handleMessage(${msg}); } catch(e) {} })(); true;`,
); );
} }
pendingMessages.current = []; // Fit sur tous les markers après un court délai
// Auto fit markers after ready + flush
setTimeout(() => { setTimeout(() => {
sendMessage({ type: "fitAll" }); webViewRef.current?.injectJavaScript(
`(function(){ try { handleMessage(${JSON.stringify({ type: "fitAll" })}); } catch(e) {} })(); true;`,
);
}, 500); }, 500);
} else if (data.type === "markerPress" && onMarkerPress) { } else if (data.type === "markerPress" && onMarkerPress) {
onMarkerPress(data.id); onMarkerPress(data.id);
@@ -134,7 +171,7 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
} }
: { latitude: 48.8566, longitude: 2.3522 }); : { latitude: 48.8566, longitude: 2.3522 });
const html = buildHtml(center, initialZoom); const html = buildHtml(center, initialZoom, TOMTOM_API_KEY);
return ( return (
<WebView <WebView
@@ -147,6 +184,7 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
scrollEnabled={false} scrollEnabled={false}
bounces={false} bounces={false}
originWhitelist={["*"]} originWhitelist={["*"]}
mixedContentMode="always"
/> />
); );
}, },
@@ -155,6 +193,7 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
function buildHtml( function buildHtml(
center: { latitude: number; longitude: number }, center: { latitude: number; longitude: number },
zoom: number, zoom: number,
apiKey: string,
) { ) {
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html> <html>
@@ -163,13 +202,13 @@ function buildHtml(
<link rel="stylesheet" href="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/6.25.0/maps/maps.css"> <link rel="stylesheet" href="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/6.25.0/maps/maps.css">
<script src="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/6.25.0/maps/maps-web.min.js"></script> <script src="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/6.25.0/maps/maps-web.min.js"></script>
<style> <style>
* { margin: 0; padding: 0; } * { margin: 0; padding: 0; box-sizing: border-box; }
html, body, #map { width: 100%; height: 100%; } html, body, #map { width: 100%; height: 100%; overflow: hidden; }
.marker-dot { .marker-dot {
width: 28px; height: 28px; border-radius: 50%; width: 28px; height: 28px; border-radius: 50%;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
border: 2px solid rgba(255,255,255,0.8); border: 2px solid rgba(255,255,255,0.8);
font-size: 12px; color: white; cursor: pointer; cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,0.4); box-shadow: 0 2px 6px rgba(0,0,0,0.4);
} }
.marker-dot.selected { .marker-dot.selected {
@@ -178,11 +217,8 @@ function buildHtml(
box-shadow: 0 0 12px rgba(124,58,237,0.6); box-shadow: 0 0 12px rgba(124,58,237,0.6);
} }
.dest-marker { .dest-marker {
width: 24px; height: 24px; border-radius: 50%; width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
border: 2px solid white;
font-size: 10px; color: white;
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
} }
</style> </style>
</head> </head>
@@ -190,39 +226,82 @@ function buildHtml(
<div id="map"></div> <div id="map"></div>
<script> <script>
var map = tt.map({ var map = tt.map({
key: '${TOMTOM_API_KEY}', key: '${apiKey}',
container: 'map', container: 'map',
center: [${center.longitude}, ${center.latitude}], center: [${center.longitude}, ${center.latitude}],
zoom: ${zoom}, zoom: ${zoom},
stylesVisibility: { trafficFlow: false, trafficIncidents: false } stylesVisibility: { trafficFlow: false, trafficIncidents: false }
}); });
var markers = {}; var markerObjects = {};
var routeLayer = null;
var destMarker = null; var destMarker = null;
var ROUTE_SOURCE = 'tt-route-src';
var ROUTE_LAYER = 'tt-route-lyr';
function clearMarkers() { // File d'attente pour les messages reçus avant que le style soit chargé
Object.values(markers).forEach(function(m) { m.remove(); }); var pendingRouteData = null;
markers = {}; var styleLoaded = false;
}
function clearRoute() { function clearRoute() {
if (routeLayer && map.getSource('route')) { try { if (map.getLayer(ROUTE_LAYER)) map.removeLayer(ROUTE_LAYER); } catch(e) {}
map.removeLayer('route-line'); try { if (map.getSource(ROUTE_SOURCE)) map.removeSource(ROUTE_SOURCE); } catch(e) {}
map.removeSource('route');
routeLayer = null;
}
if (destMarker) { destMarker.remove(); destMarker = null; } if (destMarker) { destMarker.remove(); destMarker = null; }
} }
function drawRoute(coordinates, destination) {
clearRoute();
if (!destination) return;
// Marker destination (pin rouge)
var pin = document.createElement('div');
pin.className = 'dest-marker';
pin.innerHTML = '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="#ef4444" d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5" fill="white"/></svg>';
destMarker = new tt.Marker({ element: pin })
.setLngLat([destination.longitude, destination.latitude])
.addTo(map);
if (!coordinates || coordinates.length < 2) {
map.flyTo({ center: [destination.longitude, destination.latitude], zoom: 15 });
return;
}
// Convertir les coords { latitude, longitude } en [lng, lat] pour GeoJSON
var lngLat = coordinates.map(function(c) {
return [c.longitude, c.latitude];
});
try {
map.addSource(ROUTE_SOURCE, {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: { type: 'LineString', coordinates: lngLat }
}
});
map.addLayer({
id: ROUTE_LAYER,
type: 'line',
source: ROUTE_SOURCE,
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': '#4285F4', 'line-width': 6, 'line-opacity': 0.9 }
});
var bounds = new tt.LngLatBounds();
lngLat.forEach(function(c) { bounds.extend(c); });
map.fitBounds(bounds, { padding: 60, maxZoom: 16, duration: 800 });
} catch(e) {}
}
}
function handleMessage(data) { function handleMessage(data) {
if (data.type === 'updateMarkers') { if (data.type === 'updateMarkers') {
clearMarkers(); Object.values(markerObjects).forEach(function(m) { m.remove(); });
markerObjects = {};
(data.markers || []).forEach(function(m) { (data.markers || []).forEach(function(m) {
var el = document.createElement('div'); var el = document.createElement('div');
el.className = 'marker-dot' + (m.isSelected ? ' selected' : ''); el.className = 'marker-dot' + (m.isSelected ? ' selected' : '');
el.style.backgroundColor = m.color || '#7c3aed'; el.style.backgroundColor = m.color || '#7c3aed';
el.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="white"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>'; el.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="white"><circle cx="12" cy="12" r="8"/></svg>';
el.addEventListener('click', function() { el.addEventListener('click', function() {
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'markerPress', id: m.id })); window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'markerPress', id: m.id }));
}); });
@@ -232,47 +311,30 @@ function handleMessage(data) {
.setLngLat([m.longitude, m.latitude]) .setLngLat([m.longitude, m.latitude])
.setPopup(popup) .setPopup(popup)
.addTo(map); .addTo(map);
markers[m.id] = marker; markerObjects[m.id] = marker;
}); });
} }
if (data.type === 'updateRoute') { if (data.type === 'drawRoute') {
if (!styleLoaded) {
pendingRouteData = data;
} else {
drawRoute(data.coordinates, data.destination);
}
}
if (data.type === 'clearRoute') {
pendingRouteData = null;
clearRoute(); clearRoute();
if (data.destination) {
var el = document.createElement('div');
el.className = 'dest-marker';
el.style.backgroundColor = data.destination.color || '#ef4444';
el.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="white"><path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/></svg>';
destMarker = new tt.Marker({ element: el })
.setLngLat([data.destination.longitude, data.destination.latitude])
.addTo(map);
}
if (data.route && data.route.coordinates && data.route.coordinates.length > 1) {
var coords = data.route.coordinates.map(function(c) { return [c.longitude, c.latitude]; });
map.addSource('route', {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: coords }, properties: {} }
});
map.addLayer({
id: 'route-line',
type: 'line',
source: 'route',
paint: {
'line-color': data.route.color || '#4285F4',
'line-width': 4,
'line-opacity': 0.8
}
});
routeLayer = true;
}
} }
if (data.type === 'fitAll') { if (data.type === 'fitAll') {
var allM = Object.values(markers); var all = Object.values(markerObjects);
if (allM.length > 0) { if (all.length > 0) {
var bounds = new tt.LngLatBounds(); var bounds = new tt.LngLatBounds();
allM.forEach(function(m) { bounds.extend(m.getLngLat()); }); all.forEach(function(m) { bounds.extend(m.getLngLat()); });
map.fitBounds(bounds, { padding: 60, maxZoom: 15 }); if (destMarker) bounds.extend(destMarker.getLngLat());
map.fitBounds(bounds, { padding: 60, maxZoom: 15, duration: 800 });
} }
} }
@@ -280,12 +342,17 @@ function handleMessage(data) {
if (data.coords && data.coords.length > 0) { if (data.coords && data.coords.length > 0) {
var bounds = new tt.LngLatBounds(); var bounds = new tt.LngLatBounds();
data.coords.forEach(function(c) { bounds.extend([c.longitude, c.latitude]); }); data.coords.forEach(function(c) { bounds.extend([c.longitude, c.latitude]); });
map.fitBounds(bounds, { padding: 80, maxZoom: 15 }); map.fitBounds(bounds, { padding: 80, maxZoom: 15, duration: 800 });
} }
} }
} }
map.on('load', function() { map.on('load', function() {
styleLoaded = true;
if (pendingRouteData) {
drawRoute(pendingRouteData.coordinates, pendingRouteData.destination);
pendingRouteData = null;
}
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ready' })); window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ready' }));
}); });
</script> </script>
@@ -23,7 +23,7 @@ import {
getCommandByID, getCommandByID,
} from "../../api/api_admin"; } from "../../api/api_admin";
import { geocodeAddress, calculateRoute } from "../../api/tomtom"; import { geocodeAddress, calculateRoute } from "../../api/tomtom";
import type { RouteInfo, LatLng } from "../../api/tomtom"; import type { RouteInfo } from "../../api/tomtom";
import type { DeliveryPerson } from "../../api/types"; import type { DeliveryPerson } from "../../api/types";
import { STATUS_LABELS, getStatusColors } from "../../utils/constants"; import { STATUS_LABELS, getStatusColors } from "../../utils/constants";
import LoadingSpinner from "../../components/ui/LoadingSpinner"; import LoadingSpinner from "../../components/ui/LoadingSpinner";
@@ -32,8 +32,6 @@ import Badge from "../../components/ui/Badge";
import TomTomMap, { import TomTomMap, {
TomTomMapRef, TomTomMapRef,
TomTomMarker, TomTomMarker,
TomTomRoute,
TomTomDestination,
} from "../../components/TomTomMap"; } from "../../components/TomTomMap";
const MAP_HEIGHT = 280; const MAP_HEIGHT = 280;
@@ -60,9 +58,6 @@ export default function DeliveryScreen() {
const [selectedLivreur, setSelectedLivreur] = const [selectedLivreur, setSelectedLivreur] =
useState<DeliveryPerson | null>(null); useState<DeliveryPerson | null>(null);
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null); const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
const [destinationCoords, setDestinationCoords] = useState<LatLng | null>(
null,
);
const [routeLoading, setRouteLoading] = useState(false); const [routeLoading, setRouteLoading] = useState(false);
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
@@ -111,30 +106,13 @@ export default function DeliveryScreen() {
})); }));
}, [livreursWithGPS, selectedLivreur, statusColors, colors.textMuted]); }, [livreursWithGPS, selectedLivreur, statusColors, colors.textMuted]);
// Route TomTom
const tomtomRoute: TomTomRoute | null = useMemo(() => {
if (!routeInfo || routeInfo.coordinates.length === 0) return null;
return { coordinates: routeInfo.coordinates, color: "#4285F4" };
}, [routeInfo]);
// Destination TomTom
const tomtomDestination: TomTomDestination | null = useMemo(() => {
if (!destinationCoords) return null;
return {
latitude: destinationCoords.latitude,
longitude: destinationCoords.longitude,
color: colors.danger,
};
}, [destinationCoords, colors.danger]);
// -------------------------------------------------- // --------------------------------------------------
// Track livreur — calcul de route // Track livreur — calcul de route via ref TomTomMap
// -------------------------------------------------- // --------------------------------------------------
const trackLivreur = useCallback( const trackLivreur = useCallback(
async (livreur: DeliveryPerson) => { async (livreur: DeliveryPerson) => {
setSelectedLivreur(livreur); setSelectedLivreur(livreur);
setRouteInfo(null); setRouteInfo(null);
setDestinationCoords(null);
if (!livreur.stats.current_command) return; if (!livreur.stats.current_command) return;
@@ -154,24 +132,16 @@ export default function DeliveryScreen() {
setRouteLoading(false); setRouteLoading(false);
return; return;
} }
setDestinationCoords(dest);
const origin: LatLng = { const origin = {
latitude: livreur.location.latitude, latitude: livreur.location.latitude,
longitude: livreur.location.longitude, longitude: livreur.location.longitude,
}; };
const result = await calculateRoute(origin, dest); const result = await calculateRoute(origin, dest);
if (result) { if (result) {
setRouteInfo(result.route); setRouteInfo(result.route);
// Fit la map sur les deux points const activeRef = mapFullscreen ? fullscreenMapRef : mapRef;
const ref = mapFullscreen ? fullscreenMapRef : mapRef; activeRef.current?.calcRoute(origin, dest);
ref.current?.fitToCoordinates([
{
latitude: origin.latitude,
longitude: origin.longitude,
},
{ latitude: dest.latitude, longitude: dest.longitude },
]);
} }
} catch { } catch {
/* silent */ /* silent */
@@ -184,7 +154,6 @@ export default function DeliveryScreen() {
const clearRoute = () => { const clearRoute = () => {
setSelectedLivreur(null); setSelectedLivreur(null);
setRouteInfo(null); setRouteInfo(null);
setDestinationCoords(null);
}; };
// -------------------------------------------------- // --------------------------------------------------
@@ -599,8 +568,6 @@ export default function DeliveryScreen() {
ref={mapRef} ref={mapRef}
style={styles.map} style={styles.map}
markers={tomtomMarkers} markers={tomtomMarkers}
route={tomtomRoute}
destination={tomtomDestination}
initialCenter={{ initialCenter={{
latitude: livreursWithGPS[0].location.latitude, latitude: livreursWithGPS[0].location.latitude,
longitude: livreursWithGPS[0].location.longitude, longitude: livreursWithGPS[0].location.longitude,
@@ -708,8 +675,6 @@ export default function DeliveryScreen() {
ref={fullscreenMapRef} ref={fullscreenMapRef}
style={styles.fullscreenMap} style={styles.fullscreenMap}
markers={tomtomMarkers} markers={tomtomMarkers}
route={tomtomRoute}
destination={tomtomDestination}
initialCenter={{ initialCenter={{
latitude: livreursWithGPS[0].location.latitude, latitude: livreursWithGPS[0].location.latitude,
longitude: longitude:
@@ -4,6 +4,7 @@ import React, {
useCallback, useCallback,
useRef, useRef,
useMemo, useMemo,
RefObject,
} from "react"; } from "react";
import { import {
View, View,
@@ -33,17 +34,8 @@ import {
updateDeliveryStatus, updateDeliveryStatus,
updateMyLocation, updateMyLocation,
} from "../../api/api_delivery"; } from "../../api/api_delivery";
import { import { geocodeAddress, calculateRoute } from "../../api/tomtom";
geocodeAddress, import type { RouteInfo } from "../../api/tomtom";
calculateRoute,
maneuverIcons,
maneuverTranslations,
} from "../../api/tomtom";
import type {
RouteInfo,
NavigationInstruction,
LatLng,
} from "../../api/tomtom";
import type { DeliveryStatus, DeliveryItem, QueueInfo } from "../../api/types"; import type { DeliveryStatus, DeliveryItem, QueueInfo } from "../../api/types";
import LoadingSpinner from "../../components/ui/LoadingSpinner"; import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Card from "../../components/ui/Card"; import Card from "../../components/ui/Card";
@@ -54,10 +46,10 @@ import { useAlert } from "../../hooks/useAlert";
import TomTomMap, { import TomTomMap, {
TomTomMapRef, TomTomMapRef,
TomTomMarker, TomTomMarker,
TomTomRoute,
TomTomDestination,
} from "../../components/TomTomMap"; } from "../../components/TomTomMap";
const { width: SCREEN_WIDTH } = Dimensions.get("window"); const { width: SCREEN_WIDTH } = Dimensions.get("window");
const MAP_HEIGHT = 260; const MAP_HEIGHT = 260;
const LOCATION_INTERVAL_MS = 15000; const LOCATION_INTERVAL_MS = 15000;
@@ -91,6 +83,7 @@ export default function DashboardScreen() {
lat: number; lat: number;
lng: number; lng: number;
} | null>(null); } | null>(null);
const lastCoordsRef = useRef<{ lat: number; lng: number } | null>(null);
const [lastUpdate, setLastUpdate] = useState<Date | null>(null); const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
const locationInterval = useRef<ReturnType<typeof setInterval> | null>( const locationInterval = useRef<ReturnType<typeof setInterval> | null>(
null, null,
@@ -102,17 +95,9 @@ export default function DashboardScreen() {
const fullscreenMapRef = useRef<TomTomMapRef>(null); const fullscreenMapRef = useRef<TomTomMapRef>(null);
const [mapFullscreen, setMapFullscreen] = useState(false); const [mapFullscreen, setMapFullscreen] = useState(false);
// TomTom routing
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
const [instructions, setInstructions] = useState<NavigationInstruction[]>(
[],
);
const [destinationCoords, setDestinationCoords] = useState<LatLng | null>(
null,
);
const [showInstructions, setShowInstructions] = useState(false);
const [currentInstructionIdx, setCurrentInstructionIdx] = useState(0);
const [routeLoading, setRouteLoading] = useState(false); const [routeLoading, setRouteLoading] = useState(false);
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
const pendingRouteAddress = useRef<string | null>(null);
const { alert, showError, showSuccess, hideAlert } = useAlert(); const { alert, showError, showSuccess, hideAlert } = useAlert();
const STATUS_COLORS: Record<string, string> = useMemo( const STATUS_COLORS: Record<string, string> = useMemo(
@@ -124,10 +109,6 @@ export default function DashboardScreen() {
[colors], [colors],
); );
// --------------------------------------------------
// Construire les props pour TomTomMap
// --------------------------------------------------
// Marker du livreur (position courante) // Marker du livreur (position courante)
const driverMarkers: TomTomMarker[] = useMemo(() => { const driverMarkers: TomTomMarker[] = useMemo(() => {
if (!lastCoords) return []; if (!lastCoords) return [];
@@ -148,21 +129,42 @@ export default function DashboardScreen() {
]; ];
}, [lastCoords, lastUpdate, colors.success]); }, [lastCoords, lastUpdate, colors.success]);
// Route TomTom // --------------------------------------------------
const tomtomRoute: TomTomRoute | null = useMemo(() => { // TomTom Route calculation
if (!routeInfo || routeInfo.coordinates.length === 0) return null; // Geocode + calcul dans DashboardScreen pour récupérer routeInfo
return { coordinates: routeInfo.coordinates, color: "#4285F4" }; // --------------------------------------------------
}, [routeInfo]); const calcRoute = useCallback(async (address: string, targetRef?: RefObject<TomTomMapRef | null>) => {
const coords = lastCoordsRef.current;
if (!coords) {
pendingRouteAddress.current = address;
return;
}
pendingRouteAddress.current = address;
const ref = targetRef ?? mapRef;
if (!ref.current) return;
// Destination TomTom setRouteLoading(true);
const tomtomDestination: TomTomDestination | null = useMemo(() => { try {
if (!destinationCoords) return null; const origin = { latitude: coords.lat, longitude: coords.lng };
return { const dest = await geocodeAddress(address);
latitude: destinationCoords.latitude, if (!dest) {
longitude: destinationCoords.longitude, setRouteLoading(false);
color: colors.danger, return;
}; }
}, [destinationCoords, colors.danger]); const result = await calculateRoute(origin, dest);
if (result) {
setRouteInfo(result.route);
ref.current?.calcRoute(origin, dest);
} else {
setRouteInfo(null);
ref.current?.calcRoute(origin, dest);
}
} catch {
/* silent */
} finally {
setRouteLoading(false);
}
}, []);
// -------------------------------------------------- // --------------------------------------------------
// Data // Data
@@ -245,64 +247,45 @@ export default function DashboardScreen() {
(d) => (d) =>
d.status === "in_progress" || d.status === "en_route", d.status === "in_progress" || d.status === "en_route",
) || enriched.find((d) => d.status === "assigned"); ) || enriched.find((d) => d.status === "assigned");
if (activeDelivery && activeDelivery.adresse && lastCoords) { if (activeDelivery && activeDelivery.adresse) {
calcRoute(activeDelivery.adresse); calcRoute(activeDelivery.adresse);
} }
} catch { } catch {
/* ignore */ /* ignore */
} }
setLoading(false); setLoading(false);
}, [lastCoords]); }, [calcRoute]);
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [loadData]); }, [loadData]);
// Quand GPS devient disponible, rejouer la route en attente
useEffect(() => {
if (lastCoords && pendingRouteAddress.current) {
calcRoute(pendingRouteAddress.current);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastCoords]);
// Quand le fullscreen s'ouvre, rejouer la route sur la carte fullscreen
useEffect(() => {
if (!mapFullscreen) return;
const addr = pendingRouteAddress.current;
const coords = lastCoordsRef.current;
if (!addr || !coords) return;
setTimeout(() => {
calcRoute(addr, fullscreenMapRef);
}, 1200);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mapFullscreen]);
const onRefresh = async () => { const onRefresh = async () => {
setRefreshing(true); setRefreshing(true);
await loadData(); await loadData();
setRefreshing(false); setRefreshing(false);
}; };
// --------------------------------------------------
// TomTom Route calculation
// --------------------------------------------------
const calcRoute = useCallback(
async (address: string) => {
if (!lastCoords) return;
setRouteLoading(true);
try {
const dest = await geocodeAddress(address);
if (!dest) {
setRouteLoading(false);
return;
}
setDestinationCoords(dest);
const result = await calculateRoute(
{ latitude: lastCoords.lat, longitude: lastCoords.lng },
dest,
);
if (result) {
setRouteInfo(result.route);
setInstructions(result.instructions);
setCurrentInstructionIdx(0);
// Fit la map pour afficher driver + destination
const coords = [
{ latitude: lastCoords.lat, longitude: lastCoords.lng },
{ latitude: dest.latitude, longitude: dest.longitude },
];
mapRef.current?.fitToCoordinates(coords);
}
} catch {
/* silent */
}
setRouteLoading(false);
},
[lastCoords],
);
// -------------------------------------------------- // --------------------------------------------------
// Location tracking // Location tracking
// -------------------------------------------------- // --------------------------------------------------
@@ -327,18 +310,26 @@ export default function DashboardScreen() {
!("coords" in loc) || !("coords" in loc) ||
!loc.coords !loc.coords
) { ) {
console.log("GPS invalide — update ignoré");
return; return;
} }
const coords = (loc as Location.LocationObject).coords; const coords = (loc as Location.LocationObject).coords;
const { latitude, longitude } = coords; const { latitude, longitude } = coords;
const wasNull = !lastCoordsRef.current;
lastCoordsRef.current = { lat: latitude, lng: longitude };
setLastCoords({ lat: latitude, lng: longitude }); setLastCoords({ lat: latitude, lng: longitude });
setLastUpdate(new Date()); setLastUpdate(new Date());
await updateMyLocation(latitude, longitude); await updateMyLocation(latitude, longitude);
} catch (err) { // Si c'est la première position GPS et qu'une adresse était en attente, rejouer
console.log("GPS error:", err); if (wasNull && pendingRouteAddress.current) {
const addr = pendingRouteAddress.current;
setTimeout(() => {
calcRoute(addr);
}, 1000);
}
} catch {
/* silent */
} finally { } finally {
sendingRef.current = false; sendingRef.current = false;
} }
@@ -435,9 +426,6 @@ export default function DashboardScreen() {
const res = await updateDeliveryStatus(deliveryId, "livre", lat, lng); const res = await updateDeliveryStatus(deliveryId, "livre", lat, lng);
if (res.success) { if (res.success) {
showSuccess("Succès", "Livraison terminée"); showSuccess("Succès", "Livraison terminée");
setRouteInfo(null);
setDestinationCoords(null);
setInstructions([]);
loadData(); loadData();
} else { } else {
showError("Erreur", res.error || "Erreur"); showError("Erreur", res.error || "Erreur");
@@ -450,98 +438,6 @@ export default function DashboardScreen() {
Linking.openURL(url); Linking.openURL(url);
}; };
// --------------------------------------------------
// Render: Navigation instruction bar
// --------------------------------------------------
const renderInstructionBar = () => {
if (instructions.length === 0 || !routeInfo) return null;
const current = instructions[currentInstructionIdx];
const next = instructions[currentInstructionIdx + 1];
const iconName =
maneuverIcons[current?.maneuver] || maneuverIcons.DEFAULT;
return (
<View style={styles.instructionBar}>
<View style={styles.currentInstruction}>
<View style={styles.instructionIconBox}>
<Ionicons
name={iconName as any}
size={22}
color={colors.white}
/>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.instructionText} numberOfLines={2}>
{current?.instruction || "Suivez l'itinéraire"}
</Text>
{current?.streetName && (
<Text style={styles.instructionStreet}>
{current.streetName}
</Text>
)}
</View>
<Text style={styles.instructionDist}>
{current?.distance}
</Text>
</View>
{next && (
<View style={styles.nextInstruction}>
<Text style={styles.nextLabel}>Puis</Text>
<Ionicons
name={
(maneuverIcons[next.maneuver] ||
maneuverIcons.DEFAULT) as any
}
size={14}
color={colors.textMuted}
/>
<Text style={styles.nextText} numberOfLines={1}>
{next.instruction}
</Text>
</View>
)}
<View style={styles.routeSummary}>
<View style={styles.routeInfoChip}>
<Ionicons
name="speedometer-outline"
size={14}
color={colors.accent}
/>
<Text style={styles.routeInfoValue}>
{routeInfo.distance}
</Text>
</View>
<View style={styles.routeInfoChip}>
<Ionicons
name="time-outline"
size={14}
color={colors.accent}
/>
<Text style={styles.routeInfoValue}>
{routeInfo.duration}
</Text>
</View>
<TouchableOpacity
style={styles.showStepsBtn}
onPress={() => setShowInstructions(!showInstructions)}
>
<Ionicons
name="list-outline"
size={14}
color={colors.white}
/>
<Text style={styles.showStepsBtnText}>
{showInstructions ? "Masquer" : "Étapes"} (
{instructions.length})
</Text>
</TouchableOpacity>
</View>
</View>
);
};
// -------------------------------------------------- // --------------------------------------------------
// Render delivery card // Render delivery card
// -------------------------------------------------- // --------------------------------------------------
@@ -713,23 +609,22 @@ export default function DashboardScreen() {
// -------------------------------------------------- // --------------------------------------------------
const renderHeader = () => ( const renderHeader = () => (
<View> <View>
{/* Carte TomTom */} {/* Carte TomTom — toujours montée pour que le ref soit disponible */}
{lastCoords ? (
<View style={styles.mapContainer}> <View style={styles.mapContainer}>
<TomTomMap <TomTomMap
ref={mapRef} ref={mapRef}
style={styles.map} style={styles.map}
markers={driverMarkers} markers={driverMarkers}
route={tomtomRoute} initialCenter={
destination={tomtomDestination} lastCoords
initialCenter={{ ? { latitude: lastCoords.lat, longitude: lastCoords.lng }
latitude: lastCoords.lat, : { latitude: 48.8566, longitude: 2.3522 }
longitude: lastCoords.lng, }
}}
initialZoom={14} initialZoom={14}
/> />
{/* GPS overlay */} {/* Overlay GPS */}
{lastCoords ? (
<View style={styles.gpsOverlay}> <View style={styles.gpsOverlay}>
<View <View
style={[ style={[
@@ -754,6 +649,32 @@ export default function DashboardScreen() {
</Text> </Text>
)} )}
</View> </View>
) : (
<View style={styles.gpsOverlay}>
<Ionicons
name="location-outline"
size={16}
color={colors.textMuted}
/>
<Text style={styles.gpsOverlayText}>
Récupération GPS...
</Text>
</View>
)}
{/* Overlay infos trajet */}
{routeInfo && !routeLoading && (
<View style={styles.routeInfoOverlay}>
<View style={styles.routeInfoChip}>
<Ionicons name="time-outline" size={13} color={colors.white} />
<Text style={styles.routeInfoOverlayText}>{routeInfo.duration}</Text>
</View>
<View style={styles.routeInfoChip}>
<Ionicons name="navigate-outline" size={13} color={colors.white} />
<Text style={styles.routeInfoOverlayText}>{routeInfo.distance}</Text>
</View>
</View>
)}
{/* Bouton plein écran */} {/* Bouton plein écran */}
<TouchableOpacity <TouchableOpacity
@@ -777,103 +698,6 @@ export default function DashboardScreen() {
</View> </View>
)} )}
</View> </View>
) : (
<View style={styles.noMapBox}>
<Ionicons
name="location-outline"
size={32}
color={colors.textMuted}
/>
<Text style={styles.noMapText}>
Récupération de la position GPS...
</Text>
</View>
)}
{/* Instructions navigation */}
{renderInstructionBar()}
{/* Liste complète des étapes */}
{showInstructions && instructions.length > 0 && (
<View style={styles.allInstructionsBox}>
<View style={styles.allInstructionsHeader}>
<Text style={styles.allInstructionsTitle}>
Étapes de l'itinéraire
</Text>
<TouchableOpacity
onPress={() => setShowInstructions(false)}
>
<Ionicons
name="close"
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
<ScrollView style={{ maxHeight: 250 }} nestedScrollEnabled>
{instructions.map((inst, idx) => {
const icoName =
maneuverIcons[inst.maneuver] ||
maneuverIcons.DEFAULT;
const isCurrent = idx === currentInstructionIdx;
const isPassed = idx < currentInstructionIdx;
return (
<View
key={idx}
style={[
styles.stepRow,
isCurrent && styles.stepRowActive,
isPassed && styles.stepRowPassed,
]}
>
<View
style={[
styles.stepIcon,
isCurrent && {
backgroundColor:
colors.accent + "30",
},
]}
>
<Ionicons
name={icoName as any}
size={16}
color={
isCurrent
? colors.accent
: isPassed
? colors.textMuted
: colors.textSecondary
}
/>
</View>
<View style={{ flex: 1 }}>
<Text
style={[
styles.stepText,
isPassed && {
color: colors.textMuted,
},
]}
numberOfLines={2}
>
{inst.instruction}
</Text>
{inst.streetName && (
<Text style={styles.stepStreet}>
{inst.streetName}
</Text>
)}
</View>
<Text style={styles.stepDist}>
{inst.distance}
</Text>
</View>
);
})}
</ScrollView>
</View>
)}
{/* Queue info */} {/* Queue info */}
{queue && queue.queue_size > 0 && ( {queue && queue.queue_size > 0 && (
@@ -1004,6 +828,28 @@ export default function DashboardScreen() {
alignItems: "center", alignItems: "center",
}, },
routeInfoOverlay: {
position: "absolute",
top: spacing.s,
left: spacing.s,
flexDirection: "row",
gap: spacing.xs,
},
routeInfoChip: {
flexDirection: "row",
alignItems: "center",
gap: 4,
backgroundColor: "rgba(0,0,0,0.7)",
paddingVertical: 4,
paddingHorizontal: spacing.s,
borderRadius: 12,
},
routeInfoOverlayText: {
color: colors.white,
fontSize: fontSize.xs,
fontWeight: "700",
},
// Instructions // Instructions
instructionBar: { instructionBar: {
backgroundColor: colors.bgSecondary, backgroundColor: colors.bgSecondary,
@@ -1069,20 +915,6 @@ export default function DashboardScreen() {
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: colors.borderSubtle, borderTopColor: colors.borderSubtle,
}, },
routeInfoChip: {
flexDirection: "row",
alignItems: "center",
gap: 4,
backgroundColor: colors.accent + "15",
paddingVertical: 4,
paddingHorizontal: spacing.s,
borderRadius: 12,
},
routeInfoValue: {
color: colors.accent,
fontSize: fontSize.xs,
fontWeight: "600",
},
showStepsBtn: { showStepsBtn: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
@@ -1410,20 +1242,17 @@ export default function DashboardScreen() {
> >
<StatusBar hidden={mapFullscreen} /> <StatusBar hidden={mapFullscreen} />
<View style={styles.fullscreenContainer}> <View style={styles.fullscreenContainer}>
{lastCoords && (
<TomTomMap <TomTomMap
ref={fullscreenMapRef} ref={fullscreenMapRef}
style={styles.fullscreenMap} style={styles.fullscreenMap}
markers={driverMarkers} markers={driverMarkers}
route={tomtomRoute} initialCenter={
destination={tomtomDestination} lastCoords
initialCenter={{ ? { latitude: lastCoords.lat, longitude: lastCoords.lng }
latitude: lastCoords.lat, : { latitude: 48.8566, longitude: 2.3522 }
longitude: lastCoords.lng, }
}}
initialZoom={15} initialZoom={15}
/> />
)}
{/* Top bar */} {/* Top bar */}
<View style={styles.fullscreenTopBar}> <View style={styles.fullscreenTopBar}>
@@ -1438,54 +1267,45 @@ export default function DashboardScreen() {
/> />
</TouchableOpacity> </TouchableOpacity>
<Text style={styles.fullscreenTitle}> <Text style={styles.fullscreenTitle}>
{routeInfo {pendingRouteAddress.current ? "Itinéraire en cours" : "GPS en direct"}
? `${routeInfo.distance} · ${routeInfo.duration}`
: "GPS en direct"}
</Text> </Text>
<View style={{ width: 40 }} /> <View style={{ width: 40 }} />
</View> </View>
{/* Instruction bar en plein écran */} {/* Bottom info */}
{instructions.length > 0 && ( <View style={styles.fullscreenBottomBar}>
<View style={styles.fullscreenInstructionBar}> {/* Adresse de destination */}
<View style={styles.instructionIconBox}> {pendingRouteAddress.current && (
<View style={styles.fullscreenInfoRow}>
<Ionicons <Ionicons
name={ name="location"
(maneuverIcons[ size={16}
instructions[currentInstructionIdx] color={colors.danger}
?.maneuver
] || maneuverIcons.DEFAULT) as any
}
size={22}
color={colors.white}
/> />
</View>
<View style={{ flex: 1 }}>
<Text <Text
style={styles.instructionText} style={[styles.fullscreenInfoText, { flex: 1 }]}
numberOfLines={2} numberOfLines={2}
> >
{instructions[currentInstructionIdx] {pendingRouteAddress.current}
?.instruction || "Suivez l'itinéraire"}
</Text>
{instructions[currentInstructionIdx]
?.streetName && (
<Text style={styles.instructionStreet}>
{
instructions[currentInstructionIdx]
.streetName
}
</Text>
)}
</View>
<Text style={styles.instructionDist}>
{instructions[currentInstructionIdx]?.distance}
</Text> </Text>
</View> </View>
)} )}
{/* Bottom info */} {/* Infos trajet */}
<View style={styles.fullscreenBottomBar}> {routeInfo && (
<View style={styles.fullscreenInfoRow}>
<View style={styles.routeInfoChip}>
<Ionicons name="time-outline" size={14} color={colors.white} />
<Text style={styles.routeInfoOverlayText}>{routeInfo.duration}</Text>
</View>
<View style={[styles.routeInfoChip, { marginLeft: spacing.s }]}>
<Ionicons name="navigate-outline" size={14} color={colors.white} />
<Text style={styles.routeInfoOverlayText}>{routeInfo.distance}</Text>
</View>
</View>
)}
{/* GPS status */}
<View style={styles.fullscreenInfoRow}> <View style={styles.fullscreenInfoRow}>
<View <View
style={[ style={[
@@ -1500,41 +1320,25 @@ export default function DashboardScreen() {
<Text style={styles.fullscreenInfoText}> <Text style={styles.fullscreenInfoText}>
GPS {locationEnabled ? "actif" : "inactif"} GPS {locationEnabled ? "actif" : "inactif"}
</Text> </Text>
</View>
{lastCoords && ( {lastCoords && (
<Text style={styles.fullscreenCoords}> <Text style={[styles.fullscreenCoords, { marginLeft: 8 }]}>
{lastCoords.lat.toFixed(6)},{" "} {lastCoords.lat.toFixed(4)}, {lastCoords.lng.toFixed(4)}
{lastCoords.lng.toFixed(6)}
</Text> </Text>
)} )}
{status && (
<View
style={[
styles.fullscreenStatusBadge,
{
backgroundColor:
STATUS_COLORS[status.status] + "30",
},
]}
>
<View
style={[
styles.dot,
{
backgroundColor:
STATUS_COLORS[status.status],
},
]}
/>
<Text
style={[
styles.fullscreenStatusText,
{ color: STATUS_COLORS[status.status] },
]}
>
{STATUS_LABELS[status.status]}
</Text>
</View> </View>
{/* Bouton recalculer */}
{pendingRouteAddress.current && lastCoords && (
<TouchableOpacity
style={styles.calcRouteBtn}
onPress={() => calcRoute(pendingRouteAddress.current!, fullscreenMapRef)}
activeOpacity={0.7}
>
<Ionicons name="navigate" size={16} color={colors.white} />
<Text style={styles.calcRouteBtnText}>
Recalculer l'itinéraire
</Text>
</TouchableOpacity>
)} )}
</View> </View>
</View> </View>