Skip to content

Module — Ressources humaines & Visual Planning

Synchronisé avec le code. Mettre à jour à chaque changement structurel.

Rôle

Référentiel des chauffeurs / hôtesses-stewards / musiciens / véhicules physiques utilisés par Buchard, et synchronisation avec Visual Planning (VP) — système externe propriétaire (Stilog) où les dispatchers organisent qui conduit quoi quand.

Emplacement

  • Pages : src/Web/Pages/Resources/
  • Service : src/Application/Services/Entities/ResourceService.cs
  • Sync VP : src/Application/Services/VisualPlanningService.cs, src/Application/VisualPlanning/ApiClient.cs, Entity.cs

Entités principales

  • Resource — entité Horizon liant une personne ou un véhicule à son VisualPlanningId.
    • TypeResourceType : Driver (chauffeur), Host (hôtesse/steward), Musician, Vehicle (véhicule physique).
    • Name, VisualPlanningId, Vehicle (lien vers entité Vehicle détaillée si Type=Vehicle), métadonnées (description, image musicien…).
  • OccurrenceResource : assignment d'une Resource à une Occurrence.
  • OccurrenceResourceSeat : siège dans le car alloué à la ressource.
  • OccurrenceResourceAccommodation : chambre allouée à la ressource sur place.
  • Vehicle (catalogue produit) — distinct de Resource avec Type=Vehicle. Voir product-catalog.md.

Synchronisation Visual Planning

Lecture (massive)

Le service VisualPlanningService interroge l'API HTTP de VP pour récupérer les assignments :

  • GetTravelDriversAndHostsByOccurrence(navNo, orderByDate) — chauffeurs/hôtesses du voyage.
  • GetTransferDriversAndHostsByOccurrence — transferts (retour balnéaire).
  • GetReturnVehiclesByOccurrence, GetReturnTravelDriversAndHostsByOccurrence — retour.
  • GetLoadingDriversByOccurrence, GetUnloadingDriversByOccurrence — chauffeurs des petits cars de chargement (transfert vers le grand car).
  • GetVehiclesByOccurrence, GetVehicle(id) — véhicules.
  • SyncHostsAndDrivers() — sync daily du référentiel des collaborateurs actifs (mapping VisualPlanningIdResource). Filtré par COLLABORATEUR-Fonction : seuls les collaborateurs dont la Fonction VP figure dans le dictionnaire codes codé en dur (VisualPlanningService.cs, ~6 Fonctions Driver + 2 Host) sont synchronisés. Un collaborateur dont la Fonction n'y est pas (ex: un nouveau type de sous-traitant) n'aura jamais de Resource → son VisualPlanningId ne matchera nulle part. Ajouter son code Fonction au dictionnaire pour le synchroniser.

Timeout et dégradation (VP injoignable)

ApiClient applique un timeout de 10 s par appel HTTP (VisualPlanning:TimeoutSeconds dans les appsettings, défaut 10 si absent ou invalide). Avant août 2026 aucun timeout n'était fixé : on héritait du défaut HttpClient (100 s) et, comme les TaskCanceledException de timeout n'étaient pas rattrapées (seules HttpRequestException / JsonException l'étaient), un VP lent bloquait puis faisait planter la page appelante — notamment l'éditeur de réservation (cf. booking.md, OnGetTravel).

Comportement en cas d'échec (timeout, VP down, JSON invalide) :

  • ApiClient.CallAsync / GetResourcesAsync renvoient null, UpdateResourceAsync renvoie false. L'échec est silencieux côté appelant (aucune interface ne remonte l'erreur — choix assumé pour ne pas propager un ServiceResult dans toute la chaîne) mais il est capturé dans Sentry avec le tag service=VisualPlanning et l'URL appelée.
  • VisualPlanningService renvoie alors une collection vide (NoEntities) et ne met rien en cache : l'écran s'affiche sans les données VP (véhicule planifié, chauffeurs) au lieu de tomber en erreur 500.
  • Exception : SyncHostsAndDrivers abandonne toute la sync si un appel échoue — une réponse partielle désactiverait (Enabled = false) toutes les ressources absentes de la réponse.
  • Resources/Create et Resources/Update refusent explicitement l'enregistrement d'une ressource Vehicle si VP est injoignable (le InternalNumber vient de VP).

