Skip to content

Module — Tableaux de chargement

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

Rôle

Organisation opérationnelle des cars/horaires/arrêts pour un groupe d'occurrences au moment du départ. Le tableau de chargement (aller, Returns = false) ou de dépose (retour, Returns = true) liste les passagers par arrêt et par car. Inclut le mécanisme des petits bus de chargement qui ramassent les passagers par région et les amènent au car principal via un transfert.

Emplacement

  • Pages : src/Web/Pages/LoadingTables/ (Index, Create, Update, Timetable, DriverFolder, partials)
  • Service : src/Application/Services/Entities/LoadingTableService.cs
  • DTOs : Application/Dtos/LoadingTableLineDto.cs, LoadingTableLineStopDto.cs, LoadingTableLineResourceDto.cs, LoadingTableTimetableStopDto.cs
  • Helpers : Application/Library/LoadingTableLineColors.cs, BaseModelLoadingTable.cs

Entités principales

  • LoadingTable — entête du tableau.
    • Returns : false = aller (chargement) / true = retour (dépose).
    • Seaside : flag dédié aux voyages balnéaires.
    • OrderResourcesByDate : flag de compatibilité ascendante (préserve les anciens tableaux face à la nouvelle fonction de tri par date — laissé tel quel, pas un nouveau choix conscient).
    • SearchDate, TravelCategoryId (pour Seaside) : critères de groupement.
    • Comments, StatusLoadingTableStatus (InProgress = « En cours », Completed = « Validé »). Éditable à deux endroits : la modale Update (Form.cshtml) ET directement sur la page du tableau (Timetable.cshtml, sélecteur à côté de « Sauvegarder » + badge dans l'en-tête, rendu côté serveur, rafraîchi au reload après save — pas de JS dynamique, cohérent avec le reste de la page « à l'ancienne »). Sur Timetable, le statut posté est capturé avant LoadData (qui réécrirait Input avec les valeurs DB, même pattern que Comments) puis appliqué sur LoadingTable.Status dans le bloc save de OnPost. Statut purement informatif (ne verrouille rien).
    • Occurrences, OccurrencesBack : occurrences groupées dans le tableau.
  • LoadingTableEntry — ligne éditable manuelle ajoutée par l'utilisateur. Représente un car de chargement (petit bus régional) ou un car supplémentaire.
  • LoadingTableEntryStop — un stop de cette ligne, avec nb de passagers + flag Transfer (passage du petit bus vers le grand car).
  • LoadingTableTimetableStop — surcharge des horaires pour ce tableau (supplante Timetable/TimetableStop de référence).

Règles métier spécifiques

1ère ligne éditable = chauffeur du voyage

Pattern appliqué côté catalogue (non-seaside) depuis l'origine et seaside depuis mai 2026 :

  • Le chauffeur du car principal du voyage (lu depuis Visual Planning) est recopié dans editables[0].DriverName, avec DriverEditable = false et VehicleName également.
  • Les chauffeurs de chargement/dépose VP s'écrivent à partir de editables[1..].
  • L'offset est calculé par resourceCount :
    • Catalogue : list.Count(l => l.IsTravel && !l.IsMainTravel) — nombre de lignes véhicules du voyage.
    • Seaside : 1 si editables[0].DriverEditable == false (chauffeur voyage écrit), sinon 0.
  • La copie remplace <br> par \n (la ligne éditable est rendue en <textarea> ; la ligne IsMainTravel est rendue en @Html.Raw et conserve les <br>).

Lignes du tableau

Le LoadingTableService.BuildList construit la liste affichée dans cet ordre :

  1. Lignes voyage (IsTravel = true, IsMainTravel = true pour la principale, plusieurs si multi-véhicules).
  2. Lignes accommodation (Seaside) — read-only, partagent la couleur du main travel.
  3. Lignes éditables (Editable = true = LoadingTableEntry ajoutées manuellement).
  4. Total + Total (diff) en bas.

Ordre des lignes hôtels (Seaside) : les AccommodationCategory sont issues des bookings (ReverseMap, branche else). L'entité n'a pas de champ d'ordre, donc l'ordre DB est non-déterministe. Ordre client codé en dur (seasideCategoryOrder, match par préfixe insensible casse — tolère les suffixes « de Mar ») : Rosas, Tossa, Lloret, Malgrat, Santa Susanna, Pineda (noms BD réels : Rosas, Tossa de Mar, Lloret de Mar, Malgrat de Mar, Santa Susanna, Pineda de mar). Toute catégorie hors liste passe à la fin. À migrer vers un champ Order configurable si la liste évolue souvent.

