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 6-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, nights, and per-day content (title, details) |
| 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 6 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
Edit Tour Media Page
Section titled “Edit Tour Media Page”A dedicated edit page for tour media, separate from the main Tour wizard.
Path: Admin > Suppliers > Tours > Edit > “Edit Media” header button
URL: /admin/supplier-tours/{id}/edit/media
The page hosts the same media fields as the Tour wizard’s “Visual & Media” step (hero image, gallery, arrival/departure images, per-day images, supplier-specific tour images), but on a separate Livewire component that contains only media fields. Reactive triggers from the rest of the Tour form (suppliers select, source locale, title, AI input, itinerary repeater rebuilds) cannot interrupt FilePond uploads on this page, which mitigates an upload race that intermittently dropped images on the main wizard. See #1691 for the underlying bug.
Coexistence: Both UIs are kept in parallel for now. The main Tour edit page still exposes the “Visual & Media” wizard step with identical functionality, so authors can use either flow while the dedicated page is validated. The wizard step is expected to be removed once the dedicated page is confirmed safe in production.
Data model: No schema changes. Media fields read and write through the same model attributes and JSON paths as the wizard step:
product_templates.hero_image(string)product_templates.gallery_images(JSON array)product_templates.itinerary[stop].days[day].day_image(per-day images, JSON path)product_templates.itinerary[].arrival_imageand[].departure_image(endpoint images, JSON path)supplier_tours.images(JSON array)
Save flow: Standard Filament EditRecord. mutateFormDataBeforeFill projects _day_images, _arrival_image, and _departure_image from the itinerary; mutateFormDataBeforeSave re-reads the itinerary from the database ($template->fresh()->itinerary) before merging back the form’s media fields, so concurrent structural edits to the itinerary on the main edit page are not clobbered.
Source: backend/app/Filament/Resources/Suppliers/SupplierTours/Pages/EditSupplierTourMedia.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 content if target locale differs from source locale (falls back to empty translation on failure)
- 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 — Logistics, accommodation, meal plans, SEO metadata
Each step validates before allowing progression.
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
View Page Actions
Section titled “View Page Actions”| Button | Visibility | Behavior |
|---|---|---|
| Preview on Frontend | Active products | Direct link to frontend product page |
| Preview Draft | Draft/Inactive products | Signed URL with 60-minute expiration |
| Prepare Flight Searches | Always | Creates routes and cache entries for flight configs |
| View Cached Flights | When matching routes exist | Links to DynamicFlightCaches filtered by product routes |
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, day images) remain editable and propagate through the shared
ProductTemplate. - 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) never touch translations — admins re-translate them manually.
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