La contrepartie de ce silence est la page Système > État des services et l'endpoint /api/health/services, qui sondent VP en continu (modules/monitoring.md).

Temps de réponse observés sur testvisualplanning (août 2026) : 0.07 à 0.6 s en régime normal, mais ~11 s sur le tout premier appel après une longue inactivité (démarrage à froid côté VP). Un timeout à 10 s peut donc sacrifier ce premier appel ; monter TimeoutSeconds à 15 si ce cas se manifeste en prod.

Chaque résultat est un IEnumerable<IEnumerable<Entity>> (entité VP avec GetValue("COLLABORATEUR"), GetValue("VEHICULE"), GetValue("Evénement-Date de début"), etc.).

Charge VP — incident des 17-18.08.2026

Diagnostic ARC Logiciels (fournisseur VP) : la base VP se verrouille, toutes les requêtes se bloquent et l'impatience des utilisateurs ajoute des requêtes — effet boule de neige. Deux causes cumulées : le volume d'appels venant d'Horizon, et un moteur de requêtes VP v8 dont certains filtres scannent l'intégralité du planning (optimisé seulement en v9). ARC a ajouté des index et des limites de son côté.

Coût d'un affichage de tableau de chargement, par NavNo distinct (le cache de VisualPlanningService dédoublonne à l'intérieur d'une requête, pas entre requêtes) :

Casdevelopfeature/loading-table-by-vehicule
Catalogue (ni retour ni 1 jour)6 GET /events3
Course d'un jour62
Balnéaire retour6–74–5

Ce que la branche corrige, et qu'il faut ne pas réintroduire :

  • GetVehiclesByOccurrence était appelé pour rien dans LoadingTableService (develop:940) : la variable visualPlanningOneWayVehicles n'est jamais relue. Un GET /events par NavNo et par affichage, jeté.
  • Les deux directions étaient lues pour n'en garder qu'une (GetLoadingDriversByOccurrence et GetUnloadingDriversByOccurrence, puis choix selon loadingTable.Returns) : la branche ne demande que celle qui est consommée.
  • GetVoyageByNavNo / GetCourseByVoyage mis en cache (_idCache) : UpdateCoursePassengersByVoyage coûte 3 appels (GET VOYAGE, GET COURSE, PUT), un NavNo déjà résolu dans la même requête ne coûte plus que le PUT.
  • Le prefetch est borné à 3 appels simultanés (LoadingTableService.VisualPlanningPrefetchConcurrency). PrefetchVisualPlanningAsync lance les lectures en parallèle pour la latence de la page, mais un Task.WhenAll non borné envoyait jusqu'à ~30 requêtes lourdes d'un coup — exactement la rafale qui verrouille VP. Ne pas retirer ce sémaphore : sans lui on échange une longue séquence contre un pic.

Pistes de réduction identifiées, pas encore faites (à réévaluer après le merge de la branche) :

  1. Cache inter-requêtes (IMemoryCache, 30-60 s, clé navNo + méthode + byDate) : aujourd'hui le cache meurt avec la requête, trois personnes sur le même départ = trois fois les mêmes appels.
  2. Coupe-circuit : après N échecs VP consécutifs, cesser d'appeler VP pendant 1-2 min. Le timeout de 10 s protège Horizon mais pas VP — la requête abandonnée continue de s'exécuter côté VP.
  3. Demander à ARC si l'API v2 accepte plusieurs valeurs sur VOYAGE-Voyage (opérateur IN) : un tableau de chargement passerait de N requêtes à 1.

/Tools/Misc/UpdateAllFutureOccurrencesPassengers parcourt toutes les occurrences futures par lots de 50 et fait 3 appels VP par occurrence (+3 par trajet ayant son propre NavNo) : plusieurs milliers d'appels en une seule requête HTTP. C'est le rattrapage des compteurs quand ils ont dérivé, mais à ne jamais lancer pendant un incident de performance VP.

