Module travel-catalog (mobile)
Ce fichier doit rester synchronisé avec le code du module. À mettre à jour à chaque changement structurel.
Rôle : exposer le catalogue voyages côté mobile — recherche, listing, détails, recommandations home. Aligné sur le module backend Horizon travel-catalog (voir MCP internal-docs /docs/spektrum/buchard/horizon/modules/travel-catalog.md).
Code
- Views :
views/Home.vue,views/TravelDetails.vue,views/Others.vue, et les sous-pages voyageviews/{TravelItinerary,TravelDepartures,TravelComments}.vue(routes/voyage/:slug/{itinerary,departures,comments}— ex-overlays, cf. ADR0012). Ces vues enveloppentTravelBase(re-chargement du voyage par slug, cache) et rendent les composantsoverlays-pages/{Itinerary,Departures,Comments}.vueinchangés. - Components :
components/search/—SearchBar,Search,DestinationSearch,DepartureCalendar,InterestsFilterBar,InterestsSelectioncomponents/overlays-pages/—Itinerary,Departures,Comments(panneaux plein écran, rendus par les sous-pages voyage ci-dessus)components/search/seaside/— copies dédiées Seaside liées auseasideFilterState(cf. ADR0012) :SeasideSearchBar(champ texte direct —v-modelsurseasideFilterState.searchTerm,searchSeaside()au submit, plus d'overlay) etSeasideInterestsFilterBar(barre de catégories balnéaires, icônes SVG inline). L'overlay de recherche avancée Seaside (SeasideSearch+SeasideDestinationSearch+SeasideDepartureCalendar+SeasideInterestsSelection) a été supprimé : la recherche balnéaire se réduit au champ texte + la barre de catégories (plus de filtre dates / destinations-pays côté Seaside).components/travel-details/—TravelIntro,TravelHighlights,TravelHotel,TravelDatePrices,TravelRatingCommentscomponents/cards/—TravelCardBorder,TinyTravelCardSummary,UpcomingDeparturesCard,RecommendedTravelMaps,SpecialOfferPrice(teaser prix remisé/club réutilisable — cf. « Points d'attention »)components/bases/TravelBase.vue
- Stores :
stores/travels.ts,stores/searchFilter.ts - API :
publicApi.travels.{getBySlug,getCatalog,getRandom,getSeasideCategories,search,query},publicApi.countries.listsearch/queryacceptentseaside?: boolean(TravelSearchParams), émis dans l'URL comme?seaside=true|false:false= catalogue (Home),true= balnéaires (Seaside). Émis sur les deux valeurs.search/queryacceptent aussiseasideCategoryName?: string— filtre balnéaire par destination. Format attendu :"Baln {NOM_EN_MAJUSCULES}"(ex."Baln MAJORQUE"). Émis uniquement si défini (sélection dansSeasideInterestsFilterBar). Stocké dansSearchFilter.seasideCategoryName(seaside filter state), remis àundefinedparclearSearch.getSeasideCategories→GET /travels/seaside/categories→TravelCategory[]: destinations/catégories balnéaires (données de référence, TTL 30 min commecountries.list). Champssubtitle/description/picturenullables côté backend bien que typésstring.
- Utils :
utils/travel.ts,utils/travel-formatters.ts,composables/travelRangesMap.json - Constantes :
constants/filterButtons.ts
Entités principales (types @spektrum/horizon-types)
Travel(aliasapi_travel_Travel) — produit voyageOccurrenceSummary— départ daté (vue résumé)TravelCategory,TravelRangeCountryTravelComment(consommé en lecture pour l'agrégation Travel rating)SpecialOffer(+SpecialOfferExtlocal pourisPerPerson) — rabais programmé porté parTravel.specialOffersetOccurrenceSummary.specialOffers.Price(minPrice) porte les prix teaser pré-calculés*WithSpecialOffers/*WithClubSpecialOffers. Cf. glossaire,business-rules.md→ « Offres spéciales ».
Invariants & règles spécifiques
- Deux endpoints listing distincts :
query(catalogue filtré — voyages Club, courses d'un jour, etc.) etsearch(texte libre, endpoint optimiséVITE_SEARCH_ENDPOINT). Ils renvoient le mêmePaginationResult<Travel>mais ne couvrent pas le même cas d'usage. - Catalogue vs balnéaires = deux pages, deux états indépendants : Home (
/) appellesearchTrips(?seaside=false), Seaside (/seasides) appellesearchSeaside(?seaside=true). Le backend traite les deux listings comme mutuellement exclusifs. Chaque page a son proprefilterState/seasideFilterState(storesearchFilter) et son propre résultatsearchResult/seasideSearchResult(storetravels) — filtrer l'une n'affecte jamais l'autre. Les composants de recherche Seaside sont des copies dédiées (components/search/seaside/, cf. ADR0012). Asymétrie depuis le release tweak : Home garde l'overlay de recherche complet (Search+ sous-panneaux) ; Seaside l'a perdu — saSeasideSearchBarest un simple champ texte qui lancesearchSeaside()au submit, et le filtrage avancé (dates, destinations-pays) n'existe plus côté balnéaire. getRandoma un TTL court (1 min) pour faire tourner les recommandations entre deux ouvertures de Home.countries.lista un TTL 30 min (référentiel quasi-statique).- Travel dynamique (
isDynamicTraveldansbookingConstructor) : un voyage est dynamique quand lesstops(arrêts) varient d'une occurrence à l'autre. Sinon il est statique (mêmes stops pour toutes les dates). C'est la seule dimension qui varie par occurrence. TravelType:Catalog(cas dominant),Seaside(balnéaire — autre cas dominant),OneDay(course du jour),OutOfCatalog,Group(les deux derniers rares). Voir glossaire.- Seaside :
travel.accommodation(singulier) pointe sur l'hôtel principal. Pour les autres types,travel.accommodations(collection) liste les hôtels par jour. - Voyages
Legacy(importés de l'ancien système Globe) : visibles en lecture mais traitement particulier — ne pas faire d'hypothèse forte sur leurs champs.
Dépendances
- Consomme :
occurrence-capacity(chaqueTravela une collectionoccurrencesavec capacités/prix),product-catalog(Accommodations attachés aux jours). - Consommé par :
booking(le wizard part d'un Travel),comments-moderation(les commentaires sont attachés à un Travel).
Points d'attention
- Le
searchFilterstore porte l'état des filtres entre Home et Search. Il expose deux filtres de même forme via une factorycreateFilterHelpers(state): le catalogue sous les noms plats (filterState,toggleInterest, …) et les balnéaires sous les nomsseaside-préfixés (seasideFilterState,seasideToggleInterest, …). Chacun a son propre historique save/restore (annulation de recherche). - Itinéraire / départs / commentaires = routes, pas overlays : ce sont des sous-pages plein écran (
/voyage/:slug/{itinerary,departures,comments}) pour que leur slide d'ouverture/fermeture emprunte la transition de page du shell et hérite de sa garde geste-natif iOS (plus de double-animation). Le guarduidu router court-circuite le loading/inset sur les sauts détail↔sous-page (sameTravelCtx). Cf. ADR0012,business-rules.md. TravelRatingCommentslit les avis approuvés ; les commentaires non approuvés n'apparaissent pas (filtré côté backend).- Les images voyage sont multi-rôle :
isCard/isHero/ autres. PréférerisCardpour les vignettes, fallbackisHero, puis première image dispo (voirprotectedApi.comments.getMinepour le pattern). - Teaser prix remisé/club :
SpecialOfferPrice.vue(helpersperPersonOfferTeaser/totalOfferTeaser,utils/special-offers.ts) rend le prix brut barré + le meilleur prix club-inclus + badge « Rabais Club Buchard » (offre membres uniquement), à partir des champs Horizon pré-calculés surminPrice. Deux modes (proprespectMembership) :TravelCardBorder+TravelDetailsen marketing (prix club montré à tout le monde, divergence assumée du site) ;TBStepDepartureDateen respect-adhésion (offre club cachée aux non-membres, prix brut pour eux — gate surcustomer.hasActiveClubMemberShip, le même flag que le vrai calcul, donc teaser = prix payé au checkout). Le vrai prix selon l'adhésion est calculé au paiement. Heuristique backend ≠ calcul réel (parité site). Détail complet :business-rules.md→ « Offres spéciales » → « Affichage teaser ».
Contributors
No contributors
Changelog
No recent changes

