Files
Starseed/docs/specs/M6-field-sales/spec.md
T
Matthieu de4aaa1d64 feat(commercial) : géolocalisation des adresses Tiers (lat/lng + géocodage BAN + pin ajustable) (ERP-122)
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).
2026-06-11 14:31:35 +02:00

20 KiB
Raw Blame History

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
M1-clients
M2-suppliers
Sites
Core
Shared

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 :

  1. Voir ses Tiers sur une carte interactive (pins colorés par type : client / fournisseur / prospect / custom).
  2. 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.
  3. Obtenir le temps total et le temps entre chaque étape (calcul auto), avec heure d'arrivée estimée.
  4. Cliquer « Trajet logique » (V1, heuristique gratuite) puis « Optimiser » (V2, routier réel) pour ordonner les étapes au mieux.
  5. Lancer la navigation (Waze / Google Maps / Plan) vers une étape en un tap.
  6. Dupliquer une tournée et exporter une feuille de route PDF.
  7. 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 i18n audit.entity.field_sales_tour / _tourstop dans fr.json (sinon AuditableEntitiesHaveI18nLabelTest casse make test).
  • Migration modulaire : COMMENT ON COLUMN sur chaque colonne (FR ≤ 200 car.) + helper addStandardTimestampableBlamableComments().
  • Toute collection paginée (CollectionsArePaginatedTest).
  • Front : useApi() uniquement, composants Malio*, MalioDataTable + usePaginatedList pour 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) : usePaginatedList sur 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)