Ligne « Sans hôtel Buchard » (Seaside) : quand des catégories d'hébergement existent ET qu'au moins un booking n'a pas d'hôtel Buchard (b.TravelOccurrence.Travel.Accommodation?.Category == null, typiquement un vol simple), une ligne supplémentaire est ajoutée en dernier (après les catégories ordonnées), marquée IsNoAccommodation = true, AccommodationCategoryId = null, IsMainTravel = false. Elle joue le rôle de bucket : l'affectation passagers (AccommodationCategoryId == null) y range les passagers sans hébergement, sinon non rattachés (la seule autre ligne à AccommodationCategoryId null est l'en-tête IsMainTravel, exclue de mainTravelList). Le flag IsNoAccommodation l'exclut aussi du bloc « seaside main-travel driver » (sinon elle ré-écrirait editables[0] et hériterait d'un chauffeur, contrairement aux lignes catégories). Sans aucune catégorie (vraie branche else, ligne unique AccommodationCategoryId null, IsNoAccommodation = false), ce bucket n'est pas créé : la ligne unique ramasse déjà tout le monde.

Plan de déroulement du bucket : la ligne « Sans hôtel Buchard » a son propre lien « Plan de déroulement » dans Timetable.cshtml (branche else if (loadingTable.IsNoAccommodation)), via le query param noAccommodation=true (distinct de accommodationCategoryId, qui lui ne sert qu'aux catégories réelles). Côté Plan.OnGetLoadingTableTravelListService.GetPassengersPlanByLoadingTable(..., bool noAccommodation) : quand noAccommodation, le filtre ne garde que les passagers dont Travel.Accommodation.CategoryId == null. Le titre du plan affiche « Sans hôtel Buchard » via AccommodationCategoryName. Le PDF fusionné de SeasideDates/Listing.OnGetExport n'est pas concerné : il appelle OnGetLoadingTable sans filtre → plan global incluant déjà tous les passagers.

Ressources (chauffeur/hôtesse/véhicule) sur le Plan de déroulement : Plan.OnGetLoadingTable ne remplissait pas Resources (cellules Chauffeur/Hôtesse/Véhicule vides côté seaside) — seul OnGet (chemin catalogue via occurrenceId) le faisait. La logique VP est désormais factorisée dans Plan.PopulateResourcesFromVp(navNo, returns), appelée par les deux handlers. Le navNo utilisé : pour le seaside, celui de la rotation (ISeasideDateService.GetByCategoryAndDate(TravelCategoryId, SearchDate, returns), même source NavNo que ReverseMap) ; sinon le NavNo de la 1ʳᵉ occurrence du tableau. La vue _plan.cshtml (branche LoadingTableId) rend les noms groupés par ResourceType. Si le NavNo est introuvable, Resources reste vide (pas de régression). Le constructeur de Plan prend donc ISeasideDateService : pensé à propager aux 4 sites qui l'instancient manuellement (DriverFolder, Lists ×2, SeasideDates/Listing).

Cas particulier seaside sans accommodation categories (branche else dans ReverseMap) : une seule ligne est créée, sans IsMainTravel (sinon le comptage des passagers casse — la recherche booking se fait dans list.Where(l => !l.IsMainTravel)). Pour quand même récupérer les chauffeurs VP, le bloc seaside main-travel à ReverseMap (cf. condition loadingTableLineDto.IsMainTravel || (!loadingTableLineDto.AccommodationCategoryId.HasValue && !loadingTableLineDto.IsNoAccommodation)) s'applique aussi à cette ligne unique. Le && !IsNoAccommodation exclut la ligne bucket « Sans hôtel Buchard » (voir ci-dessous), qui partage AccommodationCategoryId null mais ne doit pas porter de chauffeur. Sémantiquement, une ligne sans AccommodationCategoryId joue le rôle de "main travel" pour l'affectation chauffeur, qu'elle soit marquée IsMainTravel ou non.

OrderResourcesByDate

Quand activé, les ressources VP sont récupérées avec leur Evénement-Date de début et filtrées sur la date exacte de LoadingTableLineDto.Date.

⚠ L'endpoint events de VP trie ses résultats par le PREMIER attribute= demandé (vérifié en lecture sur la prod, août 2026 : même requête, seul l'ordre des attribute change → l'ordre des lignes change). Le tri porte sur le libellé de la ressource, pas sur son UID. Conséquences :

  • GetTravelDriversAndHostsByOccurrence / GetLoadingDriversByOccurrence / GetUnloadingDriversByOccurrence demandent COLLABORATEUR avant VEHICULE ⇒ l'ordre effectif est alphabétique par nom de chauffeur, pas l'ordre des véhicules. Comme les groupes sont ensuite affectés aux lignes par index, les chauffeurs peuvent apparaître « inversés » par rapport à l'ordre des cars.
  • Le flag ne change rien sur une course d'un jour : un NavNo 1-jour = une occurrence = une seule date, donc trier ou filtrer par date est un no-op et la clé qui départage reste COLLABORATEUR.
  • Correctif connu (non appliqué, arbitrage Buchard en cours août 2026) : passer attribute=VEHICULE avant attribute=COLLABORATEUR dans ces trois méthodes.

Aucun attribut VP ne relie un événement à une ligne du voyage : COURSE vaut la même valeur pour tous les événements d'un NavNo (c'est le voyage). Le lien ligne ↔ véhicule n'existe donc que côté Horizon, implicitement, par l'ordre.

