feat(field_sales) : entités et API Tournée + Étape (Tour/TourStop) (ERP-124)
Modèle et API CRUD du module Tournées (M6.3, scope réduit V0.2 : pas de
rapport de visite, donc TourStop sans report_id ni check-in).
- Entités Tour (table tour) + TourStop (table tour_stop) : #[Auditable],
Timestampable/Blamable, enum TourStatus (draft|planned|in_progress|done),
soft delete sur Tour.
- API Platform : GET/POST/GET/PATCH/DELETE /api/tours (DELETE = soft delete),
sous-ressource POST /api/tours/{tourId}/stops + PATCH/DELETE /api/tour_stops/{id}.
- RG-6.01 : tournée personnelle (TourProvider filtre owner ; admin/Bureau
voient tout). RG-6.03 : adresse appartient au Tiers (TourStopProcessor +
TierAddressResolver DBAL, sans import inter-module). RG-6.07 : pas d'unicité
tier_id. RG-6.12 : cohérence custom/Tiers (Assert\Callback).
- Migration racine : tables + COMMENT ON COLUMN FR + index unique
(tour_id, position) + FK CASCADE ; mirror dans ColumnCommentsCatalog.
- i18n audit (fieldsales_tour / _tourstop), mappings Doctrine + API Platform.
- Tests fonctionnels : owner, RG-6.03/6.07/6.12, pagination, unicité position,
soft delete, RBAC (17 tests).
Co-Authored-By: Matthieu <mtholot19@gmail.com>
This commit is contained in:
@@ -37,6 +37,15 @@ final class BusinessRoles
|
||||
*/
|
||||
public const string COMMERCIALE = 'commerciale';
|
||||
|
||||
/**
|
||||
* Role metier « Bureau » — code de Role RBAC. Utilise par FieldSales (M6,
|
||||
* RG-6.01) : le Bureau voit TOUTES les tournees en lecture (comme l'admin),
|
||||
* la Commerciale ne voit que les siennes. Reference ici (Shared) pour que le
|
||||
* TourProvider raisonne sur le role metier via BusinessRoleAwareInterface
|
||||
* sans importer le RbacSeeder du module Core (regle ABSOLUE n°1).
|
||||
*/
|
||||
public const string BUREAU = 'bureau';
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
// Classe de constantes : non instanciable.
|
||||
|
||||
@@ -369,6 +369,44 @@ final class ColumnCommentsCatalog
|
||||
'iban' => 'IBAN du compte (≤ 34 caracteres).',
|
||||
'position' => 'Ordre d affichage du RIB dans la liste du fournisseur (croissant).',
|
||||
] + self::timestampableBlamableComments(),
|
||||
|
||||
// === M6.3 FieldSales (ERP-124) — miroir des COMMENT de la migration
|
||||
// Version20260611140000 pour le chemin schema:update (dev/test). ===
|
||||
|
||||
'tour' => [
|
||||
'_table' => 'Tournees commerciales terrain (M6 FieldSales) — personnelles (owner), soft-deletables (deleted_at).',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'owner_id' => 'Commercial proprietaire de la tournee (RG-6.01, personnelle) — FK -> "user".id, ON DELETE RESTRICT. Pose au POST par le TourProcessor.',
|
||||
'label' => 'Nom libre de la tournee (NotBlank, <= 120 caracteres).',
|
||||
'tour_date' => 'Date de realisation de la tournee (NotNull).',
|
||||
'departure_time' => 'Heure de depart, alimente les ETA (RG-6.11). Defaut applicatif 08:00 (constructeur).',
|
||||
'start_latitude' => 'Latitude WGS84 du point de depart (site commercial ou adresse libre). NULL -> depart = 1re etape.',
|
||||
'start_longitude' => 'Longitude WGS84 du point de depart. NULL -> depart = 1re etape.',
|
||||
'start_label' => 'Libelle affichable du point de depart (<= 180 caracteres). Optionnel.',
|
||||
'default_visit_minutes' => 'Duree de visite par defaut d une etape, en minutes (defaut 30) — utilisee si l etape ne fixe pas sa propre duree.',
|
||||
'status' => 'Cycle de vie (RG-6.02) : draft | planned | in_progress | done (enum TourStatus). Transitions libres en V1. Defaut draft.',
|
||||
'total_distance_m' => 'Cache d affichage : derniere distance totale calculee, en metres (RG-6.11). Lecture seule API, alimente par le moteur de trajet (M6.4).',
|
||||
'total_duration_s' => 'Cache d affichage : derniere duree totale calculee, en secondes (RG-6.11). Lecture seule API.',
|
||||
'deleted_at' => 'Horodatage du soft-delete — pose par le DELETE API. Null = tournee active.',
|
||||
] + self::timestampableBlamableComments(),
|
||||
|
||||
'tour_stop' => [
|
||||
'_table' => 'Etapes ordonnees d une tournee (M6) — cible polymorphe (Tiers referentiel ou point custom). Pas de rapport (scope reduit V0.2).',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'tour_id' => 'FK -> tour.id, ON DELETE CASCADE — tournee proprietaire de l etape.',
|
||||
'tier_type' => 'Type de cible : client | supplier | ... | custom (point libre). Resolu via VisitableInterface. Chaine ouverte (Assert\Choice).',
|
||||
'tier_id' => 'Identifiant du Tiers referentiel cible (NULL si custom). Sans FK (cible polymorphe). RG-6.07 : aucune unicite.',
|
||||
'address_id' => 'Adresse precise visitee chez le Tiers (NULL si custom). Sans FK (client_address OU supplier_address). RG-6.03 : doit appartenir au Tiers.',
|
||||
'custom_label' => 'Libelle du point libre — obligatoire ssi tier_type = custom (RG-6.12), sinon NULL.',
|
||||
'custom_address' => 'Adresse texte du point libre (geocodee) — renseignee uniquement si custom.',
|
||||
'custom_latitude' => 'Latitude WGS84 du point libre (pin ajustable) — obligatoire ssi custom (RG-6.12).',
|
||||
'custom_longitude' => 'Longitude WGS84 du point libre — obligatoire ssi custom (RG-6.12).',
|
||||
'position' => 'Ordre de l etape dans la tournee (drag & drop). Unique par tournee (uq_tour_stop_position).',
|
||||
'visit_minutes' => 'Duree de visite specifique a l etape, en minutes — sinon tour.default_visit_minutes.',
|
||||
'leg_distance_m' => 'Cache : distance depuis l etape precedente, en metres (calcule). Lecture seule API (M6.4).',
|
||||
'leg_duration_s' => 'Cache : temps depuis l etape precedente, en secondes (calcule). Lecture seule API (M6.4).',
|
||||
'eta' => 'Heure d arrivee estimee a l etape (RG-6.11, calculee). Lecture seule API.',
|
||||
] + self::timestampableBlamableComments(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user