Skip to content

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 voyage views/{TravelItinerary,TravelDepartures,TravelComments}.vue (routes /voyage/:slug/{itinerary,departures,comments} — ex-overlays, cf. ADR 0012). Ces vues enveloppent TravelBase (re-chargement du voyage par slug, cache) et rendent les composants overlays-pages/{Itinerary,Departures,Comments}.vue inchangés.
  • Components :
    • components/search/SearchBar, Search, DestinationSearch, DepartureCalendar, InterestsFilterBar, InterestsSelection
    • components/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 au seasideFilterState (cf. ADR 0012) : SeasideSearchBar (champ texte direct — v-model sur seasideFilterState.searchTerm, searchSeaside() au submit, plus d'overlay) et SeasideInterestsFilterBar (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, TravelRatingComments
    • components/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.list
    • search/query acceptent seaside?: boolean (TravelSearchParams), émis dans l'URL comme ?seaside=true|false : false = catalogue (Home), true = balnéaires (Seaside). Émis sur les deux valeurs.
    • search/query acceptent aussi seasideCategoryName?: string — filtre balnéaire par destination. Format attendu : "Baln {NOM_EN_MAJUSCULES}" (ex. "Baln MAJORQUE"). Émis uniquement si défini (sélection dans SeasideInterestsFilterBar). Stocké dans SearchFilter.seasideCategoryName (seaside filter state), remis à undefined par clearSearch.
    • getSeasideCategoriesGET /travels/seaside/categoriesTravelCategory[] : destinations/catégories balnéaires (données de référence, TTL 30 min comme countries.list). Champs subtitle/description/picture nullables côté backend bien que typés string.
  • Utils : utils/travel.ts, utils/travel-formatters.ts, composables/travelRangesMap.json
  • Constantes : constants/filterButtons.ts

Entités principales (types @spektrum/horizon-types)

  • Travel (alias api_travel_Travel) — produit voyage
  • OccurrenceSummary — départ daté (vue résumé)
  • TravelCategory, TravelRange
  • Country
  • TravelComment (consommé en lecture pour l'agrégation Travel rating)
  • SpecialOffer (+ SpecialOfferExt local pour isPerPerson) — rabais programmé porté par Travel.specialOffers et OccurrenceSummary.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.) et search (texte libre, endpoint optimisé VITE_SEARCH_ENDPOINT). Ils renvoient le même PaginationResult<Travel> mais ne couvrent pas le même cas d'usage.
  • Catalogue vs balnéaires = deux pages, deux états indépendants : Home (/) appelle searchTrips (?seaside=false), Seaside (/seasides) appelle searchSeaside (?seaside=true). Le backend traite les deux listings comme mutuellement exclusifs. Chaque page a son propre filterState/seasideFilterState (store searchFilter) et son propre résultat searchResult/seasideSearchResult (store travels) — filtrer l'une n'affecte jamais l'autre. Les composants de recherche Seaside sont des copies dédiées (components/search/seaside/, cf. ADR 0012). Asymétrie depuis le release tweak : Home garde l'overlay de recherche complet (Search + sous-panneaux) ; Seaside l'a perdu — sa SeasideSearchBar est un simple champ texte qui lance searchSeaside() au submit, et le filtrage avancé (dates, destinations-pays) n'existe plus côté balnéaire.
  • getRandom a un TTL court (1 min) pour faire tourner les recommandations entre deux ouvertures de Home.
  • countries.list a un TTL 30 min (référentiel quasi-statique).
  • Travel dynamique (isDynamicTravel dans bookingConstructor) : un voyage est dynamique quand les stops (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 (chaque Travel a une collection occurrences avec 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 searchFilter store porte l'état des filtres entre Home et Search. Il expose deux filtres de même forme via une factory createFilterHelpers(state) : le catalogue sous les noms plats (filterState, toggleInterest, …) et les balnéaires sous les noms seaside-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 guard ui du router court-circuite le loading/inset sur les sauts détail↔sous-page (sameTravelCtx). Cf. ADR 0012, business-rules.md.
  • TravelRatingComments lit 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érer isCard pour les vignettes, fallback isHero, puis première image dispo (voir protectedApi.comments.getMine pour le pattern).
  • Teaser prix remisé/club : SpecialOfferPrice.vue (helpers perPersonOfferTeaser/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 sur minPrice. Deux modes (prop respectMembership) : TravelCardBorder + TravelDetails en marketing (prix club montré à tout le monde, divergence assumée du site) ; TBStepDepartureDate en respect-adhésion (offre club cachée aux non-membres, prix brut pour eux — gate sur customer.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