Products Admin Panel
Products are managed through Filament resources. The primary workflow starts from Tours (under “Suppliers”), which creates both the tour and its underlying ProductTemplate in a single wizard. Publishing to markets is done from the Tour edit page.
Tour-Centric Workflow
Section titled “Tour-Centric Workflow”The simplified workflow is:
- Create a Tour (Suppliers > Tours) – defines itinerary, content, hotels, activities, transfers, and travel windows in one wizard
- Publish to Market – from the Tour edit page, creates a ProductByMarket with AI translation
- Review Market Product – fine-tune flight configs, translations, and SEO in Products by Market
ProductTemplate is created and managed automatically through the Tour form. The standalone Product Templates resource is hidden from navigation but remains accessible via direct URL for debugging.
Source: backend/app/Filament/Resources/ProductTemplates/ProductTemplateResource.php (shouldRegisterNavigation() returns false)
Tour View Page
Section titled “Tour View Page”Path: Admin > Suppliers > Tours > View
The Tour view page provides a comprehensive overview of the tour state.
TourCompletionWidget
Section titled “TourCompletionWidget”A header widget showing 7-step completion progress with auto-derived TourStatus (Draft/Complete):
| Step | Checks |
|---|---|
| Tour Details | Supplier, source locale, and title filled |
| Itinerary | At least arrival + one stop with locations and nights (and a departure airport when there is a departure item) |
| Day by Day | Every day has a title and a text |
| Visual & Media | Hero image + at least 3 gallery images |
| Travel Windows | At least one rate period with start/end dates |
| Services Assignments | All itinerary stops have a Selection-tier hotel assigned |
| Supplier Details | Meals and transport descriptions filled; guide languages if guide enabled |
Status is Complete when all 7 steps pass; Draft otherwise. Status is derived automatically on every save – there is no manual status toggle.
Source: backend/app/Filament/Resources/Suppliers/SupplierTours/Widgets/TourCompletionWidget.php, backend/app/Services/SupplierTourService.php
Published Markets Section
Section titled “Published Markets Section”Below the form, a “Published Markets” section lists all ProductByMarket records linked to this tour’s template. Each entry shows market name, locale, SKU, and status as a clickable badge linking to the market product view page.
Source: backend/app/Filament/Resources/Suppliers/SupplierTours/Pages/ViewSupplierTour.php
Service Assignment Tables
Section titled “Service Assignment Tables”Three footer widgets display the current hotel, activity, and transfer assignments from the tour itinerary in read-only tables:
- Hotels – grouped by stop, showing Selection/Luxury/Grand Luxury hotels per period
- Activities – per-day assignments across Included/Extra/Substitution tiers
- Transfers – per-day assignments across Selection/Luxury/Grand Luxury tiers
Source: backend/app/Filament/Resources/ProductTemplates/Widgets/SupplierTourHotelsWidget.php, SupplierTourActivitiesWidget.php, SupplierTourTransfersWidget.php
Day by Day Step
Section titled “Day by Day Step”The tour wizard’s Day by Day step (after Itinerary) is where the customer-facing day-by-day is written, always in Spanish (DayByDayPublisher::BASE_LOCALE, es_ES) whatever the template’s source_locale. The Itinerary step only edits structure: its ItineraryRepeater is built with includeDayContent: false.
- Days – the
_day_imagesprojection (one row per day, fromItinerarySchema::flattenDaysWithPointers()) edits each day’s place (day_label), title, text (details) and photo (day_image, auto-persisted on upload). On save, texts go back intoproduct_templates.itineraryviamergeDayTextsIntoItinerary()and photos viamergeDayImagesIntoItinerary(). - Highlights – each row nests the shared
ItineraryItemsRepeater(same fields and rules as the market product’s Day Highlights & Activities), stored inproduct_templates.itinerary_itemsviaItineraryItems::fromDayRows(). The row also lists the activities the tour includes that day. - Overnight flight days – the four texts (
arrival_day1_description,arrival_day2_description,departure_day1_description,departure_day2_description, stored on the itinerary’s arrival/departure items viaItinerarySchema::setFlightDayTexts()) and the four endpoint photos (arrival_image,departure_image,arrival_day2_image,departure_day2_image). Empty texts fall back to the default copy (App\Support\FlightDayCopy), shown as placeholder. - Actions – Import supplier day-by-day PDF / Review day-by-day import (see Day-by-Day PDF Import), Rephrase with TCAI (rewrites the day texts in Spanish) and Preview on Frontend (see Supplier Tour Preview).
- Markets – a read-only table of the template’s market products: what customers see (old or new itinerary per
show_itinerary_module, flagged outdated / changes not published / product not published) and the next step, linking to the market product when it needs translating or switching on. Based on the saved tour.
Existing tours were filled by one-off data migrations (October 2026): each tour’s day texts and highlights were copied from its Spanish market product, and the former arrival/departure notes and endpoint photo swap were folded into the first/last day’s text and photo.
Source: backend/app/Filament/Resources/Suppliers/SupplierTours/Schemas/SupplierTourForm.php, backend/app/Filament/Resources/Suppliers/SupplierTours/Pages/EditSupplierTour.php, backend/app/Filament/Components/Fields/ItineraryItemsRepeater.php
Publish Itinerary Action
Section titled “Publish Itinerary Action”Header action on the Tour edit page. DayByDayPublisher::publish() copies the tour’s day texts, itinerary_items and the four flight-day texts to the es_ES translation of every es_ES market product of the template (no AI); products already in sync are skipped. Visible only when hasPendingChanges() finds a difference (a direct comparison of the saved tour against each product) and the user can update the tour. The confirmation lists what changed (days, highlight days, flight-day texts) and the products, plus non-blocking warnings from warnings(): days without a photo and highlights without a time slot.
- On a Spanish market product the itinerary days, highlights and flight-day texts are read-only, with a notice linking to the tour. Highlights are read-only on every market product: other languages get them only through Translate from Tour.
- A new Spanish market product gets the copy on creation (
publishTo(), from both Publish to Market and the market product create page). - Day and flight-day photos are not published: the API reads them from the template, so they are live once saved. Highlight photos travel with
itinerary_items.
Source: backend/app/Services/SupplierTours/DayByDayPublisher.php, backend/app/Filament/Resources/Suppliers/SupplierTours/Actions/PublishItineraryAction.php
Other-Language Translation
Section titled “Other-Language Translation”DayByDayTranslator translates the tour’s Spanish Day by Day with DayByDayTranslationAgent: day labels, titles and texts, highlight titles and descriptions (links, slots and photos are kept), the four flight-day texts, optional_activities and free_time. It runs from the market product’s itinerary Translate from Tour action (hidden on Spanish products; fills the form for review and overwrites what was there) and from Publish to Market for non-Spanish locales (on failure it is logged and left for the action). Each translation stores itinerary_source_fingerprint, a sha1 of the Spanish source; isOutdated() compares it with the tour’s current fingerprint to flag the translation in the Markets table.
Source: backend/app/Services/SupplierTours/DayByDayTranslator.php, backend/app/Ai/Agents/DayByDayTranslationAgent.php, backend/app/Filament/Concerns/HandlesProductByMarketTranslation.php
Publish to Market Action
Section titled “Publish to Market Action”Available on the Tour Edit page header. Creates a ProductByMarket from the tour’s template.
Modal form:
- Select a market (active markets only)
- Select a language from the market’s supported locales
On submit:
- Creates ProductByMarket record (status: draft) with auto-generated SKU
- Generates flight configs from the market’s default airports using
FlightRouteConfigGenerator - AI-translates the content from the template’s
source_locale(falls back to empty translation on failure), then gives the Day by Day a copy (Spanish) or a translation (other languages) – see Day by Day Step - Redirects to the new ProductByMarket edit page
Duplicate check: if a ProductByMarket already exists for the same template + market + locale, a warning is shown and no record is created.
Source: backend/app/Filament/Resources/Suppliers/SupplierTours/Actions/PublishToMarketAction.php
Products by Market Resource
Section titled “Products by Market Resource”Path: Admin > Products > Products by Market
Source: backend/app/Filament/Resources/ProductsByMarket/ProductByMarketResource.php
Create Workflow (Wizard)
Section titled “Create Workflow (Wizard)”The creation form uses a 4-step wizard for guided data entry:
- Product & Market – Select product template, market, language, status, sort order
- Flight Configuration – Departure airports, route type, search period, excluded dates
- Description and Itinerary – AI translation button, core content, per-stop itinerary with nested per-day translations (day_label, title, details), celebrity page selector
- Details & SEO – Structured “What’s included”, “Not included”, logistics, and SEO metadata
Each step validates before allowing progression.
Included / Not Included Authoring
Section titled “Included / Not Included Authoring”The Tour wizard source content and the ProductByMarket translation form now share the same structure:
- What’s included – six optional category textareas: Flights, Transfers, Accommodation, Gastronomy, Activities, and Cancellation insurance
- Not included – one free-text textarea
On the Tour / ProductTemplate side these fields define the source-locale defaults. On the ProductByMarket translation side, each category can be overridden independently per locale. Empty translation fields do not erase the template value at read time; the resolved customer-facing value is merged per key by ProductByMarket::getIncludedInPrice().
The PBM translation form shows the template text as a placeholder for each category, and includes dedicated AI actions for translating the structured inclusions and the free-text “Not included” block.
Source: backend/app/Filament/Resources/ProductsByMarket/Pages/CreateProductByMarket.php
Celebrity Page Section
Section titled “Celebrity Page Section”Both the create wizard and edit form include a collapsible “Celebrity Page” section with a market-filtered Select for linking a CmsCelebrityPage. Only active celebrity pages matching the product’s market are shown. When a celebrity page is linked, the product detail API returns a celebrity key (eyebrow, title, description, poster, CTA, trailers) instead of the gallery images. Changing the market clears any stale celebrity selection.
Source: backend/app/Filament/Resources/ProductsByMarket/Schemas/ProductByMarketForm.php:783
Itinerary Sections (Edit Page)
Section titled “Itinerary Sections (Edit Page)”The edit form has two collapsed itinerary sections (the View page infolist shows the first one under the same name):
- Itinerary Info – per-stop/per-day text (day_label, title, details), the four overnight flight-day texts, optional activities and free time. On
es_ESproducts the days and flight-day texts are read-only (published from the tour); on other locales a “Translate from Tour” header action fills them from the tour’s Day by Day (see Other-Language Translation). - Day Highlights & Activities – the editorial
itinerary_itemslist for the day-by-day itinerary module, read-only on every market product, edited on the tour (shown to customers when Show the new day-by-day itinerary is on; staff preview it with theitinerary-moduletoggle). Edit page only (not the create wizard), and hidden when the product has no itinerary days. One fixed row per day (Day N · label · title), each with a reorderable list of items: Title (required, max 80), Time slot (optional: Morning, Afternoon, Full Day, Night; no Lunch), Linked activity (optional, options limited to the activities included on that tour day; picking one prefills an empty title with its name, and an empty slot with its own slot when that is one of the four above; hidden on orphan rows), Description (optional, max 500) and Photo (optional, stored underproduct-by-market/itinerary-items). Links to activities not included that day are not saved.
Drift is reported as warnings in the section description: a stored day that no longer exists, a day whose location (poi_id) changed, or an item linking an activity no longer included. Days that no longer exist stay as Day N (no longer in the itinerary) rows instead of being deleted, and stale links are dropped on load while keeping the title. The available days are computed once when the form is filled and locked on the Livewire component, so saving validates days and links against them.
Source: backend/app/Filament/Resources/ProductsByMarket/Schemas/ProductByMarketForm.php (getItineraryActivitiesSection(), getItineraryItemsSection()), backend/app/Filament/Resources/ProductsByMarket/Pages/EditProductByMarket.php, backend/app/Support/ItineraryItems.php
Show the new day-by-day itinerary
Section titled “Show the new day-by-day itinerary”A toggle at the end of the edit page’s Product & Market section (not in the create wizard), stored in products_by_market.show_itinerary_module (default off; duplication copies it). Off, customers see the current itinerary; on, the product page renders the day-by-day itinerary module instead. It lets content launch the module product by product once each day matches the supplier’s programme, and switch it off again immediately if something is wrong: the product page reads the flag from the API on every request, so no deploy is needed. Staff can still preview the module on any product with the itinerary-module toggle (see Feature Toggles).
Source: backend/app/Filament/Resources/ProductsByMarket/Schemas/ProductByMarketForm.php (getShowItineraryModuleToggle()), frontend/src/features/product/components/ProductPage.astro
View Page Actions
Section titled “View Page Actions”| Button | Visibility | Behavior |
|---|---|---|
| Preview on Frontend | Active products | Dropdown of links to the frontend product page, one per preview state |
| Preview Draft | Draft/Inactive products | Same dropdown opened over a signed URL with 60-minute expiration |
| View Cached Flights | When matching routes exist | Links to DynamicFlightCaches filtered by product routes |
Routes and cache entries are no longer prepared from a header action (the old
“Prepare Flight Searches” was removed in #1695). Searches for the product run from the
inline Dynamic Flight Cache section on this same page; see
Operator Workflow. Missing
entries across products are backfilled by php artisan flight-cache:ensure-pending-entries.
Preview on Frontend and Preview Draft are dropdowns with the same options:
Normal, With flight diagnostic and one entry per feature toggle in
config/feature_toggles.php (e.g. With the day-by-day itinerary module). Each opens the page in exactly
the state it names, never carrying over a feature from an earlier preview. See
Feature Toggles.
Source: backend/app/Filament/Resources/ProductsByMarket/Pages/ViewProductByMarket.php
Tour Lock Behavior
Section titled “Tour Lock Behavior”A Tour is “locked” only when at least one derived ProductByMarket has status active. While every linked product is draft or inactive, the tour remains editable end-to-end. This lets authors finish configuring a tour even after pre-publishing draft market variants.
Hard lock (data-level)
Section titled “Hard lock (data-level)”SupplierTour::isLocked() returns true only when hasActiveMarketProducts() is true – an eager-loaded active_products_by_market_count subquery on the list table avoids per-row queries. When locked:
- Itinerary, suppliers, source language, duration, and structural service assignments are disabled in the form.
- Content fields (title, subtitle, descriptions, hero image, gallery, the Day by Day) remain editable and propagate through the shared
ProductTemplate; the Day by Day texts and highlights reach market products only when published or translated (see Day by Day Step). - Supplier managers lose the edit page as soon as any product exists (draft or active):
SupplierTourResource::canEdit()goes throughSupplierTourPolicy::update(); admins keep editing under the rules above. - Tour deletion is disabled when any product exists (draft or active) – delete uses
hasAnyMarketProducts(), notisLocked(). SupplierTourRate::isLocked()delegates tohasAnyMarketProducts(), keeping rates strictly locked whenever any product exists.
Source: backend/app/Models/SupplierTour.php, backend/app/Models/SupplierTourRate.php, backend/app/Filament/Resources/Suppliers/SupplierTours/SupplierTourResource.php
Admin-only gating (policy)
Section titled “Admin-only gating (policy)”SupplierTourPolicy::update() denies non-admins (including Supplier Managers) when hasAnyMarketProducts() is true – even if all products are draft. Only Admin role can use the loosened draft-only edit path; Supplier Managers keep pre-existing behavior.
Source: backend/app/Policies/SupplierTourPolicy.php
Save-time confirmation modal
Section titled “Save-time confirmation modal”When the Save Tour button is clicked on a tour with any linked products, a confirmation modal lists every affected ProductByMarket (title · market · duration) before the save runs. The native submit action is swapped for a Filament Action::requiresConfirmation() because getSaveFormAction()->submit($formId) fires a native HTML form submit that bypasses Filament’s modal flow.
Source: backend/app/Filament/Resources/Suppliers/SupplierTours/Pages/EditSupplierTour.php, backend/resources/views/filament/modals/affected-market-products.blade.php
Automatic translation reconciliation
Section titled “Automatic translation reconciliation”Per-market translations mirror the template’s stop structure. When the template’s itinerary column changes, ProductTemplateObserver::updated() (implementing ShouldHandleEventsAfterCommit) runs ProductByMarketItineraryReconciler against every linked product. The reconciler:
- Preserves translated
days[]content for stops unchanged in position, location, and nights. - Carries translated content by location when stops are reordered.
- Falls back to template source title/details for newly-added or renamed stops (flagged for admin re-translation).
- Drops translation entries for stops removed from the template.
Non-itinerary template fields (title, descriptions, highlights, SEO, included-in-price categories, not-included copy) never touch translations automatically – admins re-translate them manually or via the dedicated translation actions in the PBM form.
Source: backend/app/Observers/ProductTemplateObserver.php, backend/app/Services/ProductByMarketItineraryReconciler.php
Navigation
Section titled “Navigation”| Resource | Group | Icon | Notes |
|---|---|---|---|
| Tours | Suppliers | map-pin | Primary entry point for product creation |
| Products by Market | Products | globe-alt | Market-specific configuration |
| Product Templates | Products | rectangle-stack | Hidden from navigation (accessible via direct URL) |
Permissions
Section titled “Permissions”Resources use HasResourcePermissions trait for RBAC integration.
Source: backend/app/Filament/Traits/HasResourcePermissions.php
Related
Section titled “Related”- Product Templates - Data model
- Products by Market - Configuration details
- Suppliers Service - Tour creation wizard and service assignments
- Dynamic Flight Cache - Flight pricing cache system
- POI System - Points of Interest management