Helvetic (export)

Pour les tableaux Helvetic (vols charter Sion-PMI), un export Excel passagers est généré via Pages/Occurrences/HelveticListPassengers.cshtml.cs (et son cousin BuildHelveticWorksheet réutilisé pour Seaside via Pages/SeasideDates/HelveticListPassengers.cshtml.cs). Salutation : Mr / Ms adultes, INF (<2 ans), CHD (2-12). Nom/Prénom (LASTNAME/FORENAME) passent par FormatName : RemoveDiacritics (é→e), tirets/apostrophes → espace, 1ère lettre majuscule (même esprit que FormatStr d'EuropaPark, mais EuropaPark ne remplace pas les séparateurs).

Dossier chauffeur

Export ZIP via Pages/LoadingTables/DriverFolder.cshtml.cs (Dossier chauffeur - {Nom}.zip). Contenu :

  • Un ou plusieurs plans de déroulement (_plan.cshtml → PDF) — découpage selon le type de voyage, cf. ci-dessous
  • Plan(s) de salle véhicule (_vehiclePlan.cshtmlVéhicule - {…}.pdf) : un par véhicule pour un balnéaire, un par occurrence sinon
  • Tableau de chargement.xlsx

Découpage des plans de déroulement

  • Catalogue / balnéaire : un PDF par trajet (Drive), nommé {drive.Name}.pdf, plus un PDF par car de chargement (août 2026) nommé Chargement - {véhicule} - {chauffeur}.pdf (Dépose - … quand Returns).
    • Un car de chargement = une ligne éditable du tableau. TravelListService.GetLoadingLinesPlanByLoadingTable synthétise un DriveDto par LoadingTableEntry à OccurrenceId == null, dont les arrêts sont ceux dont la Value est renseignée, puis passe par PopulateDriveWithPassengers.
    • ⚠ Passe volontairement séparée de GetPassengersPlanByLoadingTable. PopulateDriveWithPassengers dédoublonne les arrêts et affecte chaque passager au premier arrêt correspondant : fusionner les deux jeux viderait l'un des deux PDF, un même arrêt figurant à la fois sur un trajet de ligne et sur le car de chargement qui le dessert. Conséquence assumée : un passager apparaît dans les deux PDF — deux chauffeurs, deux besoins.
    • ⚠ Seconde instance de Plan obligatoire dans DriverFolder : le PageModel mémoïse Drives, le LoadingTable et les notes ; réutiliser celle des trajets rendrait les mauvais plans. Le jeu chargement est pré-affecté à Drives avant le premier OnGetLoadingTable, ce qui court-circuite le chargement normal.
    • LoadingTableLineDto.IsLoadingLine distingue les lignes éditables « car de chargement » de celles qui recopient un véhicule du voyage (editables[0] porte le chauffeur du voyage, cf. plus haut). Posé par position dans ReverseMap, à partir de l'offset resourceCount : une ligne saisie à la main, que VP n'a jamais remplie, reste un car de chargement et sort donc son PDF.
    • L'arrêt de transfert (LoadingTableEntryStop.Transfer, là où les passagers montent dans le grand car) est reporté sur StopDto.Transfer et rendu en « - TRANSFERT » dans _plan.cshtml. Sans effet sur les autres plans, où le flag est toujours faux.
    • Une ligne sans passager ne produit aucun fichier. Deux lignes partageant véhicule et chauffeur produisent deux fichiers (un par ligne, décision Buchard août 2026), le second suffixé (2). Pas de regroupement par véhicule ici, contrairement au 1-jour.
    • Le balnéaire ne sépare pas par catégorie d'hôtel sur ces listes.
  • Course d'un jour (août 2026) : un PDF par véhicule de ligne, nommé {véhicule} - {chauffeur(s)}.pdf (ex. C052 - A Krattinger Stéphane.pdf).
    • Les « trajets » d'un plan 1-jour ne sont pas les lignes du voyage : TravelListService.GetPassengersPlanByLoadingTable en synthétise un par ligne éditable du tableau (LoadingTableEntry avec OccurrenceId == null — celles à OccurrenceId non nul portent les couleurs des lignes voyage et ne comptent pas). DriveDto.EntryId conserve ce lien.
    • Le regroupement se fait sur le VehicleName que ReverseMap affiche sur la ligne, donc sur l'ordre de sortie VP tel qu'il apparaît à l'écran : l'export ne peut pas diverger du tableau. Un véhicule qui dessert plusieurs lignes ne produit qu'un fichier (ce qui absorbe au passage la duplication décrite plus bas).
    • Le chauffeur affiché en tête de PDF et repris dans le nom de fichier est le chauffeur de ligne (VP, étape « Voyage (ou aller) », via GetTravelDriversAndHostsByOccurrence), jamais le chauffeur de chargement (étape « Chargement »). DriverFolder.GetLineResourcesByVehicle construit la table véhicule → ressources typées et écrase Plan.Resources après l'appel à OnGetLoadingTable (qui, lui, remonte toutes les ressources de l'occurrence). Seules les ressources ResourceType.Driver alimentent le nom de fichier — la méthode VP remonte aussi les hôtesses.
    • Une ligne sans véhicule et sans passager ne génère aucun fichier ; avec des passagers elle sort en Sans véhicule - {chauffeur}.pdf.
    • Les passagers dont le lieu n'est coché sur aucune ligne tombent dans le bucket « Trajet indéterminé » (PopulateDriveWithPassengers, DriveDto.Id == Guid.Empty et EntryId == null) et sortent dans Trajet indéterminé.pdf. Avant août 2026 ce bucket sortait sous le nom trompeur Trajet #N.pdf : l'ancienne boucle for (idx = 0; idx <= 30; idx++) s'arrêtait sur DriveId == null, or Guid.Empty n'est pas null. C'est un signal de saisie incomplète du tableau, pas une ligne du voyage.

