phpvms/app/Filament/Resources/FlightBundles/Resources/Flight/Tables/FlightsTable.php
Nabeel Shahzad 28f49b97c3
feat(schema-modernization): foundations for RouteForge
Schema, model, Filament, cron, and visibility-semantics foundations
required by the RouteForge change. Four phases delivered as one
coherent change. Verified against four spec files (flight-bundles,
flight-time-storage, subfleet-capability, flight-visibility).

Phase 1.1 \xe2\x80\x94 Flight time columns
- flights.departure_time, flights.arrival_time TIME NULL
- Flight model accessors (Hi-format) + mutators (FlightTimeParser)
- FlightTimeParser supports 9 formats + Z/L/tz suffix stripping
- Migration backfills inline via chunked raw DB::table->update()
- Legacy dpt_time/arr_time VARCHAR columns preserved for one release

Phase 1.2 \xe2\x80\x94 Subfleet capability columns
- subfleets.cruise_speed, max_range_nm, route_types (CSV VARCHAR(64))
- FlightTypesCast splits/sorts/dedupes/logs-on-invalid
- routeforge defaults in config/phpvms.php
- SubfleetForm Operational Capability section

Phase 1.3 \xe2\x80\x94 Flight Bundles
- flight_bundles table + flights.bundle_id NOT NULL FK
- Default bundle seeded inline; existing flights backfilled
- FlightBundle model + factory + BundleObserver (queued recompute)
- FlightBundleResource as sole Flights nav entry (slug=flights,
  icon=OutlinedMap, sort=2)
- Nested FlightResource under FlightBundleResource (slug=flight,
  parentResource, create+edit only, no nav entry)
- FlightsRelationManager with relatedResource for full-page row actions
- FlightForm: bundle selector removed (route-bound), date inputs
  hidden when parent bundle owns dates, XSS-safe placeholder link
- Date columns stored as TIMESTAMP UTC, UI converts to local tz
- Soft-deleting a bundle leaves children non-deleted; visibility
  recompute sets visible=false (no cascade delete)
- Spatie Shield permissions: flight_bundle.* seeded

Phase 1.4 \xe2\x80\x94 Visibility semantics
- flights.active renamed to flights.enabled (admin source of truth)
- flights.visible is cron-managed combined state
- SetActiveFlights cron deleted; SetVisibleFlights cron added
- Two-pass bulk SQL UPDATE scoped by bundle, chunks of 500
- RecomputeBundleVisibility queued job dispatched on bundle save/restore
- Flight::scopeVisible added; scopeActive retained as plain alias
- Pilot-facing read sites use ->visible() (AirportController fix)
- FlightResource JSON + FlightExporter CSV expose both enabled+active
- Four-state status badge (Disabled / Disabled by Bundle / Enabled
  Out of Window / Enabled & In Window) replaces Visible toggle
- 5 visibility indexes added

Verification
- 469 Pest tests passed (2058 assertions)
- phpstan level 5 clean (646 files)
- pint --test clean
- rector --dry-run clean
- All four spec files PASS (28 requirements + 36 scenarios verified
  by kimi-spec-reviewer)

Migration path documented in docs/UPGRADING.md and openspec/changes/
schema-modernization-for-routeforge/.
2026-05-23 12:05:09 -05:00

124 lines
4.3 KiB
PHP

<?php
namespace App\Filament\Resources\FlightBundles\Resources\Flight\Tables;
use App\Models\Airport;
use App\Models\Flight;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
class FlightsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('ident')
->label(trans_choice('common.flight', 1).' #')
->searchable(['flight_number'])
->sortable(['airline_id', 'flight_number']),
TextColumn::make('dpt_airport_id')
->label(__('flights.dep'))
->searchable()
->sortable(),
TextColumn::make('arr_airport_id')
->label(__('flights.arr'))
->searchable()
->sortable(),
TextColumn::make('dpt_time')
->label(__('flights.departuretime'))
->sortable()
->toggleable(),
TextColumn::make('arr_time')
->label(__('flights.arrivaltime'))
->sortable()
->toggleable(),
TextColumn::make('notes')
->label(__('common.notes'))
->toggleable(isToggledHiddenByDefault: true),
IconColumn::make('enabled')
->label(__('common.enabled'))
->boolean()
->sortable(),
TextColumn::make('status_badge')
->label(__('common.status'))
->badge()
->state(function (Flight $record): string {
if (!$record->enabled) {
return __('filament.flights.status.disabled');
}
if ($record->visible) {
return __('filament.flights.status.enabled_in_window');
}
return __('filament.flights.status.enabled_out_of_window');
})
->color(function (Flight $record): string {
if (!$record->enabled) {
return 'danger';
}
return $record->visible ? 'success' : 'warning';
}),
])
->filters([
TrashedFilter::make(),
SelectFilter::make('airline')
->relationship('airline', 'name')
->label(__('common.airline'))
->searchable()
->preload(),
SelectFilter::make('dpt_airport')
->label(__('airports.departure'))
->relationship('dpt_airport', 'icao')
->getOptionLabelFromRecordUsing(fn (Airport $record): string => $record->icao.' - '.$record->name)
->searchable()
->preload(),
SelectFilter::make('arr_airport')
->label(__('airports.arrival'))
->relationship('arr_airport', 'icao')
->getOptionLabelFromRecordUsing(fn (Airport $record): string => $record->icao.' - '.$record->name)
->searchable()
->preload(),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
ForceDeleteBulkAction::make(),
RestoreBulkAction::make(),
]),
])
->emptyStateActions([
]);
}
}