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.Type∈TravelTypeenum.Status∈Draft/Published. Beaucoup de champs marketing (Description, Highlights, Tips…). ChampDepositPercentage= source de vérité de l'acompte par voyage.TravelOccurrence— jointure N-N Travel ↔ Occurrence avec flagIsPriority(= Main travel pour cette occurrence). Mécanisme rare de "voyage copie" (cf. business-rules).TravelDay— découpage jour par jour. Lien versTravelDayAccommodation,TravelDayActivity,TravelDayVehicle(avecTravelDayVehicleJourneyStep).TravelDrive— lien Travel ↔ Drive (segments de transport).TravelLine— lien Travel ↔ Line.IsUnloading= aller (false) ou retour (true). Le champTravel.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 parSeasidequi 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.Status∈Draft(0),Published(1),DraftInternalBookable(2). Pas d'archivage applicatif (vu viaTravel.Legacypour les imports Globe). Tableau complet du comportement par couche (back-office vs public vs stats) dansdomain/business-rules.md > Travel Draft vs DraftInternalBookable vs Published.- Champs
Travel.Capacity / CapacityMax / CapacityVehicle / CapacityAccommodation / CapacityTotalsont MORTS — utiliserTravelOccupancyAndBookingStateServicepour la capacité réelle. Travel.LoadingLine(singulier) est déprécié — utiliserTravelLinesavecIsUnloadingflag.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 ⇒ ajustehotelArrival/hotelDeparturedans 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éfautfalse) = active le champ "Taille du passager" dans le formulaire de réservation du site web public (consommé viaApiTravelDto.EnableFrontendFormPassengerHeight) et dans le formulaire passager du SPA back-officebooking/steps/passengers/Passenger.vue(consommé viaTravelGeneralDto.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 SPAtravel-design. La valeur saisie est stockée par passager surBookingPassenger.HeightCm(cf. modulebooking).
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, saTravelOccurrenceest poséeIsPriority = 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 unDraftforcé (TravelService.cs:1453). Sinon l'occurrence éditée sortait de la whitelistPublished || DraftInternalBookabled'OccurrenceService.SearchAsyncet disparaissait du listing back-office. La duplication pure d'un voyage (occurrenceRelatedTravel: false, bouton dupliquer, poseTravelParentId) reste enDraft.
- Déclenchement : éditer une occurrence dont le voyage n'a pas encore d'occurrence prioritaire (
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) vsTravel.Vehicles(collection) : à clarifier, semble être le véhicule par défaut + flotte possible.
Conventions locales
- Le
travel-designSPA Vue gère toute la création/édition d'un Travel. Backend exposé viaAreas/Api/TravelController+Pages/Travels/CreateUpdate*.cshtml.cs(handlersOnGet*/OnPost*). - Sauvegarde du Travel = stockage en
Draft(champ stringDraftqui contient le JSON de l'éditeur) jusqu'à publication. Travel.Progress(int 0-100) = avancement d'édition affiché au commercial.- Output cache 60min sur
/travelscôté API public (cf. policytravelsdans 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
_Layoutjuste après "Tableau de bord" (hors section Administration), avec 2 sous-items pointant sur les 2 vues. Visible par les rôlesAdministratoretProduction. - PageModel :
src/Web/Pages/Comments/Index.cshtml.cs— handlersOnGetList,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 deTravelCommentServicedédié, on garde tout surTravelServicecomme le reste (Add/Update/Delete déjà là). À noter :UpdateCommentText(back-office, modifie SEULEMENTComment) est volontairement distinct deUpdateComment(API publique/api/comments/{id}PUT, modifieCommentETRating). Raison : la modération admin ne touche pas à la note donnée par le client. - Search model :
Domain/Models/Search/TravelCommentSearchModel.cs(filtres :IsApproved,Keywordssur 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 ledata.filterJS correspond (en PascalCase) à une propriété deTravelComment. Exception :TravelNamen'est pas une propriété (c'estTravel.Namederrière une navigation) — un branch explicite dansSearchCommentsgè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 textekt_comments_modal_edit(textarea pré-remplie avec le texte courant viarowDataById; POST/Comments/Edit?id=<guid>avectexten form data) ; modales custom pour les 3 actions bulk (kt_comments_modal_bulk_approve/disapprove/delete). Toutes ces actions soumettent en AJAX puisdatatable.ajax.reload()+toastr.success(...). Le partial génériqueDefault/_ModalDeleten'est PAS utilisé ici — il s'attend à undata-url(GET) qui ne colle pas avec notre flux POST AJAX. - ⚠ Piège : la modale de suppression unitaire s'appelle volontairement
kt_comments_modal_removeet PASkt_comments_modal_delete. Raison :Datatables.js::initDataTabledétecte automatiquement tout élément<ref>_modal_deleteet attacheonDeleteModalShow, qui fait.find('button.btn-danger').unbind().click(remove(data-url, ...))— donc il dégage notre handlerdata-single-actionet tente un GET vers undata-urlqui 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
idsau handlerOnPostBulkXxx, antiforgery token inclus. - Suppression : hard-delete (cohérent avec
DeleteCommentqui existait avant —TravelCommentn'implémente pasISoftDelete). - Invariant création : toute création de commentaire passe par
POST /api/travels/{id}/comments(TravelController.AddComment) qui forceentity.IsApproved = falsecôté serveur après le mapping. Raison : le mapping AutoMapperTravelComment ↔ TravelCommentDtoest unReverseMap()brut (cf.Profiles.cs:170) et le DTO exposeIsApproved— 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). CustomerIdnullable — héritage historique : à l'origine les commentaires étaient volontairement anonymes (pas de lien vers un client, juste le champ texteAuthor). Le rattachement à unCustomera été ajouté ultérieurement → la colonneTravelComment.CustomerIdest doncGuid?(nullable) et toutes les anciennes lignes en base portentCustomerId = NULL. Tout commentaire créé depuis cette évolution est rattaché au client connecté (cf.TravelController.AddCommentqui forceentity.CustomerId = customer.Idaprè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 sicustomerIdest 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.cshtmlaffiche un badge (classe.comments-pending-badge, masqué pard-nonequand le compteur est à 0) à droite de "Commentaires" et "En attente" pour les admins. Alimenté parGET /Comments/PendingCount(handlerOnGetPendingCount→TravelService.GetPendingCommentsCount()). Fonction globalewindow.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(consommeTravelDrivespour les lignes du tableau). - →
booking(toute réservation pointe sur un Travel via TravelOccurrence).
Contributors
No contributors
Changelog
No recent changes

