Skip to content

Flight Search UI

Native FilamentPHP v4.1 implementation of flight search interface with interactive leg selection, collapsible panels, and component-based architecture.

URL: /admin/flight-searches/flights

Navigation: Admin Panel → Flight Searches → Flights (Aerticket)

  1. Select departure and arrival airports (searchable dropdowns)
  2. Choose dates and passenger counts
  3. Configure fare options and preferences
  4. Click “Search Flights”
  5. Browse results with interactive leg selection
  6. View fare details and book flights

File: app/Filament/Resources/FlightSearches/Pages/SearchFlights.php

Components:

  • Form - Search criteria using FilamentPHP Schema
  • Table - Results display using FilamentPHP Table Builder
  • Actions - Search, clear, and fare details actions
  • Livewire Methods - Interactive leg selection
SearchFlights (Page)
├── Form (Schema)
│ ├── Section: Flight Search
│ │ ├── Grid: Origin/Destination/Dates
│ │ ├── Grid: Passengers/Cabin
│ │ ├── Section: Fare Options
│ │ ├── Section: Flight Preferences
│ │ └── Section: Airline Filtering
├── Table
│ ├── Split: Fare Summary
│ │ ├── Stack: Price Info
│ │ └── Split: Badges (Stops, Duration, Source)
│ └── Panel: Collapsible Flight Legs
│ ├── Stack: Outbound Options
│ │ └── Split: Radio + Leg Details (clickable)
│ └── Stack: Return Options
│ └── Split: Radio + Leg Details (clickable)
└── Actions
├── searchFlights()
├── clearResults()
└── viewFareDetails()
Radio::make('trip_type')
->label('Trip Type')
->options([
'one_way' => 'One Way',
'round_trip' => 'Round Trip',
'multi_city' => 'Multi-City',
])
->default('one_way')
->inline()
->live()
->columnSpanFull()

Reactive Behavior:

  • Return date field visibility tied to trip_type === 'round_trip'
  • Multi-city segments visible when trip_type === 'multi_city'
Repeater::make('segments')
->label('Trip Segments')
->visible(fn (Get $get): bool => $get('trip_type') === 'multi_city')
->minItems(2)
->maxItems(6)
->schema([
Grid::make(3)
->schema([
Select::make('departure_airport'),
Select::make('arrival_airport'),
DatePicker::make('departure_date'),
]),
])

Constraints:

  • Minimum 2 segments, maximum 6 segments
  • All segment dates must be in chronological order
  • Validated at search time
Grid::make(4)
->schema([
TextInput::make('adults')
->numeric()
->default(1)
->minValue(1)
->maxValue(9),
TextInput::make('children')
->label('Children (2-11)')
->default(0),
TextInput::make('infants')
->label('Infants (0-1)')
->default(0),
Select::make('cabin_class')
->options([
'ECONOMY' => 'Economy',
'ECONOMY_PREMIUM' => 'Premium Economy',
'BUSINESS' => 'Business',
'FIRST' => 'First',
]),
])

Validation:

  • Total passengers (adults + children + infants) ≤ 9
  • At least 1 adult required
CheckboxList::make('fare_sources')
->options([
'FSC_IATA' => 'IATA Published Fares',
'FSC_NEGO' => 'Negotiated Fares',
'FSC_CONSO' => 'Consolidator Fares',
'FSC_WEB' => 'Direct Channel & Low-Cost Fares',
])
->default(['FSC_IATA'])
TagsInput::make('included_airlines')
->label('Include Airlines')
->placeholder('Enter airline codes (e.g., LH, BA, AA)')
->separator(',')
TagsInput::make('excluded_airlines')
->label('Exclude Airlines')
->separator(',')

Validation:

  • IATA codes: 2-3 alphanumeric characters
  • Cannot have overlapping included/excluded airlines
Split::make([
// Left side: Price and fare information
Stack::make([
TextColumn::make('formattedPrice')
->weight(FontWeight::Bold)
->color('success'),
TextColumn::make('fareType'),
TextColumn::make('cabinClass')
->icon('heroicon-m-ticket'),
])->space(1),
// Right side: Badges
Split::make([
TextColumn::make('numberOfStops')
->badge()
->color(fn (int $state): string => match ($state) {
0 => 'success',
1 => 'warning',
default => 'danger',
}),
TextColumn::make('formattedDuration')
->badge()
->icon('heroicon-m-clock'),
])->from('sm'),
])->from('md')

