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,Status∈LoadingTableStatus(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 »). SurTimetable, le statut posté est capturé avantLoadData(qui réécriraitInputavec les valeurs DB, même pattern queComments) puis appliqué surLoadingTable.Statusdans le bloc save deOnPost. 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 + flagTransfer(passage du petit bus vers le grand car).LoadingTableTimetableStop— surcharge des horaires pour ce tableau (supplanteTimetable/TimetableStopde 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, avecDriverEditable = falseetVehicleNameé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 :
1sieditables[0].DriverEditable == false(chauffeur voyage écrit), sinon0.
- Catalogue :
- La copie remplace
<br>par\n(la ligne éditable est rendue en<textarea>; la ligneIsMainTravelest rendue en@Html.Rawet conserve les<br>).
Lignes du tableau
Le LoadingTableService.BuildList construit la liste affichée dans cet ordre :
- Lignes voyage (
IsTravel = true,IsMainTravel = truepour la principale, plusieurs si multi-véhicules). - Lignes accommodation (Seaside) — read-only, partagent la couleur du main travel.
- Lignes éditables (
Editable = true=LoadingTableEntryajoutées manuellement). - 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.OnGetLoadingTable → TravelListService.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/GetUnloadingDriversByOccurrencedemandentCOLLABORATEURavantVEHICULE⇒ 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=VEHICULEavantattribute=COLLABORATEURdans 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.cshtml→Vé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 - …quandReturns).- Un car de chargement = une ligne éditable du tableau.
TravelListService.GetLoadingLinesPlanByLoadingTablesynthétise unDriveDtoparLoadingTableEntryàOccurrenceId == null, dont les arrêts sont ceux dont laValueest renseignée, puis passe parPopulateDriveWithPassengers. - ⚠ Passe volontairement séparée de
GetPassengersPlanByLoadingTable.PopulateDriveWithPassengersdé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
Planobligatoire dansDriverFolder: le PageModel mémoïseDrives, leLoadingTableet les notes ; réutiliser celle des trajets rendrait les mauvais plans. Le jeu chargement est pré-affecté àDrivesavant le premierOnGetLoadingTable, ce qui court-circuite le chargement normal. LoadingTableLineDto.IsLoadingLinedistingue 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 dansReverseMap, à partir de l'offsetresourceCount: 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é surStopDto.Transferet 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.
- Un car de chargement = une ligne éditable du tableau.
- 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.GetPassengersPlanByLoadingTableen synthétise un par ligne éditable du tableau (LoadingTableEntryavecOccurrenceId == null— celles àOccurrenceIdnon nul portent les couleurs des lignes voyage et ne comptent pas).DriveDto.EntryIdconserve ce lien. - Le regroupement se fait sur le
VehicleNamequeReverseMapaffiche 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.GetLineResourcesByVehicleconstruit la tablevéhicule → ressources typéeset écrasePlan.Resourcesaprès l'appel àOnGetLoadingTable(qui, lui, remonte toutes les ressources de l'occurrence). Seules les ressourcesResourceType.Driveralimentent 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.EmptyetEntryId == null) et sortent dansTrajet indéterminé.pdf. Avant août 2026 ce bucket sortait sous le nom trompeurTrajet #N.pdf: l'ancienne bouclefor (idx = 0; idx <= 30; idx++)s'arrêtait surDriveId == null, orGuid.Emptyn'est pas null. C'est un signal de saisie incomplète du tableau, pas une ligne du voyage.
- Les « trajets » d'un plan 1-jour ne sont pas les lignes 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.Getcoû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).ReverseMapl'appelait pour lire uniquementtravel.TravelDrives. Remplacé par une projection_db.TravelDrives.AsNoTracking().Where(td => td.TravelId == …).Select(td => td.DriveId). L'ordre est préservé : la clé deTravelDriveest(TravelId, DriveId).- ⚠ Effet de bord supprimé :
occurrence.OneDayTravelDriveOccurrencesetOneDayTravelDriveOccurrences.Driven'arrivaient que par le navigation fixup deTravelService.Get.LoadingTableService.Getles inclut désormais explicitement, et le code litdrive.DriveIdau lieu dedrive.Drive.Id. Ne pas retirer cesInclude.
- ⚠ Effet de bord supprimé :
- Les lectures VP
eventssont lentes : 0,7 à 3,5 s chacune (resourcesreste à ~57 ms).ReverseMapen enchaînait six en séquence, soit ~2,2 s mesurées.GetVehiclesByOccurrenceétait assigné à une variable jamais lue → supprimé (~700 ms).GetTransferDriversAndHostsByOccurrencene sert que la brancheReturns;GetReturnVehiclesByOccurrencen'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 enTask.WhenAll(une fois par NavNo distinct) pour réchauffer le cache deVisualPlanningService; lesawaitdes 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
VisualPlanningServiceest passé enConcurrentDictionary(prérequis) etGetVoyageByNavNo/GetCourseByVoyage, qui n'étaient pas cachés, le sont désormais.
Timetable.OnPostrechargeait tout deux fois pour rien : lesLoadData()d'après-sauvegarde et d'après-suppression précédaient unRedirectToPage, qui déclenche un GET refaisant le travail. Supprimés. Celui d'aprèsAddEntryest conservé.UpdateTimeTableStopsmarquaitUpdate()sur chaque passager, même inchangé, donc unUPDATEcomplet par passager. Ne marque plus que ceux dont l'horaire change réellement (comparaison sur la FK, la navigation n'étant pas incluse parGetBookings).- 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é enawait, 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.
| avant | après | |
|---|---|---|
| 1er export après redémarrage | 31,3 s | 14,4 s |
| export suivant | 21,8 s | 6,1 s |
Ce que le diagnostic a corrigé, par ordre de gain :
- NAV
GetAllByNos: 6,9 s → ~1 s.PopulateDriveWithPassengersdemandait à 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
FontProviderpartagé est construit au démarrage parFontWarmupHostedService; 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 unTravelDto(TravelService.Get+ReverseMap) que plus aucune vue ne lisait — les plans affichentOccurrence.MainTravelOccurrence.Travel, déjà inclus parOccurrenceService.Getjusqu'àTravelLines.Line. PropriétéTravelsupprimé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 refaireGet+ReverseMap.
Second passage (11.08.2026), sur un catalogue avec cars de chargement — 41b58b41, 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êteIQueryablenon matérialisée rejouée par siège — détail dansoccurrence-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._navFicheCacheles mémoïse pour la durée de la requête (service scoped), clé = la liste triée des NavNo. ExportXlsStream(id)refaisait unGet+ unReverseMapen catalogue :AddLoadingLinePdfsretourne désormais ses lignes et le dossier les passe à la surchargeExportXlsStream(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 »), sinonligne 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)
RazorRendererServicepartage unFontProviderunique (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 auFontSetpendant la conversion, donc les exports concurrents ne font que lire. À revoir si un template embarque une police un jour.Plan.OnGetLoadingTablemémoïse leLoadingTable(requête à ~20Include), 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 dePlanet 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:84rend le fond du plan de véhicule enbackground-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 etVehiclePlan.cshtml.cs:47le lit déjà localement pour en mesurer les dimensions. IdemRazorRendererService: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.CallAsyncpeut renvoyernullsurHttpRequestException, 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 deVehicleType(catégorie type "car 44 places").OrderResourcesByDateest 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 aussiPendingWebOrMobileavecTransactionId != nullOU 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, brancheisOneDay) itère une fois par ligne voyage (Ligne 1,Ligne 2…) en réutilisant unresourceCountglobal, et reconstruitresourcesByVehicleà chaque tour à partir des events VPVoyage (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 dansSyncHostsAndDrivers). 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.NoStopTravelerscompte, par ligne, les passagers dont le lieu n'est pas une colonne du tableau —LoadingStop/UnloadingStopnull OU lieu retiré/remplacé du trajet (donc absent delt.Stops, cloné depuis les trajets). Rempli dansReverseMap(bloc d'affectation passagers), agrégé sur les lignes « voyage principal » et « Total », rendu en dernière colonne deTimetable.cshtml(après « Total »). Ces voyageurs ne sont pas comptés dans les totaux par lieu niTravelLineTotal/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éthodesFindStop/FindTimetableStopà base deLevenshteinDistanceont été retirées deLoadingTableService,Timetable.cshtml.csetTravelListService(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.xlsxHelvetic.

