chore: add gps with tomtom API
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# TomTom API Key
|
||||
# Obtenez votre cle API gratuite sur https://developer.tomtom.com/
|
||||
VITE_TOMTOM_API_KEY=MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB
|
||||
@@ -0,0 +1,513 @@
|
||||
.tomtom-map-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tomtom-map-container.navigating {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.tomtom-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Marqueur du livreur */
|
||||
.driver-marker {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.driver-marker-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
border-radius: 50%;
|
||||
border: 3px solid white;
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||
color: white;
|
||||
animation: pulse-driver 2s infinite;
|
||||
}
|
||||
|
||||
.driver-marker.navigation-mode .driver-marker-inner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.driver-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 12px solid transparent;
|
||||
border-right: 12px solid transparent;
|
||||
border-bottom: 24px solid white;
|
||||
}
|
||||
|
||||
@keyframes pulse-driver {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 15px rgba(16, 185, 129, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Marqueur de destination */
|
||||
.destination-marker {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.destination-marker-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
border-radius: 50% 50% 50% 0;
|
||||
border: 3px solid white;
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||
color: white;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.destination-marker-inner svg {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* Panneau de navigation */
|
||||
.navigation-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(26, 26, 46, 0.98) 0%,
|
||||
rgba(26, 26, 46, 0.95) 100%
|
||||
);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 2px solid #10b981;
|
||||
}
|
||||
|
||||
.current-instruction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem 1.25rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.instruction-maneuver {
|
||||
font-size: 2.5rem;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
.instruction-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.instruction-text {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.instruction-street {
|
||||
font-size: 0.9rem;
|
||||
color: #10b981;
|
||||
margin-top: 0.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.instruction-distance {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #10b981;
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.next-instruction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 0.85rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.next-label {
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.next-maneuver {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.next-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Bouton recentrer */
|
||||
.recenter-btn {
|
||||
position: absolute;
|
||||
bottom: 80px;
|
||||
right: 16px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 5;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.recenter-btn:hover {
|
||||
background: #10b981;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Chargement */
|
||||
.tomtom-map-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
padding: 1.5rem 2rem;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: #10b981;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Informations de route */
|
||||
.tomtom-route-info {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.route-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.route-info-label {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.route-info-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.route-info-divider {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Bouton afficher instructions */
|
||||
.show-instructions-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.show-instructions-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
/* Liste des instructions */
|
||||
.instructions-list {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 70px;
|
||||
background: rgba(26, 26, 46, 0.98);
|
||||
z-index: 15;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.instructions-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.instructions-header h4 {
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.close-instructions-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-instructions-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.instructions-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.instruction-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.instruction-item.active {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
border-left: 3px solid #10b981;
|
||||
}
|
||||
|
||||
.instruction-item.passed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.item-maneuver {
|
||||
font-size: 1.5rem;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-text {
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item-street {
|
||||
color: #10b981;
|
||||
font-size: 0.8rem;
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.item-distance {
|
||||
color: #9ca3af;
|
||||
font-size: 0.85rem;
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Erreur */
|
||||
.tomtom-map-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tomtom-map-error .error-icon {
|
||||
color: #f59e0b;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tomtom-map-error h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.tomtom-map-error p {
|
||||
color: #9ca3af;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tomtom-map-error ol {
|
||||
text-align: left;
|
||||
color: #9ca3af;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.tomtom-map-error a {
|
||||
color: #3b82f6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tomtom-map-error code {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
/* Toast d'erreur */
|
||||
.tomtom-map-error-toast {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(239, 68, 68, 0.95);
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
z-index: 20;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.tomtom-map-container {
|
||||
height: 350px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.tomtom-map-container.navigating {
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
.current-instruction {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.instruction-maneuver {
|
||||
font-size: 2rem;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.instruction-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.instruction-distance {
|
||||
font-size: 1.25rem;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.tomtom-route-info {
|
||||
flex-wrap: wrap;
|
||||
padding: 0.75rem 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.route-info-value {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.route-info-divider {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.recenter-btn {
|
||||
bottom: 75px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,827 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import tt from "@tomtom-international/web-sdk-maps";
|
||||
import "@tomtom-international/web-sdk-maps/dist/maps.css";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowUp,
|
||||
faArrowTurnUp,
|
||||
faRotateLeft,
|
||||
faRotateRight,
|
||||
faCircleNotch,
|
||||
faRoad,
|
||||
faSignOutAlt,
|
||||
faFlagCheckered,
|
||||
faCar,
|
||||
faLocationDot,
|
||||
faTimes,
|
||||
faRoute,
|
||||
faLocationCrosshairs,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import "./TomTomMap.css";
|
||||
|
||||
const TOMTOM_API_KEY = import.meta.env.VITE_TOMTOM_API_KEY;
|
||||
|
||||
interface TomTomMapProps {
|
||||
driverLocation: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
} | null;
|
||||
destinationAddress: string | null;
|
||||
onRouteCalculated?: (distance: string, duration: string) => void;
|
||||
onError?: (error: string) => void;
|
||||
isNavigating?: boolean;
|
||||
}
|
||||
|
||||
interface RouteInfo {
|
||||
distance: string;
|
||||
duration: string;
|
||||
distanceRemaining: number;
|
||||
timeRemaining: number;
|
||||
}
|
||||
|
||||
interface NavigationInstruction {
|
||||
instruction: string;
|
||||
distance: string;
|
||||
maneuverIcon: any;
|
||||
streetName?: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
// Mapping des maneuvres vers des icones Font Awesome
|
||||
const maneuverIcons: Record<string, any> = {
|
||||
TURN_LEFT: faArrowLeft,
|
||||
TURN_RIGHT: faArrowRight,
|
||||
TURN_SLIGHT_LEFT: faArrowTurnUp,
|
||||
TURN_SLIGHT_RIGHT: faArrowTurnUp,
|
||||
TURN_SHARP_LEFT: faRotateLeft,
|
||||
TURN_SHARP_RIGHT: faRotateRight,
|
||||
KEEP_LEFT: faArrowLeft,
|
||||
KEEP_RIGHT: faArrowRight,
|
||||
STRAIGHT: faArrowUp,
|
||||
ENTER_ROUNDABOUT: faCircleNotch,
|
||||
EXIT_ROUNDABOUT: faArrowRight,
|
||||
MOTORWAY_ENTER: faRoad,
|
||||
MOTORWAY_EXIT: faSignOutAlt,
|
||||
ARRIVE: faFlagCheckered,
|
||||
ARRIVE_LEFT: faFlagCheckered,
|
||||
ARRIVE_RIGHT: faFlagCheckered,
|
||||
DEPART: faCar,
|
||||
U_TURN: faRotateLeft,
|
||||
FOLLOW: faArrowUp,
|
||||
WAYPOINT_REACHED: faLocationDot,
|
||||
DEFAULT: faArrowUp,
|
||||
};
|
||||
|
||||
// Mapping des maneuvres vers des textes en francais
|
||||
const maneuverTranslations: Record<string, string> = {
|
||||
TURN_LEFT: "Tournez a gauche",
|
||||
TURN_RIGHT: "Tournez a droite",
|
||||
TURN_SLIGHT_LEFT: "Tournez legerement a gauche",
|
||||
TURN_SLIGHT_RIGHT: "Tournez legerement a droite",
|
||||
TURN_SHARP_LEFT: "Tournez fortement a gauche",
|
||||
TURN_SHARP_RIGHT: "Tournez fortement a droite",
|
||||
KEEP_LEFT: "Restez a gauche",
|
||||
KEEP_RIGHT: "Restez a droite",
|
||||
STRAIGHT: "Continuez tout droit",
|
||||
ENTER_ROUNDABOUT: "Entrez dans le rond-point",
|
||||
EXIT_ROUNDABOUT: "Sortez du rond-point",
|
||||
MOTORWAY_ENTER: "Entrez sur l'autoroute",
|
||||
MOTORWAY_EXIT: "Sortez de l'autoroute",
|
||||
ARRIVE: "Vous etes arrive",
|
||||
ARRIVE_LEFT: "Destination a gauche",
|
||||
ARRIVE_RIGHT: "Destination a droite",
|
||||
DEPART: "Depart",
|
||||
U_TURN: "Faites demi-tour",
|
||||
FOLLOW: "Suivez",
|
||||
WAYPOINT_REACHED: "Point de passage atteint",
|
||||
};
|
||||
|
||||
function TomTomMap({
|
||||
driverLocation,
|
||||
destinationAddress,
|
||||
onRouteCalculated,
|
||||
onError,
|
||||
isNavigating = false,
|
||||
}: TomTomMapProps) {
|
||||
const mapContainer = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mapInstance = useRef<any>(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const driverMarker = useRef<any>(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const destinationMarker = useRef<any>(null);
|
||||
const routeLayerId = useRef<string>("route-layer");
|
||||
const routeCoordinates = useRef<[number, number][]>([]);
|
||||
|
||||
const [isMapReady, setIsMapReady] = useState(false);
|
||||
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
|
||||
const [destinationCoords, setDestinationCoords] = useState<{
|
||||
lat: number;
|
||||
lng: number;
|
||||
} | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [instructions, setInstructions] = useState<NavigationInstruction[]>(
|
||||
[],
|
||||
);
|
||||
const [currentInstructionIndex, setCurrentInstructionIndex] = useState(0);
|
||||
const [showAllInstructions, setShowAllInstructions] = useState(false);
|
||||
|
||||
// Calculer la distance entre deux points (formule Haversine)
|
||||
const calculateDistance = useCallback(
|
||||
(lat1: number, lon1: number, lat2: number, lon2: number): number => {
|
||||
const R = 6371e3; // Rayon de la Terre en metres
|
||||
const φ1 = (lat1 * Math.PI) / 180;
|
||||
const φ2 = (lat2 * Math.PI) / 180;
|
||||
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
|
||||
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
|
||||
|
||||
const a =
|
||||
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
||||
Math.cos(φ1) *
|
||||
Math.cos(φ2) *
|
||||
Math.sin(Δλ / 2) *
|
||||
Math.sin(Δλ / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Formater la distance
|
||||
const formatDistance = useCallback((meters: number): string => {
|
||||
if (meters < 1000) {
|
||||
return `${Math.round(meters)} m`;
|
||||
}
|
||||
return `${(meters / 1000).toFixed(1)} km`;
|
||||
}, []);
|
||||
|
||||
// Initialiser la carte
|
||||
useEffect(() => {
|
||||
if (
|
||||
!mapContainer.current ||
|
||||
!TOMTOM_API_KEY ||
|
||||
TOMTOM_API_KEY === "YOUR_TOMTOM_API_KEY_HERE"
|
||||
) {
|
||||
setError(
|
||||
"Cle API TomTom non configuree. Veuillez ajouter VITE_TOMTOM_API_KEY dans .env",
|
||||
);
|
||||
onError?.("Cle API TomTom non configuree");
|
||||
return;
|
||||
}
|
||||
|
||||
const map = tt.map({
|
||||
key: TOMTOM_API_KEY,
|
||||
container: mapContainer.current,
|
||||
center: driverLocation
|
||||
? [driverLocation.longitude, driverLocation.latitude]
|
||||
: [2.3522, 48.8566],
|
||||
zoom: 15,
|
||||
language: "fr-FR",
|
||||
});
|
||||
|
||||
map.addControl(new tt.NavigationControl());
|
||||
|
||||
map.on("load", () => {
|
||||
console.log("TomTom Map chargee");
|
||||
setIsMapReady(true);
|
||||
});
|
||||
|
||||
mapInstance.current = map;
|
||||
|
||||
return () => {
|
||||
if (mapInstance.current) {
|
||||
mapInstance.current.remove();
|
||||
mapInstance.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Creer le marqueur du livreur (style navigation)
|
||||
const createDriverMarkerElement = useCallback(() => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "driver-marker navigation-mode";
|
||||
el.innerHTML = `
|
||||
<div class="driver-marker-inner">
|
||||
<div class="driver-arrow"></div>
|
||||
</div>
|
||||
`;
|
||||
return el;
|
||||
}, []);
|
||||
|
||||
// Creer le marqueur de destination
|
||||
const createDestinationMarkerElement = useCallback(() => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "destination-marker";
|
||||
el.innerHTML = `
|
||||
<div class="destination-marker-inner">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
|
||||
<path 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-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
return el;
|
||||
}, []);
|
||||
|
||||
// Mettre a jour la position du livreur et centrer la carte en mode navigation
|
||||
useEffect(() => {
|
||||
if (!isMapReady || !mapInstance.current || !driverLocation) return;
|
||||
|
||||
const { latitude, longitude } = driverLocation;
|
||||
|
||||
if (driverMarker.current) {
|
||||
driverMarker.current.setLngLat([longitude, latitude]);
|
||||
} else {
|
||||
driverMarker.current = new tt.Marker({
|
||||
element: createDriverMarkerElement(),
|
||||
})
|
||||
.setLngLat([longitude, latitude])
|
||||
.addTo(mapInstance.current);
|
||||
}
|
||||
|
||||
// En mode navigation, suivre le livreur
|
||||
if (isNavigating && destinationCoords) {
|
||||
mapInstance.current.easeTo({
|
||||
center: [longitude, latitude],
|
||||
zoom: 17,
|
||||
pitch: 60,
|
||||
bearing: calculateBearing(
|
||||
latitude,
|
||||
longitude,
|
||||
destinationCoords.lat,
|
||||
destinationCoords.lng,
|
||||
),
|
||||
});
|
||||
|
||||
// Mettre a jour l'instruction active en fonction de la position
|
||||
updateCurrentInstruction(latitude, longitude);
|
||||
} else if (!destinationCoords) {
|
||||
mapInstance.current.flyTo({
|
||||
center: [longitude, latitude],
|
||||
zoom: 15,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
isMapReady,
|
||||
driverLocation,
|
||||
isNavigating,
|
||||
destinationCoords,
|
||||
createDriverMarkerElement,
|
||||
]);
|
||||
|
||||
// Calculer l'angle de direction (bearing)
|
||||
const calculateBearing = (
|
||||
lat1: number,
|
||||
lon1: number,
|
||||
lat2: number,
|
||||
lon2: number,
|
||||
): number => {
|
||||
const φ1 = (lat1 * Math.PI) / 180;
|
||||
const φ2 = (lat2 * Math.PI) / 180;
|
||||
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
|
||||
|
||||
const y = Math.sin(Δλ) * Math.cos(φ2);
|
||||
const x =
|
||||
Math.cos(φ1) * Math.sin(φ2) -
|
||||
Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
|
||||
const θ = Math.atan2(y, x);
|
||||
|
||||
return ((θ * 180) / Math.PI + 360) % 360;
|
||||
};
|
||||
|
||||
// Mettre a jour l'instruction courante basee sur la position
|
||||
const updateCurrentInstruction = useCallback(
|
||||
(lat: number, lng: number) => {
|
||||
if (instructions.length === 0) return;
|
||||
|
||||
// Trouver l'instruction la plus proche
|
||||
let minDistance = Infinity;
|
||||
let closestIndex = currentInstructionIndex;
|
||||
|
||||
// Chercher parmi les instructions restantes
|
||||
for (
|
||||
let i = currentInstructionIndex;
|
||||
i < instructions.length;
|
||||
i++
|
||||
) {
|
||||
// Utiliser les coordonnees de la route pour trouver le point le plus proche
|
||||
if (routeCoordinates.current.length > 0) {
|
||||
const segmentStart = Math.floor(
|
||||
(i / instructions.length) *
|
||||
routeCoordinates.current.length,
|
||||
);
|
||||
if (segmentStart < routeCoordinates.current.length) {
|
||||
const [pointLng, pointLat] =
|
||||
routeCoordinates.current[segmentStart];
|
||||
const dist = calculateDistance(
|
||||
lat,
|
||||
lng,
|
||||
pointLat,
|
||||
pointLng,
|
||||
);
|
||||
if (dist < minDistance) {
|
||||
minDistance = dist;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si on est a moins de 50m d'une instruction, passer a la suivante
|
||||
if (minDistance < 50 && closestIndex > currentInstructionIndex) {
|
||||
setCurrentInstructionIndex(closestIndex);
|
||||
// Mettre a jour les instructions pour marquer l'active
|
||||
setInstructions((prev) =>
|
||||
prev.map((inst, idx) => ({
|
||||
...inst,
|
||||
isActive: idx === closestIndex,
|
||||
})),
|
||||
);
|
||||
}
|
||||
},
|
||||
[instructions, currentInstructionIndex, calculateDistance],
|
||||
);
|
||||
|
||||
// Geocoder l'adresse de destination
|
||||
useEffect(() => {
|
||||
if (
|
||||
!destinationAddress ||
|
||||
!TOMTOM_API_KEY ||
|
||||
TOMTOM_API_KEY === "YOUR_TOMTOM_API_KEY_HERE"
|
||||
)
|
||||
return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Utiliser l'API REST directement pour le geocodage
|
||||
const searchUrl = `https://api.tomtom.com/search/2/geocode/${encodeURIComponent(destinationAddress)}.json?key=${TOMTOM_API_KEY}&countrySet=FR&limit=1`;
|
||||
|
||||
fetch(searchUrl)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.results && data.results.length > 0) {
|
||||
const result = data.results[0];
|
||||
if (
|
||||
result.position?.lat !== undefined &&
|
||||
result.position?.lon !== undefined
|
||||
) {
|
||||
const coords = {
|
||||
lat: result.position.lat,
|
||||
lng: result.position.lon,
|
||||
};
|
||||
console.log("Destination geocodee:", coords);
|
||||
setDestinationCoords(coords);
|
||||
} else {
|
||||
setError("Position de destination invalide");
|
||||
onError?.("Position de destination invalide");
|
||||
}
|
||||
} else {
|
||||
setError("Adresse de destination introuvable");
|
||||
onError?.("Adresse de destination introuvable");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Erreur geocodage:", err);
|
||||
setError("Erreur lors du geocodage de l'adresse");
|
||||
onError?.("Erreur lors du geocodage de l'adresse");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [destinationAddress, onError]);
|
||||
|
||||
// Ajouter le marqueur de destination et calculer l'itineraire
|
||||
useEffect(() => {
|
||||
if (!isMapReady || !mapInstance.current || !destinationCoords) return;
|
||||
|
||||
if (destinationMarker.current) {
|
||||
destinationMarker.current.setLngLat([
|
||||
destinationCoords.lng,
|
||||
destinationCoords.lat,
|
||||
]);
|
||||
} else {
|
||||
destinationMarker.current = new tt.Marker({
|
||||
element: createDestinationMarkerElement(),
|
||||
})
|
||||
.setLngLat([destinationCoords.lng, destinationCoords.lat])
|
||||
.addTo(mapInstance.current);
|
||||
}
|
||||
|
||||
if (driverLocation) {
|
||||
calculateRoute();
|
||||
} else {
|
||||
mapInstance.current.flyTo({
|
||||
center: [destinationCoords.lng, destinationCoords.lat],
|
||||
zoom: 15,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
isMapReady,
|
||||
destinationCoords,
|
||||
driverLocation,
|
||||
createDestinationMarkerElement,
|
||||
]);
|
||||
|
||||
// Calculer l'itineraire avec instructions via API REST directe
|
||||
const calculateRoute = useCallback(async () => {
|
||||
if (
|
||||
!mapInstance.current ||
|
||||
!driverLocation ||
|
||||
!destinationCoords ||
|
||||
!TOMTOM_API_KEY ||
|
||||
TOMTOM_API_KEY === "YOUR_TOMTOM_API_KEY_HERE"
|
||||
)
|
||||
return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Utiliser l'API REST directement pour avoir les points de la route
|
||||
const startPoint = `${driverLocation.latitude},${driverLocation.longitude}`;
|
||||
const endPoint = `${destinationCoords.lat},${destinationCoords.lng}`;
|
||||
|
||||
const routeUrl = `https://api.tomtom.com/routing/1/calculateRoute/${startPoint}:${endPoint}/json?key=${TOMTOM_API_KEY}&instructionsType=text&language=fr-FR&traffic=true&travelMode=car`;
|
||||
|
||||
console.log("Appel API TomTom Routing...");
|
||||
const response = await fetch(routeUrl);
|
||||
const routeData = await response.json();
|
||||
|
||||
console.log("Route response:", routeData);
|
||||
|
||||
if (!routeData.routes || routeData.routes.length === 0) {
|
||||
setError("Aucun itineraire trouve");
|
||||
onError?.("Aucun itineraire trouve");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const route = routeData.routes[0];
|
||||
let coordinates: [number, number][] = [];
|
||||
|
||||
// Extraire les coordonnees des legs
|
||||
if (route.legs && route.legs.length > 0) {
|
||||
route.legs.forEach((leg: any) => {
|
||||
if (leg.points && leg.points.length > 0) {
|
||||
leg.points.forEach((point: any) => {
|
||||
coordinates.push([point.longitude, point.latitude]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Coordonnees extraites:", coordinates.length, "points");
|
||||
|
||||
// Fallback: ligne droite si pas de points
|
||||
if (coordinates.length === 0) {
|
||||
console.log("Fallback: utilisation ligne droite");
|
||||
coordinates = [
|
||||
[driverLocation.longitude, driverLocation.latitude],
|
||||
[destinationCoords.lng, destinationCoords.lat],
|
||||
];
|
||||
}
|
||||
|
||||
routeCoordinates.current = coordinates;
|
||||
|
||||
// Extraire les instructions de navigation
|
||||
const navInstructions: NavigationInstruction[] = [];
|
||||
if (
|
||||
route.guidance?.instructions &&
|
||||
route.guidance.instructions.length > 0
|
||||
) {
|
||||
route.guidance.instructions.forEach(
|
||||
(inst: any, index: number) => {
|
||||
const maneuverKey = inst.maneuver || "STRAIGHT";
|
||||
const icon =
|
||||
maneuverIcons[maneuverKey] || maneuverIcons.DEFAULT;
|
||||
const text =
|
||||
maneuverTranslations[maneuverKey] || "Continuez";
|
||||
|
||||
navInstructions.push({
|
||||
instruction: inst.message || text,
|
||||
distance: formatDistance(
|
||||
inst.routeOffsetInMeters || 0,
|
||||
),
|
||||
maneuverIcon: icon,
|
||||
streetName: inst.street,
|
||||
isActive: index === 0,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Si pas d'instructions, creer des instructions basiques
|
||||
if (navInstructions.length === 0) {
|
||||
navInstructions.push({
|
||||
instruction: "Dirigez-vous vers votre destination",
|
||||
distance: formatDistance(
|
||||
route.summary?.lengthInMeters || 0,
|
||||
),
|
||||
maneuverIcon: faCar,
|
||||
isActive: true,
|
||||
});
|
||||
navInstructions.push({
|
||||
instruction: "Vous etes arrive a destination",
|
||||
distance: "0 m",
|
||||
maneuverIcon: faFlagCheckered,
|
||||
isActive: false,
|
||||
});
|
||||
}
|
||||
|
||||
setInstructions(navInstructions);
|
||||
setCurrentInstructionIndex(0);
|
||||
|
||||
// Construire le GeoJSON pour la ligne
|
||||
const geojson = {
|
||||
type: "Feature" as const,
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: "LineString" as const,
|
||||
coordinates: coordinates,
|
||||
},
|
||||
};
|
||||
|
||||
console.log("GeoJSON cree avec", coordinates.length, "points");
|
||||
|
||||
// Supprimer l'ancien itineraire s'il existe
|
||||
try {
|
||||
if (mapInstance.current?.getLayer(routeLayerId.current)) {
|
||||
mapInstance.current.removeLayer(routeLayerId.current);
|
||||
}
|
||||
if (mapInstance.current?.getSource(routeLayerId.current)) {
|
||||
mapInstance.current.removeSource(routeLayerId.current);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Pas d'ancien itineraire a supprimer");
|
||||
}
|
||||
|
||||
// Ajouter le nouvel itineraire
|
||||
mapInstance.current?.addSource(routeLayerId.current, {
|
||||
type: "geojson",
|
||||
data: geojson,
|
||||
});
|
||||
|
||||
mapInstance.current?.addLayer({
|
||||
id: routeLayerId.current,
|
||||
type: "line",
|
||||
source: routeLayerId.current,
|
||||
layout: {
|
||||
"line-join": "round",
|
||||
"line-cap": "round",
|
||||
},
|
||||
paint: {
|
||||
"line-color": "#4285F4",
|
||||
"line-width": 6,
|
||||
"line-opacity": 0.8,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Itineraire ajoute a la carte");
|
||||
|
||||
// Informations de l'itineraire
|
||||
const summary = route.summary;
|
||||
const distanceKm = (summary.lengthInMeters / 1000).toFixed(1);
|
||||
const durationMin = Math.round(summary.travelTimeInSeconds / 60);
|
||||
|
||||
const info: RouteInfo = {
|
||||
distance: `${distanceKm} km`,
|
||||
duration: `${durationMin} min`,
|
||||
distanceRemaining: summary.lengthInMeters,
|
||||
timeRemaining: summary.travelTimeInSeconds,
|
||||
};
|
||||
setRouteInfo(info);
|
||||
onRouteCalculated?.(info.distance, info.duration);
|
||||
|
||||
// Ajuster la vue pour montrer tout l'itineraire
|
||||
const bounds = new tt.LngLatBounds();
|
||||
bounds.extend([driverLocation.longitude, driverLocation.latitude]);
|
||||
bounds.extend([destinationCoords.lng, destinationCoords.lat]);
|
||||
|
||||
mapInstance.current?.fitBounds(bounds, {
|
||||
padding: 80,
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Itineraire calcule avec",
|
||||
navInstructions.length,
|
||||
"instructions",
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Erreur calcul itineraire:", err);
|
||||
setError("Erreur lors du calcul de l'itineraire");
|
||||
onError?.("Erreur lors du calcul de l'itineraire");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [
|
||||
driverLocation,
|
||||
destinationCoords,
|
||||
onRouteCalculated,
|
||||
onError,
|
||||
formatDistance,
|
||||
]);
|
||||
|
||||
// Recentrer sur le livreur
|
||||
const centerOnDriver = () => {
|
||||
if (mapInstance.current && driverLocation) {
|
||||
mapInstance.current.flyTo({
|
||||
center: [driverLocation.longitude, driverLocation.latitude],
|
||||
zoom: 17,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
error &&
|
||||
(!TOMTOM_API_KEY || TOMTOM_API_KEY === "YOUR_TOMTOM_API_KEY_HERE")
|
||||
) {
|
||||
return (
|
||||
<div className="tomtom-map-container">
|
||||
<div className="tomtom-map-error">
|
||||
<div className="error-icon">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
width="48"
|
||||
height="48"
|
||||
>
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Configuration requise</h3>
|
||||
<p>{error}</p>
|
||||
<ol>
|
||||
<li>
|
||||
Allez sur{" "}
|
||||
<a
|
||||
href="https://developer.tomtom.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
developer.tomtom.com
|
||||
</a>
|
||||
</li>
|
||||
<li>Creez un compte gratuit</li>
|
||||
<li>Obtenez une cle API</li>
|
||||
<li>
|
||||
Ajoutez-la dans le fichier <code>.env</code>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const currentInstruction = instructions[currentInstructionIndex];
|
||||
const nextInstruction = instructions[currentInstructionIndex + 1];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`tomtom-map-container ${isNavigating ? "navigating" : ""}`}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="tomtom-map-loading">
|
||||
<div className="loading-spinner"></div>
|
||||
<span>Calcul de l'itineraire...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Panneau d'instruction principale */}
|
||||
{isNavigating && currentInstruction && (
|
||||
<div className="navigation-panel">
|
||||
<div className="current-instruction">
|
||||
<div className="instruction-maneuver">
|
||||
<FontAwesomeIcon
|
||||
icon={currentInstruction.maneuverIcon}
|
||||
/>
|
||||
</div>
|
||||
<div className="instruction-details">
|
||||
<div className="instruction-text">
|
||||
{currentInstruction.instruction}
|
||||
</div>
|
||||
{currentInstruction.streetName && (
|
||||
<div className="instruction-street">
|
||||
{currentInstruction.streetName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="instruction-distance">
|
||||
{currentInstruction.distance}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{nextInstruction && (
|
||||
<div className="next-instruction">
|
||||
<span className="next-label">Puis</span>
|
||||
<span className="next-maneuver">
|
||||
<FontAwesomeIcon
|
||||
icon={nextInstruction.maneuverIcon}
|
||||
/>
|
||||
</span>
|
||||
<span className="next-text">
|
||||
{nextInstruction.instruction}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={mapContainer} className="tomtom-map" />
|
||||
|
||||
{/* Bouton recentrer */}
|
||||
{driverLocation && (
|
||||
<button
|
||||
className="recenter-btn"
|
||||
onClick={centerOnDriver}
|
||||
title="Recentrer"
|
||||
>
|
||||
<FontAwesomeIcon icon={faLocationCrosshairs} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Infos de route */}
|
||||
{routeInfo && (
|
||||
<div className="tomtom-route-info">
|
||||
<div className="route-info-item">
|
||||
<span className="route-info-label">Distance</span>
|
||||
<span className="route-info-value">
|
||||
{routeInfo.distance}
|
||||
</span>
|
||||
</div>
|
||||
<div className="route-info-divider" />
|
||||
<div className="route-info-item">
|
||||
<span className="route-info-label">Duree</span>
|
||||
<span className="route-info-value">
|
||||
{routeInfo.duration}
|
||||
</span>
|
||||
</div>
|
||||
{instructions.length > 0 && (
|
||||
<>
|
||||
<div className="route-info-divider" />
|
||||
<button
|
||||
className="show-instructions-btn"
|
||||
onClick={() =>
|
||||
setShowAllInstructions(!showAllInstructions)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faRoute}
|
||||
style={{ marginRight: "0.5rem" }}
|
||||
/>
|
||||
{showAllInstructions ? "Masquer" : "Etapes"} (
|
||||
{instructions.length})
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Liste des instructions */}
|
||||
{showAllInstructions && instructions.length > 0 && (
|
||||
<div className="instructions-list">
|
||||
<div className="instructions-header">
|
||||
<h4>Etapes de l'itineraire</h4>
|
||||
<button
|
||||
className="close-instructions-btn"
|
||||
onClick={() => setShowAllInstructions(false)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="instructions-scroll">
|
||||
{instructions.map((inst, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`instruction-item ${index === currentInstructionIndex ? "active" : ""} ${index < currentInstructionIndex ? "passed" : ""}`}
|
||||
>
|
||||
<span className="item-maneuver">
|
||||
<FontAwesomeIcon icon={inst.maneuverIcon} />
|
||||
</span>
|
||||
<div className="item-details">
|
||||
<span className="item-text">
|
||||
{inst.instruction}
|
||||
</span>
|
||||
{inst.streetName && (
|
||||
<span className="item-street">
|
||||
{inst.streetName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="item-distance">
|
||||
{inst.distance}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div className="tomtom-map-error-toast">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TomTomMap;
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
endAlert,
|
||||
getMyAlerts,
|
||||
} from "../../api/api_delivery";
|
||||
import TomTomMap from "../../components/TomTomMap";
|
||||
|
||||
interface DeliveryStats {
|
||||
todayDeliveries: number;
|
||||
@@ -117,6 +118,9 @@ function DeliveryDashboard() {
|
||||
|
||||
const [isAlertTriggered, setIsAlertTriggered] = useState(false);
|
||||
const [policeAlertLoading, setPoliceAlertLoading] = useState(false);
|
||||
const [showMap, setShowMap] = useState(false);
|
||||
const [routeDistance, setRouteDistance] = useState<string | null>(null);
|
||||
const [routeDuration, setRouteDuration] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
@@ -409,6 +413,11 @@ function DeliveryDashboard() {
|
||||
const clientInfo =
|
||||
detailsResult.delivery.client_info;
|
||||
|
||||
const deliveryStatus =
|
||||
delivery.status === "assigned"
|
||||
? "assigned"
|
||||
: "in_route";
|
||||
|
||||
setCurrentDelivery({
|
||||
id: delivery.id,
|
||||
orderNumber: `CMD-${delivery.id}`,
|
||||
@@ -420,15 +429,17 @@ function DeliveryDashboard() {
|
||||
"+33 X XX XX XX XX",
|
||||
deliveryAddress:
|
||||
delivery.adresse || "Adresse de livraison",
|
||||
status:
|
||||
delivery.status === "assigned"
|
||||
? "assigned"
|
||||
: "in_route",
|
||||
status: deliveryStatus,
|
||||
distance: "N/A",
|
||||
// ✅ FIX: Cast to any pour accéder à items qui peut exister mais n'est pas typé
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
items: (currentDeliveryData as any).items || [],
|
||||
totalPrice: currentDeliveryData.total_prix || 0,
|
||||
});
|
||||
|
||||
// Afficher automatiquement la carte si livraison en cours
|
||||
if (deliveryStatus === "in_route") {
|
||||
setShowMap(true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setCurrentDelivery(null);
|
||||
@@ -516,10 +527,14 @@ function DeliveryDashboard() {
|
||||
}
|
||||
|
||||
console.log("✅ [DELIVERY_DASHBOARD] Livraison démarrée");
|
||||
|
||||
// Afficher automatiquement la carte en mode navigation
|
||||
setShowMap(true);
|
||||
|
||||
showStyledAlert(
|
||||
"success",
|
||||
"Livraison démarrée",
|
||||
"La livraison a été démarrée avec succès !",
|
||||
"La livraison a été démarrée avec succès ! Suivez l'itinéraire sur la carte.",
|
||||
() => window.location.reload(),
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -592,6 +607,20 @@ function DeliveryDashboard() {
|
||||
};
|
||||
|
||||
const handleNavigate = () => {
|
||||
setShowMap((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleRouteCalculated = (distance: string, duration: string) => {
|
||||
setRouteDistance(distance);
|
||||
setRouteDuration(duration);
|
||||
console.log(`Route calculee: ${distance}, ${duration}`);
|
||||
};
|
||||
|
||||
const handleMapError = (error: string) => {
|
||||
console.error("Erreur carte TomTom:", error);
|
||||
};
|
||||
|
||||
const openExternalNavigation = () => {
|
||||
if (currentDelivery) {
|
||||
const address = encodeURIComponent(currentDelivery.deliveryAddress);
|
||||
window.open(
|
||||
@@ -1273,7 +1302,7 @@ function DeliveryDashboard() {
|
||||
onClick={handleStartDelivery}
|
||||
>
|
||||
<Package size={20} />
|
||||
Démarrer la Livraison
|
||||
Demarrer la Livraison
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1283,7 +1312,7 @@ function DeliveryDashboard() {
|
||||
onClick={handleCompleteDelivery}
|
||||
>
|
||||
<CheckCircle size={20} />
|
||||
Marquer comme Livrée
|
||||
Marquer comme Livree
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1292,9 +1321,84 @@ function DeliveryDashboard() {
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
<Navigation size={20} />
|
||||
Navigation GPS
|
||||
{showMap
|
||||
? "Masquer la carte"
|
||||
: "Afficher la carte"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Carte TomTom GPS */}
|
||||
{showMap && (
|
||||
<div style={{ marginTop: "1.5rem" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "1rem",
|
||||
}}
|
||||
>
|
||||
<h4
|
||||
style={{
|
||||
margin: 0,
|
||||
color: "#fff",
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: "600",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<Navigation size={20} color="#10b981" />
|
||||
Navigation GPS TomTom
|
||||
</h4>
|
||||
{routeDistance && routeDuration && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1rem",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "#9ca3af" }}>
|
||||
Distance:{" "}
|
||||
<strong
|
||||
style={{ color: "#10b981" }}
|
||||
>
|
||||
{routeDistance}
|
||||
</strong>
|
||||
</span>
|
||||
<span style={{ color: "#9ca3af" }}>
|
||||
Temps:{" "}
|
||||
<strong
|
||||
style={{ color: "#10b981" }}
|
||||
>
|
||||
{routeDuration}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TomTomMap
|
||||
driverLocation={
|
||||
location.latitude && location.longitude
|
||||
? {
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
}
|
||||
: null
|
||||
}
|
||||
destinationAddress={
|
||||
currentDelivery.deliveryAddress
|
||||
}
|
||||
onRouteCalculated={handleRouteCalculated}
|
||||
onError={handleMapError}
|
||||
isNavigating={
|
||||
currentDelivery.status === "in_route"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="current-delivery-card no-delivery-card">
|
||||
|
||||
Reference in New Issue
Block a user