From db34640acc335cdaa009b171b18f3ee72e352504 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Sat, 24 Jan 2026 21:33:29 +0100 Subject: [PATCH] chore: add gps with tomtom API --- README.md | 1125 ++++++++++++++++- frontend-prep/.env | 3 + frontend-prep/src/components/TomTomMap.css | 513 ++++++++ frontend-prep/src/components/TomTomMap.tsx | 827 ++++++++++++ .../src/pages/Livreur/DeliveryDashboard.tsx | 122 +- 5 files changed, 2580 insertions(+), 10 deletions(-) create mode 100644 frontend-prep/.env create mode 100644 frontend-prep/src/components/TomTomMap.css create mode 100644 frontend-prep/src/components/TomTomMap.tsx diff --git a/README.md b/README.md index 9acecc82..ce858e42 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ 4. [API Admin (v2)](#api-admin-v2) 5. [API Cabine (v1)](#api-cabine-v1) 6. [API Livreur (v1)](#api-livreur-v1) -7. [Codes d'Erreur](#codes-derreur) +7. [Systeme GPS Integre](#-systeme-gps-integre) +8. [Codes d'Erreur](#codes-derreur) --- @@ -32,6 +33,491 @@ Cette API REST complète gère une plateforme de livraison avec assignation auto - **⚡ Workers automatiques** (nettoyage, assignation, notifications) - **🎯 Système de pénalités** pour la gestion des comportements +### 🏗️ Architecture Globale du Systeme + +```mermaid +graph TB + subgraph Frontend["🖥️ Frontend (React/TypeScript)"] + UI_CLIENT[Interface Client] + UI_ADMIN[Interface Admin] + UI_LIVREUR[Interface Livreur] + UI_CABINE[Interface Cabine] + TOMTOM_MAP[TomTomMap Component] + end + + subgraph API_GATEWAY["🌐 API Gateway (Go/Gin)"] + AUTH[Middleware Auth JWT] + ROUTES[Router] + end + + subgraph API_V1["📱 API v1"] + CLIENT_API["/api/v1/client"] + LIVREUR_API["/api/v1/livreur"] + CABINE_API["/api/v1/cabine"] + PUBLIC_API["/api/v1/public"] + end + + subgraph API_V2["👨‍💼 API v2 Admin"] + ADMIN_API["/api/v2/admin"] + end + + subgraph HANDLERS["⚙️ Handlers"] + H_AUTH[Auth Handler] + H_PRODUCT[Product Handler] + H_PANIER[Panier Handler] + H_COMMAND[Command Handler] + H_DELIVERY[Delivery Handler] + H_GPS[GPS Handler] + H_ETA[ETA Handler] + H_PENALTY[Penalty Handler] + end + + subgraph SERVICES["🔧 Services"] + S_GEO[Geo Service] + S_TOMTOM[TomTom Service] + S_QUEUE[Queue Service] + S_NOTIF[Notification Service] + end + + subgraph EXTERNAL["🌍 APIs Externes"] + NOMINATIM[Nominatim API] + TOMTOM_API[TomTom API] + end + + subgraph STORAGE["💾 Stockage"] + POSTGRES[(PostgreSQL)] + REDIS[(Redis)] + end + + subgraph WORKERS["⚡ Workers"] + W_ASSIGN[Auto-Assign Worker] + W_CLEANUP[Cleanup Worker] + W_ETA[ETA Update Worker] + end + + %% Frontend connections + UI_CLIENT --> AUTH + UI_ADMIN --> AUTH + UI_LIVREUR --> AUTH + UI_CABINE --> AUTH + TOMTOM_MAP --> TOMTOM_API + + %% API Gateway + AUTH --> ROUTES + ROUTES --> API_V1 + ROUTES --> API_V2 + + %% API to Handlers + CLIENT_API --> H_AUTH + CLIENT_API --> H_PANIER + CLIENT_API --> H_COMMAND + LIVREUR_API --> H_DELIVERY + LIVREUR_API --> H_GPS + CABINE_API --> H_COMMAND + PUBLIC_API --> H_PRODUCT + ADMIN_API --> H_COMMAND + ADMIN_API --> H_DELIVERY + ADMIN_API --> H_GPS + ADMIN_API --> H_PENALTY + + %% Handlers to Services + H_GPS --> S_GEO + H_GPS --> S_TOMTOM + H_ETA --> S_TOMTOM + H_DELIVERY --> S_QUEUE + H_COMMAND --> S_NOTIF + + %% Services to External + S_GEO --> NOMINATIM + S_TOMTOM --> TOMTOM_API + + %% Services to Storage + S_GEO --> REDIS + S_QUEUE --> REDIS + H_COMMAND --> POSTGRES + H_PRODUCT --> POSTGRES + H_DELIVERY --> POSTGRES + + %% Workers + W_ASSIGN --> S_GEO + W_ASSIGN --> S_QUEUE + W_ASSIGN --> POSTGRES + W_CLEANUP --> REDIS + W_CLEANUP --> POSTGRES + W_ETA --> S_TOMTOM + W_ETA --> REDIS +``` + +### 🔄 Flux des Donnees par Role + +```mermaid +flowchart LR + subgraph Client["👤 Client"] + C1[Consulter Produits] + C2[Gerer Panier] + C3[Passer Commande] + C4[Suivre Livraison] + C5[Approuver/Annuler] + end + + subgraph Admin["👨‍💼 Admin"] + A1[Gerer Produits] + A2[Voir Commandes] + A3[Assigner Livreurs] + A4[Gerer Penalites] + A5[Surveiller GPS] + end + + subgraph Livreur["🚚 Livreur"] + L1[Voir Mes Livraisons] + L2[Mettre a Jour Position] + L3[Changer Statut] + L4[Voir Ma Queue] + end + + subgraph Cabine["🏭 Cabine/Cuisine"] + K1[Voir Articles] + K2[Preparer Commande] + K3[Marquer Pret] + end + + subgraph Backend["⚙️ Backend"] + API[API REST] + GPS[Service GPS] + QUEUE[Gestion Queue] + end + + C1 & C2 & C3 & C4 & C5 --> API + A1 & A2 & A3 & A4 & A5 --> API + L1 & L2 & L3 & L4 --> API + K1 & K2 & K3 --> API + API --> GPS + API --> QUEUE +``` + +### 📡 Architecture des Endpoints API + +```mermaid +graph TD + subgraph PUBLIC["🔓 Endpoints Publics"] + P1["GET /api/v1/products"] + P2["GET /api/v1/products/:id"] + P3["POST /api/v1/auth/register"] + P4["POST /api/v1/auth/login"] + P5["POST /api/v1/geocode"] + end + + subgraph CLIENT_AUTH["🔐 Client (JWT Required)"] + C1["POST /api/v1/panier/add"] + C2["GET /api/v1/panier/:username"] + C3["DELETE /api/v1/panier/remove"] + C4["POST /api/v1/checkout"] + C5["GET /api/v1/my-commands"] + C6["GET /api/v1/commands/:id/status"] + C7["GET /api/v1/commands/:id/tracking"] + C8["GET /api/v1/commands/:id/eta"] + C9["POST /api/v1/commands/:id/approve"] + C10["POST /api/v1/commands/:id/cancel"] + end + + subgraph ADMIN_AUTH["👨‍💼 Admin (JWT Required)"] + A1["GET /api/v2/admin/protected/orders"] + A2["POST /api/v2/admin/protected/orders/:id/auto-assign"] + A3["POST /api/v2/admin/protected/orders/auto-assign-all"] + A4["GET /api/v2/admin/protected/delivery-persons"] + A5["GET /api/v2/admin/protected/delivery-persons/:username/location"] + A6["GET /api/v2/admin/protected/delivery/queues"] + A7["POST /api/v2/admin/protected/delivery/distances"] + A8["GET /api/v2/admin/protected/commands/:id/navigation-links"] + A9["POST /api/v2/admin/protected/products"] + A10["POST /api/v2/admin/protected/penalty"] + end + + subgraph LIVREUR_AUTH["🚚 Livreur (JWT Required)"] + L1["GET /api/v1/livreur/deliveries"] + L2["PUT /api/v1/livreur/deliveries/:id/status"] + L3["POST /api/v1/livreur/location/update"] + L4["GET /api/v1/livreur/location"] + L5["POST /api/v1/livreur/update/status"] + L6["GET /api/v1/livreur/queue"] + end + + subgraph CABINE_AUTH["🏭 Cabine (JWT Required)"] + K1["GET /api/v1/cabine/commands/:id/items"] + K2["PUT /api/v1/cabine/items/:item_id/status"] + end + + GW[API Gateway :8080] + GW --> PUBLIC + GW --> CLIENT_AUTH + GW --> ADMIN_AUTH + GW --> LIVREUR_AUTH + GW --> CABINE_AUTH +``` + +### 🔐 Flux d'Authentification + +```mermaid +sequenceDiagram + participant U as Utilisateur + participant F as Frontend + participant API as Backend API + participant DB as PostgreSQL + participant R as Redis + + rect rgb(200, 230, 200) + Note over U,R: Registration + U->>F: Remplir formulaire inscription + F->>API: POST /auth/register + API->>DB: Creer utilisateur + DB-->>API: User cree + API-->>F: 201 Created + User info + F-->>U: Compte cree + end + + rect rgb(200, 200, 230) + Note over U,R: Login + U->>F: Entrer credentials + F->>API: POST /auth/login + API->>DB: Verifier credentials + DB-->>API: User valide + API->>API: Generer JWT + API->>R: Stocker session + API-->>F: 200 OK + JWT Token + F->>F: Stocker token (localStorage) + F-->>U: Connecte + end + + rect rgb(230, 200, 200) + Note over U,R: Requete Authentifiee + U->>F: Action protegee + F->>API: Request + Authorization: Bearer JWT + API->>API: Valider JWT + API->>R: Verifier session active + R-->>API: Session valide + API->>DB: Executer action + DB-->>API: Resultat + API-->>F: Response + F-->>U: Resultat affiche + end +``` + +### 📦 Flux Complet d'une Commande + +```mermaid +sequenceDiagram + participant C as Client + participant F as Frontend + participant API as Backend + participant GPS as Service GPS + participant Q as Queue Service + participant L as Livreur + participant DB as PostgreSQL + participant R as Redis + + rect rgb(230, 245, 230) + Note over C,R: 1. Creation Commande + C->>F: Valider panier + F->>API: POST /checkout {address} + API->>GPS: Geocoder adresse + GPS->>R: Check cache + R-->>GPS: Miss + GPS->>GPS: Appel Nominatim + GPS->>R: Cache resultat (7j) + GPS-->>API: Coordonnees + API->>DB: Creer commande + API->>Q: Chercher livreur proche + Q->>R: Get positions livreurs + Q->>Q: Calcul distances Haversine + Q->>GPS: Calculer ETA (TomTom) + Q-->>API: Livreur assigne + API->>DB: Update commande + API->>R: Ajouter a queue livreur + API-->>F: Commande creee + ETA + F-->>C: Confirmation + end + + rect rgb(230, 230, 245) + Note over C,R: 2. Livraison + L->>API: GET /livreur/deliveries + API-->>L: Liste commandes + L->>API: PUT /deliveries/:id/status {support} + L->>API: POST /location/update {lat, lng} + API->>R: Update position + API->>R: Publish update + R-->>F: Notification position + F-->>C: Carte mise a jour + L->>API: PUT /deliveries/:id/status {en_route} + L->>API: PUT /deliveries/:id/status {arrived} + L->>API: PUT /deliveries/:id/status {livre} + end + + rect rgb(245, 230, 230) + Note over C,R: 3. Finalisation + C->>F: Approuver livraison + F->>API: POST /commands/:id/approve {rating} + API->>DB: Update statut approved + API->>R: Retirer de queue + API-->>F: Confirmation + F-->>C: Livraison terminee + end +``` + +### 💾 Schema de la Base de Donnees + +```mermaid +erDiagram + CLIENTS { + int id PK + string username UK + string password + string nom + string prenom + string telephone UK + int command + int point + int points_zipette + float amende + int cancellations_count + timestamp created_at + } + + ADMINS { + int id PK + string username UK + string password + string role + timestamp created_at + } + + LIVREURS { + int id PK + string username UK + string password + string status + timestamp created_at + } + + CABINES { + int id PK + string username UK + string password + timestamp created_at + } + + PRODUCTS { + int id PK + string nom + string description + string category + int stock + float prix + timestamp created_at + } + + PRODUCT_PRICES { + int id PK + int product_id FK + int quantity + float price + } + + PRODUCT_MEDIA { + int id PK + int product_id FK + string type + string url + } + + PANIER { + int id PK + string username FK + string name_product + string category + int quantity + float price + timestamp created_at + } + + COMMANDS { + int id PK + string username FK + string status + float total + string adresse + float dest_latitude + float dest_longitude + string livreur_assign FK + timestamp created_at + timestamp updated_at + } + + COMMAND_ITEMS { + int id PK + int command_id FK + string produit + int quantite + float prix + string status + string category + } + + RATINGS { + int id PK + int command_id FK + string deliveryman + int rating + string comment + timestamp created_at + } + + CLIENTS ||--o{ PANIER : "possede" + CLIENTS ||--o{ COMMANDS : "passe" + PRODUCTS ||--o{ PRODUCT_PRICES : "a" + PRODUCTS ||--o{ PRODUCT_MEDIA : "a" + COMMANDS ||--o{ COMMAND_ITEMS : "contient" + COMMANDS ||--o| RATINGS : "a" + LIVREURS ||--o{ COMMANDS : "livre" +``` + +### 🗄️ Structure Redis + +```mermaid +graph LR + subgraph Sessions["🔐 Sessions"] + S1["session:{token}
TTL: 5h (client) / 2h (admin)"] + end + + subgraph Positions["📍 Positions GPS"] + P1["delivery:location:{username}
TTL: 2h"] + P2["delivery:status:{username}
TTL: 1h"] + end + + subgraph Queues["📋 Queues Livraison"] + Q1["livreur:queue:{username}
List of command_ids"] + Q2["queue:size:{username}
Integer"] + end + + subgraph Cache["💾 Cache"] + C1["geocode:cache:{hash}
TTL: 7j"] + C2["command:destination:{id}
TTL: 4h"] + C3["product:cache:{id}
TTL: 1h"] + end + + subgraph PubSub["�� Pub/Sub Channels"] + PS1["channel:position_updates"] + PS2["channel:order_status"] + PS3["channel:notifications"] + end + + REDIS[(Redis Server)] + REDIS --- Sessions + REDIS --- Positions + REDIS --- Queues + REDIS --- Cache + REDIS --- PubSub +``` + --- ## 🔐 Authentication @@ -1209,6 +1695,643 @@ Authorization: Bearer --- +## 🗺️ Systeme GPS Integre + +Le systeme GPS est au coeur de la plateforme de livraison. Il permet l'assignation automatique des livreurs, le calcul d'ETA en temps reel, et le suivi des livraisons. + +### Architecture GPS + +#### Flux GPS Complet + +```mermaid +flowchart TD + A[1. Commande creee par le Client] --> B[2. Geocodage adresse] + B --> |Nominatim API| C[(Cache Redis 7 jours)] + C --> D[3. Worker Auto-Assignation] + D --> |Toutes les 5 min| E[4. Recherche livreur le plus proche] + E --> |Haversine + TomTom| F[5. Calcul ETA avec trafic reel] + F --> G[6. Assignation a la queue du livreur] + G --> H[7. Livreur accepte et met a jour sa position] + H --> |Redis Pub/Sub| I[8. Client/Admin recoivent les mises a jour] + I --> J[9. Navigation vers destination] + J --> |TomTom Routing| K[10. Livraison terminee] +``` + +#### Architecture des Services GPS + +```mermaid +graph TB + subgraph Frontend + MAP[TomTomMap Component] + UI[Interface Utilisateur] + end + + subgraph Backend + GEO[Service Geocodage] + DIST[Service Distance] + ETA[Service ETA] + ASSIGN[Service Auto-Assignation] + end + + subgraph APIs Externes + NOM[Nominatim API] + TOM[TomTom API] + end + + subgraph Stockage + REDIS[(Redis Cache)] + PG[(PostgreSQL)] + end + + UI --> MAP + MAP --> TOM + GEO --> NOM + GEO --> REDIS + DIST --> GEO + ETA --> TOM + ETA --> DIST + ASSIGN --> DIST + ASSIGN --> ETA + ASSIGN --> PG + ASSIGN --> REDIS +``` + +### Services GPS Utilises + +| Service | Utilisation | Cache | +|---------|-------------|-------| +| **Nominatim** (OpenStreetMap) | Geocodage d'adresses | Redis 7 jours | +| **TomTom Routing API** | ETA avec trafic reel, itineraires | Non | +| **Haversine** (local) | Calcul de distance a vol d'oiseau | Non | + +### Configuration Requise + +**Variables d'environnement Backend:** +```bash +TOMTOM_API_KEY= +``` + +**Variables d'environnement Frontend:** +```bash +VITE_TOMTOM_API_KEY= +``` + +> **Note:** Obtenez une cle API TomTom gratuite sur [developer.tomtom.com](https://developer.tomtom.com/) + +--- + +### Geocodage d'Adresses + +Le geocodage convertit une adresse textuelle en coordonnees GPS (latitude/longitude). + +#### Geocoder une adresse + +```bash +POST /api/v1/geocode +Content-Type: application/json +``` + +**Requete:** +```json +{ + "address": "15 Rue de la Paix, 75002 Paris, France" +} +``` + +**Reponse (200 OK):** +```json +{ + "success": true, + "address": "15 Rue de la Paix, 75002 Paris, France", + "coordinates": { + "latitude": 48.8698, + "longitude": 2.3311 + }, + "cached": false +} +``` + +**Erreurs possibles:** +- `400` - Adresse manquante ou invalide +- `404` - Adresse non trouvee (geocodage echoue) + +--- + +#### Valider une adresse + +Verifie si une adresse peut etre geocodee sans la stocker. + +```bash +POST /api/v1/validate-address +Content-Type: application/json +``` + +**Requete:** +```json +{ + "address": "15 Rue de la Paix, 75002 Paris" +} +``` + +**Reponse (200 OK):** +```json +{ + "success": true, + "valid": true, + "address": "15 Rue de la Paix, 75002 Paris", + "coordinates": { + "latitude": 48.8698, + "longitude": 2.3311 + } +} +``` + +--- + +### Calcul de Distance et ETA + +#### Formule Haversine + +Le systeme utilise la formule Haversine pour calculer la distance a vol d'oiseau entre deux points GPS: + +``` +a = sin²(Δlat/2) + cos(lat1) × cos(lat2) × sin²(Δlon/2) +c = 2 × atan2(√a, √(1-a)) +distance = R × c + +Ou R = 6371 km (rayon de la Terre) +``` + +#### ETA avec Trafic (TomTom) + +L'ETA est calcule en utilisant l'API TomTom qui prend en compte: +- Les conditions de trafic en temps reel +- Les incidents routiers +- Les travaux +- L'heure de la journee + +```mermaid +flowchart TD + A[Demande ETA] --> B{TomTom API disponible?} + B -->|Oui| C[Appel TomTom Routing API] + C --> D[ETA avec trafic reel] + B -->|Non| E[Calcul Haversine] + E --> F[Distance a vol d'oiseau] + F --> G[Vitesse moyenne 30 km/h] + G --> H[ETA estime] + D --> I[Retourner ETA] + H --> I + I --> J{Fallback utilise?} + J -->|Oui| K[Ajouter flag fallback_used: true] + J -->|Non| L[Response standard] +``` + +**Fallback:** Si l'API TomTom est indisponible, le systeme utilise un calcul local base sur: +- Distance Haversine +- Vitesse moyenne estimee (30 km/h en ville) + +--- + +### Auto-Assignation GPS + +Le systeme assigne automatiquement les commandes au livreur le plus proche. + +#### Processus d'Auto-Assignation + +```mermaid +flowchart LR + A[Nouvelle Commande] --> B{Adresse geocodee?} + B -->|Non| C[Geocoder adresse] + C --> D + B -->|Oui| D[Recuperer livreurs disponibles] + D --> E[Calculer distances Haversine] + E --> F[Trier par proximite] + F --> G{Livreur avec capacite?} + G -->|Oui| H[Calculer ETA TomTom] + G -->|Non| I{Forcer assignation?} + I -->|Oui| H + I -->|Non| J[Commande en attente] + H --> K[Assigner a la queue] + K --> L[Notifier livreur] +``` + +#### Regles de Capacite + +- Chaque livreur peut avoir jusqu'a **10 commandes** dans sa queue +- Si tous les livreurs sont a capacite maximale, le systeme peut forcer l'assignation +- Les livreurs avec le statut `offline` ne recoivent pas de commandes + +#### Auto-assigner une commande specifique + +```bash +POST /api/v2/admin/protected/orders/:id/auto-assign +Authorization: Bearer +``` + +**Reponse (200 OK):** +```json +{ + "success": true, + "message": "Commande assignee automatiquement", + "command_id": 1234, + "assigned_to": "john_deliveryman", + "assignment_details": { + "distance_km": 1.5, + "eta_minutes": 8, + "queue_position": 3, + "method": "nearest_available" + } +} +``` + +--- + +#### Lister les livreurs par distance + +Obtient la liste des livreurs tries par distance depuis une adresse. + +```bash +POST /api/v2/admin/protected/delivery/distances +Authorization: Bearer +Content-Type: application/json +``` + +**Requete:** +```json +{ + "address": "15 Rue de la Paix, 75002 Paris" +} +``` + +**Reponse (200 OK):** +```json +{ + "success": true, + "address": "15 Rue de la Paix, 75002 Paris", + "destination": { + "latitude": 48.8698, + "longitude": 2.3311 + }, + "delivery_persons": [ + { + "username": "john_deliveryman", + "distance_km": 1.5, + "eta_minutes": 8, + "status": "available", + "queue_size": 2, + "location": { + "latitude": 48.8566, + "longitude": 2.3522 + } + }, + { + "username": "jane_delivery", + "distance_km": 3.2, + "eta_minutes": 15, + "status": "available", + "queue_size": 5, + "location": { + "latitude": 48.8456, + "longitude": 2.3789 + } + } + ] +} +``` + +--- + +### Liens de Navigation + +Le systeme genere des liens vers plusieurs applications de cartographie. + +#### Obtenir les liens carte d'un livreur + +```bash +GET /api/v2/admin/protected/delivery-persons/:username/map-links +Authorization: Bearer +``` + +**Reponse (200 OK):** +```json +{ + "success": true, + "deliveryman": "john_deliveryman", + "location": { + "latitude": 48.8566, + "longitude": 2.3522 + }, + "map_links": { + "google_maps": "https://www.google.com/maps?q=48.8566,2.3522", + "google_maps_app": "comgooglemaps://?center=48.8566,2.3522", + "waze": "https://www.waze.com/ul?ll=48.8566,2.3522&navigate=yes", + "waze_app": "waze://?ll=48.8566,2.3522&navigate=yes", + "apple_maps": "https://maps.apple.com/?ll=48.8566,2.3522", + "openstreetmap": "https://www.openstreetmap.org/?mlat=48.8566&mlon=2.3522", + "bing_maps": "https://www.bing.com/maps?cp=48.8566~2.3522", + "here_maps": "https://share.here.com/l/48.8566,2.3522" + } +} +``` + +--- + +#### Obtenir les liens de navigation pour une commande + +Genere des liens de navigation depuis la position du livreur vers la destination de livraison. + +```bash +GET /api/v2/admin/protected/commands/:id/navigation-links +Authorization: Bearer +``` + +**Reponse (200 OK):** +```json +{ + "success": true, + "command_id": 1234, + "origin": { + "latitude": 48.8566, + "longitude": 2.3522, + "description": "Position du livreur" + }, + "destination": { + "latitude": 48.8698, + "longitude": 2.3311, + "address": "15 Rue de la Paix, 75002 Paris" + }, + "navigation_links": { + "google_maps": "https://www.google.com/maps/dir/48.8566,2.3522/48.8698,2.3311", + "waze": "https://www.waze.com/ul?ll=48.8698,2.3311&navigate=yes&from=48.8566,2.3522", + "apple_maps": "https://maps.apple.com/?saddr=48.8566,2.3522&daddr=48.8698,2.3311" + } +} +``` + +--- + +### Gestion des Positions en Temps Reel + +#### Flux de Mise a Jour de Position + +```mermaid +sequenceDiagram + participant L as Livreur (App) + participant API as Backend API + participant R as Redis + participant PS as Redis Pub/Sub + participant A as Admin/Client + + L->>API: POST /location/update {lat, lng} + API->>API: Valider coordonnees + API->>R: SET delivery:location:{username} + API->>PS: PUBLISH position_update + PS-->>A: Notification temps reel + API-->>L: 200 OK {location, updated_at} +``` + +#### Mise a jour de position (Livreur) + +```bash +POST /api/v1/livreur/location/update +Authorization: Bearer +Content-Type: application/json +``` + +**Requete:** +```json +{ + "latitude": 48.8566, + "longitude": 2.3522 +} +``` + +**Validation des coordonnees:** +- Latitude: entre -90 et +90 +- Longitude: entre -180 et +180 + +**Reponse (200 OK):** +```json +{ + "success": true, + "message": "Position mise a jour", + "location": { + "latitude": 48.8566, + "longitude": 2.3522, + "updated_at": "2025-01-18T17:35:00Z" + } +} +``` + +**Erreurs possibles:** +- `400` - Coordonnees invalides (hors limites) +- `401` - Non authentifie + +--- + +### Stockage Redis des Positions + +Les positions GPS sont stockees dans Redis pour un acces rapide: + +```mermaid +graph LR + subgraph Redis Cache + A[delivery:location:username
TTL: 2h] + B[delivery:status:username
TTL: 1h] + C[geocode:cache:address_hash
TTL: 7j] + D[command:destination:id
TTL: 4h] + end + + subgraph Donnees + A --> A1[latitude, longitude, updated_at] + B --> B1[status, latitude, longitude] + C --> C1[latitude, longitude, address] + D --> D1[dest_lat, dest_lng] + end +``` + +| Cle Redis | Description | TTL | +|-----------|-------------|-----| +| `delivery:location:{username}` | Position actuelle du livreur | 2 heures | +| `delivery:status:{username}` | Statut + position du livreur | 1 heure | +| `geocode:cache:{address_hash}` | Cache geocodage adresse | 7 jours | +| `command:destination:{command_id}` | Coordonnees destination | 4 heures | + +--- + +### Composant Carte Frontend (TomTomMap) + +Le frontend inclut un composant React/TypeScript pour afficher la carte interactive. + +#### Fonctionnalites du Composant + +- **Carte interactive** avec marqueurs livreur/destination +- **Calcul d'itineraire** en temps reel +- **Instructions de navigation** (tourner a gauche, a droite, rond-point, etc.) +- **Affichage distance/duree** du trajet +- **Mode 3D** avec orientation selon la direction +- **Support francais** pour toutes les instructions + +#### Exemple d'Utilisation + +```tsx +import TomTomMap from './components/TomTomMap'; + +function DeliveryTracking() { + return ( + + ); +} +``` + +#### Props du Composant + +| Prop | Type | Description | +|------|------|-------------| +| `driverPosition` | `{lat: number, lng: number}` | Position du livreur | +| `destinationPosition` | `{lat: number, lng: number}` | Destination de livraison | +| `showRoute` | `boolean` | Afficher l'itineraire | +| `showInstructions` | `boolean` | Afficher le panneau d'instructions | +| `onEtaUpdate` | `(eta: number) => void` | Callback quand l'ETA change | + +--- + +### Worker d'Auto-Assignation + +Un worker CRON s'execute automatiquement pour assigner les commandes en attente. + +#### Configuration + +- **Frequence:** Toutes les 5 minutes +- **Fichier:** `backend/gestion/workers/cron_auto_assign.go` + +#### Processus du Worker + +```mermaid +flowchart TD + START((Demarrage CRON)) --> A[Recuperer commandes pending] + A --> B{Commandes a traiter?} + B -->|Non| END((Fin)) + B -->|Oui| C[Prendre commande suivante] + C --> D{Adresse geocodee?} + D -->|Non| E[Geocoder via Nominatim] + E --> F + D -->|Oui| F[Recuperer livreurs disponibles] + F --> G[Calculer distances] + G --> H[Selectionner le plus proche] + H --> I{Capacite disponible?} + I -->|Non| J[Marquer pour retry] + I -->|Oui| K[Calculer ETA TomTom] + K --> L[Assigner commande] + L --> M[Mettre a jour statut] + M --> N{Autres commandes?} + J --> N + N -->|Oui| C + N -->|Non| O[Logger resultats] + O --> END +``` + +#### Logs d'Exemple + +``` +[AUTO-ASSIGN] Processing 5 pending orders +[AUTO-ASSIGN] Order #1234: Geocoded to (48.8698, 2.3311) +[AUTO-ASSIGN] Order #1234: Nearest driver is john_deliveryman (1.5 km) +[AUTO-ASSIGN] Order #1234: ETA calculated: 8 minutes +[AUTO-ASSIGN] Order #1234: Assigned successfully +[AUTO-ASSIGN] Completed: 5 assigned, 0 failed +``` + +--- + +### Cycle de Vie d'une Livraison (GPS) + +```mermaid +stateDiagram-v2 + [*] --> pending: Commande creee + pending --> assigned: Auto-assignation GPS + pending --> pending: Geocodage en cours + + assigned --> support: Livreur prend en charge + assigned --> cancelled: Annulation + + support --> en_route: Livreur demarre + support --> cancelled: Annulation + + en_route --> arrived: Position = Destination + en_route --> en_route: Mise a jour position + + arrived --> livre: Remise au client + arrived --> failed: Client absent + + livre --> approved: Client confirme + livre --> failed: Probleme signale + + approved --> [*] + cancelled --> [*] + failed --> [*] + + note right of en_route + Position GPS mise a jour + toutes les 30 secondes + ETA recalcule en temps reel + end note +``` + +--- + +### Gestion des Erreurs GPS + +#### Geocodage Echoue + +Si une adresse ne peut pas etre geocodee: +- La commande reste en statut `pending` +- Un log d'erreur est genere +- L'admin peut corriger l'adresse manuellement + +#### API TomTom Indisponible + +Le systeme bascule automatiquement sur le calcul local: +- Utilise la formule Haversine pour la distance +- Estime l'ETA avec une vitesse moyenne de 30 km/h +- Un flag `fallback_used: true` est ajoute a la reponse + +#### Position Livreur Obsolete + +- Les positions de plus de 2 heures sont considerees obsoletes +- Le livreur doit mettre a jour sa position pour recevoir des commandes +- Un warning est affiche dans l'interface admin + +--- + +### Bonnes Pratiques + +#### Pour les Livreurs + +1. **Mettre a jour la position** frequemment (toutes les 30 secondes recommande) +2. **Verifier le statut** avant de commencer une livraison +3. **Utiliser les liens de navigation** generes par l'API + +#### Pour les Admins + +1. **Surveiller les queues** des livreurs pour eviter la surcharge +2. **Verifier les adresses** qui echouent au geocodage +3. **Utiliser l'endpoint distances** pour l'assignation manuelle si necessaire + +#### Pour les Developpeurs + +1. **Toujours valider** les coordonnees avant stockage +2. **Utiliser le cache** Redis pour les adresses frequentes +3. **Implementer un fallback** si TomTom est indisponible +4. **Logger les erreurs** de geocodage pour analyse + +--- + ## ❌ Codes d'Erreur ### Erreurs HTTP Standards diff --git a/frontend-prep/.env b/frontend-prep/.env new file mode 100644 index 00000000..eb9dfb30 --- /dev/null +++ b/frontend-prep/.env @@ -0,0 +1,3 @@ +# TomTom API Key +# Obtenez votre cle API gratuite sur https://developer.tomtom.com/ +VITE_TOMTOM_API_KEY=MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB diff --git a/frontend-prep/src/components/TomTomMap.css b/frontend-prep/src/components/TomTomMap.css new file mode 100644 index 00000000..641c9a6e --- /dev/null +++ b/frontend-prep/src/components/TomTomMap.css @@ -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; + } +} diff --git a/frontend-prep/src/components/TomTomMap.tsx b/frontend-prep/src/components/TomTomMap.tsx new file mode 100644 index 00000000..6ae20f29 --- /dev/null +++ b/frontend-prep/src/components/TomTomMap.tsx @@ -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 = { + 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 = { + 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(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mapInstance = useRef(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const driverMarker = useRef(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const destinationMarker = useRef(null); + const routeLayerId = useRef("route-layer"); + const routeCoordinates = useRef<[number, number][]>([]); + + const [isMapReady, setIsMapReady] = useState(false); + const [routeInfo, setRouteInfo] = useState(null); + const [destinationCoords, setDestinationCoords] = useState<{ + lat: number; + lng: number; + } | null>(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [instructions, setInstructions] = useState( + [], + ); + 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 = ` +
+
+
+ `; + return el; + }, []); + + // Creer le marqueur de destination + const createDestinationMarkerElement = useCallback(() => { + const el = document.createElement("div"); + el.className = "destination-marker"; + el.innerHTML = ` +
+ + + +
+ `; + 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 ( +
+
+
+ + + +
+

Configuration requise

+

{error}

+
    +
  1. + Allez sur{" "} + + developer.tomtom.com + +
  2. +
  3. Creez un compte gratuit
  4. +
  5. Obtenez une cle API
  6. +
  7. + Ajoutez-la dans le fichier .env +
  8. +
+
+
+ ); + } + + const currentInstruction = instructions[currentInstructionIndex]; + const nextInstruction = instructions[currentInstructionIndex + 1]; + + return ( +
+ {isLoading && ( +
+
+ Calcul de l'itineraire... +
+ )} + + {/* Panneau d'instruction principale */} + {isNavigating && currentInstruction && ( +
+
+
+ +
+
+
+ {currentInstruction.instruction} +
+ {currentInstruction.streetName && ( +
+ {currentInstruction.streetName} +
+ )} +
+
+ {currentInstruction.distance} +
+
+ + {nextInstruction && ( +
+ Puis + + + + + {nextInstruction.instruction} + +
+ )} +
+ )} + +
+ + {/* Bouton recentrer */} + {driverLocation && ( + + )} + + {/* Infos de route */} + {routeInfo && ( +
+
+ Distance + + {routeInfo.distance} + +
+
+
+ Duree + + {routeInfo.duration} + +
+ {instructions.length > 0 && ( + <> +
+ + + )} +
+ )} + + {/* Liste des instructions */} + {showAllInstructions && instructions.length > 0 && ( +
+
+

Etapes de l'itineraire

+ +
+
+ {instructions.map((inst, index) => ( +
+ + + +
+ + {inst.instruction} + + {inst.streetName && ( + + {inst.streetName} + + )} +
+ + {inst.distance} + +
+ ))} +
+
+ )} + + {error &&
{error}
} +
+ ); +} + +export default TomTomMap; diff --git a/frontend-prep/src/pages/Livreur/DeliveryDashboard.tsx b/frontend-prep/src/pages/Livreur/DeliveryDashboard.tsx index 06178fac..b7a3c551 100644 --- a/frontend-prep/src/pages/Livreur/DeliveryDashboard.tsx +++ b/frontend-prep/src/pages/Livreur/DeliveryDashboard.tsx @@ -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(null); + const [routeDuration, setRouteDuration] = useState(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} > - Démarrer la Livraison + Demarrer la Livraison )} @@ -1283,7 +1312,7 @@ function DeliveryDashboard() { onClick={handleCompleteDelivery} > - Marquer comme Livrée + Marquer comme Livree )} @@ -1292,9 +1321,84 @@ function DeliveryDashboard() { onClick={handleNavigate} > - Navigation GPS + {showMap + ? "Masquer la carte" + : "Afficher la carte"}
+ + {/* Carte TomTom GPS */} + {showMap && ( +
+
+

+ + Navigation GPS TomTom +

+ {routeDistance && routeDuration && ( +
+ + Distance:{" "} + + {routeDistance} + + + + Temps:{" "} + + {routeDuration} + + +
+ )} +
+ +
+ )}
) : (