Sélection multi-trajets : Plan.DriveIds + Plan.SelectedDrives() (DriveIds → sinon DriveId → sinon tous) permettent de rendre un PDF couvrant plusieurs trajets. _plan.cshtml passe par ce helper partout, et Plan.HasDriveSelection pilote l'affichage du total « sur la ligne ». Les appelants historiques (Lists, SeasideDates/Listing, écran Plan) ne passent pas driveIds et gardent leur comportement.

Performance (chantier du 11.08.2026)

L'écran Timetable, l'export et surtout la sauvegarde étaient lents. Mesures faites via sys.dm_exec_query_stats et chronométrage des appels VP :

  • TravelService.Get coûtait 43 requêtes SQL, ~294 ms de SQL et 28 231 lignes matérialisées et trackées, par appel — pour un voyage 1-jour type Europa-Park (86 occurrences, 1 740 réservations, 4 751 passagers). ReverseMap l'appelait pour lire uniquement travel.TravelDrives. Remplacé par une projection _db.TravelDrives.AsNoTracking().Where(td => td.TravelId == …).Select(td => td.DriveId). L'ordre est préservé : la clé de TravelDrive est (TravelId, DriveId).
    • Effet de bord supprimé : occurrence.OneDayTravelDriveOccurrences et OneDayTravelDriveOccurrences.Drive n'arrivaient que par le navigation fixup de TravelService.Get. LoadingTableService.Get les inclut désormais explicitement, et le code lit drive.DriveId au lieu de drive.Drive.Id. Ne pas retirer ces Include.
  • Les lectures VP events sont lentes : 0,7 à 3,5 s chacune (resources reste à ~57 ms). ReverseMap en enchaînait six en séquence, soit ~2,2 s mesurées.
    • GetVehiclesByOccurrence était assigné à une variable jamais lue → supprimé (~700 ms).
    • GetTransferDriversAndHostsByOccurrence ne sert que la branche Returns ; GetReturnVehiclesByOccurrence n'est nécessaire que si !isOneDay || Returns ; la boucle chargement/dépose lisait les deux directions pour n'en garder qu'une. Tout est conditionné.
    • PrefetchVisualPlanningAsync émet les lectures restantes en Task.WhenAll (une fois par NavNo distinct) pour réchauffer le cache de VisualPlanningService ; les await des boucles tombent ensuite sur le cache. Appelé deux fois : sur les NavNo d'occurrence, puis sur ceux des lignes construites (le balnéaire porte le NavNo de la rotation, inconnu avant).
    • Le cache de VisualPlanningService est passé en ConcurrentDictionary (prérequis) et GetVoyageByNavNo / GetCourseByVoyage, qui n'étaient pas cachés, le sont désormais.
  • Timetable.OnPost rechargeait tout deux fois pour rien : les LoadData() d'après-sauvegarde et d'après-suppression précédaient un RedirectToPage, qui déclenche un GET refaisant le travail. Supprimés. Celui d'après AddEntry est conservé.
  • UpdateTimeTableStops marquait Update() sur chaque passager, même inchangé, donc un UPDATE complet par passager. Ne marque plus que ceux dont l'horaire change réellement (comparaison sur la FK, la navigation n'étant pas incluse par GetBookings).
  • Le push des compteurs vers VP sort de la requête via IBackgroundTaskQueue — cf. architecture/adr/0008-in-process-background-task-queue.md.
  • Export xls : le _userManager.FindByEmailAsync(...).Result (sync-over-async) est passé en await, sorti avant la construction des cellules. Un utilisateur introuvable ne fait plus planter l'export (?.FullName).

