chore: fix gps
This commit is contained in:
@@ -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.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"devices": []
|
|
||||||
}
|
|
||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,5 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.expo
|
.expo/
|
||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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') {
|
||||||
clearRoute();
|
if (!styleLoaded) {
|
||||||
if (data.destination) {
|
pendingRouteData = data;
|
||||||
var el = document.createElement('div');
|
} else {
|
||||||
el.className = 'dest-marker';
|
drawRoute(data.coordinates, data.destination);
|
||||||
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 === 'clearRoute') {
|
||||||
|
pendingRouteData = null;
|
||||||
|
clearRoute();
|
||||||
|
}
|
||||||
|
|
||||||
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}
|
initialCenter={
|
||||||
route={tomtomRoute}
|
lastCoords
|
||||||
destination={tomtomDestination}
|
? { latitude: lastCoords.lat, longitude: lastCoords.lng }
|
||||||
initialCenter={{
|
: { latitude: 48.8566, longitude: 2.3522 }
|
||||||
latitude: lastCoords.lat,
|
}
|
||||||
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,126 +649,55 @@ export default function DashboardScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
) : (
|
||||||
{/* Bouton plein écran */}
|
<View style={styles.gpsOverlay}>
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.expandBtn}
|
|
||||||
onPress={() => setMapFullscreen(true)}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="expand-outline"
|
name="location-outline"
|
||||||
size={20}
|
size={16}
|
||||||
color={colors.white}
|
color={colors.textMuted}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
<Text style={styles.gpsOverlayText}>
|
||||||
|
Récupération GPS...
|
||||||
{/* 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>
|
</Text>
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => setShowInstructions(false)}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name="close"
|
|
||||||
size={20}
|
|
||||||
color={colors.textMuted}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
<ScrollView style={{ maxHeight: 250 }} nestedScrollEnabled>
|
)}
|
||||||
{instructions.map((inst, idx) => {
|
|
||||||
const icoName =
|
{/* Overlay infos trajet */}
|
||||||
maneuverIcons[inst.maneuver] ||
|
{routeInfo && !routeLoading && (
|
||||||
maneuverIcons.DEFAULT;
|
<View style={styles.routeInfoOverlay}>
|
||||||
const isCurrent = idx === currentInstructionIdx;
|
<View style={styles.routeInfoChip}>
|
||||||
const isPassed = idx < currentInstructionIdx;
|
<Ionicons name="time-outline" size={13} color={colors.white} />
|
||||||
return (
|
<Text style={styles.routeInfoOverlayText}>{routeInfo.duration}</Text>
|
||||||
<View
|
</View>
|
||||||
key={idx}
|
<View style={styles.routeInfoChip}>
|
||||||
style={[
|
<Ionicons name="navigate-outline" size={13} color={colors.white} />
|
||||||
styles.stepRow,
|
<Text style={styles.routeInfoOverlayText}>{routeInfo.distance}</Text>
|
||||||
isCurrent && styles.stepRowActive,
|
</View>
|
||||||
isPassed && styles.stepRowPassed,
|
</View>
|
||||||
]}
|
)}
|
||||||
>
|
|
||||||
<View
|
{/* Bouton plein écran */}
|
||||||
style={[
|
<TouchableOpacity
|
||||||
styles.stepIcon,
|
style={styles.expandBtn}
|
||||||
isCurrent && {
|
onPress={() => setMapFullscreen(true)}
|
||||||
backgroundColor:
|
activeOpacity={0.7}
|
||||||
colors.accent + "30",
|
>
|
||||||
},
|
<Ionicons
|
||||||
]}
|
name="expand-outline"
|
||||||
>
|
size={20}
|
||||||
<Ionicons
|
color={colors.white}
|
||||||
name={icoName as any}
|
/>
|
||||||
size={16}
|
</TouchableOpacity>
|
||||||
color={
|
|
||||||
isCurrent
|
{/* Indicateur calcul de route */}
|
||||||
? colors.accent
|
{routeLoading && (
|
||||||
: isPassed
|
<View style={styles.routeLoadingOverlay}>
|
||||||
? colors.textMuted
|
<Text style={styles.routeLoadingText}>
|
||||||
: colors.textSecondary
|
Calcul de l'itinéraire...
|
||||||
}
|
</Text>
|
||||||
/>
|
</View>
|
||||||
</View>
|
)}
|
||||||
<View style={{ flex: 1 }}>
|
</View>
|
||||||
<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}
|
initialCenter={
|
||||||
route={tomtomRoute}
|
lastCoords
|
||||||
destination={tomtomDestination}
|
? { latitude: lastCoords.lat, longitude: lastCoords.lng }
|
||||||
initialCenter={{
|
: { latitude: 48.8566, longitude: 2.3522 }
|
||||||
latitude: lastCoords.lat,
|
}
|
||||||
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 */}
|
|
||||||
{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 */}
|
{/* Bottom info */}
|
||||||
<View style={styles.fullscreenBottomBar}>
|
<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={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, { marginLeft: 8 }]}>
|
||||||
<Text style={styles.fullscreenCoords}>
|
{lastCoords.lat.toFixed(4)}, {lastCoords.lng.toFixed(4)}
|
||||||
{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]}
|
|
||||||
</Text>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user