Users can select different flight options for each direction:

Panel::make([
Stack::make(fn ($record) => $this->buildFlightLegComponents($record))
->space(3),
])->collapsible()->collapsed(true)

Leg Structure (Round-Trip):

Panel (Collapsible)
├── Outbound Header
├── Leg Option 1 (Split: Radio + Details) [Clickable]
├── Leg Option 2 (Split: Radio + Details) [Clickable]
├── Return Header
├── Leg Option 1 (Split: Radio + Details) [Clickable]
└── Leg Option 2 (Split: Radio + Details) [Clickable]

Leg Structure (Multi-City):

Panel (Collapsible)
├── Leg 1 Header
├── Leg Option 1 [Clickable]
├── Leg 2 Header
├── Leg Option 1 [Clickable]
└── ... (up to 6 legs)
public function selectLeg(string $fareId, string $direction, int $legIndex): void
{
if (!isset($this->selectedLegs[$fareId])) {
$this->selectedLegs[$fareId] = [
'Outbound' => 0,
'Return' => 0,
];
}
$this->selectedLegs[$fareId][$direction] = $legIndex;
}
ViewAncillariesAction::make()
->rawFareData($this->rawFareData)
->selectedLegs($this->selectedLegs)

Features:

  • Opens modal with available ancillary services
  • Component-level caching prevents duplicate API requests
  • Displays services grouped by type (BAGGAGE, MEAL, SEAT)
ViewFareDetailsAction::make()
->rawFareData($this->rawFareData)

Features:

  • Complete fare breakdown
  • Passenger pricing
  • Baggage allowance
  • Booking action
UpsellAction::make()
->rawFareData($this->rawFareData)

Features:

  • Searches for upgraded fare options
  • Uses user’s selected leg choices
  • Replaces search results with upsell results
  • sm - 640px
  • md - 768px
  • lg - 1024px

Usage:

Split::make([/* ... */])->from('md') // Horizontal on medium+
Stack::make([/* ... */]) // Always vertical
  • Full-width fields on mobile
  • Grid collapses to single column
  • Collapsible panels for details
  • Touch-friendly input sizes
  • Limit to 10 results for dropdowns
  • Uses GIN index for sub-millisecond queries
  • See Airport Search documentation
  • Component-level cache prevents duplicate API requests
  • Cache key: fareId|itinerary1:itinerary2
  • Automatic invalidation on search state changes
public function clearResults(): void
{
$this->searchResults = collect();
$this->rawFareData = [];
$this->selectedLegs = [];
$this->fareAncillaryCache = [];
$this->hasSearched = false;
}
/admin/flight-searches/flights
# Access flight search page
# Test scenarios:
1. One-way search: BCN MAD, 1 adult, Economy
2. Round-trip search: LHR JFK, 2 adults + 1 child, Business
3. Multi-city search: BCN BKK CNX BCN, 2 adults
4. Multi-leg selection: Expand panel, click different options
5. Fare details: Click "View Details" action
6. Validation: Try >9 passengers
use function Pest\Livewire\livewire;
test('flight search form renders correctly', function () {
livewire(SearchFlights::class)
->assertFormExists()
->assertFormFieldExists('departure_airport')
->assertFormFieldExists('arrival_airport')
->assertFormFieldExists('departure_date')
->assertFormFieldExists('adults');
});
test('leg selection updates state', function () {
$component = livewire(SearchFlights::class);
$component->selectLeg('fare-123', 'Outbound', 1);
expect($component->selectedLegs['fare-123']['Outbound'])->toBe(1);
});
  1. Check API credentials configured
  2. Verify AerTicket service accessible
  3. Validate airport codes
  4. Check date range (max 11 months)
Terminal window
php artisan aerticket:test-connection
php artisan pail --filter="Aerticket"
  1. Check Livewire scripts loaded
  2. Check browser console for errors
  3. Verify wire:click attribute present