Skip to content

Module — Voyage & Catalogue

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

Rôle

Définition des produits voyage vendus par Buchard hors balnéaires (gérés à part dans seaside.md). Couvre les types : Catalog (voyages catalogue régulier, dominant), OutOfCatalog (rare), Group (rare), OneDay (course d'un jour, régulier).

Emplacement

  • Pages : src/Web/Pages/Travels/ + src/Web/Pages/TravelCategories/, TravelRanges/
  • Service : src/Application/Services/Entities/TravelService.cs
  • DTOs : src/Application/Dtos/Travel/, src/Application/Dtos/TravelDto.cs
  • Vue (édition) : src/Web/Frontend/Vue/src/travel-design/ (SPA)
  • Templates PDF : src/Application/TemplatesPdf/Catalog.liquid

Entités principales

  • Travel — produit voyage. TypeTravelType enum. StatusDraft/Published. Beaucoup de champs marketing (Description, Highlights, Tips…). Champ DepositPercentage = source de vérité de l'acompte par voyage.
  • TravelOccurrence — jointure N-N Travel ↔ Occurrence avec flag IsPriority (= Main travel pour cette occurrence). Mécanisme rare de "voyage copie" (cf. business-rules).
  • TravelDay — découpage jour par jour. Lien vers TravelDayAccommodation, TravelDayActivity, TravelDayVehicle (avec TravelDayVehicleJourneyStep).
  • TravelDrive — lien Travel ↔ Drive (segments de transport).
  • TravelLine — lien Travel ↔ Line. IsUnloading = aller (false) ou retour (true). Le champ Travel.LoadingLine (singulier) est déprécié.
  • TravelActivity — activités attachées au voyage.
  • TravelDocument, TravelPicture, TravelLink, TravelComment (avis client — modération back-office sur /Comments), TravelAlert.
  • TravelRange + MainTravelRange — gammes éditoriales (regroupement marketing).
  • TravelCategory — catégorie produit (utilisée différemment par Seaside qui s'en sert pour grouper les rotations).
  • StageProgress + StageProgressValue + TravelStageProgressValue — workflow de création/validation interne (peu / pas utilisé).
  • SpecialOffer — promos attachées au voyage.

Règles métier spécifiques

  • Travel.StatusDraft (0), Published (1), DraftInternalBookable (2). Pas d'archivage applicatif (vu via Travel.Legacy pour les imports Globe). Tableau complet du comportement par couche (back-office vs public vs stats) dans domain/business-rules.md > Travel Draft vs DraftInternalBookable vs Published.
  • Champs Travel.Capacity / CapacityMax / CapacityVehicle / CapacityAccommodation / CapacityTotal sont MORTS — utiliser TravelOccupancyAndBookingStateService pour la capacité réelle.
  • Travel.LoadingLine (singulier) est déprécié — utiliser TravelLines avec IsUnloading flag.
  • Travel.Legacy = true ⇒ entité importée de Globe, ne pas éditer.
  • Travel.Favorite = true ⇒ voyage mis en avant en home page du site web.
  • Travel.BusinessAxis = axe commercial (segmentation analytique CA, pour reporting).
  • Travel.FirstNightInTheBus / LastNightInTheBus = nuit dans le car aller/retour ⇒ ajuste hotelArrival / hotelDeparture dans le voucher (cf. PdfGeneratorService.cs).
  • Travel.DepositPercentage = pourcentage d'acompte demandé. Le site web doit lire ce champ via API, ne pas hardcoder.
  • Travel.EarlyBooking + EarlyBookingType/Date/Value = remise early booking jusqu'à une date.
  • Travel.MainTravelRange (singulier) + Travel.TravelRanges (collection) — un voyage a une gamme principale + peut figurer dans plusieurs gammes.
  • Travel.EnableFrontendFormPassengerHeight (bool, défaut false) = active le champ "Taille du passager" dans le formulaire de réservation du site web public (consommé via ApiTravelDto.EnableFrontendFormPassengerHeight) et dans le formulaire passager du SPA back-office booking/steps/passengers/Passenger.vue (consommé via TravelGeneralDto.EnableFrontendFormPassengerHeight). Usage type : voyages avec vélos à réserver — la taille est nécessaire pour dimensionner le vélo. Le flag est posé dans le step General/GeneralOneDay/GeneralSeaside du SPA travel-design. La valeur saisie est stockée par passager sur BookingPassenger.HeightCm (cf. module booking).

Points d'attention / pièges

  • TravelOccurrence + IsPriority "voyage copie" : mécanisme rare, alerte UI dédiée. À éviter en tant que dev — ne pas créer si on peut l'éviter.
    • Déclenchement : éditer une occurrence dont le voyage n'a pas encore d'occurrence prioritaire (Design.cshtml.cs:OnGetOccurrenceDetails, !travel.TravelOccurrences.Any(top => top.IsPriority)) → TravelService.ReverseMapAndClean(..., occurrenceRelatedTravel: true) fabrique une copie du voyage. À la sauvegarde, sa TravelOccurrence est posée IsPriority = true (TravelService.cs:804), donc la copie devient le main travel de l'occurrence (OrderByDescending(IsPriority).First()).
    • Status de la copie : depuis mai 2026, la copie liée à une occurrence hérite du status du voyage de base (entity.Status), pas un Draft forcé (TravelService.cs:1453). Sinon l'occurrence éditée sortait de la whitelist Published || DraftInternalBookable d'OccurrenceService.SearchAsync et disparaissait du listing back-office. La duplication pure d'un voyage (occurrenceRelatedTravel: false, bouton dupliquer, pose TravelParentId) reste en Draft.
  • TravelParent / TravelParentId : relation à un voyage parent (variantes). Présence à valider avec l'équipe avant utilisation.
  • OccurrenceRelatedTravel : flag dont l'usage exact est flou (mineur, marginal).
  • Travel.Slug : utilisé par le site web pour l'URL — modifier avec précaution (impact SEO / liens cassés).
  • Travel.Vehicle (singulier) vs Travel.Vehicles (collection) : à clarifier, semble être le véhicule par défaut + flotte possible.

Conventions locales

  • Le travel-design SPA Vue gère toute la création/édition d'un Travel. Backend exposé via Areas/Api/TravelController + Pages/Travels/CreateUpdate*.cshtml.cs (handlers OnGet* / OnPost*).
  • Sauvegarde du Travel = stockage en Draft (champ string Draft qui contient le JSON de l'éditeur) jusqu'à publication.
  • Travel.Progress (int 0-100) = avancement d'édition affiché au commercial.
  • Output cache 60min sur /travels côté API public (cf. policy travels dans Startup).

Modération des commentaires (/Comments)

Page back-office (rôles Administrator et Production) pour modérer les TravelComment :

  • Routes : /Comments?approved=true (vue Approuvés) et /Comments?approved=false (vue En attente). Pas de filtre par défaut → vue "Tous" si aucun query param.
  • Menu : item "Commentaires" placé en top-level du _Layout juste après "Tableau de bord" (hors section Administration), avec 2 sous-items pointant sur les 2 vues. Visible par les rôles Administrator et Production.
  • PageModel : src/Web/Pages/Comments/Index.cshtml.cs — handlers OnGetList, OnPostApprove/Disapprove/Delete/Edit, OnPostBulkApprove/Disapprove/Delete. Toutes les actions individuelles passent par les handlers POST + reload datatable (pas de page autonome /Comments/Approve/{id} etc. : on reste sur la page de listing).
  • Service : méthodes ajoutées sur TravelService (SearchComments, SetCommentApprovalStatus, BulkSetCommentApprovalStatus, BulkDeleteComments, UpdateCommentText) — pas de TravelCommentService dédié, on garde tout sur TravelService comme le reste (Add/Update/Delete déjà là). À noter : UpdateCommentText (back-office, modifie SEULEMENT Comment) est volontairement distinct de UpdateComment (API publique /api/comments/{id} PUT, modifie Comment ET Rating). Raison : la modération admin ne touche pas à la note donnée par le client.
  • Search model : Domain/Models/Search/TravelCommentSearchModel.cs (filtres : IsApproved, Keywords sur Author/Comment/Travel.Name, TravelId). Pas de recherche avancée côté UI — seul le champ texte + le filtre ?approved= sont exposés.
  • Tri colonnes : reflection dynamique (query.OrderBy("Rating") etc.) marche pour toute colonne dont le data.filter JS correspond (en PascalCase) à une propriété de TravelComment. Exception : TravelName n'est pas une propriété (c'est Travel.Name derrière une navigation) — un branch explicite dans SearchComments gère le tri par nom de voyage. Si on ajoute d'autres colonnes "navigation" (ex. CustomerFullName), même traitement à prévoir.
  • Modales : modales custom de confirmation pour les 3 actions individuelles "modération" (kt_comments_modal_approve, kt_comments_modal_disapprove, kt_comments_modal_remove) + modale d'édition de texte kt_comments_modal_edit (textarea pré-remplie avec le texte courant via rowDataById ; POST /Comments/Edit?id=<guid> avec text en form data) ; modales custom pour les 3 actions bulk (kt_comments_modal_bulk_approve/disapprove/delete). Toutes ces actions soumettent en AJAX puis datatable.ajax.reload() + toastr.success(...). Le partial générique Default/_ModalDelete n'est PAS utilisé ici — il s'attend à un data-url (GET) qui ne colle pas avec notre flux POST AJAX.
  • ⚠ Piège : la modale de suppression unitaire s'appelle volontairement kt_comments_modal_remove et PAS kt_comments_modal_delete. Raison : Datatables.js::initDataTable détecte automatiquement tout élément <ref>_modal_delete et attache onDeleteModalShow, qui fait .find('button.btn-danger').unbind().click(remove(data-url, ...)) — donc il dégage notre handler data-single-action et tente un GET vers un data-url qui n'existe pas. Renommer l'id échappe à l'auto-binding.
  • Bulk : checkbox par ligne + "select all" header, toolbar avec compteur de sélection qui apparaît dès ≥1 sélection. Le POST envoie le tableau ids au handler OnPostBulkXxx, antiforgery token inclus.
  • Suppression : hard-delete (cohérent avec DeleteComment qui existait avant — TravelComment n'implémente pas ISoftDelete).
  • Invariant création : toute création de commentaire passe par POST /api/travels/{id}/comments (TravelController.AddComment) qui force entity.IsApproved = false côté serveur après le mapping. Raison : le mapping AutoMapper TravelComment ↔ TravelCommentDto est un ReverseMap() brut (cf. Profiles.cs:170) et le DTO expose IsApproved — sans ce forçage, un client malveillant peut bypass la modération en envoyant {"isApproved": true}. Si tu touches au controller ou au mapping, garde ce forçage explicite (ou exclus le champ de la ReverseMap).
  • CustomerId nullable — héritage historique : à l'origine les commentaires étaient volontairement anonymes (pas de lien vers un client, juste le champ texte Author). Le rattachement à un Customer a été ajouté ultérieurement → la colonne TravelComment.CustomerId est donc Guid? (nullable) et toutes les anciennes lignes en base portent CustomerId = NULL. Tout commentaire créé depuis cette évolution est rattaché au client connecté (cf. TravelController.AddComment qui force entity.CustomerId = customer.Id après auth Api). Conséquence côté UI : dans la liste et la modale de modération, le nom de l'auteur n'est rendu en lien cliquable (/Customers?id=...) que si customerId est présent ; sinon on laisse le texte brut. Ne pas ajouter de migration de backfill — l'anonymat des anciennes entrées est une donnée d'origine assumée, pas une dette.
  • Badge "en attente" dans le menu : _Layout.cshtml affiche un badge (classe .comments-pending-badge, masqué par d-none quand le compteur est à 0) à droite de "Commentaires" et "En attente" pour les admins. Alimenté par GET /Comments/PendingCount (handler OnGetPendingCountTravelService.GetPendingCommentsCount()). Fonction globale window.refreshCommentsPendingBadge() exposée dans le layout, appelée au load et après chaque action de modération côté /Comments.

Dépendances

  • occurrence-capacity (génération des dates de départ).
  • product-catalog (Accommodations, Activities, RoomTypes, Vehicles).
  • lines-stops (TravelLines, TravelDrives).
  • loading-tables (consomme TravelDrives pour les lignes du tableau).
  • booking (toute réservation pointe sur un Travel via TravelOccurrence).

Contributors

No contributors

Changelog

No recent changes