Ajoute la géolocalisation aux adresses Client et Fournisseur, socle de la tournée commerciale (M6 field-sales). Back : - migration : latitude/longitude NUMERIC(10,7), geo_manual BOOLEAN, geocoded_at TIMESTAMPTZ sur client_address et supplier_address (+ COMMENT ON COLUMN FR) - GeolocatableAddressInterface (Shared/Domain/Contract) implémenté par les deux entités ; bornes WGS84 validées (Range -90/90, -180/180, messages FR) - GeocoderInterface + BanGeocoder (api-adresse.data.gouv.fr), branché via AddressGeocoder dans les processors ; géocodage auto au create/update - RG-6.08 : geo_manual=true fige les coordonnées (pas de réécriture auto) - symfony/http-client passe en dépendance de production Front : - AddressGeoPin (Leaflet + OSM) : marqueur déplaçable -> PATCH lat/lng + geoManual=true, bouton Re-géocoder, badges « à géolocaliser » / « pin manuel » - intégration dans les blocs adresse Client et Fournisseur Tests : PHPUnit (géocodage create, non-réécriture RG-6.08, mapping BAN, bornes) + Vitest (drag du pin, badges, re-géocodage).
20 KiB
module, nom, ecran, owner_spec, backup_spec, version, date_redaction, spec_front, maquette_figma, lesstime_taskgroup_id, lesstime_project_id, statut_global, depend_de
| module | nom | ecran | owner_spec | backup_spec | version | date_redaction | spec_front | maquette_figma | lesstime_taskgroup_id | lesstime_project_id | statut_global | depend_de | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| M6 | Tournées commerciales terrain | tournees-terrain | Matthieu | V0.2 | 2026-06-11 | ./spec-front.md | 28 | 6 | en_dev |
|
Spec — Module 6 : Tournées commerciales terrain (field_sales)
Périmètre V0.2 (réduit) : géolocalisation des adresses Tiers, carte interactive, planification de tournées (étapes, optimisation, navigation Waze/Maps, feuille de route PDF) et onglet « Carte » dans les fiches Client/Fournisseur. Hors scope : tout rapport de visite (compte-rendu, note, offres de prix, fichiers, saisie vocale) et le mode terrain mobile dédié.
1. Contexte & objectif
Donner aux commerciaux terrain (technico-commerciaux agricoles : visites d'exploitations, coopératives, négoces) un outil de planification de tournées intégré à Starseed, reposant sur le référentiel Tiers existant (Clients M1 + Fournisseurs M2). Fonctionne sur desktop et mobile/tablette (responsive, pas d'offline en V1).
Le commercial doit pouvoir :
- Voir ses Tiers sur une carte interactive (pins colorés par type : client / fournisseur / prospect / custom).
- Construire une tournée lui-même : ajouter des étapes (une étape = une adresse précise d'un Tiers ou un point libre), les réordonner en drag & drop, fixer une heure de départ.
- Obtenir le temps total et le temps entre chaque étape (calcul auto), avec heure d'arrivée estimée.
- Cliquer « Trajet logique » (V1, heuristique gratuite) puis « Optimiser » (V2, routier réel) pour ordonner les étapes au mieux.
- Lancer la navigation (Waze / Google Maps / Plan) vers une étape en un tap.
- Dupliquer une tournée et exporter une feuille de route PDF.
- Consulter un onglet « Carte » dans la fiche Client/Fournisseur affichant les adresses géolocalisées du Tiers.
2. Inspiration — logiciels de tournée de référence
| Logiciel | Pattern repris | Application Starseed |
|---|---|---|
| Badger Maps | Lasso tool : on entoure des Tiers sur la carte → route optimisée auto. Pins colorés par type. | Sélection lasso/rectangle sur la carte pour bâtir la tournée. Pins par type. |
| SPOTIO | Réordonnancement drag & drop, lieu de départ + bouton Optimize. | Liste d'étapes draggable, point de départ paramétrable, boutons « Trajet logique » / « Optimiser ». |
| Portatour | Optimisation en quelques secondes, temps entre RDV. | Durée de visite paramétrable par étape, intégrée au temps total. |
| Nomadia Field Sales | Carte + tournée dans un outil mobile responsive. | Écran de planification responsive desktop + mobile. |
3. Décisions d'architecture
3.1 Nouveau module field_sales
La tournée est transverse : elle vise aussi bien des Clients (M1) que des Fournisseurs (M2). Module dédié
src/Module/FieldSales/ (ID field_sales, label « Tournées »), REQUIRED = false (activable via
config/modules.php).
Règle ABSOLUE n°1 respectée : FieldSales n'importe aucune classe de Commercial. Il référence les
Tiers visités via un contrat partagé App\Shared\Domain\Contract\VisitableInterface (getId(),
getDisplayName(), getVisitableType() = client|supplier) résolu par resolve_target_entities, comme
ClientAddress référence SiteInterface / CategoryInterface.
3.1.bis Une étape vise tout Tiers — et même un point libre
Une étape n'est pas limitée à Client/Fournisseur : elle vise tout type de Tiers (Client, Fournisseur,
Prestataire à venir) via VisitableInterface (extensible sans toucher au module FieldSales), ou un point
custom (prospect/RDV sans fiche : libellé + adresse + coordonnées saisis à la main). L'enum tier_type est
volontairement ouvert (string + Assert\Choice = types Visitable enregistrés + custom).
3.2 Géolocalisation portée par l'adresse Tiers — FAIT (ticket M6.1 / ERP-122)
latitude / longitude / geo_manual / geocoded_at sur client_address et supplier_address, géocodage
api-adresse.data.gouv.fr + pin ajustable. Prérequis routage : une étape sans coordonnées reste utilisable mais
exclue du calcul de trajet (badge « à géolocaliser »).
3.3 Carte interactive — Leaflet + OpenStreetMap (pas Google Maps JS)
Pour l'affichage carte/pins : Leaflet + tuiles OpenStreetMap (ou IGN). Gratuit, RGPD-friendly, pas
de clé facturée pour le rendu. Composant carte encapsulé dans frontend/modules/field-sales/ ; côté
formulaire/filtre on reste sur les composants Malio*. La carte est une exception documentée à
@malio/layer-ui (type non couvert). Le routing réel (matrice de temps) est un service distinct (§ 3.4).
3.4 Stratégie de calcul de trajet — phasée
| Phase | Bouton | Moteur | Coût |
|---|---|---|---|
| V1 | « Trajet logique » | Heuristique maison plus proche voisin (Haversine), départ fixé. Temps estimé = distance × vitesse moyenne paramétrable. | 0 € |
| V2 | « Optimiser » | Matrix API (temps routiers réels) + optimisation TSP (OpenRouteService / OSRM / Mapbox). | Par appel (cache, debounce) |
Contrat RouteEngineInterface (computeMatrix, optimizeOrder, estimateLegDurations) posé dès la V1 avec
HaversineRouteEngine. La V2 ajoute OrsRouteEngine sans toucher au front. On n'écrit jamais l'algo routier
— on branche un fournisseur.
3.5 IDs entier auto-increment, Audit, Timestampable/Blamable
Cohérent avec M0/M1/M2. Toutes les entités métier : #[Auditable], implements TimestampableInterface, BlamableInterface + use TimestampableBlamableTrait. Entités auditées : Tour, TourStop.
4. Modèle de données
4.1 Adresses Tiers (M1 + M2) — FAIT (ERP-122)
Colonnes latitude NUMERIC(10,7), longitude NUMERIC(10,7), geo_manual BOOLEAN, geocoded_at TIMESTAMPTZ
sur client_address et supplier_address ; contrat GeolocatableAddressInterface côté Shared.
4.2 Tour (tournée) — tour
| Champ | Type | Règle |
|---|---|---|
id |
int PK | |
owner_id |
FK User | Commercial propriétaire. Tournée personnelle (RG-6.01). |
label |
varchar(120) | Nom libre. NotBlank. |
tour_date |
date | Date de réalisation. NotBlank. |
departure_time |
time | Heure de départ (alimente les ETA). Défaut 08:00. |
start_latitude / start_longitude |
numeric null | Point de départ (site commercial ou adresse libre). NULL → départ = 1re étape. |
start_label |
varchar(180) null | Libellé du point de départ. |
default_visit_minutes |
smallint default 30 | Durée de visite par défaut (temps total). |
status |
enum draft|planned|in_progress|done |
Cycle de vie (RG-6.02). |
total_distance_m / total_duration_s |
int null | Derniers totaux calculés (cache d'affichage). |
#[Auditable], Timestampable/Blamable, soft delete (deleted_at). GetCollection paginée, filtrée par owner.
4.3 TourStop (étape) — tour_stop
| Champ | Type | Règle |
|---|---|---|
id |
int PK | |
tour_id |
FK Tour | onDelete CASCADE. |
tier_type |
string client|supplier|…|custom |
Cible (résolue via VisitableInterface). custom = point libre. |
tier_id |
int null | ID du Tiers référentiel. NULL si custom. |
address_id |
int null | Adresse précise visitée (un Tiers a plusieurs adresses — RG-6.03). NULL si custom. |
custom_label |
varchar(180) null | Libellé du point libre (obligatoire ssi custom). |
custom_address |
varchar(255) null | Adresse texte du point libre (ssi custom), géocodée. |
custom_latitude / custom_longitude |
numeric null | Coordonnées du point libre (pin ajustable). |
position |
smallint | Ordre dans la tournée (drag & drop). |
visit_minutes |
smallint null | Durée de visite spécifique (sinon tour.default_visit_minutes). |
leg_distance_m / leg_duration_s |
int null | Distance/temps depuis l'étape précédente (calculés). |
eta |
time null | Heure d'arrivée estimée. |
#[Auditable], Timestampable/Blamable. Unicité (tour_id, position). Pas de rapport rattaché (scope réduit).
Deux étapes peuvent viser le même Tiers (RG-6.07) — pas d'unicité sur
tier_id.
5. API (API Platform — providers/processors, jamais de controller)
| Méthode | Endpoint | Sécurité | Note |
|---|---|---|---|
| GET | /api/tours |
field_sales.tours.view |
Paginé, filtré sur owner courant (admin/bureau voient tout). |
| POST | /api/tours |
field_sales.tours.manage |
Crée une tournée draft. |
| GET/PATCH/DELETE | /api/tours/{id} |
view / manage | DELETE = soft delete. |
| POST | /api/tours/{tourId}/stops |
field_sales.tours.manage |
Sous-ressource (Link toProperty tour, pattern ClientAddress). |
| PATCH/DELETE | /api/tour_stops/{id} |
field_sales.tours.manage |
PATCH position = drag & drop. |
| POST | /api/tours/{id}/compute |
field_sales.tours.manage |
Recalcule legs + ETA + totaux (HaversineRouteEngine). |
| POST | /api/tours/{id}/optimize |
field_sales.tours.manage |
Réordonne via optimizeOrder() puis recompute. |
| POST | /api/tours/{id}/duplicate |
field_sales.tours.manage |
Duplique étapes + départ à une nouvelle tourDate (RG-6.13). |
| GET | /api/tours/{id}/roadbook.pdf |
field_sales.tours.view |
Feuille de route PDF (skill pdf). |
| GET | /api/visitable_tiers?bbox=...&q=...&type=client,supplier |
field_sales.tours.view |
Pins dans la zone visible (carte). Paginé / ?pagination=false. |
Toutes les collections sont paginées (règle ABSOLUE n°13). /api/visitable_tiers retourne un Paginator,
borné par bbox.
6. Écrans
6.1 Planification de tournée (carte interactive — responsive desktop + mobile)
Layout split inspiré de Badger/SPOTIO :
- Carte interactive Leaflet : pins des Tiers de la zone (couleur par type, filtrables). Sélection lasso/rectangle → ajoute les Tiers entourés comme étapes. Clic pin → popup (nom, adresse, « + Ajouter »). Tracé de la tournée dessiné par-dessus (polyline numérotée).
- Panneau tournée : nom, date, heure de départ, point de départ, liste d'étapes draggable (n° + nom + adresse + ETA + temps depuis étape précédente), totaux (distance / durée / nb visites). Boutons « Trajet logique », « Optimiser », « Dupliquer », « PDF ».
- Chaque étape : menu « Y aller » (Waze / Google Maps / Plan via deep links), « Voir le Tiers ».
- Ajout d'un point libre
custom(libellé + adresse + pin). - En mobile, layout empilé : la navigation se fait via le bouton « Y aller » de chaque étape (pas de mode terrain dédié).
6.2 Onglet « Carte » dans la fiche Client / Fournisseur
Nouvel onglet « Carte » dans la fiche Client (M1) et Fournisseur (M2) : mini-carte Leaflet
affichant toutes les adresses géolocalisées du Tiers (un marqueur par adresse, popup avec le libellé de
l'adresse). Vue d'ensemble des implantations du Tiers. Le pin reste ajustable par adresse (réutilise le
composant de l'onglet Adresse, déjà livré en ERP-122). Adresses sans coordonnées listées comme
« à géolocaliser ». Onglet visible sous field_sales.tours.view ; masqué si le module field_sales est désactivé.
6.3 Ajustement du pin (fiche adresse) — FAIT (ERP-122)
Mini-carte Leaflet avec marqueur déplaçable dans le bloc adresse M1/M2 ; drag → latitude/longitude +
geo_manual = true ; bouton « Re-géocoder depuis l'adresse ».
7. Géocodage des adresses — FAIT (ERP-122)
Service GeocoderInterface / BanGeocoder (api-adresse.data.gouv.fr). Correction manuelle systématique via le
pin (geo_manual = true fige — RG-6.08).
8. RBAC — 3 miroirs obligatoires
Permissions du module field_sales (méthode permissions() de FieldSalesModule.php) — uniquement les
tournées (plus de permissions reports.* depuis la réduction de scope) :
| Permission | Sens | Admin | Commerciale | Bureau |
|---|---|---|---|---|
field_sales.tours.view |
Voir les tournées + l'onglet Carte. | ✅ (toutes) | ✅ (les siennes) | ✅ (consultation) |
field_sales.tours.manage |
Créer/éditer/optimiser/dupliquer/supprimer une tournée. | ✅ | ✅ | ❌ |
Attribution : Commerciale + Admin = manage ; Bureau = view ; Compta exclue. À synchroniser dans les
3 miroirs (règle ABSOLUE n°8) : config/sidebar.php (section « Tournées » : item tours + i18n
sidebar.field_sales.*), frontend/tests/e2e/_fixtures/personas.ts, SeedE2ECommand.php. Sync :
app:sync-permissions.
9. Conventions & garde-fous (rappel)
declare(strict_types=1);partout ; commentaires FR, code EN.- Entités métier :
#[Auditable]+ Timestampable/Blamable. Libellés i18naudit.entity.field_sales_tour/_tourstopdansfr.json(sinonAuditableEntitiesHaveI18nLabelTestcassemake test). - Migration modulaire :
COMMENT ON COLUMNsur chaque colonne (FR ≤ 200 car.) + helperaddStandardTimestampableBlamableComments(). - Toute collection paginée (
CollectionsArePaginatedTest). - Front :
useApi()uniquement, composantsMalio*,MalioDataTable+usePaginatedListpour les listes, pas d'état de tableau dans l'URL. Carte Leaflet = exception documentée.
10. Règles de gestion (RG)
| RG | Règle | Garde-fou |
|---|---|---|
| RG-6.01 | Tournée personnelle (owner). Commerciale ne voit/édite que les siennes ; Admin/Bureau voient tout en lecture. |
Filtre Provider sur owner + RBAC. |
| RG-6.02 | Cycle de vie draft → planned → in_progress → done (transitions libres en V1). |
Enum + Assert\Choice. |
| RG-6.03 | Une étape sur Tiers référentiel vise une adresse de ce Tiers (qui en a plusieurs). Ne s'applique pas aux custom. |
Assert\Callback. |
| RG-6.05 | Une étape n'entre dans le calcul que si son adresse a latitude ET longitude. Sinon « à géolocaliser », exclue des totaux. |
RouteEngine + signalement front. |
| RG-6.07 | Deux étapes peuvent viser le même Tiers (repasser plus tard). Unicité uniquement sur (tour_id, position). |
Index unique partiel. |
| RG-6.08 | geo_manual = true fige les coordonnées (le géocodage auto ne réécrit plus). |
Garde dans le géocodeur (FAIT). |
| RG-6.11 | eta = departure_time + Σ(trajets précédents) + Σ(durées de visite précédentes). |
RouteEngine::estimateLegDurations(). |
| RG-6.12 | Une étape vise tout Tiers ou un point custom. Si custom : tier_id/address_id NULL, custom_label + coordonnées obligatoires. |
Assert\Choice + Assert\Callback. |
| RG-6.13 | Dupliquer copie départ + étapes (ordre/adresses/durées) à une nouvelle date ; ne copie pas les calculs (ETA/legs recalculés). | Service TourDuplicator. |
11. Tests à automatiser
- Architecture (cassent
make test) :ColumnsHaveSqlCommentTest,AuditableEntitiesHaveI18nLabelTest(audit.entity.field_sales_tour/_tourstop),EntitiesAreTimestampableBlamableTest(Tour, TourStop),CollectionsArePaginatedTest,EntityConstraintsHaveFrenchMessageTest. - Back (PHPUnit) : RG-6.03 (adresse hors Tiers → 422), RG-6.05 (étape sans coord exclue), RG-6.07 (doublon
Tiers accepté), RG-6.11 (ETA), RG-6.12 (custom cohérent), RG-6.13 (duplication sans calculs), filtre
owner,HaversineRouteEngine(ordre plus proche voisin sur un jeu de coordonnées connu). - Front (Vitest) :
usePaginatedListsur tournées, composable de planification (réordonnancement, totaux, deep links), onglet Carte (marqueurs des adresses). Pas de E2E (règle d'or).
12. Hors-périmètre (HP)
- HP-M6-1 : rapport de visite (compte-rendu, note /5, offres de prix, fichiers, catégorie, saisie vocale, historique des visites) — retiré du scope (V0.2), à réintroduire dans un module/lot ultérieur si besoin.
- HP-M6-2 : mode terrain mobile dédié (vue du jour + check-in) — retiré ; navigation via l'écran de planification responsive.
- HP-M6-3 : routing routier réel + optimisation TSP (Matrix API) — V2 (V1 = heuristique Haversine).
- HP-M6-4 : suggestion automatique des Tiers « à visiter » (façon Portatour) — V2.
- HP-M6-5 : offline réel (PWA + synchro) — V3.
- HP-M6-6 : partage / affectation de tournées entre commerciaux, planning d'équipe — V3.
- HP-M6-7 : navigation multi-étapes poussée dans Waze (impossible techniquement) — navigation étape par étape.
13. Phasage
- V1 (livrable) : géoloc adresses + pin (FAIT) ; carte interactive + lasso ; tournée (création, drag & drop,
heure de départ, point de départ) sur tout Tiers + point
custom; « Trajet logique » + ETA + totaux ; deep links Waze/Maps ; duplication ; feuille de route PDF ; onglet Carte dans les fiches Client/Fournisseur ; responsive desktop + mobile. - V2 : bouton « Optimiser » (routing routier réel ORS/OSRM), temps trafic, suggestion des Tiers proches.
- V3 : offline réel, partage/affectation de tournées.
14. Risques / points ouverts
- Coût/quota routing en V2 : multi-tenant → cache, debounce, plafonds par tenant.
- Limite Waze multi-étapes : Waze ne prend qu'une destination → navigation étape par étape (assumé).
- Reste à cadrer techniquement : périmètre de visibilité Bureau (toutes les tournées vs les siennes).
📦 Tickets Lesstime (scope réduit V0.2)
TaskGroup Lesstime : #28 — M6 Tournées commerciales terrain (projet STARSEED #6). Tickets gros grain, chacun avec un prompt Fable prêt à coller (consigne « adapte-toi à la config actuelle » incluse).
| # | Réf | Ticket | Effort | Tag | État |
|---|---|---|---|---|---|
| M6.1 | ERP-122 | Géolocaliser les adresses Tiers (lat/lng + pin) | L | Back+Front | ✅ Fait |
| M6.2 | ERP-123 | Fondations module field_sales + VisitableInterface + RBAC (tournées) | M | Back | Prêt à dev |
| M6.3 | ERP-124 | Entités & API Tournée + Étape | L | Back | Prêt à dev |
| M6.4 | ERP-125 | Calcul trajet, optimisation, duplication & roadbook PDF | L | Back | Prêt à dev |
| M6.5 | ERP-127 | Carte interactive + écran planification (responsive) | L | Front | Prêt à dev |
| M6.6 | ERP-129 | Onglet « Carte » dans les fiches Client & Fournisseur | M | Front | Prêt à dev |
| M6.7 | ERP-130 | Vérification : garde-fous archi, tests RG & golden path | M | Back+Front | Prêt à dev |
Supprimés à la réduction de scope : ERP-126 (rapport de visite) et ERP-128 (mode terrain mobile + formulaire rapport).
Ordre d'exécution : M6.2 → M6.3 → M6.4 → M6.5 → M6.6 → M6.7.
Sources d'inspiration (logiciels de référence)
- Badger Maps — Lasso + carte : https://www.badgermapping.com/features/
- SPOTIO — drag & drop des étapes + optimize : https://support.spotio.com/hc/en-us/articles/360061370754-Routing-How-to-Build-and-Manage-Routes
- Portatour — multi-stop + recalcul auto : https://www.portatour.com/features/en
- Nomadia Field Sales — carte + tournée mobile : https://www.nomadia.com/ressources/blog/logiciel-commerciaux-itinerants/