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
+1 -1
View File
@@ -84,7 +84,7 @@ export async function geocodeAddress(
address: string,
): Promise<LatLng | null> {
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);
if (data.results && data.results.length > 0) {
const pos = data.results[0].position;
+154 -87
View File
@@ -7,6 +7,7 @@ import React, {
} from "react";
import { StyleSheet } from "react-native";
import { WebView } from "react-native-webview";
import { geocodeAddress, calculateRoute } from "../api/tomtom";
const TOMTOM_API_KEY = "MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB";
@@ -20,29 +21,25 @@ export interface TomTomMarker {
isSelected?: boolean;
}
export interface TomTomRoute {
coordinates: { latitude: number; longitude: number }[];
color?: string;
}
export interface TomTomDestination {
latitude: number;
longitude: number;
color?: string;
}
export interface TomTomMapRef {
fitAllMarkers: () => void;
fitToCoordinates: (
coords: { latitude: number; longitude: number }[],
) => 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 {
style?: any;
markers?: TomTomMarker[];
route?: TomTomRoute | null;
destination?: TomTomDestination | null;
initialCenter?: { latitude: number; longitude: number };
initialZoom?: number;
onMarkerPress?: (markerId: string) => void;
@@ -53,10 +50,8 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
{
style,
markers = [],
route,
destination,
initialCenter,
initialZoom = 12,
initialZoom = 14,
onMarkerPress,
},
ref,
@@ -69,7 +64,7 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
const json = JSON.stringify(msg);
if (isReady.current) {
webViewRef.current?.injectJavaScript(
`handleMessage(${json}); true;`,
`(function(){ try { handleMessage(${json}); } catch(e) {} })(); true;`,
);
} else {
pendingMessages.current.push(json);
@@ -82,6 +77,53 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
fitAllMarkers: () => sendMessage({ type: "fitAll" }),
fitToCoordinates: (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],
);
@@ -90,30 +132,25 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
sendMessage({ type: "updateMarkers", markers });
}, [markers, sendMessage]);
useEffect(() => {
sendMessage({
type: "updateRoute",
route: route || null,
destination: destination || null,
});
}, [route, destination, sendMessage]);
const onMessage = useCallback(
(event: any) => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (data.type === "ready") {
isReady.current = true;
// flush pending
for (const msg of pendingMessages.current) {
// Rejouer les messages en attente
const pending = pendingMessages.current.slice();
pendingMessages.current = [];
for (const msg of pending) {
webViewRef.current?.injectJavaScript(
`handleMessage(${msg}); true;`,
`(function(){ try { handleMessage(${msg}); } catch(e) {} })(); true;`,
);
}
pendingMessages.current = [];
// Auto fit markers after ready + flush
// Fit sur tous les markers après un court délai
setTimeout(() => {
sendMessage({ type: "fitAll" });
webViewRef.current?.injectJavaScript(
`(function(){ try { handleMessage(${JSON.stringify({ type: "fitAll" })}); } catch(e) {} })(); true;`,
);
}, 500);
} else if (data.type === "markerPress" && onMarkerPress) {
onMarkerPress(data.id);
@@ -134,7 +171,7 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
}
: { latitude: 48.8566, longitude: 2.3522 });
const html = buildHtml(center, initialZoom);
const html = buildHtml(center, initialZoom, TOMTOM_API_KEY);
return (
<WebView
@@ -147,6 +184,7 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
scrollEnabled={false}
bounces={false}
originWhitelist={["*"]}
mixedContentMode="always"
/>
);
},
@@ -155,6 +193,7 @@ const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
function buildHtml(
center: { latitude: number; longitude: number },
zoom: number,
apiKey: string,
) {
return `<!DOCTYPE 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">
<script src="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/6.25.0/maps/maps-web.min.js"></script>
<style>
* { margin: 0; padding: 0; }
html, body, #map { width: 100%; height: 100%; }
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body, #map { width: 100%; height: 100%; overflow: hidden; }
.marker-dot {
width: 28px; height: 28px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
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);
}
.marker-dot.selected {
@@ -178,11 +217,8 @@ function buildHtml(
box-shadow: 0 0 12px rgba(124,58,237,0.6);
}
.dest-marker {
width: 24px; height: 24px; border-radius: 50%;
width: 32px; height: 32px;
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>
</head>
@@ -190,39 +226,82 @@ function buildHtml(
<div id="map"></div>
<script>
var map = tt.map({
key: '${TOMTOM_API_KEY}',
key: '${apiKey}',
container: 'map',
center: [${center.longitude}, ${center.latitude}],
zoom: ${zoom},
stylesVisibility: { trafficFlow: false, trafficIncidents: false }
});
var markers = {};
var routeLayer = null;
var markerObjects = {};
var destMarker = null;
var ROUTE_SOURCE = 'tt-route-src';
var ROUTE_LAYER = 'tt-route-lyr';
function clearMarkers() {
Object.values(markers).forEach(function(m) { m.remove(); });
markers = {};
}
// File d'attente pour les messages reçus avant que le style soit chargé
var pendingRouteData = null;
var styleLoaded = false;
function clearRoute() {
if (routeLayer && map.getSource('route')) {
map.removeLayer('route-line');
map.removeSource('route');
routeLayer = null;
}
try { if (map.getLayer(ROUTE_LAYER)) map.removeLayer(ROUTE_LAYER); } catch(e) {}
try { if (map.getSource(ROUTE_SOURCE)) map.removeSource(ROUTE_SOURCE); } catch(e) {}
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) {
if (data.type === 'updateMarkers') {
clearMarkers();
Object.values(markerObjects).forEach(function(m) { m.remove(); });
markerObjects = {};
(data.markers || []).forEach(function(m) {
var el = document.createElement('div');
el.className = 'marker-dot' + (m.isSelected ? ' selected' : '');
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() {
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'markerPress', id: m.id }));
});
@@ -232,47 +311,30 @@ function handleMessage(data) {
.setLngLat([m.longitude, m.latitude])
.setPopup(popup)
.addTo(map);
markers[m.id] = marker;
markerObjects[m.id] = marker;
});
}
if (data.type === 'updateRoute') {
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 === 'drawRoute') {
if (!styleLoaded) {
pendingRouteData = data;
} else {
drawRoute(data.coordinates, data.destination);
}
}
if (data.type === 'clearRoute') {
pendingRouteData = null;
clearRoute();
}
if (data.type === 'fitAll') {
var allM = Object.values(markers);
if (allM.length > 0) {
var all = Object.values(markerObjects);
if (all.length > 0) {
var bounds = new tt.LngLatBounds();
allM.forEach(function(m) { bounds.extend(m.getLngLat()); });
map.fitBounds(bounds, { padding: 60, maxZoom: 15 });
all.forEach(function(m) { bounds.extend(m.getLngLat()); });
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) {
var bounds = new tt.LngLatBounds();
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() {
styleLoaded = true;
if (pendingRouteData) {
drawRoute(pendingRouteData.coordinates, pendingRouteData.destination);
pendingRouteData = null;
}
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ready' }));
});
</script>
@@ -23,7 +23,7 @@ import {
getCommandByID,
} from "../../api/api_admin";
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 { STATUS_LABELS, getStatusColors } from "../../utils/constants";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
@@ -32,8 +32,6 @@ import Badge from "../../components/ui/Badge";
import TomTomMap, {
TomTomMapRef,
TomTomMarker,
TomTomRoute,
TomTomDestination,
} from "../../components/TomTomMap";
const MAP_HEIGHT = 280;
@@ -60,9 +58,6 @@ export default function DeliveryScreen() {
const [selectedLivreur, setSelectedLivreur] =
useState<DeliveryPerson | null>(null);
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
const [destinationCoords, setDestinationCoords] = useState<LatLng | null>(
null,
);
const [routeLoading, setRouteLoading] = useState(false);
const loadData = useCallback(async () => {
@@ -111,30 +106,13 @@ export default function DeliveryScreen() {
}));
}, [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(
async (livreur: DeliveryPerson) => {
setSelectedLivreur(livreur);
setRouteInfo(null);
setDestinationCoords(null);
if (!livreur.stats.current_command) return;
@@ -154,24 +132,16 @@ export default function DeliveryScreen() {
setRouteLoading(false);
return;
}
setDestinationCoords(dest);
const origin: LatLng = {
const origin = {
latitude: livreur.location.latitude,
longitude: livreur.location.longitude,
};
const result = await calculateRoute(origin, dest);
if (result) {
setRouteInfo(result.route);
// Fit la map sur les deux points
const ref = mapFullscreen ? fullscreenMapRef : mapRef;
ref.current?.fitToCoordinates([
{
latitude: origin.latitude,
longitude: origin.longitude,
},
{ latitude: dest.latitude, longitude: dest.longitude },
]);
const activeRef = mapFullscreen ? fullscreenMapRef : mapRef;
activeRef.current?.calcRoute(origin, dest);
}
} catch {
/* silent */
@@ -184,7 +154,6 @@ export default function DeliveryScreen() {
const clearRoute = () => {
setSelectedLivreur(null);
setRouteInfo(null);
setDestinationCoords(null);
};
// --------------------------------------------------
@@ -599,8 +568,6 @@ export default function DeliveryScreen() {
ref={mapRef}
style={styles.map}
markers={tomtomMarkers}
route={tomtomRoute}
destination={tomtomDestination}
initialCenter={{
latitude: livreursWithGPS[0].location.latitude,
longitude: livreursWithGPS[0].location.longitude,
@@ -708,8 +675,6 @@ export default function DeliveryScreen() {
ref={fullscreenMapRef}
style={styles.fullscreenMap}
markers={tomtomMarkers}
route={tomtomRoute}
destination={tomtomDestination}
initialCenter={{
latitude: livreursWithGPS[0].location.latitude,
longitude:
@@ -4,6 +4,7 @@ import React, {
useCallback,
useRef,
useMemo,
RefObject,
} from "react";
import {
View,
@@ -33,17 +34,8 @@ import {
updateDeliveryStatus,
updateMyLocation,
} from "../../api/api_delivery";
import {
geocodeAddress,
calculateRoute,
maneuverIcons,
maneuverTranslations,
} from "../../api/tomtom";
import type {
RouteInfo,
NavigationInstruction,
LatLng,
} from "../../api/tomtom";
import { geocodeAddress, calculateRoute } from "../../api/tomtom";
import type { RouteInfo } from "../../api/tomtom";
import type { DeliveryStatus, DeliveryItem, QueueInfo } from "../../api/types";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Card from "../../components/ui/Card";
@@ -54,10 +46,10 @@ import { useAlert } from "../../hooks/useAlert";
import TomTomMap, {
TomTomMapRef,
TomTomMarker,
TomTomRoute,
TomTomDestination,
} from "../../components/TomTomMap";
const { width: SCREEN_WIDTH } = Dimensions.get("window");
const MAP_HEIGHT = 260;
const LOCATION_INTERVAL_MS = 15000;
@@ -91,6 +83,7 @@ export default function DashboardScreen() {
lat: number;
lng: number;
} | null>(null);
const lastCoordsRef = useRef<{ lat: number; lng: number } | null>(null);
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
const locationInterval = useRef<ReturnType<typeof setInterval> | null>(
null,
@@ -102,17 +95,9 @@ export default function DashboardScreen() {
const fullscreenMapRef = useRef<TomTomMapRef>(null);
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 [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
const pendingRouteAddress = useRef<string | null>(null);
const { alert, showError, showSuccess, hideAlert } = useAlert();
const STATUS_COLORS: Record<string, string> = useMemo(
@@ -124,10 +109,6 @@ export default function DashboardScreen() {
[colors],
);
// --------------------------------------------------
// Construire les props pour TomTomMap
// --------------------------------------------------
// Marker du livreur (position courante)
const driverMarkers: TomTomMarker[] = useMemo(() => {
if (!lastCoords) return [];
@@ -148,21 +129,42 @@ export default function DashboardScreen() {
];
}, [lastCoords, lastUpdate, colors.success]);
// Route TomTom
const tomtomRoute: TomTomRoute | null = useMemo(() => {
if (!routeInfo || routeInfo.coordinates.length === 0) return null;
return { coordinates: routeInfo.coordinates, color: "#4285F4" };
}, [routeInfo]);
// --------------------------------------------------
// TomTom Route calculation
// Geocode + calcul dans DashboardScreen pour récupérer 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
const tomtomDestination: TomTomDestination | null = useMemo(() => {
if (!destinationCoords) return null;
return {
latitude: destinationCoords.latitude,
longitude: destinationCoords.longitude,
color: colors.danger,
};
}, [destinationCoords, colors.danger]);
setRouteLoading(true);
try {
const origin = { latitude: coords.lat, longitude: coords.lng };
const dest = await geocodeAddress(address);
if (!dest) {
setRouteLoading(false);
return;
}
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
@@ -245,64 +247,45 @@ export default function DashboardScreen() {
(d) =>
d.status === "in_progress" || d.status === "en_route",
) || enriched.find((d) => d.status === "assigned");
if (activeDelivery && activeDelivery.adresse && lastCoords) {
if (activeDelivery && activeDelivery.adresse) {
calcRoute(activeDelivery.adresse);
}
} catch {
/* ignore */
}
setLoading(false);
}, [lastCoords]);
}, [calcRoute]);
useEffect(() => {
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 () => {
setRefreshing(true);
await loadData();
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
// --------------------------------------------------
@@ -327,18 +310,26 @@ export default function DashboardScreen() {
!("coords" in loc) ||
!loc.coords
) {
console.log("GPS invalide — update ignoré");
return;
}
const coords = (loc as Location.LocationObject).coords;
const { latitude, longitude } = coords;
const wasNull = !lastCoordsRef.current;
lastCoordsRef.current = { lat: latitude, lng: longitude };
setLastCoords({ lat: latitude, lng: longitude });
setLastUpdate(new Date());
await updateMyLocation(latitude, longitude);
} catch (err) {
console.log("GPS error:", err);
// Si c'est la première position GPS et qu'une adresse était en attente, rejouer
if (wasNull && pendingRouteAddress.current) {
const addr = pendingRouteAddress.current;
setTimeout(() => {
calcRoute(addr);
}, 1000);
}
} catch {
/* silent */
} finally {
sendingRef.current = false;
}
@@ -435,9 +426,6 @@ export default function DashboardScreen() {
const res = await updateDeliveryStatus(deliveryId, "livre", lat, lng);
if (res.success) {
showSuccess("Succès", "Livraison terminée");
setRouteInfo(null);
setDestinationCoords(null);
setInstructions([]);
loadData();
} else {
showError("Erreur", res.error || "Erreur");
@@ -450,98 +438,6 @@ export default function DashboardScreen() {
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
// --------------------------------------------------
@@ -713,23 +609,22 @@ export default function DashboardScreen() {
// --------------------------------------------------
const renderHeader = () => (
<View>
{/* Carte TomTom */}
{lastCoords ? (
<View style={styles.mapContainer}>
<TomTomMap
ref={mapRef}
style={styles.map}
markers={driverMarkers}
route={tomtomRoute}
destination={tomtomDestination}
initialCenter={{
latitude: lastCoords.lat,
longitude: lastCoords.lng,
}}
initialZoom={14}
/>
{/* Carte TomTom — toujours montée pour que le ref soit disponible */}
<View style={styles.mapContainer}>
<TomTomMap
ref={mapRef}
style={styles.map}
markers={driverMarkers}
initialCenter={
lastCoords
? { latitude: lastCoords.lat, longitude: lastCoords.lng }
: { latitude: 48.8566, longitude: 2.3522 }
}
initialZoom={14}
/>
{/* GPS overlay */}
{/* Overlay GPS */}
{lastCoords ? (
<View style={styles.gpsOverlay}>
<View
style={[
@@ -754,126 +649,55 @@ export default function DashboardScreen() {
</Text>
)}
</View>
{/* Bouton plein écran */}
<TouchableOpacity
style={styles.expandBtn}
onPress={() => setMapFullscreen(true)}
activeOpacity={0.7}
>
) : (
<View style={styles.gpsOverlay}>
<Ionicons
name="expand-outline"
size={20}
color={colors.white}
name="location-outline"
size={16}
color={colors.textMuted}
/>
</TouchableOpacity>
{/* Indicateur calcul de route */}
{routeLoading && (
<View style={styles.routeLoadingOverlay}>
<Text style={styles.routeLoadingText}>
Calcul de l'itinéraire...
</Text>
</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 style={styles.gpsOverlayText}>
Récupération GPS...
</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>
)}
)}
{/* 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 */}
<TouchableOpacity
style={styles.expandBtn}
onPress={() => setMapFullscreen(true)}
activeOpacity={0.7}
>
<Ionicons
name="expand-outline"
size={20}
color={colors.white}
/>
</TouchableOpacity>
{/* Indicateur calcul de route */}
{routeLoading && (
<View style={styles.routeLoadingOverlay}>
<Text style={styles.routeLoadingText}>
Calcul de l'itinéraire...
</Text>
</View>
)}
</View>
{/* Queue info */}
{queue && queue.queue_size > 0 && (
@@ -1004,6 +828,28 @@ export default function DashboardScreen() {
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
instructionBar: {
backgroundColor: colors.bgSecondary,
@@ -1069,20 +915,6 @@ export default function DashboardScreen() {
borderTopWidth: 1,
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: {
flexDirection: "row",
alignItems: "center",
@@ -1410,20 +1242,17 @@ export default function DashboardScreen() {
>
<StatusBar hidden={mapFullscreen} />
<View style={styles.fullscreenContainer}>
{lastCoords && (
<TomTomMap
ref={fullscreenMapRef}
style={styles.fullscreenMap}
markers={driverMarkers}
route={tomtomRoute}
destination={tomtomDestination}
initialCenter={{
latitude: lastCoords.lat,
longitude: lastCoords.lng,
}}
initialZoom={15}
/>
)}
<TomTomMap
ref={fullscreenMapRef}
style={styles.fullscreenMap}
markers={driverMarkers}
initialCenter={
lastCoords
? { latitude: lastCoords.lat, longitude: lastCoords.lng }
: { latitude: 48.8566, longitude: 2.3522 }
}
initialZoom={15}
/>
{/* Top bar */}
<View style={styles.fullscreenTopBar}>
@@ -1438,54 +1267,45 @@ export default function DashboardScreen() {
/>
</TouchableOpacity>
<Text style={styles.fullscreenTitle}>
{routeInfo
? `${routeInfo.distance} · ${routeInfo.duration}`
: "GPS en direct"}
{pendingRouteAddress.current ? "Itinéraire en cours" : "GPS en direct"}
</Text>
<View style={{ width: 40 }} />
</View>
{/* Instruction bar en plein écran */}
{instructions.length > 0 && (
<View style={styles.fullscreenInstructionBar}>
<View style={styles.instructionIconBox}>
<Ionicons
name={
(maneuverIcons[
instructions[currentInstructionIdx]
?.maneuver
] || maneuverIcons.DEFAULT) as any
}
size={22}
color={colors.white}
/>
</View>
<View style={{ flex: 1 }}>
<Text
style={styles.instructionText}
numberOfLines={2}
>
{instructions[currentInstructionIdx]
?.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>
</View>
)}
{/* Bottom info */}
<View style={styles.fullscreenBottomBar}>
{/* Adresse de destination */}
{pendingRouteAddress.current && (
<View style={styles.fullscreenInfoRow}>
<Ionicons
name="location"
size={16}
color={colors.danger}
/>
<Text
style={[styles.fullscreenInfoText, { flex: 1 }]}
numberOfLines={2}
>
{pendingRouteAddress.current}
</Text>
</View>
)}
{/* Infos trajet */}
{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={[
@@ -1500,41 +1320,25 @@ export default function DashboardScreen() {
<Text style={styles.fullscreenInfoText}>
GPS {locationEnabled ? "actif" : "inactif"}
</Text>
</View>
{lastCoords && (
<Text style={styles.fullscreenCoords}>
{lastCoords.lat.toFixed(6)},{" "}
{lastCoords.lng.toFixed(6)}
</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]}
{lastCoords && (
<Text style={[styles.fullscreenCoords, { marginLeft: 8 }]}>
{lastCoords.lat.toFixed(4)}, {lastCoords.lng.toFixed(4)}
</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>