Chiffres mesurés (dossier aaaaaaaaaa, 17.11.2026 Conthey — 98 passagers, 8 lignes éditables, 45 arrêts)

Instrumentation en place : DriverFolder log ses phases, RazorRendererService sépare rendu Razor / polices / iText par PDF, TravelListService sépare drives / passagers / NAV / populate. Tout en Information, visible dans la console Serilog.

avantaprès
1er export après redémarrage31,3 s14,4 s
export suivant21,8 s6,1 s

Ce que le diagnostic a corrigé, par ordre de gain :

  • NAV GetAllByNos : 6,9 s → ~1 s. PopulateDriveWithPassengers demandait à BC la fiche de tous les clients du tableau (42) alors que les fiches ne servent que de repli téléphone, quand ni le passager ni son client n'en ont un dans Horizon — 4 clients concernés ici. La liste est désormais réduite à ces clients-là, et l'appel disparaît quand personne ne manque de téléphone.
  • Polices iText : 0,5 à 6 s sortis du chemin de la requête. Le FontProvider partagé est construit au démarrage par FontWarmupHostedService ; c'était le premier export après un redémarrage qui payait le scan des polices système.
  • VehiclePlan.OnGet : ~3 s supprimées. La ligne construisait un TravelDto (TravelService.Get + ReverseMap) que plus aucune vue ne lisait — les plans affichent Occurrence.MainTravelOccurrence.Travel, déjà inclus par OccurrenceService.Get jusqu'à TravelLines.Line. Propriété Travel supprimée avec l'assignation.
  • xlsx : 1,2 s → 0,3 s. ExportXlsStream(LoadingTable, lines) réutilise ce que le dossier chauffeur vient de calculer au lieu de refaire Get + ReverseMap.

