diff --git a/app/Console/Commands/PopulateAirportTimezones.php b/app/Console/Commands/PopulateAirportTimezones.php index dd97396..d709afa 100644 --- a/app/Console/Commands/PopulateAirportTimezones.php +++ b/app/Console/Commands/PopulateAirportTimezones.php @@ -17,7 +17,10 @@ class PopulateAirportTimezones extends Command */ public function handle() { - Airport::whereNull('timezone')->chunkById(100, function ($airports) { + Airport::where(function ($query) { + $query->whereNull('timezone') + ->orWhere('timezone', 'Undefined'); + })->chunkById(100, function ($airports) { foreach ($airports as $airport) { $zoneName = null; $attempts = 0; diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 490f167..6d53615 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -58,28 +58,38 @@ class SearchController extends Controller { $q = request('q', ''); $len = strlen($q); + $includeInactive = request()->boolean('include_inactive'); if ($len < 3) return []; return Airport::with('region.country') + ->when(!$includeInactive, fn($query) => $query->where('active', true)) ->when($len === 3, fn($query) => $query->where('iata_code', 'ilike', $q)) ->when($len >= 4, fn($query) => $query->where(function ($sub) use ($q, $len) { $sub->when($len === 4, fn($s) => $s->where('icao_code', 'ilike', $q)) ->orWhere('name', 'ilike', "%{$q}%") ->orWhere('municipality', 'ilike', "%{$q}%"); - })->orderByRaw(" - CASE - WHEN icao_code = ? THEN 0 - WHEN iata_code = ? THEN 1 - ELSE 2 - END - ", [$q, $q])) + })) + ->orderByRaw(" + CASE + WHEN active THEN 0 + ELSE 1 + END + ") + ->orderByRaw(" + CASE + WHEN icao_code = ? THEN 0 + WHEN iata_code = ? THEN 1 + ELSE 2 + END + ", [$q, $q]) ->limit(15) - ->get(['id', 'name', 'municipality', 'iata_code', 'icao_code', 'region_id']) - ->map(fn($airport) => [ + ->get(['id', 'name', 'municipality', 'iata_code', 'icao_code', 'region_id', 'active']) + ->map(fn(Airport $airport) => [ 'value' => $airport->id, 'title' => $airport->display_name, 'country_code' => strtolower($airport->region->country->code), + 'active' => $airport->active, ]) ->values(); } diff --git a/app/Models/Airport.php b/app/Models/Airport.php index 2327e03..967575b 100644 --- a/app/Models/Airport.php +++ b/app/Models/Airport.php @@ -20,12 +20,14 @@ class Airport extends Model 'icao_code', 'iata_code', 'local_code', + 'active', ]; protected $casts = [ 'latitude_deg' => 'float', 'longitude_deg' => 'float', 'elevation_ft' => 'integer', + 'active' => 'boolean', ]; protected $appends = [ @@ -37,6 +39,8 @@ class Airport extends Model 'LHR', 'LGW', 'STN', 'LTN', 'LCY', 'SEN', ]; + + protected function displayName() : Attribute{ return Attribute::make( get: function () { @@ -53,6 +57,11 @@ class Airport extends Model ); } + public function scopeActive($query) + { + return $query->where('active', true); + } + public function region(): BelongsTo { return $this->belongsTo(Region::class); diff --git a/database/migrations/2026_08_22_031806_add_historic_airports.php b/database/migrations/2026_08_22_031806_add_historic_airports.php new file mode 100644 index 0000000..61e065f --- /dev/null +++ b/database/migrations/2026_08_22_031806_add_historic_airports.php @@ -0,0 +1,133 @@ +boolean('active')->default(true); + }); + + Airport::whereIataCode('TXL')->update(['active' => false]); + $this->importCsv(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('airports', function (Blueprint $table) { + $table->dropColumn('active'); + }); + } + + private function importCsv(): void + { + $path = storage_path('app/private/seed_data/airports.csv'); + + if (! file_exists($path)) { + throw new \RuntimeException("Airports CSV not found at: {$path}"); + } + + $handle = fopen($path, 'rb'); + + if ($handle === false) { + throw new \RuntimeException("Failed to open airports CSV at: {$path}"); + } + + // Skip header row + fgetcsv($handle); + + $regionMap = DB::table('regions')->pluck('id', 'code')->all(); + + $batch = []; + $batchSize = 500; + $now = now()->toDateTimeString(); + + while (($row = fgetcsv($handle)) !== false) { + if (count($row) < 19) { + continue; + } + + // id, ident, type, name, latitude_deg, longitude_deg, elevation_ft, + // continent, iso_country, iso_region, municipality, scheduled_service, + // icao_code, iata_code, gps_code, local_code, home_link, wikipedia_link, keywords + [ + , + , + $type, + $name, + $latitudeDeg, + $longitudeDeg, + $elevationFt, + , + , + $isoRegion, + $municipality, + , + $icaoCode, + $iataCode, + , + $localCode, + ] = $row; + + $icaoCode = trim(str_replace(["\r", "\n"], '', $icaoCode)); + $iataCode = trim(str_replace(["\r", "\n"], '', $iataCode)); + + // Only importing airports with no IATA/ICAO code — coded airports are already in the DB + if ($icaoCode !== '' || $iataCode !== '') { + continue; + } + + $type = trim($type); + $name = trim($name); + $localCode = trim($localCode); + $isoRegion = trim($isoRegion); + + if (! isset($regionMap[$isoRegion])) { + continue; + } + + $elevationFt = trim($elevationFt); + + $batch[] = [ + 'type' => $type, + 'name' => $name, + 'latitude_deg' => (float) trim($latitudeDeg), + 'longitude_deg' => (float) trim($longitudeDeg), + 'elevation_ft' => $elevationFt !== '' ? (int) $elevationFt : null, + 'region_id' => $regionMap[$isoRegion], + 'municipality' => trim($municipality) !== '' ? trim($municipality) : null, + 'icao_code' => null, + 'iata_code' => null, + 'local_code' => $localCode !== '' ? $localCode : null, + 'timezone' => 'Undefined', + 'active' => $type !== 'closed', + 'created_at' => $now, + 'updated_at' => $now, + ]; + + echo "Added: {$name}\n"; + + if (count($batch) >= $batchSize) { + DB::table('airports')->insert($batch); + $batch = []; + } + } + + fclose($handle); + + if (! empty($batch)) { + DB::table('airports')->insert($batch); + } + } +}; diff --git a/resources/js/Components/FlightsGoneBy/AirportSearchBox.vue b/resources/js/Components/FlightsGoneBy/AirportSearchBox.vue index 1894328..502eaff 100644 --- a/resources/js/Components/FlightsGoneBy/AirportSearchBox.vue +++ b/resources/js/Components/FlightsGoneBy/AirportSearchBox.vue @@ -1,5 +1,5 @@ -