Dérive assumée des compteurs : pendant un gel VP, un PUT de compteur passagers dépasse les 10 s de timeout (relevé ARC : jusqu'à 445 s) et est abandonné en silence. Les compteurs VP dérivent alors jusqu'au prochain enregistrement de la réservation ou au rattrapage manuel ci-dessus. Arbitrage Buchard du 18.08.2026 : acceptable, ne pas bloquer l'utilisateur pour ça.

Écriture (limitée)

Horizon écrit dans VP uniquement le nombre de passagers (aller / retour) sur les COURSE. Aucun assignment chauffeur/véhicule n'est poussé — le dispatcher fait ce travail dans VP directement.

  • UpdatePassengersTotalByOccurrence(occurrenceId) — appelée à chaque sauvegarde de résa / tableau de chargement : pousse le compteur du départ (passagers comptés par OccurrenceId, aller = retour) puis un compteur par trajet ayant son propre NavNo, avec aller et retour distincts (Journées Buchard). Détail du rattachement passager → trajet et de ses limites : domain/business-rules.md > NavNo par trajet — compteurs passagers Visual Planning.
  • UpdatePassengersTotalByNavNo(navNo)balnéaire uniquement (LoadingTableService, branche Seaside) : la rotation n'est identifiée que par son SeasideDate.NavNo, les passagers sont donc comptés via Occurrence.NavNo == navNo. Partout ailleurs on passe par l'id.
  • UpdateCoursePassengersByVoyage(navNo, aller, retour) — primitive bas niveau (résout VOYAGE → COURSE → écrit) : 3 appels HTTP, sans cache.

⚠ Les boucles qui sauvegardent plusieurs résas d'un même départ (pages Billing, /Occurrences/Cancel) appellent BookingService.UpdateAsync(booking, syncVisualPlanning: false) et poussent une fois par occurrence après la boucle. Cf. domain/business-rules.md.

Règles métier spécifiques

  • Resource.Type=VehicleVehicle entité : le premier est l'instance physique sync VP (ex: "MAN 238 acheté 2020-01-08"), le second est le modèle/configuration (ex: "car 44 places").
  • OrderResourcesByDate (sur LoadingTable) : si activé, les Entities VP retournées doivent être filtrées par date exacte (Evénement-Date de début parsé en dd/MM/yyyy HH:mm invariant culture).
  • Tableau de chargement : la lecture VP alimente automatiquement les DriverName des lignes éditables et de la ligne IsMainTravel (cf. loading-tables.md).
  • Daily worker :
    • VisualPlanningService.SyncHostsAndDrivers rafraîchit la liste des Resource actives.
    • CustomerService.CleanGuestCustomers (parallèle) purge les comptes guest.

Écran « Resources par dates » (/Resources/Occurrences)

Liste des départs à planifier (bouton Assigner/Resources/Assign). Réutilise OccurrenceService.SearchAsync (whitelist statuts Published / DraftInternalBookable).

  • Sous-menu En cours / Archivées (dans le menu latéral, comme les Dates) : liens ?archive=false / ?archive=true. OnGet(bool? archive) alimente le champ caché DataTableSearchModelInput.Archive (false = Occurrence.End >= now, true = <= now), sérialisé dans la requête DataTable via #advanced-search-form.
  • Recherche : via le champ global DataTable (search[value]GetKeyWords). Les mots-clés doivent être joints par espace (String.Join(" ", …)) — un join par virgule laissait des virgules parasites qui cassaient la recherche multi-mots (le SearchModel.Keywords re-split sur l'espace).
  • Recherche restreinte (identity-only) : ce seul écran met KeywordsIdentityOnly = trueApplyKeywordFilter ne matche que Name / ReferenceNumber / Category.Name / Country.Name, pas le corps éditorial (Description, Highlights, TravelDays.Description, …). Évite qu'un mot comme « croisière », présent dans le programme jour-par-jour d'un voyage terrestre (Oberland), ne le fasse remonter. La page Dates principale (/Occurrences) garde la recherche large.
  • Tri des colonnes : les clés filter du DataTable doivent matcher exactement (casse comprise) le nom de propriété de l'entité Occurrence. LinqExtensions.OrderBy(string) fait un p.Name == name sensible à la casse et retourne la liste non triée sans erreur si rien ne matche. D'où filter: "Start" / "End" (et non start/end) pour trier par date de départ/retour.

Points d'attention / pièges

  • VP HTTP peut être lent ou indisponible — toujours wrap dans try/catch côté LoadingTableService qui tolère les VP vides.
  • VisualPlanningId est l'ID externe ; ne pas le considérer comme stable (un collaborateur peut être recréé dans VP). La sync daily traite ces cas.
  • Mapping entity field names : les noms VP sont en français accentué (Evénement-Date de début, VEHICULE-Véhicule). Ne pas modifier sans mettre à jour côté VP.
  • Reference.cs du Connected Service Services.Nav.Resources ≠ Visual Planning. NAV "Resources" = côté ERP, c'est différent du référentiel chauffeurs Horizon (peut être utilisé pour rémunération via NAV).
  • Collaborateur VP non synchronisé → effet de bord sur le tableau de chargement 1-jour : un COLLABORATEUR sans Resource correspondante est ignoré dans LoadingTableService (lookup VisualPlanningIdnull). Sur une course 1-jour multi-lignes (Ligne A+B), la boucle d'affectation itère une fois par ligne voyage en réutilisant un resourceCount global ; si un groupe véhicule est manquant, le groupe survivant est recopié sur la ligne suivante (chauffeur ET véhicule dupliqués) au lieu de laisser la case vide. Symptôme observé : même chauffeur sur deux lignes. Cause = le collaborateur de la 2ᵉ ligne (souvent un sous-traitant) n'était pas dans les Fonctions synchronisées. Cf. loading-tables.md.
  • OccurrenceResourceAccommodation orphelines après changement d'hôtel : Resources/Assign (Vue occurrence-assign-resources) ne rend que les lignes dont l'accommodationId figure encore dans les TravelDays.Accommodations (Accommodations.vue, filtre du v-for), mais save() repost la totalité de resourceAssignment.accommodations et Assign.OnPost réécrit tout tel quel. Conséquence : si l'hôtel du voyage change après la planification des ressources, les chambres accompagnateurs de l'ancien hôtel deviennent invisibles dans l'écran de saisie mais persistent en base indéfiniment. Observé juillet 2026 sur 4 départs (Gand, Menton/Nice, croisières MS Rhône Princess/MS Van Gogh), toujours 2 chambres fantômes sans ressource assignée.
  • Toujours filtrer Occurrence.ResourceAccommodations par hébergement avant de compter : la collection couvre tous les hôtels du départ (y compris les orphelins ci-dessus). Filtre : ra.Room.RoomType.AccommodationId == accommodation.Id. Accommodations/Occupancy.cshtml.cs l'a oublié jusqu'en juillet 2026 → la ligne « Accompagnateurs » comptait 4 chambres single au lieu de 2, avec 2 lignes vides sans nom dans la liste sous le tableau (les côtés « Clients » filtraient déjà, eux).
  • Nom affiché = nom du COLLABORATEUR VP, pas le contenu des remarques. Si le vrai chauffeur est saisi en texte libre dans la remarque mais que le collaborateur de l'étape est un placeholder générique (« Mo sous traitant »), c'est le placeholder qui s'affiche. Pour afficher le vrai nom, il doit être le collaborateur de l'étape.

Conventions locales

  • L'API VP est appelée en HTTP (pas SOAP). Voir Application/VisualPlanning/ApiClient.cs.
  • Aucune authentification utilisateur (auth système). Token/credentials dans appsettings.json.

Dépendances

  • loading-tables (consomme les sync VP).
  • occurrence-capacity (assignment via OccurrenceResource).
  • → daily Worker (sync planifiée).

Contributors

No contributors

Changelog

No recent changes