Second passage (11.08.2026), sur un catalogue avec cars de chargement41b58b41, Croisière Seine, 90 passagers, 4 trajets de ligne + 5 lignes de chargement, donc 10 PDF au lieu de 5 : 18,9 s → 11,5 s à chaud.

  • OccurrenceService.GetTravelVehicleSeatOccupancy : 5 872 ms → 141 ms, soit 35 % de l'export à lui seul. Requête IQueryable non matérialisée rejouée par siège — détail dans occurrence-capacity.md > Points d'attention. Pré-existant, et partagé par plusieurs écrans.
  • NAV appelé deux fois (2,4 s + 2,2 s) depuis l'ajout des PDF de chargement : les deux passes calculent le même jeu de passagers, donc les mêmes fiches BC. TravelListService._navFicheCache les mémoïse pour la durée de la requête (service scoped), clé = la liste triée des NavNo.
  • ExportXlsStream(id) refaisait un Get + un ReverseMap en catalogue : AddLoadingLinePdfs retourne désormais ses lignes et le dossier les passe à la surcharge ExportXlsStream(loadingTable, lines). 1,2 s → 139 ms.
  • Reste : NAV 2,3 s (13 clients sans téléphone dans Horizon) et ~4,8 s d'iText répartis sur 9 PDF.
  • Nommage des PDF de chargement : véhicule + chauffeur, sinon le libellé saisi sur la ligne (LoadingTableLineDto.TravelName, ex. « CAR 1 »), sinon ligne N.

⚠ Piste abandonnée sur mesure : alléger l'arbre d'Include de LoadingTableService.Get. Il coûte 2,2 s au premier appel puis 34 ms — c'était de la compilation de plan d'exécution SQL, pas du volume de données. Toucher aux Include (avec le risque de fixup) ne gagnerait rien.

Plancher actuel : iText, 2,75 s pour le plan de déroulement de 98 passagers (122 Ko de HTML → 33 Ko de PDF), soit ~45 % de l'export à chaud. Le rendu Razor lui-même est négligeable (3 ms). Descendre plus bas suppose de toucher au balisage ou au CSS des templates PDF (border-collapse: collapse + bordure sur chaque cellule sont les coupables classiques côté iText), donc d'accepter un risque visuel.

Génération PDF (dossier chauffeur)

  • RazorRendererService partage un FontProvider unique (Lazy<FontProvider> statique). Sans provider explicite, iText en construit un par conversion et re-scanne les polices système à chaque PDF — payé N fois pour un dossier chauffeur. Même jeu de polices que le défaut implicite d'iText (standard + shipped + système), simplement construit une fois. ⚠ Partageable uniquement parce qu'aucun template PDF ne déclare @font-face : rien n'ajoute de police au FontSet pendant la conversion, donc les exports concurrents ne font que lire. À revoir si un template embarque une police un jour.
  • Plan.OnGetLoadingTable mémoïse le LoadingTable (requête à ~20 Include), les notes d'occurrence et le NavNo de rotation balnéaire, sur la clé (loadingTableId, returns). Un dossier chauffeur rend un PDF par véhicule depuis la même instance de Plan et refaisait ces trois requêtes à chaque PDF. Les appelants qui n'appellent qu'une fois ne voient aucune différence.
  • Piste non prise (11.08.2026, arbitrage utilisateur — crainte de régression sur les chemins de fichiers) : _VehicleDeck.cshtml:84 rend le fond du plan de véhicule en background-image: url('https://{host}/Uploads/…'), donc iText ouvre une connexion HTTPS vers l'application elle-même, par deck et par PDF, depuis l'intérieur de la requête qui génère le PDF. Le fichier est pourtant sur le disque et VehiclePlan.cshtml.cs:47 le lit déjà localement pour en mesurer les dimensions. Idem RazorRendererService:83, SetBaseUri("file:///app/assets/") est un chemin container codé en dur. Probablement le plus gros gain restant sur l'export PDF.
  • Reste non traité : VisualPlanningService.CallAsync peut renvoyer null sur HttpRequestException, ce qui fait planter la mise en cache juste après.

Points d'attention / pièges

  • Visual Planning bidirectionnel : Horizon lit beaucoup depuis VP (via VisualPlanningService.GetTravelDriversAndHostsByOccurrence, GetLoadingDriversByOccurrence, etc.). Horizon écrit uniquement le nombre de passagers par date (events) aller/retour. Aucune écriture d'assignment chauffeur/véhicule.
  • Resource.Type = Vehicle = véhicule physique (ex: "MAN 238 acheté 2020-01-08"), distinct de VehicleType (catégorie type "car 44 places").
  • OrderResourcesByDate est un flag legacy : pas un choix conscient récurrent. Ne pas activer pour de "nouvelles" tables sans raison.
  • Couleurs auto : LoadingTableLineColors.Colors[idx] pour les colorations par ligne ; les stops avec passagers récupèrent la couleur de la ligne éditable correspondante.
  • HasImpactOnOccurrenceOccupancy.Contains(...) est utilisé pour filtrer les bookings retenus dans le tableau — inclure aussi PendingWebOrMobile avec TransactionId != null OU en hold (CreatedAt >= ValidatingBookingStatus.PendingHoldCutoff()) quand on étend ce filtre. Cf. domain/business-rules.md > Hold des réservations web.
  • Lignes éditables vides : si l'utilisateur n'a pas cliqué "Ajouter une ligne", le chauffeur du voyage n'apparaît pas (cohérent avec catalogue, pas de magie auto).
  • Course 1-jour multi-lignes — duplication chauffeur quand VP a moins de véhicules que de lignes : la boucle d'affectation 1-jour (LoadingTableService.ReverseMap, branche isOneDay) itère une fois par ligne voyage (Ligne 1, Ligne 2…) en réutilisant un resourceCount global, et reconstruit resourcesByVehicle à chaque tour à partir des events VP Voyage (ou aller) — or le NavNo est le même à chaque itération, donc les mêmes groupes. Dès qu'il y a moins de groupes que de lignes voyage, les groupes sont réémis sur les lignes suivantes → même chauffeur + même véhicule sur deux lignes éditables. Deux causes courantes : un seul car dessert plusieurs lignes (cas normal, VP a raison), ou un collaborateur non synchronisé fait disparaître son groupe (cf. resources-visual-planning.md : Fonctions VP codées en dur dans SyncHostsAndDrivers). Non corrigé volontairement (arbitrage Buchard, août 2026) : ajouter une ligne au tableau reste la façon de saisir un car supplémentaire. Le dossier chauffeur n'en souffre pas, son regroupement par véhicule fusionne les doublons.
  • Colonne « Aucun lieu » : LoadingTableLineDto.NoStopTravelers compte, par ligne, les passagers dont le lieu n'est pas une colonne du tableau — LoadingStop/UnloadingStop null OU lieu retiré/remplacé du trajet (donc absent de lt.Stops, cloné depuis les trajets). Rempli dans ReverseMap (bloc d'affectation passagers), agrégé sur les lignes « voyage principal » et « Total », rendu en dernière colonne de Timetable.cshtml (après « Total »). Ces voyageurs ne sont pas comptés dans les totaux par lieu ni TravelLineTotal/LoadingUnloadigLineTotal (informationnel). Remplace l'ancien comportement où un lieu inconnu créait une colonne fantôme invisible qui gonflait le total de ligne.
  • Plus de rapprochement flou (Levenshtein) : le matching lieu↔colonne / passager↔stop se fait strictement par Stop.Id (catalogue de Stops centralisé fiable). Toutes les méthodes FindStop/FindTimetableStop à base de LevenshteinDistance ont été retirées de LoadingTableService, Timetable.cshtml.cs et TravelListService (juin 2026). Conséquence voulue : un lieu réellement absent n'est plus rattaché de force → il tombe en « Aucun lieu » (tableau) ou « Trajet indéterminé » (plan). Le legacy (GlobeLegacyService, WebSiteOldService) garde son Levenshtein (code mort, hors-scope).

Conventions locales

  • Razor : Pages/LoadingTables/Timetable.cshtml(.cs) est l'écran principal d'édition (comme l'a noté l'auteur en commentaire : "maybe the worse written code I've ever made"). Mêmes méthodes appelées par GET et POST.
  • Couleurs HTML : générées par LoadingTableLineDto.GetHtmlColor().

Dépendances

  • occurrence-capacity (groupe d'occurrences cible).
  • lines-stops (Lines, Drives, Stops, Timetables consommés).
  • resources-visual-planning (chauffeurs/hôtesses lus de VP).
  • → exports : DriverFolder.xlsx, Passengers.xlsx Helvetic.

Contributors

No contributors

Changelog

No recent changes