Compare commits
80 Commits
5760f8e576
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e3af37a46 | |||
| b94b1d8ec2 | |||
| 14aed7bf6e | |||
| f6d5b97784 | |||
| 1821b99524 | |||
| f8bac7f85a | |||
| 924e03334c | |||
| de183995b6 | |||
| 678096b463 | |||
| 110ed5b984 | |||
| bd8ef98d30 | |||
| d68e23e93a | |||
| 9e995eedef | |||
| a57775e141 | |||
| e007824fa9 | |||
| 061ee9dd07 | |||
| 5deefcbfb3 | |||
| 8d7d8f02d3 | |||
| 4244b8835d | |||
| d90f338321 | |||
| 63d6fb9e76 | |||
| 147bf43f09 | |||
| 5066052013 | |||
| a535521834 | |||
| 2a657bbbf7 | |||
| 4110b52ba5 | |||
| a9aa65f0d2 | |||
| 0f84ec023e | |||
| f335951784 | |||
| 95624f345c | |||
| e83fd3bdca | |||
| 7a07616f03 | |||
| 43f5c8ac3e | |||
| 79469c02cf | |||
| 2ad8c65b86 | |||
| b53c92de36 | |||
| 968272754e | |||
| 8da717a400 | |||
| fcbf021af7 | |||
| 9631e7949d | |||
| d0fe6d78d3 | |||
| b4e2caad07 | |||
| e55c771318 | |||
| 8a1581641b | |||
| bf34c20d85 | |||
| 509efbe821 | |||
| 7f62c31456 | |||
| 30b56ece8a | |||
| 548e838e81 | |||
| baff2066e6 | |||
| 6bb6ff7f71 | |||
| 4ed4110ba0 | |||
| 236f075df4 | |||
| bfe246ab44 | |||
| 877caa3291 | |||
| 6a88d0cdfb | |||
| 063a393168 | |||
| 89135a554a | |||
| 1d77ce140b | |||
| d7140f1554 | |||
| 8ec4e92541 | |||
| 4366c8f6ea | |||
| d0191dbabd | |||
| b6ffeea8ad | |||
| 2056f6b03a | |||
| baa08f0b3a | |||
| 65f009d222 | |||
| 0b14b40fd6 | |||
| 4adba94109 | |||
| 6bc72d88c2 | |||
| cfb0ab0298 | |||
| 98d24e9b38 | |||
| fc9e67ad2b | |||
| 2c51a10bb0 | |||
| dfa68b0ad9 | |||
| e4e5afafa7 | |||
| dd69f1e623 | |||
| 22d978f6ca | |||
| e7ad41aede | |||
| 70f7943c0f |
+4
-1
@@ -1,4 +1,7 @@
|
||||
.env
|
||||
vendor/
|
||||
.git/
|
||||
storage/app/private/images
|
||||
storage/app/private
|
||||
node_modules
|
||||
public/hot
|
||||
public/build
|
||||
|
||||
@@ -4,6 +4,9 @@ APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_DOMAIN=flightsgoneby.test
|
||||
API_DOMAIN=api.flightsgoneby.test
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
@@ -27,6 +30,14 @@ DB_DATABASE=flightsapi
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
PRODUCTION_DB_CONNECTION=
|
||||
PRODUCTION_DB_HOST=
|
||||
PRODUCTION_DB_PORT=
|
||||
PRODUCTION_DB_DATABASE=
|
||||
PRODUCTION_DB_USERNAME=
|
||||
PRODUCTION_DB_PASSWORD=
|
||||
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Airport;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
#[Signature('app:populate-airport-timezones')]
|
||||
#[Description('Populate timezone data for all airports using lat/lng')]
|
||||
class PopulateAirportTimezones extends Command
|
||||
{
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Airport::whereNull('timezone')->chunkById(100, function ($airports) {
|
||||
foreach ($airports as $airport) {
|
||||
$zoneName = null;
|
||||
$attempts = 0;
|
||||
|
||||
while ($zoneName === null && $attempts < 3) {
|
||||
$response = Http::withoutVerifying()
|
||||
->withOptions(['allow_redirects' => false])
|
||||
->get('http://vip.timezonedb.com/v2.1/get-time-zone', [
|
||||
'key' => config('app.timezone_api_key'),
|
||||
'format' => 'json',
|
||||
'by' => 'position',
|
||||
'lat' => $airport->latitude_deg,
|
||||
'lng' => $airport->longitude_deg,
|
||||
]);
|
||||
|
||||
$zoneName = $response->json('zoneName');
|
||||
$attempts++;
|
||||
|
||||
if ($zoneName === null) {
|
||||
$this->warn("✗ {$airport->name} — attempt {$attempts} failed: " . $response->body());
|
||||
sleep(5);
|
||||
}
|
||||
}
|
||||
|
||||
if ($zoneName) {
|
||||
$airport->update(['timezone' => $zoneName]);
|
||||
$this->info("✓ {$airport->name} — {$zoneName}");
|
||||
} else {
|
||||
$this->warn("✗ {$airport->name} — giving up after {$attempts} attempts".$response->body());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->info('Done!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UpDb extends Command
|
||||
{
|
||||
protected $signature = 'db:updb
|
||||
{--no-backup : Skip saving a local backup before wiping}
|
||||
{--force : Skip confirmation prompt}';
|
||||
|
||||
protected $description = 'Download the production database and replace the local database with it';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if (! $this->option('force') && ! $this->confirm('This will wipe your local database and replace it with production. Are you sure?')) {
|
||||
$this->info('Aborted.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$local = $this->connectionConfig('local');
|
||||
$production = $this->connectionConfig('production');
|
||||
|
||||
$timestamp = now()->format('Y_m_d_His');
|
||||
$dumpPath = storage_path("app/private/db_dumps/production_{$timestamp}.dump");
|
||||
$backupPath = storage_path("app/private/db_dumps/local_backup_{$timestamp}.dump");
|
||||
|
||||
if (! is_dir(storage_path('app/private/db_dumps'))) {
|
||||
mkdir(storage_path('app/private/db_dumps'), 0755, true);
|
||||
}
|
||||
|
||||
// Step 1: Dump production
|
||||
$this->info('Dumping production database...');
|
||||
$dumpResult = $this->pgDump($production, $dumpPath);
|
||||
|
||||
if ($dumpResult !== 0) {
|
||||
$this->error('Failed to dump production database.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("Production dump saved to: {$dumpPath}");
|
||||
|
||||
// Step 2: Back up local (unless skipped)
|
||||
if (! $this->option('no-backup')) {
|
||||
$this->info('Backing up local database...');
|
||||
$backupResult = $this->pgDump($local, $backupPath);
|
||||
|
||||
if ($backupResult !== 0) {
|
||||
$this->error('Failed to back up local database. Aborting to be safe. Use --no-backup to skip.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("Local backup saved to: {$backupPath}");
|
||||
}
|
||||
|
||||
// Step 3: Drop and recreate the local database
|
||||
$this->info('Wiping local database...');
|
||||
$this->dropAndRecreateLocalDatabase($local);
|
||||
|
||||
// Step 4: Restore production dump into local
|
||||
$this->info('Restoring production dump to local database...');
|
||||
$restoreResult = $this->pgRestore($local, $dumpPath);
|
||||
|
||||
if ($restoreResult !== 0) {
|
||||
$this->error('Restore failed. Your local backup is at: ' . ($this->option('no-backup') ? 'N/A (backup was skipped)' : $backupPath));
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Done. Local database now mirrors production.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function connectionConfig(string $type): array
|
||||
{
|
||||
$connection = $type === 'production' ? 'pgsql_production' : config('database.default');
|
||||
|
||||
return [
|
||||
'host' => config("database.connections.{$connection}.host"),
|
||||
'port' => config("database.connections.{$connection}.port"),
|
||||
'database' => config("database.connections.{$connection}.database"),
|
||||
'username' => config("database.connections.{$connection}.username"),
|
||||
'password' => config("database.connections.{$connection}.password"),
|
||||
];
|
||||
}
|
||||
|
||||
private function pgDump(array $config, string $outputPath): int
|
||||
{
|
||||
$pgpass = $this->writePgPass($config);
|
||||
|
||||
$command = sprintf(
|
||||
'pg_dump -Fc -h %s -p %s -U %s %s -f %s',
|
||||
escapeshellarg($config['host']),
|
||||
escapeshellarg($config['port']),
|
||||
escapeshellarg($config['username']),
|
||||
escapeshellarg($config['database']),
|
||||
escapeshellarg($outputPath),
|
||||
);
|
||||
|
||||
$resultCode = $this->runWithPgPass($pgpass, $command);
|
||||
|
||||
unlink($pgpass);
|
||||
|
||||
return $resultCode;
|
||||
}
|
||||
|
||||
private function pgRestore(array $config, string $dumpPath): int
|
||||
{
|
||||
$pgpass = $this->writePgPass($config);
|
||||
|
||||
$command = sprintf(
|
||||
'pg_restore -h %s -p %s -U %s -d %s --no-owner --no-privileges %s',
|
||||
escapeshellarg($config['host']),
|
||||
escapeshellarg($config['port']),
|
||||
escapeshellarg($config['username']),
|
||||
escapeshellarg($config['database']),
|
||||
escapeshellarg($dumpPath),
|
||||
);
|
||||
|
||||
$resultCode = $this->runWithPgPass($pgpass, $command);
|
||||
|
||||
unlink($pgpass);
|
||||
|
||||
return $resultCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a temporary pgpass file and return its path.
|
||||
* This is the cross-platform alternative to PGPASSWORD which doesn't work on Windows.
|
||||
*/
|
||||
private function writePgPass(array $config): string
|
||||
{
|
||||
// Escape any colons or backslashes in the password per pgpass format rules
|
||||
$password = str_replace(['\\', ':'], ['\\\\', '\\:'], $config['password']);
|
||||
$content = "{$config['host']}:{$config['port']}:*:{$config['username']}:{$password}";
|
||||
$path = tempnam(sys_get_temp_dir(), 'pgpass_');
|
||||
|
||||
file_put_contents($path, $content);
|
||||
chmod($path, 0600);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function runWithPgPass(string $pgpassFile, string $command): int
|
||||
{
|
||||
// PGPASSFILE is supported on all platforms including Windows
|
||||
putenv("PGPASSFILE={$pgpassFile}");
|
||||
passthru($command, $resultCode);
|
||||
putenv('PGPASSFILE');
|
||||
|
||||
return $resultCode;
|
||||
}
|
||||
|
||||
private function dropAndRecreateLocalDatabase(array $config): void
|
||||
{
|
||||
$database = $config['database'];
|
||||
|
||||
// Connect to the postgres maintenance database to drop/recreate
|
||||
$pdo = new \PDO(
|
||||
"pgsql:host={$config['host']};port={$config['port']};dbname=postgres",
|
||||
$config['username'],
|
||||
$config['password'],
|
||||
);
|
||||
|
||||
$pdo->exec("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = " . $pdo->quote($database));
|
||||
$pdo->exec("DROP DATABASE IF EXISTS " . '"' . $database . '"');
|
||||
$pdo->exec("CREATE DATABASE " . '"' . $database . '"');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Achievement;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class AchievementController extends Controller
|
||||
{
|
||||
public function index(User $user)
|
||||
{
|
||||
$achievements = Achievement::with(['category', 'difficulty'])
|
||||
->get()
|
||||
->groupBy(fn(Achievement $a) => $a->category->name)
|
||||
->map(fn($group) => $group->sortBy('id')->values());
|
||||
|
||||
$userAchievements = $user->achievements()
|
||||
->with('achievement')
|
||||
->orderBy('achievement_id')
|
||||
->get()
|
||||
->keyBy('achievement_id');
|
||||
|
||||
return Inertia::render('UserAchievements', [
|
||||
'user' => $user,
|
||||
'canEdit' => auth()->id() === $user->id,
|
||||
'isFollowing' => auth()->check() && auth()->user()->isFollowing($user),
|
||||
'achievements' => $achievements,
|
||||
'userAchievements' => $userAchievements,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Airline;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AirlineController extends Controller
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Airline;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class AirlineApiController extends ApiController
|
||||
{
|
||||
const array CONDOR_LOGOS = ['BEACH', 'ISLAND', 'PASSION', 'SEA', 'SUNSHINE'];
|
||||
|
||||
public function getAirlineLogo(?Airline $airline)
|
||||
{
|
||||
$logoFile = $airline?->logo ?? 'blank.png';
|
||||
$cacheLimit = 60 * 60 * 24;
|
||||
|
||||
if ($airline?->internal_name == 'condor') {
|
||||
$logoKey = array_rand(self::CONDOR_LOGOS);
|
||||
$logoFile = 'DE_' . self::CONDOR_LOGOS[$logoKey] . '.png';
|
||||
$cacheLimit = 1;
|
||||
}
|
||||
|
||||
$path = 'images/logos/tail/' . $logoFile;
|
||||
if (!Storage::disk('local')->exists($path)) {
|
||||
$path = 'images/logos/tail/blank.png';
|
||||
}
|
||||
|
||||
$fullPath = Storage::disk('local')->path($path);
|
||||
$lastModified = filemtime($fullPath);
|
||||
|
||||
return response()->file($fullPath, [
|
||||
'Content-Type' => 'image/png',
|
||||
'Cache-Control' => 'public, max-age='.$cacheLimit, // 24 hours
|
||||
'Last-Modified' => gmdate('D, d M Y H:i:s', $lastModified) . ' GMT',
|
||||
'ETag' => md5($path . $lastModified),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function getLogoByInternalName(string $internalName){
|
||||
$airline = Airline::where('internal_name', $internalName)
|
||||
->first();
|
||||
|
||||
return $this->getAirlineLogo($airline);
|
||||
}
|
||||
|
||||
function parseAirlineData(Airline $airline){
|
||||
$countryCode = $airline->country->code;
|
||||
|
||||
$result = $airline->toArray();
|
||||
unset($result['id']);
|
||||
unset($result['logo']);
|
||||
unset($result['country_id']);
|
||||
unset($result['country']);
|
||||
$result['slug'] = $result['internal_name'];
|
||||
unset($result['internal_name']);
|
||||
$result['country_code'] = $countryCode;
|
||||
return $result;
|
||||
}
|
||||
|
||||
function getByCode(string $code){
|
||||
$lookupColumn = strlen($code) === 3 ? 'ICAO_code' : 'IATA_code';
|
||||
$airlines = Airline::where($lookupColumn, strtoupper($code))->get()->map(fn($airline) => $this->parseAirlineData($airline));
|
||||
return response()->json($airlines);
|
||||
|
||||
}
|
||||
|
||||
function get(string $internalName){
|
||||
$airline = Airline::where('internal_name', $internalName)->first();
|
||||
return response()->json($this->parseAirlineData($airline));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class UserApiController extends ApiController
|
||||
{
|
||||
public function nextFlight(string $username): JsonResponse
|
||||
{
|
||||
$user = User::where('name', 'ilike', $username)->first();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json(['message' => 'User not found'], 404);
|
||||
}
|
||||
|
||||
$flight = UserFlight::with(['departureAirport', 'arrivalAirport', 'airline', 'aircraft'])
|
||||
->where('user_id', $user->id)
|
||||
->where('departure_date', '>', now()->utc())
|
||||
->orderBy('departure_date', 'asc')
|
||||
->first();
|
||||
|
||||
if (!$flight) {
|
||||
return response()->json(['message' => 'No upcoming flights found'], 404);
|
||||
}
|
||||
|
||||
$departure = Carbon::parse($flight->departure_date)->setTimezone($flight->departureAirport->timezone);
|
||||
$arrival = Carbon::parse($flight->arrival_date)->setTimezone($flight->arrivalAirport->timezone);
|
||||
|
||||
return response()->json([
|
||||
'departureAirportCode' => $flight->departureAirport->iata_code,
|
||||
'departureCity' => $flight->departureAirport->municipality,
|
||||
'departureDateReadable' => $departure->format('F j'),
|
||||
'departureTime' => $departure->format('H:i'),
|
||||
'arrivalAirportCode' => $flight->arrivalAirport->iata_code,
|
||||
'arrivalCity' => $flight->arrivalAirport->municipality,
|
||||
'arrivalDateReadable' => $arrival->format('F j'),
|
||||
'arrivalTime' => $arrival->format('H:i'),
|
||||
'flightNumber' => $flight->flight_number,
|
||||
'airlineName' => $flight->airline->name,
|
||||
'aircraftType' => $flight->aircraft->manufacturer_code . ' ' . $flight->aircraft->model_full_name,
|
||||
'logoUrl' => $flight->airline?->logo_url ?? 'undefined',
|
||||
]);
|
||||
}
|
||||
|
||||
public function flights(string $username): JsonResponse
|
||||
{
|
||||
$user = User::where('name', 'ilike', $username)->first();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json(['message' => 'User not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json($user->FlightController()->flights());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ApiController extends Controller
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the login view.
|
||||
*/
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('Auth/Login', [
|
||||
'canResetPassword' => Route::has('password.request'),
|
||||
'status' => session('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function store(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$request->authenticate();
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ConfirmablePasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the confirm password view.
|
||||
*/
|
||||
public function show(): Response
|
||||
{
|
||||
return Inertia::render('Auth/ConfirmPassword');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user's password.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if (! Auth::guard('web')->validate([
|
||||
'email' => $request->user()->email,
|
||||
'password' => $request->password,
|
||||
])) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('auth.password'),
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->put('auth.password_confirmed_at', time());
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailVerificationNotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Send a new email verification notification.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
return back()->with('status', 'verification-link-sent');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class EmailVerificationPromptController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the email verification prompt.
|
||||
*/
|
||||
public function __invoke(Request $request): RedirectResponse|Response
|
||||
{
|
||||
return $request->user()->hasVerifiedEmail()
|
||||
? redirect()->intended(route('dashboard', absolute: false))
|
||||
: Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset view.
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return Inertia::render('Auth/ResetPassword', [
|
||||
'email' => $request->email,
|
||||
'token' => $request->route('token'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming new password request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => 'required',
|
||||
'email' => 'required|email',
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
if ($status == Password::PASSWORD_RESET) {
|
||||
return redirect()->route('login')->with('status', __($status));
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [trans($status)],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PasswordResetLinkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset link request view.
|
||||
*/
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('Auth/ForgotPassword', [
|
||||
'status' => session('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming password reset link request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
// We will send the password reset link to this user. Once we have attempted
|
||||
// to send the link, we will examine the response then see the message we
|
||||
// need to show to the user. Finally, we'll send out a proper response.
|
||||
$status = Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
if ($status == Password::RESET_LINK_SENT) {
|
||||
return back()->with('status', __($status));
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [trans($status)],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the registration view.
|
||||
*/
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('Auth/Register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:32|alpha_dash',
|
||||
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$validator->after(function ($validator) use ($request) {
|
||||
if (User::whereRaw(DB::raw('LOWER(name) = ?'), [strtolower($request->name)])->exists()) {
|
||||
$validator->errors()->add('name', 'The name has already been taken.');
|
||||
}
|
||||
});
|
||||
|
||||
$validator->validate();
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return redirect(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\UserAction;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class FeedController extends Controller
|
||||
{
|
||||
public function view()
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
$followeeIds = $user->following()->pluck('followee_id');
|
||||
|
||||
$feed = UserAction::whereIn('user_id', $followeeIds)
|
||||
->whereNotIn('type', ['flight_deleted'])
|
||||
->with([
|
||||
'user',
|
||||
])
|
||||
|
||||
->latest()
|
||||
->limit(50)
|
||||
->get();
|
||||
return Inertia::render('Feed', [
|
||||
'user' => $user,
|
||||
'feed' => $feed,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Aircraft;
|
||||
use App\Models\Airline;
|
||||
use App\Models\Airport;
|
||||
use App\Models\CrewType;
|
||||
use App\Models\FlightClass;
|
||||
use App\Models\FlightReason;
|
||||
use App\Models\SeatType;
|
||||
use App\Models\UserAction;
|
||||
use App\Models\UserFlight;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
class FlightController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'flight_number' => ['nullable', 'string', 'max:10'],
|
||||
'departure_date' => ['required', 'date'],
|
||||
'arrival_date' => ['required', 'date'],
|
||||
'from_id' => ['required', 'integer', 'exists:airports,id'],
|
||||
'to_id' => ['required', 'integer', 'exists:airports,id'],
|
||||
'airline_id' => ['nullable', 'integer', 'exists:airlines,id'],
|
||||
'aircraft_id' => ['nullable', 'integer', 'exists:aircraft,id'],
|
||||
'aircraft_registration' => ['nullable', 'string', 'max:10'],
|
||||
'seat_number' => ['nullable', 'string', 'max:10'],
|
||||
'seat_type_id' => ['integer', 'exists:seat_types,id'],
|
||||
'flight_class_id' => ['integer', 'exists:flight_classes,id'],
|
||||
'flight_reason_id' => ['integer', 'exists:flight_reasons,id'],
|
||||
'note' => ['nullable', 'string', 'max:5000'],
|
||||
'auto_update' => ['boolean'],
|
||||
'crew_type_id' => ['nullable', 'exists:crew_types,id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function lookup(Request $request)
|
||||
{
|
||||
$number = strtoupper(trim($request->query('number', '')));
|
||||
|
||||
// Extract the airline code prefix — letters at the start e.g. "QF" from "QF1"
|
||||
preg_match('/^([A-Z]{2,3})/', $number, $matches);
|
||||
$code = $matches[1] ?? null;
|
||||
$isIata = strlen($code) === 2;
|
||||
$codeColumn = $isIata ? 'IATA_code' : 'ICAO_code';
|
||||
|
||||
$airlines = $code
|
||||
? Airline::where($codeColumn, $code)
|
||||
->get()
|
||||
->map(fn ($airline) => ['value' => $airline->id, 'title' => $airline->display_name, 'logo_url' => $airline->logo_url])
|
||||
->values()
|
||||
->toArray()
|
||||
: collect()->toArray();
|
||||
return response()->json([
|
||||
'airline_options' => $airlines,
|
||||
'from_options' => [],
|
||||
'to_options' => [],
|
||||
'aircraft_options' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
private function convertedDates(array $validated): array
|
||||
{
|
||||
$departureAirport = Airport::find($validated['from_id']);
|
||||
$arrivalAirport = Airport::find($validated['to_id']);
|
||||
|
||||
return [
|
||||
Carbon::createFromFormat('Y-m-d\TH:i', $validated['departure_date'], $departureAirport->timezone)->utc(),
|
||||
Carbon::createFromFormat('Y-m-d\TH:i', $validated['arrival_date'], $arrivalAirport->timezone)->utc(),
|
||||
];
|
||||
}
|
||||
|
||||
private function recordChanges(UserFlight $flight, array $dirty, array $original, array $updated): void
|
||||
{
|
||||
$changes = [];
|
||||
foreach ($dirty as $field => $newValue) {
|
||||
$changes[] = $this->formatChange($field, $flight->getOriginal($field), $newValue);
|
||||
}
|
||||
|
||||
UserAction::create([
|
||||
'user_id' => $flight->user_id,
|
||||
'data' => [
|
||||
'changes' => $changes,
|
||||
'original' => $original,
|
||||
'updated' => $updated,
|
||||
],
|
||||
'type' => 'flight_updated',
|
||||
]);
|
||||
}
|
||||
|
||||
private array $labelCache = [];
|
||||
|
||||
private function resolveLabel(string $field, mixed $value): string
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
$cacheKey = "{$field}:{$value}";
|
||||
|
||||
if (isset($this->labelCache[$cacheKey])) {
|
||||
return $this->labelCache[$cacheKey];
|
||||
}
|
||||
|
||||
$label = match($field) {
|
||||
|
||||
'airline_id' => Airline::find($value)?->display_name ?? $value,
|
||||
'departure_airport_id',
|
||||
'arrival_airport_id' => Airport::find($value)?->display_name ?? $value,
|
||||
'aircraft_id' => Aircraft::find($value)?->display_name_short ?? $value,
|
||||
'seat_type_id' => SeatType::find($value)?->name ?? $value,
|
||||
'flight_class_id' => FlightClass::find($value)?->name ?? $value,
|
||||
'flight_reason_id' => FlightReason::find($value)?->name ?? $value,
|
||||
'crew_type_id' => CrewType::find($value)?->name ?? $value,
|
||||
'departure_date',
|
||||
'arrival_date' => Carbon::parse($value)->format('j F Y \a\t H:iA'),
|
||||
default => (string) $value,
|
||||
};
|
||||
|
||||
return $this->labelCache[$cacheKey] = $label;
|
||||
}
|
||||
|
||||
private function formatChange(string $field, mixed $from, mixed $to): array
|
||||
{
|
||||
$label = str($field)->replace('_id', '')->replace('_', ' ')->title();
|
||||
$fromLabel = $this->resolveLabel($field, $from);
|
||||
$toLabel = $this->resolveLabel($field, $to);
|
||||
|
||||
return [
|
||||
'field' => $field,
|
||||
'from' => $fromLabel,
|
||||
'to' => $toLabel,
|
||||
];
|
||||
}
|
||||
|
||||
private function flightPayload(array $validated): array
|
||||
{
|
||||
[$departureUtc, $arrivalUtc] = $this->convertedDates($validated);
|
||||
return [
|
||||
'departure_date' => $departureUtc,
|
||||
'arrival_date' => $arrivalUtc,
|
||||
'flight_number' => $validated['flight_number'],
|
||||
'departure_airport_id' => $validated['from_id'],
|
||||
'arrival_airport_id' => $validated['to_id'],
|
||||
'airline_id' => $validated['airline_id'],
|
||||
'aircraft_id' => $validated['aircraft_id'],
|
||||
'aircraft_registration' => $validated['aircraft_registration'],
|
||||
'seat_number' => $validated['seat_number'],
|
||||
'seat_type_id' => $validated['seat_type_id'],
|
||||
'flight_class_id' => $validated['flight_class_id'],
|
||||
'flight_reason_id' => $validated['flight_reason_id'],
|
||||
'note' => $validated['note'],
|
||||
'auto_update' => $validated['auto_update'],
|
||||
'crew_type_id' => $validated['crew_type_id'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate($this->rules());
|
||||
|
||||
$newFlight = auth()->user()->flights()->create($this->flightPayload($validated));
|
||||
|
||||
UserAction::create([
|
||||
'user_id' => $newFlight->user_id,
|
||||
'type' => $newFlight->departure_date->isFuture() ? 'flight_booked' : 'flight_logged',
|
||||
'data' => [
|
||||
'flight' => $newFlight->snapshot($newFlight->id),
|
||||
],
|
||||
]);
|
||||
|
||||
return redirect()->route('profile.departure-board', [Auth::user()->name, $newFlight->id]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function update(Request $request, UserFlight $flight)
|
||||
{
|
||||
$this->authorize('update', $flight);
|
||||
|
||||
$validated = $request->validate($this->rules());
|
||||
|
||||
$flight->fill($this->flightPayload($validated));
|
||||
|
||||
if (!$flight->isDirty()) {
|
||||
return redirect()->route('profile.departure-board', [Auth::user()->name, $flight->id]);
|
||||
}
|
||||
|
||||
$dirty = $flight->getDirty();
|
||||
$original = $flight->snapshot($flight->id);
|
||||
|
||||
$flight->save();
|
||||
|
||||
$updated = $flight->snapshot($flight->id);
|
||||
$this->recordChanges($flight, $dirty, $original, $updated);
|
||||
|
||||
return redirect()->route('profile.departure-board', [Auth::user()->name, $flight->id]);
|
||||
}
|
||||
|
||||
public function delete(UserFlight $flight)
|
||||
{
|
||||
$this->authorize('delete', $flight);
|
||||
|
||||
$snapshot = $this->flightSnapshot($flight->id);
|
||||
|
||||
if(now()->utc()->isBefore($flight->departure_date)){
|
||||
$action = 'flight_deleted';
|
||||
} else {
|
||||
$action = 'flight_cancelled';
|
||||
}
|
||||
|
||||
UserAction::create([
|
||||
'user_id' => $flight->user_id,
|
||||
'type' => $action,
|
||||
'data' => [
|
||||
'flight' => $snapshot,
|
||||
]
|
||||
]);
|
||||
|
||||
$flight->delete();
|
||||
return redirect()->route('profile.departure-board', [Auth::user()->name]);
|
||||
}
|
||||
|
||||
public function staticData() : array {
|
||||
return [
|
||||
'seat_types' => SeatType::orderBy('id')->get()->toArray(),
|
||||
'flight_reasons' => FlightReason::orderBy('id')->get()->toArray(),
|
||||
'flight_classes' => FlightClass::orderBy('id')->get()->toArray(),
|
||||
'crew_types' => CrewType::orderBy('id')->get()->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
public function add(){
|
||||
return Inertia::render('AddFlight', $this->staticData());
|
||||
}
|
||||
|
||||
public function edit(UserFlight $flight)
|
||||
{
|
||||
$this->authorize('update', $flight);
|
||||
$flight->load('airline', 'aircraft', 'departureAirport.region.country', 'arrivalAirport.region.country', 'seatType', 'flightClass', 'flightReason');
|
||||
$flightData = [
|
||||
'id' => $flight->id,
|
||||
'flight_number' => $flight->flight_number,
|
||||
'departure_date' => $flight->departure_date->setTimezone($flight->departureAirport->timezone)->format('Y-m-d\TH:i'),
|
||||
'arrival_date' => $flight->arrival_date->setTimezone($flight->arrivalAirport->timezone)->format('Y-m-d\TH:i'),
|
||||
'aircraft_registration' => $flight->aircraft_registration,
|
||||
'seat_number' => $flight->seat_number,
|
||||
'note' => $flight->note,
|
||||
'auto_update' => $flight->auto_update,
|
||||
'seat_type' => $flight->seatType->toArray(),
|
||||
'flight_class' => $flight->flightClass->toArray(),
|
||||
'crew_type' => $flight->crewType?->toArray() ?? [],
|
||||
'flight_reason' => $flight->flightReason->toArray(),
|
||||
'airline_options' => $flight->airline
|
||||
? [['value' => $flight->airline->id, 'title' => $flight->airline->display_name, 'logo_url' => $flight->airline->logo_url]]
|
||||
: [],
|
||||
'from_options' => [['value' => $flight->departureAirport->id, 'title' => $flight->departureAirport->display_name, 'country_code' => strtolower($flight->departureAirport->region->country->code)]],
|
||||
'to_options' => [['value' => $flight->arrivalAirport->id, 'title' => $flight->arrivalAirport->display_name, 'country_code' => strtolower($flight->arrivalAirport->region->country->code)]],
|
||||
'aircraft_options' => $flight->aircraft
|
||||
? [['value' => $flight->aircraft->id, 'title' => $flight->aircraft->display_name]]
|
||||
: [],
|
||||
];
|
||||
return Inertia::render('AddFlight', [
|
||||
'flight' => $flightData,
|
||||
...$this->staticData(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Aircraft;
|
||||
use App\Models\Airline;
|
||||
use App\Models\Airport;
|
||||
use App\Models\FlightClass;
|
||||
use App\Models\FlightReason;
|
||||
use App\Models\ImportedFlight;
|
||||
use App\Models\SeatType;
|
||||
use App\Models\UserAction;
|
||||
use App\Models\UserFlight;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class FlightImportController extends Controller
|
||||
{
|
||||
private function formatTime(?string $time): string
|
||||
{
|
||||
if (!$time) return '00:00';
|
||||
|
||||
[$hours, $minutes] = explode(':', $time);
|
||||
return str_pad($hours, 2, '0', STR_PAD_LEFT) . ':' . $minutes;
|
||||
}
|
||||
|
||||
private function selectOptions($model): array
|
||||
{
|
||||
return $model::orderBy('id')
|
||||
->get(['id', 'name'])
|
||||
->map(fn($item) => ['value' => $item->id, 'title' => $item->name])
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getPossibleAircraft(string $aircraftQuery) {
|
||||
preg_match('/\((\w+)\)/', $aircraftQuery, $matches);
|
||||
$designator = $matches[1] ?? null;
|
||||
|
||||
$sortOverrides = [
|
||||
'B788' => "CASE WHEN model_full_name ILIKE '%BBJ%' THEN 1 ELSE 0 END",
|
||||
'B789' => "CASE WHEN model_full_name ILIKE '%BBJ%' THEN 1 ELSE 0 END",
|
||||
];
|
||||
|
||||
if(!$designator){
|
||||
$aircraft = [];
|
||||
} else {
|
||||
|
||||
$aircraft = Aircraft::when($designator, fn($query) => $query->where('designator', 'ilike', $designator))
|
||||
->when(
|
||||
isset($sortOverrides[$designator]),
|
||||
fn($q) => $q->orderByRaw($sortOverrides[$designator])
|
||||
)
|
||||
->orderBy('model_full_name')
|
||||
->limit(10)
|
||||
->get(['id', 'manufacturer_code', 'model_full_name', 'designator'])
|
||||
->map(fn($aircraft) => [
|
||||
'value' => $aircraft->id,
|
||||
'title' => $aircraft->display_name,
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
}
|
||||
|
||||
return $aircraft;
|
||||
}
|
||||
|
||||
public function getPossibleAirports(string $airportQuery) {
|
||||
preg_match('/\((\w{3})\/(\w{4})\)/', $airportQuery, $matches);
|
||||
$iata = $matches[1] ?? null;
|
||||
$icao = $matches[2] ?? null;
|
||||
|
||||
if (!$iata && !$icao) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$airports = Airport::with('region.country')
|
||||
->where(function ($q) use ($iata, $icao) {
|
||||
$q->where('iata_code', 'ilike', $iata)
|
||||
->orWhere('icao_code', 'ilike', $icao);
|
||||
})
|
||||
->orderByRaw("
|
||||
CASE
|
||||
WHEN iata_code = ? AND icao_code = ? THEN 0
|
||||
WHEN iata_code = ? THEN 1
|
||||
WHEN icao_code = ? THEN 2
|
||||
ELSE 3
|
||||
END
|
||||
", [$iata, $icao, $iata, $icao])
|
||||
->limit(10)
|
||||
->orderBy('id')
|
||||
->get(['id', 'name', 'municipality', 'iata_code', 'icao_code', 'region_id'])
|
||||
->map(fn(Airport $airport) => [
|
||||
'value' => $airport->id,
|
||||
'title' => $airport->display_name,
|
||||
'country_code' => strtolower($airport?->region->country->code ?? ''),
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return $airports;
|
||||
}
|
||||
|
||||
public function getPossibleAirlines(string $airlineQuery) {
|
||||
preg_match('/\((\w{2,3})\/(\w{3,4})\)/', $airlineQuery, $matches);
|
||||
$iata = $matches[1] ?? null;
|
||||
$icao = $matches[2] ?? null;
|
||||
|
||||
if(!$iata && !$icao){
|
||||
return [];
|
||||
}
|
||||
|
||||
$airlines = Airline::when($iata || $icao, function ($query) use ($iata, $icao) {
|
||||
$query->orderByRaw("
|
||||
CASE
|
||||
WHEN \"IATA_code\" = ? AND \"ICAO_code\" = ? THEN 0
|
||||
WHEN \"IATA_code\" = ? THEN 1
|
||||
WHEN \"ICAO_code\" = ? THEN 2
|
||||
ELSE 3
|
||||
END
|
||||
", [$iata, $icao, $iata, $icao])
|
||||
->where(function ($q) use ($iata, $icao) {
|
||||
$q->where('IATA_code', $iata)
|
||||
->orWhere('ICAO_code', $icao);
|
||||
});
|
||||
})
|
||||
->orderByDesc('active')
|
||||
->limit(10)
|
||||
->get(['id', 'name', 'IATA_code', 'ICAO_code', 'internal_name'])
|
||||
->map(fn($airline) => [
|
||||
'value' => $airline->id,
|
||||
'title' => $airline->display_name,
|
||||
'logo_url' => $airline->logo_url,
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
|
||||
return $airlines;
|
||||
}
|
||||
|
||||
public function reconcile(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$flightToReconcile = ImportedFlight::where('user_id', $user->id)->orderBy('date', 'asc')->first();
|
||||
|
||||
if (!$flightToReconcile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$date = null;
|
||||
if ($flightToReconcile->date) {
|
||||
$date = Carbon::createFromFormat('Y-m-d', $flightToReconcile->date)->format('Y-m-d');
|
||||
}
|
||||
|
||||
|
||||
return [
|
||||
'imported_flight_id' => $flightToReconcile->id,
|
||||
'flight_classes' => $this->selectOptions(FlightClass::class),
|
||||
'flight_reasons' => $this->selectOptions(FlightReason::class),
|
||||
'seat_types' => $this->selectOptions(SeatType::class),
|
||||
'flight_number' => $flightToReconcile->flight_number ?? '',
|
||||
'date' => $date ?? '',
|
||||
'dep_time' => $this->formatTime($flightToReconcile->dep_time),
|
||||
'arr_time' => $this->formatTime($flightToReconcile->arr_time),
|
||||
'duration' => $this->formatTime($flightToReconcile->duration),
|
||||
'registration' => $flightToReconcile->registration ?? '',
|
||||
'note' => $flightToReconcile->note ?? '',
|
||||
'seat_number' => $flightToReconcile->seat_number ?? '',
|
||||
'flight_class' => $flightToReconcile->flight_class ?? '',
|
||||
'seat_type' => $flightToReconcile->seat_type ?? '',
|
||||
'flight_reason' => $flightToReconcile->flight_reason ?? '',
|
||||
'airline_options' => $this->getPossibleAirlines($flightToReconcile->airline ?? ''),
|
||||
'to_options' => $this->getPossibleAirports($flightToReconcile->to ?? ''),
|
||||
'from_options' => $this->getPossibleAirports($flightToReconcile->from ?? ''),
|
||||
'aircraft_options' => $this->getPossibleAircraft($flightToReconcile->aircraft ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'date' => 'required|date',
|
||||
'imported_flight_id' => 'required|exists:imported_flights,id',
|
||||
'flight_number' => 'nullable|string',
|
||||
'from_id' => 'required|integer|exists:airports,id',
|
||||
'to_id' => 'required|integer|exists:airports,id',
|
||||
'dep_time' => 'nullable|date_format:H:i',
|
||||
'arr_time' => 'nullable|date_format:H:i',
|
||||
'duration' => 'required|date_format:H:i',
|
||||
'airline_id' => 'nullable|integer|exists:airlines,id',
|
||||
'aircraft_id' => 'nullable|integer|exists:aircraft,id',
|
||||
'registration' => 'nullable|string',
|
||||
'seat_number' => 'nullable|string',
|
||||
'seat_type_id' => 'required|integer|exists:seat_types,id',
|
||||
'flight_class_id' => 'required|integer|exists:flight_classes,id',
|
||||
'flight_reason_id' => 'required|integer|exists:flight_reasons,id',
|
||||
'note' => 'nullable|string',
|
||||
], [
|
||||
'imported_flight_id.required' => 'The flight you are trying to reconcile needs to be reimported or refreshed.',
|
||||
'from_id.required' => 'Please select a departure airport.',
|
||||
'to_id.required' => 'Please select an arrival airport.',
|
||||
'dep_time.date_format' => 'Departure time must be in HH:MM format, i.e: 01:25',
|
||||
'arr_time.date_format' => 'Arrival time must be in HH:MM format, i.e: 12:25',
|
||||
'duration.date_format' => 'Must be in HH:MM format, e.g: 03:37',
|
||||
'duration.required' => 'A duration is required to be able to accurately calculate the arrival date',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
$departureAirport = Airport::find($validated['from_id']);
|
||||
$arrivalAirport = Airport::find($validated['to_id']);
|
||||
|
||||
// Parse departure in local airport timezone, then convert to UTC
|
||||
$depTime = $validated['dep_time'] ?? '00:00';
|
||||
$departure = Carbon::createFromFormat(
|
||||
'Y-m-d H:i',
|
||||
$validated['date'] . ' ' . $depTime,
|
||||
$departureAirport->timezone
|
||||
)->utc();
|
||||
|
||||
// Calculate duration-based arrival in UTC
|
||||
[$durationHours, $durationMinutes] = explode(':', $validated['duration']);
|
||||
$durationArrival = $departure->copy()
|
||||
->addHours((int) $durationHours)
|
||||
->addMinutes((int) $durationMinutes);
|
||||
|
||||
// If arrival time provided, parse it in arrival airport timezone and convert to UTC
|
||||
if (!empty($validated['arr_time'])) {
|
||||
$arrival = Carbon::createFromFormat(
|
||||
'Y-m-d H:i',
|
||||
$validated['date'] . ' ' . $validated['arr_time'],
|
||||
$arrivalAirport->timezone
|
||||
)->utc();
|
||||
|
||||
// If arrival is not after departure, fall back to duration-based arrival
|
||||
if ($arrival->lte($departure)) {
|
||||
$arrival = $durationArrival;
|
||||
}
|
||||
} else {
|
||||
$arrival = $durationArrival;
|
||||
}
|
||||
|
||||
$newFlight = UserFlight::create([
|
||||
'user_id' => $user->id,
|
||||
'departure_date' => $departure,
|
||||
'arrival_date' => $arrival,
|
||||
'flight_number' => $validated['flight_number'],
|
||||
'departure_airport_id' => $validated['from_id'],
|
||||
'arrival_airport_id' => $validated['to_id'],
|
||||
'airline_id' => $validated['airline_id'] ?? null,
|
||||
'aircraft_id' => $validated['aircraft_id'] ?? null,
|
||||
'aircraft_registration' => $validated['registration'] ?? null,
|
||||
'seat_number' => $validated['seat_number'] ?? null,
|
||||
'seat_type_id' => $validated['seat_type_id'] ?? 0,
|
||||
'flight_class_id' => $validated['flight_class_id'] ?? 0,
|
||||
'flight_reason_id' => $validated['flight_reason_id'] ?? 0,
|
||||
'note' => $validated['note'] ?? null,
|
||||
]);
|
||||
|
||||
UserAction::create([
|
||||
'user_id' => $newFlight->user_id,
|
||||
'type' => $newFlight->departure_date->isFuture() ? 'flight_booked' : 'flight_imported',
|
||||
'data' => [
|
||||
'flight' => $newFlight->snapshot($newFlight->id),
|
||||
],
|
||||
]);
|
||||
|
||||
ImportedFlight::destroy($validated['imported_flight_id']);
|
||||
return to_route('reconcile');
|
||||
}
|
||||
|
||||
|
||||
private function validateCsvFormat(string $path): ?string
|
||||
{
|
||||
$handle = fopen($path, 'r');
|
||||
|
||||
// Must have empty first line
|
||||
$firstLine = fgetcsv($handle);
|
||||
if (!empty(array_filter($firstLine))) {
|
||||
fclose($handle);
|
||||
return 'CSV must match the MyFlightRadar24 export format.';
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
$expectedHeaders = [
|
||||
'date', 'flight_number', 'from', 'to', 'dep_time', 'arr_time',
|
||||
'duration', 'airline', 'aircraft', 'registration', 'seat_number',
|
||||
'seat_type', 'flight_class', 'flight_reason', 'note',
|
||||
'dep_id', 'arr_id', 'airline_id', 'aircraft_id'
|
||||
];
|
||||
|
||||
$headers = array_map(
|
||||
fn($h) => strtolower(trim(str_replace([' ', '"'], ['_', ''], $h))),
|
||||
fgetcsv($handle)
|
||||
);
|
||||
|
||||
if ($headers !== $expectedHeaders) {
|
||||
fclose($handle);
|
||||
return 'CSV headers do not match the expected format.';
|
||||
}
|
||||
|
||||
// Validate data rows
|
||||
$row = 1;
|
||||
while (($data = fgetcsv($handle)) !== false) {
|
||||
$row++;
|
||||
|
||||
if (count($data) !== count($expectedHeaders)) {
|
||||
fclose($handle);
|
||||
return "Row {$row} has the wrong number of columns.";
|
||||
}
|
||||
|
||||
$combined = array_combine($expectedHeaders, $data);
|
||||
|
||||
if (empty($combined['date']) || empty($combined['from']) || empty($combined['to'])) {
|
||||
fclose($handle);
|
||||
return "Row {$row} is missing a required field (date, from, or to).";
|
||||
}
|
||||
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $combined['date'])) {
|
||||
fclose($handle);
|
||||
return "Row {$row} has an invalid date format. Expected YYYY-MM-DD, got '{$combined['date']}'.";
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
return null;
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'csv' => ['required', 'file', 'mimes:csv,txt', 'max:10240'],
|
||||
]);
|
||||
|
||||
$path = $request->file('csv')->getRealPath();
|
||||
|
||||
$validationError = $this->validateCsvFormat($path);
|
||||
if ($validationError) {
|
||||
return response()->json(['message' => $validationError], 422);
|
||||
}
|
||||
|
||||
$handle = fopen($path, 'r');
|
||||
|
||||
fgetcsv($handle);
|
||||
|
||||
// Read and normalise header row
|
||||
$headers = array_map(
|
||||
fn($h) => strtolower(trim(str_replace(' ', '_', $h))),
|
||||
fgetcsv($handle)
|
||||
);
|
||||
|
||||
\Log::debug('CSV headers', $headers);
|
||||
$firstRow = fgetcsv($handle);
|
||||
\Log::debug('First row', $firstRow ?? ['empty']);
|
||||
\Log::debug('Header count: ' . count($headers) . ', Row count: ' . count($firstRow ?? []));
|
||||
|
||||
$map = [
|
||||
'date' => 'date',
|
||||
'flight_number' => 'flight_number',
|
||||
'from' => 'from',
|
||||
'to' => 'to',
|
||||
'dep_time' => 'dep_time',
|
||||
'arr_time' => 'arr_time',
|
||||
'duration' => 'duration',
|
||||
'airline' => 'airline',
|
||||
'aircraft' => 'aircraft',
|
||||
'registration' => 'registration',
|
||||
'seat_number' => 'seat_number',
|
||||
'seat_type' => 'seat_type',
|
||||
'flight_class' => 'flight_class',
|
||||
'flight_reason' => 'flight_reason',
|
||||
'note' => 'note',
|
||||
];
|
||||
|
||||
$userId = Auth::id();
|
||||
$imported = 0;
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
if (count($row) !== count($headers)) {
|
||||
continue; // skip malformed rows
|
||||
}
|
||||
|
||||
$raw = array_combine($headers, $row);
|
||||
$data = ['user_id' => $userId];
|
||||
|
||||
foreach ($map as $csvKey => $column) {
|
||||
$data[$column] = $raw[$csvKey] ?? null;
|
||||
}
|
||||
|
||||
ImportedFlight::create($data);
|
||||
$imported++;
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return response()->json(['imported' => $imported]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use App\Http\Resources\UserFlightResource;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class FlightProfileController extends Controller
|
||||
{
|
||||
public function profileData(User $user, string $view, ?int $selectedFlightId = null) : array {
|
||||
return [
|
||||
'user' => $user,
|
||||
'canEdit' => auth()->check() && auth()->id() === $user->id,
|
||||
'initialView' => $view,
|
||||
'selectedFlightId' => $selectedFlightId,
|
||||
'flight_api_url' => '/data/user/'.$user->name.'/flights',
|
||||
'isFollowing' => auth()->check() && auth()->user()->isFollowing($user),
|
||||
];
|
||||
}
|
||||
|
||||
public function departureBoard(User $user, ?UserFlight $flight = null){
|
||||
$profileData = $this->profileData($user, 'board', $flight?->id);
|
||||
return Inertia::render('UserProfile', $profileData);
|
||||
}
|
||||
|
||||
public function map(User $user){
|
||||
$profileData = $this->profileData($user, 'map');
|
||||
return Inertia::render('UserProfile', $profileData);
|
||||
}
|
||||
|
||||
public function boardingPasses(User $user){
|
||||
$profileData = $this->profileData($user, 'passes');
|
||||
return Inertia::render('UserProfile', $profileData);
|
||||
}
|
||||
|
||||
public function view(User $user)
|
||||
{
|
||||
return $this->departureBoard($user);
|
||||
}
|
||||
}
|
||||
@@ -8,43 +8,85 @@ use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class LogoController extends Controller
|
||||
{
|
||||
public function getAirlineLogo(?Airline $airline){
|
||||
|
||||
$logoFile = $airline?->logo ?? 'blank.png';
|
||||
$path = 'images/logos/tail/' . $logoFile;
|
||||
if (!Storage::disk('local')->exists($path)) {
|
||||
$path = 'images/logos/tail/blank.png';
|
||||
public static function deduplicateLogo(string $logo, array $correctInternalNames)
|
||||
{
|
||||
$log = fn(string $message) => print($message . PHP_EOL);
|
||||
|
||||
if (empty($correctInternalNames)) {
|
||||
$nulled = Airline::where('logo', $logo)->get(['name', 'internal_name']);
|
||||
Airline::where('logo', $logo)->update(['logo' => null]);
|
||||
foreach ($nulled as $airline) {
|
||||
$log(" Logo nulled: {$airline->name} ({$airline->internal_name})");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return response()->file(Storage::disk('local')->path($path), [
|
||||
'Content-Type' => 'image/png',
|
||||
]);
|
||||
$existing = Airline::whereIn('internal_name', $correctInternalNames)
|
||||
->pluck('internal_name')
|
||||
->toArray();
|
||||
|
||||
$missing = array_diff($correctInternalNames, $existing);
|
||||
|
||||
if (!empty($missing)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'The following internal names do not exist in the database: ' . implode(', ', $missing)
|
||||
);
|
||||
}
|
||||
|
||||
$nulled = Airline::where('logo', $logo)
|
||||
->whereNotIn('internal_name', $correctInternalNames)
|
||||
->get(['name', 'internal_name']);
|
||||
|
||||
Airline::where('logo', $logo)
|
||||
->whereNotIn('internal_name', $correctInternalNames)
|
||||
->update(['logo' => null]);
|
||||
|
||||
foreach ($nulled as $airline) {
|
||||
$log(" Logo nulled: {$airline->name} ({$airline->internal_name})");
|
||||
}
|
||||
}
|
||||
|
||||
public function getLogoByIATACode(string $code)
|
||||
public static function nullMissingLogoFiles()
|
||||
{
|
||||
$airline = Airline::where('IATA_code', strtoupper($code))
|
||||
->whereNotNull('logo')
|
||||
->latest('id')
|
||||
->first();
|
||||
$log = fn(string $message) => print($message . PHP_EOL);
|
||||
|
||||
return $this->getAirlineLogo($airline);
|
||||
$airlines = Airline::whereNotNull('logo')->get(['id', 'name', 'logo']);
|
||||
|
||||
foreach ($airlines as $airline) {
|
||||
$path = storage_path('app/private/images/logos/tail/' . $airline->logo);
|
||||
|
||||
if (!file_exists($path)) {
|
||||
$airline->update(['logo' => null]);
|
||||
$log(" Logo nulled (file missing): {$airline->name} — {$airline->logo}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getLogoByICAOCode(string $code)
|
||||
public static function renameLogoFiles()
|
||||
{
|
||||
$airline = Airline::where('ICAO_code', strtoupper($code))
|
||||
->whereNotNull('logo')
|
||||
->latest('id')
|
||||
->first();
|
||||
$log = fn(string $message) => print($message . PHP_EOL);
|
||||
|
||||
return $this->getAirlineLogo($airline);
|
||||
}
|
||||
$airlines = Airline::whereNotNull('logo')->get(['id', 'name', 'internal_name', 'logo']);
|
||||
|
||||
public function getLogoByCode(string $code){
|
||||
foreach ($airlines as $airline) {
|
||||
$expectedName = $airline->internal_name . '.png';
|
||||
|
||||
return strlen($code) == 2
|
||||
? $this->getLogoByIATACode($code)
|
||||
: $this->getLogoByICAOCode($code);
|
||||
if ($airline->logo === $expectedName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldPath = storage_path('app/private/images/logos/tail/' . $airline->logo);
|
||||
$newPath = storage_path('app/private/images/logos/tail/' . $expectedName);
|
||||
|
||||
if (!file_exists($oldPath)) {
|
||||
$log(" Skipping (file missing): {$airline->name} — {$airline->logo}");
|
||||
continue;
|
||||
}
|
||||
|
||||
rename($oldPath, $newPath);
|
||||
$airline->update(['logo' => $expectedName]);
|
||||
$log(" Renamed: {$airline->logo} → {$expectedName} ({$airline->name})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Notification;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
public function markRead(Request $request, Notification $notification)
|
||||
{
|
||||
$this->authorize('update', $notification);
|
||||
$notification->markAsRead();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ProfileUpdateRequest;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the user's profile form.
|
||||
*/
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
return Inertia::render('Profile/Edit', [
|
||||
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
|
||||
'status' => session('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$request->user()->fill($request->validated());
|
||||
|
||||
if ($request->user()->isDirty('email')) {
|
||||
$request->user()->email_verified_at = null;
|
||||
}
|
||||
|
||||
$request->user()->save();
|
||||
|
||||
return Redirect::route('profile.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's account.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return Redirect::to('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Aircraft;
|
||||
use App\Models\Airline;
|
||||
use App\Models\Airport;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
public function airlines(){
|
||||
$q = request('q', '');
|
||||
|
||||
return Airline::orderByDesc('active')
|
||||
->where(function ($query) use ($q) {
|
||||
$len = strlen($q);
|
||||
if ($len === 2) {
|
||||
$query->where('IATA_code', 'ilike', $q);
|
||||
} elseif ($len === 3) {
|
||||
$query->where('ICAO_code', 'ilike', $q);
|
||||
} else {
|
||||
$query->where('name', 'ilike', "%{$q}%")
|
||||
->orWhere('IATA_code', 'ilike', "%{$q}%")
|
||||
->orWhere('ICAO_code', 'ilike', "%{$q}%");
|
||||
}
|
||||
})
|
||||
->limit(50)
|
||||
->get()
|
||||
->map(fn($airline) => [
|
||||
'value' => $airline->id,
|
||||
'title' => $airline->display_name,
|
||||
'logo_url' => $airline->logo_url,
|
||||
])
|
||||
->values();
|
||||
}
|
||||
|
||||
public function aircraft()
|
||||
{
|
||||
$q = request('q', '');
|
||||
$replacedQuery = str_replace(['A3', 'A2'], ['A-3', 'A-2'], $q);
|
||||
|
||||
return Aircraft::where('designator', 'ilike', "%{$q}%")
|
||||
->orWhereRaw("CONCAT(manufacturer_code, ' ', model_full_name) ilike ?", ["%{$q}%"])
|
||||
->orWhereRaw("CONCAT(manufacturer_code, ' ', model_full_name) ilike ?", ["%{$replacedQuery}%"])
|
||||
->limit(200)
|
||||
->orderBy('id', 'asc')
|
||||
->get(['id', 'manufacturer_code', 'model_full_name', 'designator'])
|
||||
->map(fn($aircraft) => [
|
||||
'value' => $aircraft->id,
|
||||
'title' => $aircraft->display_name,
|
||||
])
|
||||
->values();
|
||||
}
|
||||
|
||||
public function airports()
|
||||
{
|
||||
$q = request('q', '');
|
||||
$len = strlen($q);
|
||||
|
||||
if ($len < 3) return [];
|
||||
|
||||
return Airport::with('region.country')
|
||||
->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]))
|
||||
->limit(15)
|
||||
->get(['id', 'name', 'municipality', 'iata_code', 'icao_code', 'region_id'])
|
||||
->map(fn($airport) => [
|
||||
'value' => $airport->id,
|
||||
'title' => $airport->display_name,
|
||||
'country_code' => strtolower($airport->region->country->code),
|
||||
])
|
||||
->values();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Followee;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function follow(User $user): JsonResponse
|
||||
{
|
||||
$existing = Followee::where('user_id', auth()->id())
|
||||
->where('followee_id', $user->id)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$existing->delete();
|
||||
return response()->json(['following' => false]);
|
||||
}
|
||||
|
||||
Followee::create([
|
||||
'user_id' => auth()->id(),
|
||||
'followee_id' => $user->id,
|
||||
]);
|
||||
|
||||
return response()->json(['following' => true]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserFlightController extends Controller
|
||||
{
|
||||
|
||||
protected User $user;
|
||||
|
||||
function __construct(User $user){
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function flights(){
|
||||
return UserFlight::where('user_id', $this->user->id)
|
||||
->with([
|
||||
'departureAirport.region.country',
|
||||
'departureAirport.region.continent',
|
||||
'arrivalAirport.region.country',
|
||||
'arrivalAirport.region.continent',
|
||||
'airline.country',
|
||||
'airline.alliance',
|
||||
'aircraft',
|
||||
'seatType',
|
||||
'flightReason',
|
||||
'flightClass',
|
||||
'crewType'
|
||||
])
|
||||
->orderBy('departure_date', 'desc')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
/**
|
||||
* The root template that is loaded on the first page visit.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rootView = 'app';
|
||||
|
||||
/**
|
||||
* Determine the current asset version.
|
||||
*/
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the props that are shared by default.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
return [
|
||||
...parent::share($request),
|
||||
'logo_api_url' => config('app.logo_api_url'),
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
],
|
||||
'achievement_notifications' => fn() => $request->user()
|
||||
? $request->user()
|
||||
->notifications()
|
||||
->with('achievement')
|
||||
->where('is_achievement', true)
|
||||
->whereNull('read_at')
|
||||
->latest()
|
||||
->get()
|
||||
: [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limiting throttle key for the request.
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($this->user()->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\UserFlight;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/** @mixin UserFlight */
|
||||
class UserFlightResource extends JsonResource
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $internal_name
|
||||
* @property string $short_description
|
||||
* @property string $long_description
|
||||
* @property string $icon
|
||||
* @property bool $progressive
|
||||
* @property string|null $difficulty_description
|
||||
* @property int $achievement_category_id
|
||||
* @property int $achievement_difficulty_id
|
||||
*
|
||||
* @property-read AchievementCategory $category
|
||||
* @property-read AchievementDifficulty $difficulty
|
||||
* @property-read Collection<int, UserAchievement> $userAchievements
|
||||
* @property-read Collection<int, User> $users
|
||||
*/
|
||||
class Achievement extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'internal_name',
|
||||
'short_description',
|
||||
'long_description',
|
||||
'icon',
|
||||
'progressive',
|
||||
'difficulty_description',
|
||||
'achievement_category_id',
|
||||
'achievement_difficulty_id',
|
||||
'threshold',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'progressive' => 'boolean',
|
||||
'threshold' => 'integer',
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Relationships
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AchievementCategory::class, 'achievement_category_id');
|
||||
}
|
||||
|
||||
public function difficulty(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AchievementDifficulty::class, 'achievement_difficulty_id');
|
||||
}
|
||||
|
||||
public function userAchievements(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserAchievement::class, 'achievement_id');
|
||||
}
|
||||
|
||||
/** Users who have unlocked (or are progressing toward) this achievement. */
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'user_achievements')
|
||||
->withPivot('progress')
|
||||
->withTimestamps();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $internal_name
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
*
|
||||
* @property-read Collection<int, Achievement> $achievements
|
||||
*/
|
||||
class AchievementCategory extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'internal_name',
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Relationships
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function achievements(): HasMany
|
||||
{
|
||||
return $this->hasMany(Achievement::class, 'achievement_category_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $internal_name
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
*
|
||||
* @property-read Collection<int, Achievement> $achievements
|
||||
*/
|
||||
class AchievementDifficulty extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'internal_name',
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Relationships
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function achievements(): HasMany
|
||||
{
|
||||
return $this->hasMany(Achievement::class, 'achievement_difficulty_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Aircraft extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'designator',
|
||||
'manufacturer_code',
|
||||
'model_full_name',
|
||||
'aircraft_description',
|
||||
'engine_type',
|
||||
'engine_count',
|
||||
'wtc',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'engine_count' => 'integer',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'display_name',
|
||||
'display_name_short'
|
||||
];
|
||||
|
||||
protected function displayName() : Attribute{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return "{$this->manufacturer_code} {$this->model_full_name} ({$this->designator})";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function displayNameShort(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$name = "{$this->manufacturer_code} {$this->model_full_name}";
|
||||
return trim(preg_replace('/\s*\(.*?\)/', '', $name));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+36
-2
@@ -3,7 +3,9 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Airline extends Model
|
||||
{
|
||||
@@ -14,8 +16,8 @@ class Airline extends Model
|
||||
'ICAO_code',
|
||||
'name',
|
||||
'internal_name',
|
||||
'country_code',
|
||||
'country_name',
|
||||
'country_id',
|
||||
'alliance_id',
|
||||
'active',
|
||||
'logo',
|
||||
];
|
||||
@@ -25,4 +27,36 @@ class Airline extends Model
|
||||
];
|
||||
|
||||
public $timestamps = false;
|
||||
protected $appends = [
|
||||
'display_name',
|
||||
'logo_url',
|
||||
];
|
||||
|
||||
protected function displayName() : Attribute{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$codes = array_filter([$this->IATA_code, $this->ICAO_code]);
|
||||
$codeString = count($codes) ? ' (' . implode('/', $codes) . ')' : '';
|
||||
return "{$this->name}{$codeString}";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function logoUrl() : Attribute{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return config('app.logo_api_url') . "/airline/$this->internal_name/logo/tail";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function alliance(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Alliance::class);
|
||||
}
|
||||
|
||||
public function country(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Country::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Airport extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'type',
|
||||
'name',
|
||||
'latitude_deg',
|
||||
'longitude_deg',
|
||||
'elevation_ft',
|
||||
'timezone',
|
||||
'region_id',
|
||||
'municipality',
|
||||
'icao_code',
|
||||
'iata_code',
|
||||
'local_code',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'latitude_deg' => 'float',
|
||||
'longitude_deg' => 'float',
|
||||
'elevation_ft' => 'integer',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'display_code',
|
||||
'display_name',
|
||||
];
|
||||
|
||||
protected function displayName() : Attribute{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$codes = array_filter([$this->iata_code, $this->icao_code]);
|
||||
$codeString = count($codes) ? ' (' . implode('/', $codes) . ')' : '';
|
||||
return "{$this->municipality} / {$this->name}{$codeString}";
|
||||
}
|
||||
);
|
||||
}
|
||||
protected function displayCode(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->iata_code ?? $this->icao_code ?? $this->local_code ?? '---'
|
||||
);
|
||||
}
|
||||
|
||||
public function region(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Region::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Alliance extends Model
|
||||
{
|
||||
protected $table = 'alliances';
|
||||
|
||||
protected $fillable = [
|
||||
'internal_name',
|
||||
'name',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'internal_name' => 'string',
|
||||
];
|
||||
|
||||
public function airlines(): HasMany
|
||||
{
|
||||
return $this->hasMany(Airline::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Continent extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
'internal_name',
|
||||
];
|
||||
|
||||
public function countries(): HasMany
|
||||
{
|
||||
return $this->hasMany(Country::class);
|
||||
}
|
||||
|
||||
public function regions(): HasMany
|
||||
{
|
||||
return $this->hasMany(Region::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Country extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
'continent',
|
||||
];
|
||||
|
||||
public function regions(): HasMany
|
||||
{
|
||||
return $this->hasMany(Region::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CrewType extends Model
|
||||
{
|
||||
protected $table = 'crew_types';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'internal_name',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FlightClass extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'internal_name',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FlightReason extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Followee extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'followee_id',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function followee(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'followee_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ImportedFlight extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'date',
|
||||
'flight_number',
|
||||
'from',
|
||||
'to',
|
||||
'dep_time',
|
||||
'arr_time',
|
||||
'duration',
|
||||
'airline',
|
||||
'aircraft',
|
||||
'registration',
|
||||
'seat_number',
|
||||
'seat_type',
|
||||
'flight_class',
|
||||
'flight_reason',
|
||||
'note',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property string $title
|
||||
* @property string $body
|
||||
* @property string|null $url
|
||||
* @property bool $is_achievement
|
||||
* @property int|null $achievement_id
|
||||
* @property \Carbon\Carbon|null $read_at
|
||||
* @property \Carbon\Carbon|null $dismissed_at
|
||||
* @property \Carbon\Carbon|null $expires_at
|
||||
*
|
||||
* @property-read User $user
|
||||
* @property-read Achievement|null $achievement
|
||||
*
|
||||
* @method static Builder unread()
|
||||
* @method static Builder undismissed()
|
||||
* @method static Builder active()
|
||||
* @method static Builder forAchievements()
|
||||
*/
|
||||
class Notification extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'title',
|
||||
'body',
|
||||
'url',
|
||||
'is_achievement',
|
||||
'achievement_id',
|
||||
'read_at',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_achievement' => 'boolean',
|
||||
'read_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Relationships
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function achievement(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Achievement::class);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Scopes
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/** Notifications the user hasn't opened yet. */
|
||||
public function scopeUnread(Builder $query): void
|
||||
{
|
||||
$query->whereNull('read_at');
|
||||
}
|
||||
|
||||
/** Not expired (no expiry set, or expiry is in the future). */
|
||||
public function scopeActive(Builder $query): void
|
||||
{
|
||||
$query->where(function (Builder $q) {
|
||||
$q->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
});
|
||||
}
|
||||
|
||||
/** Only achievement notifications. */
|
||||
public function scopeForAchievements(Builder $query): void
|
||||
{
|
||||
$query->where('is_achievement', true);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function markAsRead(): void
|
||||
{
|
||||
if (! $this->read_at) {
|
||||
$this->update(['read_at' => now()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at !== null && $this->expires_at->isPast();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Region extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'local_code',
|
||||
'name',
|
||||
'continent',
|
||||
'country_id',
|
||||
];
|
||||
|
||||
public function country(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Country::class);
|
||||
}
|
||||
|
||||
public function continent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Continent::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SeatType extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
];
|
||||
public $timestamps = false;
|
||||
}
|
||||
+53
-3
@@ -2,20 +2,26 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use App\Http\Controllers\UserFlightController;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use App\Traits\HasAchievements;
|
||||
use App\Models\Notification;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
#[Fillable(['name', 'email', 'password'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
use HasFactory, HasAchievements, HasApiTokens;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
@@ -29,4 +35,48 @@ class User extends Authenticatable
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
public function achievements(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserAchievement::class);
|
||||
}
|
||||
|
||||
public function resolveRouteBinding($value, $field = null): ?User
|
||||
{
|
||||
return $this->where('name', 'ilike', $value)->firstOrFail();
|
||||
}
|
||||
|
||||
public function FlightController(): UserFlightController
|
||||
{
|
||||
return new UserFlightController($this);
|
||||
}
|
||||
|
||||
public function flights(): HasMany {
|
||||
return $this->hasMany(UserFlight::class);
|
||||
}
|
||||
|
||||
public function ImportedFlights(): HasMany
|
||||
{
|
||||
return $this->hasMany(ImportedFlight::class);
|
||||
}
|
||||
|
||||
public function following(): HasMany
|
||||
{
|
||||
return $this->hasMany(Followee::class, 'user_id');
|
||||
}
|
||||
|
||||
public function followers(): HasMany
|
||||
{
|
||||
return $this->hasMany(Followee::class, 'followee_id');
|
||||
}
|
||||
|
||||
public function isFollowing(User $user): bool
|
||||
{
|
||||
return $this->following()->where('followee_id', $user->id)->exists();
|
||||
}
|
||||
|
||||
public function notifications(): HasMany
|
||||
{
|
||||
return $this->hasMany(Notification::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property int $achievement_id
|
||||
* @property int|null $progress
|
||||
*
|
||||
* @property-read User $user
|
||||
* @property-read Achievement $achievement
|
||||
*/
|
||||
class UserAchievement extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'achievement_id',
|
||||
'progress',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'progress' => 'integer',
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Relationships
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function achievement(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Achievement::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserAction extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'type',
|
||||
'data'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
'data' => 'array',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'display_type',
|
||||
];
|
||||
|
||||
protected function displayType(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => match ($this->type) {
|
||||
'flight_booked' => 'Flight Booked',
|
||||
'flight_cancelled' => 'Flight Cancelled',
|
||||
'flight_updated' => 'Flight Updated',
|
||||
'flight_imported' => 'Flight Imported from FR24',
|
||||
'flight_logged' => 'Flight Logged',
|
||||
'flight_deleted' => 'Flight Deleted',
|
||||
default => 'Unknown Action'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserFlight extends Model
|
||||
{
|
||||
protected $table = 'user_flights';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'departure_date',
|
||||
'arrival_date',
|
||||
'flight_number',
|
||||
'departure_airport_id',
|
||||
'arrival_airport_id',
|
||||
'airline_id',
|
||||
'aircraft_id',
|
||||
'aircraft_registration',
|
||||
'seat_number',
|
||||
'seat_type_id',
|
||||
'flight_class_id',
|
||||
'flight_reason_id',
|
||||
'crew_type_id',
|
||||
'note',
|
||||
'auto_update',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'departure_date' => 'immutable_datetime',
|
||||
'arrival_date' => 'immutable_datetime',
|
||||
'auto_update' => 'boolean',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'departure_date_display',
|
||||
'departure_time_display',
|
||||
'arrival_date_display',
|
||||
'arrival_time_display',
|
||||
'arrival_day_difference',
|
||||
'duration',
|
||||
'duration_display',
|
||||
'distance',
|
||||
];
|
||||
|
||||
public function calculateGreatCircleDistance(): float{
|
||||
$earthRadiusKm = 6371;
|
||||
[$depLat, $depLong] = [$this->departureAirport->latitude_deg, $this->departureAirport->longitude_deg];
|
||||
[$arrLat, $arrLong] = [$this->arrivalAirport->latitude_deg, $this->arrivalAirport->longitude_deg];
|
||||
|
||||
|
||||
$latDelta = deg2rad($arrLat - $depLat);
|
||||
$longDelta = deg2rad($arrLong - $depLong);
|
||||
|
||||
$a = sin($latDelta / 2) * sin($latDelta / 2)
|
||||
+ cos(deg2rad($depLat)) * cos(deg2rad($arrLat))
|
||||
* sin($longDelta / 2) * sin($longDelta / 2);
|
||||
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
|
||||
return $earthRadiusKm * $c;
|
||||
}
|
||||
|
||||
protected function departureDateDisplay(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->departure_date
|
||||
->toMutable()
|
||||
->setTimezone($this->departureAirport->timezone)
|
||||
->format('j M Y')
|
||||
);
|
||||
}
|
||||
|
||||
protected function departureTimeDisplay(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->departure_date
|
||||
->toMutable()
|
||||
->setTimezone($this->departureAirport->timezone)
|
||||
->format('g:iA')
|
||||
);
|
||||
}
|
||||
|
||||
protected function arrivalDateDisplay(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->arrival_date
|
||||
?->copy()
|
||||
->setTimezone($this->arrivalAirport->timezone)
|
||||
->format('j M Y')
|
||||
);
|
||||
}
|
||||
|
||||
protected function arrivalTimeDisplay(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->arrival_date
|
||||
?->copy()
|
||||
->setTimezone($this->arrivalAirport->timezone)
|
||||
->format('g:iA')
|
||||
);
|
||||
}
|
||||
|
||||
protected function arrivalDayDifference(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if (!$this->arrival_date) return 0;
|
||||
|
||||
$departureLocal = $this->departure_date->copy()->setTimezone($this->departureAirport->timezone);
|
||||
$arrivalLocal = $this->arrival_date->copy()->setTimezone($this->arrivalAirport->timezone);
|
||||
|
||||
return (int) abs(
|
||||
Carbon::parse($arrivalLocal->toDateString())
|
||||
->diffInDays(Carbon::parse($departureLocal->toDateString()))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function duration(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->departure_date->diffInMinutes($this->arrival_date)
|
||||
);
|
||||
}
|
||||
|
||||
protected function durationDisplay(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$hours = intdiv($this->duration, 60);
|
||||
$minutes = $this->duration % 60;
|
||||
|
||||
return $hours . 'h ' . str_pad($minutes, 2, '0', STR_PAD_LEFT) . 'm';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function distance(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->calculateGreatCircleDistance()
|
||||
);
|
||||
}
|
||||
|
||||
public function isDomestic() : bool{
|
||||
return $this->departureAirport->region->country_id == $this->arrivalAirport->region->country_id;
|
||||
}
|
||||
|
||||
public function isInternational() : bool{
|
||||
return !$this->isDomestic();
|
||||
}
|
||||
|
||||
public static function snapshot($userFlightId){
|
||||
return UserFlight::with([
|
||||
'departureAirport',
|
||||
'departureAirport.region.country',
|
||||
'arrivalAirport',
|
||||
'arrivalAirport.region.country',
|
||||
'aircraft',
|
||||
'airline',
|
||||
'airline.country',
|
||||
'airline.alliance',
|
||||
'flightClass',
|
||||
'seatType',
|
||||
'flightReason',
|
||||
'crewType',
|
||||
])->find($userFlightId)->toArray();
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function departureAirport(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Airport::class, 'departure_airport_id');
|
||||
}
|
||||
|
||||
public function arrivalAirport(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Airport::class, 'arrival_airport_id');
|
||||
}
|
||||
|
||||
public function crewType(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CrewType::class);
|
||||
}
|
||||
|
||||
public function airline(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Airline::class);
|
||||
}
|
||||
|
||||
|
||||
public function aircraft(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Aircraft::class);
|
||||
}
|
||||
|
||||
public function seatType(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SeatType::class);
|
||||
}
|
||||
|
||||
public function flightClass(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(FlightClass::class);
|
||||
}
|
||||
|
||||
public function flightReason(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(FlightReason::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\Achievement;
|
||||
use App\Models\Airline;
|
||||
use App\Models\Alliance;
|
||||
|
||||
class AirlineObserver
|
||||
{
|
||||
public function created(Airline $airline): void
|
||||
{
|
||||
if ($airline->alliance_id !== null) {
|
||||
$this->syncAllianceThresholds();
|
||||
}
|
||||
}
|
||||
|
||||
public function updated(Airline $airline): void
|
||||
{
|
||||
if ($airline->wasChanged('alliance_id')) {
|
||||
$this->syncAllianceThresholds();
|
||||
}
|
||||
}
|
||||
|
||||
public function deleted(Airline $airline): void
|
||||
{
|
||||
$this->syncAllianceThresholds();
|
||||
}
|
||||
|
||||
private function syncAllianceThresholds(): void
|
||||
{
|
||||
Alliance::withCount('airlines')->each(function (Alliance $alliance) {
|
||||
Achievement::where('internal_name', "airlines_alliances.all_{$alliance->internal_name}")
|
||||
->update(['threshold' => $alliance->airlines_count]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\UserFlight;
|
||||
|
||||
class FlightObserver
|
||||
{
|
||||
/**
|
||||
* Recalculate after a flight is created.
|
||||
*/
|
||||
public function created(UserFlight $flight): void
|
||||
{
|
||||
$flight->user->calculateAchievements();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate after a flight is updated.
|
||||
* Cabin class, flight type, airline, etc. may have changed,
|
||||
* which could unlock or revoke achievements.
|
||||
*/
|
||||
public function updated(UserFlight $flight): void
|
||||
{
|
||||
$flight->user->calculateAchievements();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate after a flight is deleted.
|
||||
* Previously earned achievements may no longer be valid.
|
||||
*/
|
||||
public function deleted(UserFlight $flight): void
|
||||
{
|
||||
$flight->user->calculateAchievements();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Notification;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class NotificationPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Notification $notification): bool
|
||||
{
|
||||
return $user->id === $notification->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class UserFlightPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, UserFlight $userFlight): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, UserFlight $userFlight): bool
|
||||
{
|
||||
return $user->id === $userFlight->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, UserFlight $userFlight): bool
|
||||
{
|
||||
return $user->id === $userFlight->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, UserFlight $userFlight): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, UserFlight $userFlight): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Airline;
|
||||
use App\Models\UserFlight;
|
||||
use App\Observers\AirlineObserver;
|
||||
use App\Observers\FlightObserver;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -19,6 +24,8 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
Vite::prefetch(concurrency: 3);
|
||||
UserFlight::observe(FlightObserver::class);
|
||||
Airline::observe(AirlineObserver::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements;
|
||||
|
||||
use App\Models\Achievement;
|
||||
use App\Models\Notification;
|
||||
use App\Models\User;
|
||||
use App\Models\UserAchievement;
|
||||
use App\Models\UserFlight;
|
||||
use App\Services\Achievements\Checkers\AchievementCheckerInterface;
|
||||
use App\Services\Achievements\Checkers\AircraftChecker;
|
||||
use App\Services\Achievements\Checkers\CountriesAndContinentsChecker;
|
||||
use App\Services\Achievements\Checkers\AirlinesAndAlliancesChecker;
|
||||
use App\Services\Achievements\Checkers\FunChallengesChecker;
|
||||
use App\Services\Achievements\Checkers\GeneralFlyingChecker;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class AchievementService
|
||||
{
|
||||
/** Cached achievement lookups so checkers don't each hit the DB. */
|
||||
private Collection $achievementCache;
|
||||
|
||||
/** The user currently being evaluated — set per calculate() call. */
|
||||
private User $user;
|
||||
|
||||
private array $checkers = [
|
||||
GeneralFlyingChecker::class,
|
||||
CountriesAndContinentsChecker::class,
|
||||
AircraftChecker::class,
|
||||
AirlinesAndAlliancesChecker::class,
|
||||
FunChallengesChecker::class,
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->achievementCache = collect();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Orchestration
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @var Collection<int,UserFlight> $flights
|
||||
*/
|
||||
private Collection $flights;
|
||||
|
||||
public function calculate(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->achievementCache = Achievement::all()->keyBy('internal_name');
|
||||
|
||||
// Load once with every relationship any checker could need
|
||||
$this->flights = $user->flights()->with([
|
||||
'airline.alliance',
|
||||
'aircraft',
|
||||
'flightClass',
|
||||
'departureAirport.region',
|
||||
'arrivalAirport.region',
|
||||
'departureAirport.region.continent',
|
||||
'arrivalAirport.region.continent',
|
||||
])->get();
|
||||
|
||||
foreach ($this->checkers as $checkerClass) {
|
||||
$checker = new $checkerClass($this);
|
||||
$checker->check($user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int,UserFlight>
|
||||
*/
|
||||
public function getFlights(): Collection
|
||||
{
|
||||
return $this->flights;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Award / revoke
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Upsert a user_achievement row and fire a notification if this
|
||||
* is a newly unlocked achievement.
|
||||
*/
|
||||
public function award(Achievement $achievement, User $user, ?int $progress = null): void
|
||||
{
|
||||
$existing = UserAchievement::where('user_id', $user->id)
|
||||
->where('achievement_id', $achievement->id)
|
||||
->first();
|
||||
|
||||
$alreadyUnlocked = $existing !== null;
|
||||
|
||||
UserAchievement::updateOrCreate(
|
||||
['user_id' => $user->id, 'achievement_id' => $achievement->id],
|
||||
['progress' => $progress],
|
||||
);
|
||||
|
||||
if (! $alreadyUnlocked) {
|
||||
$this->createAchievementNotification($user, $achievement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress on a progressive achievement without marking it
|
||||
* as unlocked (i.e. progress exists but threshold not yet met).
|
||||
* Creates the row if it doesn't exist yet.
|
||||
*/
|
||||
public function updateProgress(Achievement $achievement, User $user, int $progress): void
|
||||
{
|
||||
UserAchievement::updateOrCreate(
|
||||
['user_id' => $user->id, 'achievement_id' => $achievement->id],
|
||||
['progress' => $progress],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user_achievement row and its associated notification
|
||||
* if the achievement is no longer valid for this user.
|
||||
*/
|
||||
public function revoke(Achievement $achievement, User $user): void
|
||||
{
|
||||
UserAchievement::where('user_id', $user->id)
|
||||
->where('achievement_id', $achievement->id)
|
||||
->delete();
|
||||
|
||||
Notification::where('user_id', $user->id)
|
||||
->where('achievement_id', $achievement->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Helpers for checkers
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function resolveAchievement(string $internalName): ?Achievement
|
||||
{
|
||||
$achievement = $this->achievementCache->get($internalName);
|
||||
return $achievement instanceof Achievement ? $achievement : null;
|
||||
}
|
||||
|
||||
public function currentUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Notifications
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private function createAchievementNotification(User $user, Achievement $achievement): void
|
||||
{
|
||||
Notification::create([
|
||||
'user_id' => $user->id,
|
||||
'title' => $achievement->name,
|
||||
'body' => $achievement->short_description,
|
||||
'is_achievement' => true,
|
||||
'achievement_id' => $achievement->id,
|
||||
'expires_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
interface AchievementCheckerInterface
|
||||
{
|
||||
/**
|
||||
* Check all achievements in this category for the given user.
|
||||
* Implementations should call $this->service->award() or $this->service->revoke()
|
||||
* — never touch user_achievements directly.
|
||||
*/
|
||||
public function check(): void;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class AircraftChecker extends BaseChecker
|
||||
{
|
||||
private const array BOEING_FAMILIES = [
|
||||
'707' => ['B701', 'B703', 'B720'],
|
||||
'717' => ['B712', 'B717'],
|
||||
'727' => ['B721', 'B722', 'B727'],
|
||||
'737' => ['B731', 'B732', 'B733', 'B734', 'B735', 'B736', 'B737', 'B738', 'B739', 'B37M', 'B38M', 'B39M'],
|
||||
'747' => ['B741', 'B742', 'B743', 'B744', 'B748', 'B74D', 'B74R', 'B74S'],
|
||||
'757' => ['B752', 'B753', 'B757'],
|
||||
'767' => ['B762', 'B763', 'B764', 'B767'],
|
||||
'777' => ['B772', 'B773', 'B77L', 'B77W', 'B778', 'B779'],
|
||||
'787' => ['B788', 'B789', 'B78X'],
|
||||
];
|
||||
|
||||
private const array AIRBUS_FAMILIES = [
|
||||
'A300' => ['A30B', 'A300', 'A306'],
|
||||
'A310' => ['A310', 'A312', 'A313'],
|
||||
'A318' => ['A318'],
|
||||
'A319' => ['A319', 'A31X'],
|
||||
'A320' => ['A320', 'A20N'],
|
||||
'A321' => ['A321', 'A21N'],
|
||||
'A330' => ['A330', 'A332', 'A333', 'A338', 'A339'],
|
||||
'A340' => ['A340', 'A342', 'A343', 'A345', 'A346'],
|
||||
'A350' => ['A350', 'A358', 'A359', 'A35K'],
|
||||
'A380' => ['A380', 'A388'],
|
||||
];
|
||||
|
||||
private const array DOUBLE_DECKER_DESIGNATORS = [
|
||||
// A380
|
||||
'A380', 'A388',
|
||||
// 747 variants
|
||||
'B741', 'B742', 'B743', 'B744', 'B748', 'B74D', 'B74R', 'B74S',
|
||||
];
|
||||
|
||||
public function check(): void
|
||||
{
|
||||
/**
|
||||
* @var $flights Collection<int, UserFlight>
|
||||
*/
|
||||
$flights = $this->flights();
|
||||
|
||||
|
||||
$flightsWithAircraft = $flights->filter(fn($f) => $f->aircraft !== null);
|
||||
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => in_array($f->aircraft->aircraft_description, ['LandPlane', 'SeaPlane'])
|
||||
),
|
||||
'aircraft.fly_on_a_plane'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => $f->aircraft->aircraft_description === 'Helicopter'
|
||||
),
|
||||
'aircraft.fly_on_a_helicopter'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => $f->aircraft->engine_type === 'Jet'
|
||||
),
|
||||
'aircraft.fly_on_a_jet'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => $f->aircraft->engine_type === 'Turboprop/Turboshaft' || $f->aircraft->engine_type === 'Piston'
|
||||
),
|
||||
'aircraft.fly_on_a_prop'
|
||||
);
|
||||
|
||||
// --- Engine count achievements ---
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(fn(UserFlight $f) => $f->aircraft->engine_count === 1 && $f->aircraft->aircraft_description !== 'Helicopter'),
|
||||
'aircraft.single_engine'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(fn(UserFlight $f) => $f->aircraft->engine_count === 2 && $f->aircraft->aircraft_description !== 'Helicopter'),
|
||||
'aircraft.twin_engine'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(fn(UserFlight $f) => $f->aircraft->engine_count === 3 && $f->aircraft->aircraft_description !== 'Helicopter'),
|
||||
'aircraft.tri_engine'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(fn(UserFlight $f) => $f->aircraft->engine_count === 4 && $f->aircraft->aircraft_description !== 'Helicopter'),
|
||||
'aircraft.quad_engine'
|
||||
);
|
||||
|
||||
// --- Double decker ---
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => in_array($f->aircraft->designator, self::DOUBLE_DECKER_DESIGNATORS)
|
||||
),
|
||||
'aircraft.double_decker'
|
||||
);
|
||||
|
||||
// --- Smaller manufacturer (scheduled service only) ---
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => $f->airline && $f->flight_number && !in_array(strtolower($f->aircraft->manufacturer_code), ['boeing', 'airbus'])
|
||||
),
|
||||
'aircraft.smaller_manufacturer'
|
||||
);
|
||||
|
||||
// --- Boeing 7x7 families ---
|
||||
|
||||
$flownBoeingFamilies = collect(self::BOEING_FAMILIES)
|
||||
->filter(fn($designators) =>
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => in_array($f->aircraft->designator, $designators)
|
||||
)
|
||||
)
|
||||
->count();
|
||||
|
||||
$this->awardProgress($flownBoeingFamilies, 'aircraft.all_boeing_7x7');
|
||||
|
||||
// --- Airbus A3xx families ---
|
||||
|
||||
$flownAirbusFamilie = collect(self::AIRBUS_FAMILIES)
|
||||
->filter(fn($designators) =>
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => in_array($f->aircraft->designator, $designators)
|
||||
)
|
||||
)
|
||||
->count();
|
||||
|
||||
$this->awardProgress($flownAirbusFamilie, 'aircraft.all_airbus_a3xx');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\Alliance;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class AirlinesAndAlliancesChecker extends BaseChecker
|
||||
{
|
||||
const array US_3 = ['american-airlines', 'delta', 'united-airlines'];
|
||||
const array ME_3 = ['emirates', 'etihad-airways', 'qatar-airways'];
|
||||
const array CN_3 = ['china-southern-airlines', 'china-eastern', 'air-china'];
|
||||
|
||||
public function check(): void
|
||||
{
|
||||
$flights = $this->flights();
|
||||
|
||||
$alliances = Alliance::withCount('airlines')->pluck('id', 'internal_name');
|
||||
|
||||
$flownAllianceAirlines = $flights
|
||||
->filter(fn(UserFlight $f) => $f->airline?->alliance !== null)
|
||||
->groupBy(fn(UserFlight $f) => $f->airline->alliance->internal_name)
|
||||
->map(fn($group) => $group->pluck('airline.internal_name')->unique()->count());
|
||||
|
||||
$check = fn(string $alliance): int => $flownAllianceAirlines->get($alliance, 0);
|
||||
|
||||
$this->awardProgress($check('skyteam'), 'airlines_alliances.all_skyteam');
|
||||
$this->awardProgress($check('oneworld'), 'airlines_alliances.all_oneworld');
|
||||
$this->awardProgress($check('star_alliance'), 'airlines_alliances.all_star_alliance');
|
||||
$this->awardProgress($check('vanilla_alliance'), 'airlines_alliances.all_vanilla_alliance');
|
||||
|
||||
$flownAirlines = $flights
|
||||
->pluck('airline.internal_name')
|
||||
->filter()
|
||||
->unique();
|
||||
|
||||
$checkGroup = fn(array $group): int => $flownAirlines->intersect($group)->count();
|
||||
|
||||
$this->awardProgress($checkGroup(self::ME_3), 'airlines_alliances.me3');
|
||||
$this->awardProgress($checkGroup(self::US_3), 'airlines_alliances.us3');
|
||||
$this->awardProgress($checkGroup(self::CN_3), 'airlines_alliances.cn3');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\Achievement;
|
||||
use App\Services\Achievements\AchievementService;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
abstract class BaseChecker implements AchievementCheckerInterface
|
||||
{
|
||||
public function __construct(protected AchievementService $service) {}
|
||||
|
||||
protected function flights(): Collection
|
||||
{
|
||||
return $this->service->getFlights();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an achievement ID from its internal name.
|
||||
* Results are cached on the service so repeated lookups are free.
|
||||
*/
|
||||
protected function achievement(string $internalName): ?Achievement
|
||||
{
|
||||
return $this->service->resolveAchievement($internalName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Award a boolean (non-progressive) achievement if the condition is met,
|
||||
* or revoke it if the condition is no longer met.
|
||||
*/
|
||||
protected function awardIf(bool $condition, string $internalName): void
|
||||
{
|
||||
$achievement = $this->achievement($internalName);
|
||||
|
||||
if (! $achievement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($condition) {
|
||||
$this->service->award($achievement, $this->currentUser());
|
||||
} else {
|
||||
$this->service->revoke($achievement, $this->currentUser());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Award a progressive achievement, updating progress and
|
||||
* unlocking/revoking based on whether progress meets the threshold.
|
||||
*/
|
||||
protected function awardProgress(int $progress, string $internalName): void
|
||||
{
|
||||
$achievement = $this->achievement($internalName);
|
||||
if (! $achievement) return;
|
||||
|
||||
if ($progress >= $achievement->threshold) {
|
||||
$this->service->award($achievement, $this->currentUser(), $progress);
|
||||
} else {
|
||||
$this->service->updateProgress($achievement, $this->currentUser(), $progress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user currently being evaluated.
|
||||
* Set by AchievementService before calling check().
|
||||
*/
|
||||
protected function currentUser(): \App\Models\User
|
||||
{
|
||||
return $this->service->currentUser();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\Continent;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class CountriesAndContinentsChecker extends BaseChecker
|
||||
{
|
||||
private const INHABITED_CONTINENTS = [
|
||||
'africa', 'asia', 'europe', 'north_america', 'oceania', 'south_america',
|
||||
];
|
||||
|
||||
public function check(): void
|
||||
{
|
||||
$flights = $this->flights();
|
||||
// Resolve internal_name → id once, used throughout
|
||||
$continents = Continent::pluck('id', 'internal_name');
|
||||
|
||||
$arrivalContinentIds = $flights->pluck('arrivalAirport.region.continent_id')->unique();
|
||||
|
||||
$has = fn(string $name) => $arrivalContinentIds->contains($continents[$name]);
|
||||
|
||||
// --- Simple "fly to X continent" achievements ---
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn(UserFlight $f) =>
|
||||
$f->departureAirport->region->continent_id !== $f->arrivalAirport->region->continent_id
|
||||
),
|
||||
'countries_continents.intercontinental'
|
||||
);
|
||||
|
||||
$this->awardIf($has('africa'), 'countries_continents.fly_to_africa');
|
||||
$this->awardIf($has('asia'), 'countries_continents.fly_to_asia');
|
||||
$this->awardIf($has('oceania'), 'countries_continents.fly_to_oceania');
|
||||
$this->awardIf($has('antarctica'), 'countries_continents.fly_to_antarctica');
|
||||
$this->awardIf($has('europe'), 'countries_continents.fly_to_europe');
|
||||
$this->awardIf($has('south_america'), 'countries_continents.fly_to_south_america');
|
||||
$this->awardIf($has('north_america'), 'countries_continents.fly_to_north_america');
|
||||
|
||||
// --- All inhabited continents ---
|
||||
|
||||
$inhabitedIds = collect(self::INHABITED_CONTINENTS)->map(fn($name) => $continents[$name]);
|
||||
|
||||
$visitedInhabited = $arrivalContinentIds
|
||||
->intersect($inhabitedIds)
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedInhabited, 'countries_continents.all_inhabited_continents');
|
||||
|
||||
// --- All continents including Antarctica ---
|
||||
|
||||
$this->awardProgress(
|
||||
$arrivalContinentIds->unique()->count(),
|
||||
'countries_continents.all_continents'
|
||||
);
|
||||
|
||||
// --- Continent route pairs ---
|
||||
|
||||
$this->checkContinentPairs($flights);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, UserFlight> $flights
|
||||
* @return void
|
||||
*/
|
||||
private function checkContinentPairs(Collection $flights): void
|
||||
{
|
||||
$oneWayRoutes = collect();
|
||||
$directedRoutes = collect();
|
||||
|
||||
foreach ($flights as $flight) {
|
||||
$dep = $flight->departureAirport->region->continent->internal_name;
|
||||
$arr = $flight->arrivalAirport->region->continent->internal_name;
|
||||
|
||||
// Directed route key e.g. "europe→asia"
|
||||
$directedRoutes->push("{$dep}→{$arr}");
|
||||
|
||||
// Undirected — sort alphabetically so "europe→asia" and "asia→europe" are the same
|
||||
$undirected = implode('→', collect([$dep, $arr])->sort()->values()->all());
|
||||
$oneWayRoutes->push($undirected);
|
||||
}
|
||||
|
||||
$this->awardProgress(
|
||||
$oneWayRoutes->unique()->count(),
|
||||
'countries_continents.all_continent_pairs_one_way'
|
||||
);
|
||||
|
||||
$this->awardProgress(
|
||||
$directedRoutes->unique()->count(),
|
||||
'countries_continents.all_continent_pairs_both_ways'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
|
||||
class FunChallengesChecker extends BaseChecker
|
||||
{
|
||||
const array US_STATES = [
|
||||
'US-AK', 'US-AL', 'US-AR', 'US-AZ', 'US-CA', 'US-CO', 'US-CT', 'US-DC',
|
||||
'US-DE', 'US-FL', 'US-GA', 'US-HI', 'US-IA', 'US-ID', 'US-IL', 'US-IN',
|
||||
'US-KS', 'US-KY', 'US-LA', 'US-MA', 'US-MD', 'US-ME', 'US-MI', 'US-MN',
|
||||
'US-MO', 'US-MS', 'US-MT', 'US-NC', 'US-ND', 'US-NE', 'US-NH', 'US-NJ',
|
||||
'US-NM', 'US-NV', 'US-NY', 'US-OH', 'US-OK', 'US-OR', 'US-PA', 'US-RI',
|
||||
'US-SC', 'US-SD', 'US-TN', 'US-TX', 'US-UT', 'US-VA', 'US-VT', 'US-WA',
|
||||
'US-WI', 'US-WV', 'US-WY',
|
||||
];
|
||||
|
||||
const array AUSTRALIAN_STATES = [
|
||||
'AU-ACT', 'AU-NSW', 'AU-NT', 'AU-QLD', 'AU-SA', 'AU-TAS', 'AU-VIC', 'AU-WA',
|
||||
];
|
||||
|
||||
const array CHINESE_PROVINCES = [
|
||||
'CN-11', 'CN-12', 'CN-13', 'CN-14', 'CN-15', 'CN-21', 'CN-22', 'CN-23',
|
||||
'CN-31', 'CN-32', 'CN-33', 'CN-34', 'CN-35', 'CN-36', 'CN-37', 'CN-41',
|
||||
'CN-42', 'CN-43', 'CN-44', 'CN-45', 'CN-46', 'CN-50', 'CN-51', 'CN-52',
|
||||
'CN-53', 'CN-54', 'CN-61', 'CN-62', 'CN-63', 'CN-64', 'CN-65',
|
||||
|
||||
];
|
||||
|
||||
const array BRAZILIAN_STATES = [
|
||||
'BR-AC', 'BR-AL', 'BR-AM', 'BR-AP', 'BR-BA', 'BR-CE', 'BR-DF', 'BR-ES',
|
||||
'BR-GO', 'BR-MA', 'BR-MG', 'BR-MS', 'BR-MT', 'BR-PA', 'BR-PB', 'BR-PE',
|
||||
'BR-PI', 'BR-PR', 'BR-RJ', 'BR-RN', 'BR-RO', 'BR-RR', 'BR-RS', 'BR-SC',
|
||||
'BR-SE', 'BR-SP', 'BR-TO',
|
||||
];
|
||||
|
||||
const array CANADIAN_PROVINCES = [
|
||||
'CA-AB', 'CA-BC', 'CA-MB', 'CA-NB', 'CA-NL', 'CA-NS', 'CA-NT',
|
||||
'CA-NU', 'CA-ON', 'CA-PE', 'CA-QC', 'CA-SK', 'CA-YT',
|
||||
];
|
||||
|
||||
public function check(): void
|
||||
{
|
||||
$flights = $this->flights();
|
||||
|
||||
$airlineLetters = $flights
|
||||
->filter(fn(UserFlight $f) => $f->airline?->IATA_code !== null)
|
||||
->map(fn(UserFlight $f) => strtoupper($f->airline->IATA_code[0]))
|
||||
->filter(fn($letter) => ctype_alpha($letter))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($airlineLetters, 'fun_challenges.airline_alphabet');
|
||||
|
||||
|
||||
$airportLetters = $flights
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->iata_code,
|
||||
$f->arrivalAirport?->iata_code,
|
||||
])
|
||||
->filter()
|
||||
->map(fn($code) => strtoupper($code[0]))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($airportLetters, 'fun_challenges.airport_alphabet');
|
||||
|
||||
// --- US States ---
|
||||
$visitedUsStates = $flights
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->region?->code,
|
||||
$f->arrivalAirport?->region?->code,
|
||||
])
|
||||
->filter()
|
||||
->filter(fn($code) => in_array($code, self::US_STATES))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedUsStates, 'fun_challenges.us_states');
|
||||
|
||||
// --- Australian States ---
|
||||
$visitedAustralianStates = $flights
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->region?->code,
|
||||
$f->arrivalAirport?->region?->code,
|
||||
])
|
||||
->filter()
|
||||
->filter(fn($code) => in_array($code, self::AUSTRALIAN_STATES))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedAustralianStates, 'fun_challenges.australian_states');
|
||||
|
||||
// --- Chinese Provinces ---
|
||||
$visitedChineseProvinces = $flights
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->region?->code,
|
||||
$f->arrivalAirport?->region?->code,
|
||||
])
|
||||
->filter()
|
||||
->filter(fn($code) => in_array($code, self::CHINESE_PROVINCES))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedChineseProvinces, 'fun_challenges.chinese_provinces');
|
||||
|
||||
// --- Brazilian States ---
|
||||
$visitedBrazilianStates = $flights
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->region?->code,
|
||||
$f->arrivalAirport?->region?->code,
|
||||
])
|
||||
->filter()
|
||||
->filter(fn($code) => in_array($code, self::BRAZILIAN_STATES))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedBrazilianStates, 'fun_challenges.brazilian_states');
|
||||
|
||||
// --- Canadian Provinces ---
|
||||
$visitedCanadianProvinces = $flights
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->region?->code,
|
||||
$f->arrivalAirport?->region?->code,
|
||||
])
|
||||
->filter()
|
||||
->filter(fn($code) => in_array($code, self::CANADIAN_PROVINCES))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedCanadianProvinces, 'fun_challenges.canadian_provinces');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class GeneralFlyingChecker extends BaseChecker
|
||||
{
|
||||
public function check(): void
|
||||
{
|
||||
/**
|
||||
* @var $flights Collection<int, UserFlight>
|
||||
*/
|
||||
$flights = $this->flights();
|
||||
$count = $flights->count();
|
||||
|
||||
// --- Boolean achievements ---
|
||||
|
||||
$this->awardIf($count >= 1, 'general_flying.first_flight');
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->isDomestic()),
|
||||
'general_flying.domestic_flight'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->isInternational()),
|
||||
'general_flying.international_flight'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->flightClass->internal_name === 'business'),
|
||||
'general_flying.business_class'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->flightClass->internal_name === 'first'),
|
||||
'general_flying.first_class'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->flightClass->internal_name === 'premium_economy'),
|
||||
'general_flying.premium_economy'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => in_array($f->flightClass->internal_name, ['business', 'first'])),
|
||||
'general_flying.business_or_first'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->flightClass->internal_name === 'private'),
|
||||
'general_flying.fly_private'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->flightClass->internal_name === 'general_aviation'),
|
||||
'general_flying.general_aviation'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->filter(fn (UserFlight $f) => $f->isDomestic())
|
||||
->map(fn (UserFlight $f) => $f->departureAirport->region->country_id)
|
||||
->unique()
|
||||
->count() >= 2,
|
||||
'general_flying.domestic_two_countries'
|
||||
);
|
||||
|
||||
// --- Progressive achievements ---
|
||||
|
||||
$this->awardProgress($count,'general_flying.10_flights');
|
||||
$this->awardProgress($count,'general_flying.50_flights');
|
||||
$this->awardProgress($count,'general_flying.100_flights');
|
||||
$this->awardProgress($count,'general_flying.500_flights');
|
||||
$this->awardProgress($count,'general_flying.1000_flights');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\Achievements\AchievementService;
|
||||
|
||||
/**
|
||||
* @mixin User
|
||||
*/
|
||||
trait HasAchievements
|
||||
{
|
||||
public function calculateAchievements(): void
|
||||
{
|
||||
/** @var User $this */
|
||||
app(AchievementService::class)->calculate($this);
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,16 @@ use Illuminate\Foundation\Configuration\Middleware;
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->web(append: [
|
||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
||||
]);
|
||||
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
|
||||
+8
-3
@@ -9,12 +9,17 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"php": "^8.4",
|
||||
"ext-pdo": "*",
|
||||
"inertiajs/inertia-laravel": "^2.0",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/tinker": "^3.0"
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^3.0",
|
||||
"tightenco/ziggy": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/breeze": "^2.4",
|
||||
"laravel/pail": "^1.2.5",
|
||||
"laravel/pint": "^1.27",
|
||||
"mockery/mockery": "^1.6",
|
||||
@@ -86,4 +91,4 @@
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+270
-2
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "91e8e1dfc8379caaf1c302443d694dfa",
|
||||
"content-hash": "0e560320885031dd36bb08bb44fe05d4",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -1053,6 +1053,79 @@
|
||||
],
|
||||
"time": "2025-08-22T14:27:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "inertiajs/inertia-laravel",
|
||||
"version": "v2.0.22",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/inertiajs/inertia-laravel.git",
|
||||
"reference": "4d5849328d4c64231f886d1422fdc945882f9094"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/4d5849328d4c64231f886d1422fdc945882f9094",
|
||||
"reference": "4d5849328d4c64231f886d1422fdc945882f9094",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"laravel/framework": "^10.0|^11.0|^12.0|^13.0",
|
||||
"php": "^8.1.0",
|
||||
"symfony/console": "^6.2|^7.0|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"laravel/boost": "<2.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"larastan/larastan": "^3.0",
|
||||
"laravel/pint": "^1.16",
|
||||
"mockery/mockery": "^1.3.3",
|
||||
"orchestra/testbench": "^8.0|^9.2|^10.0|^11.0",
|
||||
"phpunit/phpunit": "^10.4|^11.5|^12.0",
|
||||
"roave/security-advisories": "dev-master"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Inertia\\ServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"./helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Inertia\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jonathan Reinink",
|
||||
"email": "jonathan@reinink.ca",
|
||||
"homepage": "https://reinink.ca"
|
||||
}
|
||||
],
|
||||
"description": "The Laravel adapter for Inertia.js.",
|
||||
"keywords": [
|
||||
"inertia",
|
||||
"laravel"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/inertiajs/inertia-laravel/issues",
|
||||
"source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.22"
|
||||
},
|
||||
"time": "2026-03-11T15:51:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v13.2.0",
|
||||
@@ -1333,6 +1406,69 @@
|
||||
},
|
||||
"time": "2026-03-23T14:35:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/sanctum",
|
||||
"version": "v4.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/sanctum.git",
|
||||
"reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76",
|
||||
"reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"illuminate/console": "^11.0|^12.0|^13.0",
|
||||
"illuminate/contracts": "^11.0|^12.0|^13.0",
|
||||
"illuminate/database": "^11.0|^12.0|^13.0",
|
||||
"illuminate/support": "^11.0|^12.0|^13.0",
|
||||
"php": "^8.2",
|
||||
"symfony/console": "^7.0|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.6",
|
||||
"orchestra/testbench": "^9.15|^10.8|^11.0",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Sanctum\\SanctumServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Sanctum\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
|
||||
"keywords": [
|
||||
"auth",
|
||||
"laravel",
|
||||
"sanctum"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/sanctum/issues",
|
||||
"source": "https://github.com/laravel/sanctum"
|
||||
},
|
||||
"time": "2026-02-07T17:19:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/serializable-closure",
|
||||
"version": "v2.0.10",
|
||||
@@ -5671,6 +5807,76 @@
|
||||
],
|
||||
"time": "2026-02-15T10:53:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tightenco/ziggy",
|
||||
"version": "v2.6.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tighten/ziggy.git",
|
||||
"reference": "8a0b645921623f77dceaf543d61ecd51a391d96e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/tighten/ziggy/zipball/8a0b645921623f77dceaf543d61ecd51a391d96e",
|
||||
"reference": "8a0b645921623f77dceaf543d61ecd51a391d96e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"laravel/framework": ">=9.0",
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/folio": "^1.1",
|
||||
"orchestra/testbench": "^8.0 || ^9.0 || ^10.0",
|
||||
"pestphp/pest": "^2.0 || ^3.0 || ^4.0",
|
||||
"pestphp/pest-plugin-laravel": "^2.0 || ^3.0 || ^4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Tighten\\Ziggy\\ZiggyServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Tighten\\Ziggy\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Daniel Coulbourne",
|
||||
"email": "daniel@tighten.co"
|
||||
},
|
||||
{
|
||||
"name": "Jake Bathman",
|
||||
"email": "jake@tighten.co"
|
||||
},
|
||||
{
|
||||
"name": "Jacob Baker-Kretzmar",
|
||||
"email": "jacob@tighten.co"
|
||||
}
|
||||
],
|
||||
"description": "Use your Laravel named routes in JavaScript.",
|
||||
"homepage": "https://github.com/tighten/ziggy",
|
||||
"keywords": [
|
||||
"Ziggy",
|
||||
"javascript",
|
||||
"laravel",
|
||||
"routes"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/tighten/ziggy/issues",
|
||||
"source": "https://github.com/tighten/ziggy/tree/v2.6.2"
|
||||
},
|
||||
"time": "2026-03-05T14:41:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tijsverkoyen/css-to-inline-styles",
|
||||
"version": "v2.4.0",
|
||||
@@ -6333,6 +6539,67 @@
|
||||
},
|
||||
"time": "2025-03-19T14:43:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/breeze",
|
||||
"version": "v2.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/breeze.git",
|
||||
"reference": "28cefeaf6af20177ddf5cc7b93e87e4ad79d533f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/breeze/zipball/28cefeaf6af20177ddf5cc7b93e87e4ad79d533f",
|
||||
"reference": "28cefeaf6af20177ddf5cc7b93e87e4ad79d533f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/console": "^11.0|^12.0|^13.0",
|
||||
"illuminate/filesystem": "^11.0|^12.0|^13.0",
|
||||
"illuminate/support": "^11.0|^12.0|^13.0",
|
||||
"illuminate/validation": "^11.0|^12.0|^13.0",
|
||||
"php": "^8.2.0",
|
||||
"symfony/console": "^7.0|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/framework": "^11.0|^12.0|^13.0",
|
||||
"orchestra/testbench-core": "^9.0|^10.0|^11.0",
|
||||
"phpstan/phpstan": "^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Breeze\\BreezeServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Breeze\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.",
|
||||
"keywords": [
|
||||
"auth",
|
||||
"laravel"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/breeze/issues",
|
||||
"source": "https://github.com/laravel/breeze"
|
||||
},
|
||||
"time": "2026-03-10T19:59:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/pail",
|
||||
"version": "v1.2.6",
|
||||
@@ -9102,7 +9369,8 @@
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.3"
|
||||
"php": "^8.4",
|
||||
"ext-pdo": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
|
||||
+4
-1
@@ -53,7 +53,10 @@ return [
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
'domain' => env('APP_DOMAIN', 'flightsgoneby.com'),
|
||||
'api_domain' => env('API_DOMAIN', 'api.flightsgoneby.com'),
|
||||
'logo_api_url' => env('LOGO_API_URL', 'https://api.flightsgoneby.com'),
|
||||
'timezone_api_key' => env('TIMEZONE_API_KEY', '1234567890'),
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|
||||
@@ -99,6 +99,18 @@ return [
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'pgsql_production' => [
|
||||
'driver' => 'pgsql',
|
||||
'host' => env('PRODUCTION_DB_HOST', '127.0.0.1'),
|
||||
'port' => env('PRODUCTION_DB_PORT', '5432'),
|
||||
'database' => env('PRODUCTION_DB_DATABASE', ''),
|
||||
'username' => env('PRODUCTION_DB_USERNAME', ''),
|
||||
'password' => env('PRODUCTION_DB_PASSWORD', ''),
|
||||
'charset' => 'utf8',
|
||||
'prefix' => '',
|
||||
'schema' => 'public',
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stateful Domains
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Requests from the following domains / hosts will receive stateful API
|
||||
| authentication cookies. Typically, these should include your local
|
||||
| and production domains which access your API via a frontend SPA.
|
||||
|
|
||||
*/
|
||||
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'%s%s',
|
||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
Sanctum::currentApplicationUrlWithPort(),
|
||||
// Sanctum::currentRequestHost(),
|
||||
))),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This array contains the authentication guards that will be checked when
|
||||
| Sanctum is trying to authenticate a request. If none of these guards
|
||||
| are able to authenticate the request, Sanctum will use the bearer
|
||||
| token that's present on an incoming request for authentication.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expiration Minutes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the number of minutes until an issued token will be
|
||||
| considered expired. This will override any values set in the token's
|
||||
| "expires_at" attribute, but first-party sessions are not affected.
|
||||
|
|
||||
*/
|
||||
|
||||
'expiration' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Token Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||
| security scanning initiatives maintained by open source platforms
|
||||
| that notify developers if they commit tokens into repositories.
|
||||
|
|
||||
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||
|
|
||||
*/
|
||||
|
||||
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When authenticating your first-party SPA with Sanctum you may need to
|
||||
| customize some of the middleware Sanctum uses while processing the
|
||||
| request. You may change the middleware listed below as required.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => [
|
||||
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||
],
|
||||
|
||||
];
|
||||
@@ -41,9 +41,11 @@ return new class extends Migration
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('airlines')->where('internal_name', 'bonza')->delete();
|
||||
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'AB')
|
||||
->update(['logo' => 'AB.png']);
|
||||
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'AB')
|
||||
->where('ICAO_code', 'BER')
|
||||
|
||||
@@ -11,7 +11,10 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
//
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'CM')
|
||||
->where('ICAO_code', 'CMP')
|
||||
->update(['logo' => 'CM_1.png']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,6 +22,9 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'CM')
|
||||
->where('ICAO_code', 'CMP')
|
||||
->update(['logo' => 'CM.png']);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'CM')
|
||||
->where(function($query) {
|
||||
$query->whereNull('ICAO_code')
|
||||
->orWhere('ICAO_code', '');
|
||||
})
|
||||
->update(['logo' => null]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'CM')
|
||||
->where(function($query) {
|
||||
$query->whereNull('ICAO_code')
|
||||
->orWhere('ICAO_code', '');
|
||||
})
|
||||
->update(['logo' => 'CM.png']);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
//Update Virgin Australia's ICAO Code
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'VA')
|
||||
->where('internal_name', 'virgin-australia')
|
||||
->update(['ICAO_code' => 'VOZ']);
|
||||
|
||||
DB::table('airlines')->insert([
|
||||
'IATA_code' => 'VA',
|
||||
'ICAO_code' => 'VAU',
|
||||
'name' => 'V Australia',
|
||||
'internal_name' => 'v-australia',
|
||||
'country_code' => 'AU',
|
||||
'country_name' => 'Australia',
|
||||
'active' => false,
|
||||
'logo' => 'VA_2.png',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'VA')
|
||||
->where('internal_name', 'virgin-australia')
|
||||
->update(['ICAO_code' => 'VAU']);
|
||||
|
||||
DB::table('airlines')->where('internal_name', 'v-australia')->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'DJ')
|
||||
->where('internal_name', 'virgin-blue-airlines')
|
||||
->update(['logo' => 'VA.png']);
|
||||
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'VA')
|
||||
->where('internal_name', 'virgin-australia')
|
||||
->update(['logo' => 'VA.png']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'DJ')
|
||||
->where('internal_name', 'virgin-blue-airlines')
|
||||
->update(['logo' => 'DJ.png']);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'virgin-blue-airlines')
|
||||
->update(['logo' => 'DJ_1.png']);
|
||||
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'continental-airlines')
|
||||
->update([
|
||||
'logo' => 'CO_1.png',
|
||||
'IATA_code' => 'CO'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'virgin-blue-airlines')
|
||||
->update(['logo' => 'VA.png']);
|
||||
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'continental-airlines')
|
||||
->update([
|
||||
'logo' => null,
|
||||
'IATA_code' => null
|
||||
]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'virgin-australia-regional')
|
||||
->update(['logo' => 'VA.png']);
|
||||
|
||||
|
||||
DB::table('airlines')->insert([
|
||||
'IATA_code' => 'XR',
|
||||
'ICAO_code' => 'OZW',
|
||||
'name' => 'Skywest',
|
||||
'internal_name' => 'skywest',
|
||||
'country_code' => 'AU',
|
||||
'country_name' => 'Australia',
|
||||
'active' => false,
|
||||
'logo' => 'XR_1.png',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('airlines')->where('internal_name', 'skywest')->delete();
|
||||
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'virgin-australia-regional')
|
||||
->update(['logo' => 'XR.png']);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('airlines')->insert([
|
||||
'IATA_code' => 'TZ',
|
||||
'ICAO_code' => 'TDS',
|
||||
'name' => 'Tsaradia',
|
||||
'internal_name' => 'tsaradia',
|
||||
'country_code' => 'MG',
|
||||
'country_name' => 'Madagascar',
|
||||
'active' => false,
|
||||
'logo' => 'TZ.png',
|
||||
]);
|
||||
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'TZ')
|
||||
->where('internal_name', 'scoot-private')
|
||||
->update([
|
||||
'active' => false,
|
||||
'logo' => 'TR.png',
|
||||
'name' => 'Scoot'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('airlines')->where('internal_name', 'tsaradia')->delete();
|
||||
DB::table('airlines')
|
||||
->where('IATA_code', 'TZ')
|
||||
->where('internal_name', 'scoot-private')
|
||||
->update([
|
||||
'active' => true,
|
||||
'logo' => 'TZ.png',
|
||||
'name' => 'Scoot Private Limited'
|
||||
]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('aircraft', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('designator');
|
||||
$table->string('manufacturer_code');
|
||||
$table->string('model_full_name');
|
||||
$table->string('aircraft_description');
|
||||
$table->string('engine_type');
|
||||
$table->unsignedTinyInteger('engine_count');
|
||||
$table->string('wtc');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
$this->importCsv();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('aircraft');
|
||||
}
|
||||
|
||||
/**
|
||||
* Import aircraft data from CSV file.
|
||||
*/
|
||||
private function importCsv(): void
|
||||
{
|
||||
$path = storage_path('app/private/seed_data/aircraft.csv');
|
||||
|
||||
if (! file_exists($path)) {
|
||||
throw new \RuntimeException("Aircraft CSV not found at: {$path}");
|
||||
}
|
||||
|
||||
$handle = fopen($path, 'rb');
|
||||
|
||||
if ($handle === false) {
|
||||
throw new \RuntimeException("Failed to open aircraft CSV at: {$path}");
|
||||
}
|
||||
|
||||
// Skip the header row
|
||||
fgetcsv($handle);
|
||||
|
||||
$batch = [];
|
||||
$batchSize = 500;
|
||||
$now = now()->toDateTimeString();
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
if (count($row) < 7) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$designator, $manufacturerCode, $modelFullName, $aircraftDescription, $engineType, $engineCount, $wtc] = $row;
|
||||
|
||||
$batch[] = [
|
||||
'designator' => trim($designator),
|
||||
'manufacturer_code' => trim($manufacturerCode),
|
||||
'model_full_name' => trim($modelFullName),
|
||||
'aircraft_description' => trim($aircraftDescription),
|
||||
'engine_type' => trim($engineType),
|
||||
'engine_count' => (int) trim($engineCount),
|
||||
'wtc' => trim(str_replace(["\r", "\n"], '', $wtc)),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if (count($batch) >= $batchSize) {
|
||||
DB::table('aircraft')->insert($batch);
|
||||
$batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
if (! empty($batch)) {
|
||||
DB::table('aircraft')->insert($batch);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('countries', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code', 2)->unique();
|
||||
$table->string('name');
|
||||
$table->string('continent', 2);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
$this->importCsv();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('countries');
|
||||
}
|
||||
|
||||
private function importCsv(): void
|
||||
{
|
||||
$path = storage_path('app/private/seed_data/countries.csv');
|
||||
|
||||
if (! file_exists($path)) {
|
||||
throw new \RuntimeException("Countries CSV not found at: {$path}");
|
||||
}
|
||||
|
||||
$handle = fopen($path, 'rb');
|
||||
|
||||
if ($handle === false) {
|
||||
throw new \RuntimeException("Failed to open countries CSV at: {$path}");
|
||||
}
|
||||
|
||||
// Skip header row
|
||||
fgetcsv($handle);
|
||||
|
||||
$batch = [];
|
||||
$batchSize = 500;
|
||||
$now = now()->toDateTimeString();
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
if (count($row) < 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// id, code, name, continent, wikipedia_link, keywords
|
||||
[, $code, $name, $continent] = $row;
|
||||
|
||||
$batch[] = [
|
||||
'code' => trim($code),
|
||||
'name' => trim($name),
|
||||
'continent' => trim($continent),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if (count($batch) >= $batchSize) {
|
||||
DB::table('countries')->insert($batch);
|
||||
$batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
if (! empty($batch)) {
|
||||
DB::table('countries')->insert($batch);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('regions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->foreignId('country_id')->constrained('countries');
|
||||
$table->string('code');
|
||||
$table->string('local_code');
|
||||
$table->string('continent', 2);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
$this->importCsv();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('regions');
|
||||
}
|
||||
|
||||
private function importCsv(): void
|
||||
{
|
||||
$path = storage_path('app/private/seed_data/regions.csv');
|
||||
|
||||
if (! file_exists($path)) {
|
||||
throw new \RuntimeException("Regions CSV not found at: {$path}");
|
||||
}
|
||||
|
||||
$handle = fopen($path, 'rb');
|
||||
|
||||
if ($handle === false) {
|
||||
throw new \RuntimeException("Failed to open regions CSV at: {$path}");
|
||||
}
|
||||
|
||||
// Skip header row
|
||||
fgetcsv($handle);
|
||||
|
||||
// Build a lookup map of country code -> id to resolve the foreign key
|
||||
$countryMap = DB::table('countries')->pluck('id', 'code')->all();
|
||||
|
||||
$batch = [];
|
||||
$batchSize = 500;
|
||||
$now = now()->toDateTimeString();
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
if (count($row) < 6) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// id, code, local_code, name, continent, iso_country, wikipedia_link, keywords
|
||||
[, $code, $localCode, $name, $continent, $isoCountry] = $row;
|
||||
|
||||
$isoCountry = trim($isoCountry);
|
||||
|
||||
if (! isset($countryMap[$isoCountry])) {
|
||||
// Skip regions whose country wasn't imported (shouldn't happen with a complete dataset)
|
||||
continue;
|
||||
}
|
||||
|
||||
$batch[] = [
|
||||
'code' => trim($code),
|
||||
'local_code' => trim($localCode),
|
||||
'name' => trim($name),
|
||||
'continent' => trim($continent),
|
||||
'country_id' => $countryMap[$isoCountry],
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if (count($batch) >= $batchSize) {
|
||||
DB::table('regions')->insert($batch);
|
||||
$batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
if (! empty($batch)) {
|
||||
DB::table('regions')->insert($batch);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('continents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code', 2)->unique();
|
||||
$table->string('name');
|
||||
$table->string('internal_name');
|
||||
});
|
||||
|
||||
DB::table('continents')->insert($this->continents());
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('continents');
|
||||
}
|
||||
|
||||
private function continents(): array
|
||||
{
|
||||
|
||||
return [
|
||||
['code' => 'AF', 'name' => 'Africa', 'internal_name' => 'africa', ],
|
||||
['code' => 'AN', 'name' => 'Antarctica', 'internal_name' => 'antarctica', ],
|
||||
['code' => 'AS', 'name' => 'Asia', 'internal_name' => 'asia', ],
|
||||
['code' => 'EU', 'name' => 'Europe', 'internal_name' => 'europe', ],
|
||||
['code' => 'NA', 'name' => 'North America', 'internal_name' => 'north_america', ],
|
||||
['code' => 'OC', 'name' => 'Oceania', 'internal_name' => 'oceania', ],
|
||||
['code' => 'SA', 'name' => 'South America', 'internal_name' => 'south_america',],
|
||||
];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('countries', function (Blueprint $table) {
|
||||
$table->foreignId('continent_id')->nullable()->constrained('continents');
|
||||
});
|
||||
|
||||
// Resolve continent codes already in the table
|
||||
$continentMap = DB::table('continents')->pluck('id', 'code')->all();
|
||||
|
||||
foreach ($continentMap as $code => $id) {
|
||||
DB::table('countries')
|
||||
->where('continent', $code)
|
||||
->update(['continent_id' => $id]);
|
||||
}
|
||||
|
||||
Schema::table('countries', function (Blueprint $table) {
|
||||
$table->foreignId('continent_id')->nullable(false)->change();
|
||||
$table->dropColumn('continent');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('countries', function (Blueprint $table) {
|
||||
$table->string('continent', 2);
|
||||
});
|
||||
|
||||
$continentMap = DB::table('continents')->pluck('code', 'id')->all();
|
||||
|
||||
foreach ($continentMap as $id => $code) {
|
||||
DB::table('countries')
|
||||
->where('continent_id', $id)
|
||||
->update(['continent' => $code]);
|
||||
}
|
||||
|
||||
Schema::table('countries', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('continent_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Add the FK column first, nullable so we can populate it before constraining
|
||||
Schema::table('regions', function (Blueprint $table) {
|
||||
$table->foreignId('continent_id')->nullable()->constrained('continents');
|
||||
});
|
||||
|
||||
// Resolve continent codes already in the table
|
||||
$continentMap = DB::table('continents')->pluck('id', 'code')->all();
|
||||
|
||||
foreach ($continentMap as $code => $id) {
|
||||
DB::table('regions')
|
||||
->where('continent', $code)
|
||||
->update(['continent_id' => $id]);
|
||||
}
|
||||
|
||||
// Now make it non-nullable and drop the old string column
|
||||
Schema::table('regions', function (Blueprint $table) {
|
||||
$table->foreignId('continent_id')->nullable(false)->change();
|
||||
$table->dropColumn('continent');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('regions', function (Blueprint $table) {
|
||||
$table->string('continent', 2);
|
||||
});
|
||||
|
||||
$continentMap = DB::table('continents')->pluck('code', 'id')->all();
|
||||
|
||||
foreach ($continentMap as $id => $code) {
|
||||
DB::table('regions')
|
||||
->where('continent_id', $id)
|
||||
->update(['continent' => $code]);
|
||||
}
|
||||
|
||||
Schema::table('regions', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('continent_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('airports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->decimal('latitude_deg', 10, 6);
|
||||
$table->decimal('longitude_deg', 10, 6);
|
||||
$table->integer('elevation_ft')->nullable();
|
||||
$table->foreignId('region_id')->constrained('regions');
|
||||
$table->string('municipality')->nullable();
|
||||
$table->string('icao_code')->nullable();
|
||||
$table->string('iata_code')->nullable();
|
||||
$table->string('local_code')->nullable();
|
||||
$table->string('type');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
$this->importCsv();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('airports');
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// Skip rows without at least one of iata or icao
|
||||
if ($icaoCode === '' && $iataCode === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isoRegion = trim($isoRegion);
|
||||
|
||||
if (! isset($regionMap[$isoRegion])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$elevationFt = trim($elevationFt);
|
||||
|
||||
$batch[] = [
|
||||
'type' => trim($type),
|
||||
'name' => trim($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' => $icaoCode !== '' ? $icaoCode : null,
|
||||
'iata_code' => $iataCode !== '' ? $iataCode : null,
|
||||
'local_code' => trim($localCode) !== '' ? trim($localCode) : null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if (count($batch) >= $batchSize) {
|
||||
DB::table('airports')->insert($batch);
|
||||
$batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
if (! empty($batch)) {
|
||||
DB::table('airports')->insert($batch);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private string $icaoCode = 'YSWS';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
$region = DB::table('regions')->where('code', 'AU-NSW')->firstOrFail();
|
||||
|
||||
DB::table('airports')->insert([
|
||||
'type' => 'large_airport', // large_airport, medium_airport, small_airport, heliport, seaplane_base, balloonport, closed
|
||||
'name' => 'Western Sydney International Airport',
|
||||
'latitude_deg' => -33.883461,
|
||||
'longitude_deg'=> 150.712981,
|
||||
'elevation_ft' => 262,
|
||||
'region_id' => $region->id,
|
||||
'municipality' => 'Sydney',
|
||||
'icao_code' => $this->icaoCode,
|
||||
'iata_code' => 'WSI',
|
||||
'local_code' => null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('airports')->where('icao_code', $this->icaoCode)->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('imported_flights', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
|
||||
$table->string('date')->nullable();
|
||||
$table->string('flight_number')->nullable();
|
||||
$table->string('from')->nullable();
|
||||
$table->string('to')->nullable();
|
||||
$table->string('dep_time')->nullable();
|
||||
$table->string('arr_time')->nullable();
|
||||
$table->string('duration')->nullable();
|
||||
$table->string('airline')->nullable();
|
||||
$table->string('aircraft')->nullable();
|
||||
$table->string('registration')->nullable();
|
||||
$table->string('seat_number')->nullable();
|
||||
$table->string('seat_type')->nullable();
|
||||
$table->string('flight_class')->nullable();
|
||||
$table->string('flight_reason')->nullable();
|
||||
$table->text('note')->nullable();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('imported_flights');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('airports', function (Blueprint $table) {
|
||||
$table->string('timezone')->nullable()->after('elevation_ft');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('airports', function (Blueprint $table) {
|
||||
$table->dropColumn('timezone');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('flight_classes', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('id')->primary();
|
||||
$table->string('name');
|
||||
});
|
||||
|
||||
Schema::create('seat_types', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('id')->primary();
|
||||
$table->string('name');
|
||||
});
|
||||
|
||||
Schema::create('flight_reasons', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('id')->primary();
|
||||
$table->string('name');
|
||||
});
|
||||
|
||||
DB::table('flight_classes')->insert([
|
||||
['id' => 0, 'name' => 'Unspecified'],
|
||||
['id' => 1, 'name' => 'Economy'],
|
||||
['id' => 2, 'name' => 'Business'],
|
||||
['id' => 3, 'name' => 'First'],
|
||||
['id' => 4, 'name' => 'Premium Economy'],
|
||||
['id' => 5, 'name' => 'Private'],
|
||||
]);
|
||||
|
||||
DB::table('seat_types')->insert([
|
||||
['id' => 0, 'name' => 'Unspecified'],
|
||||
['id' => 1, 'name' => 'Window'],
|
||||
['id' => 2, 'name' => 'Middle'],
|
||||
['id' => 3, 'name' => 'Aisle'],
|
||||
]);
|
||||
|
||||
DB::table('flight_reasons')->insert([
|
||||
['id' => 0, 'name' => 'No Particular Reason'],
|
||||
['id' => 1, 'name' => 'Pleasure'],
|
||||
['id' => 2, 'name' => 'Business'],
|
||||
['id' => 3, 'name' => 'Crew'],
|
||||
['id' => 4, 'name' => 'Other'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('flight_reasons');
|
||||
Schema::dropIfExists('seat_types');
|
||||
Schema::dropIfExists('flight_classes');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_flights', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->timestampTz('departure_date');
|
||||
$table->timestampTz('arrival_date');
|
||||
$table->string('flight_number')->nullable();
|
||||
$table->foreignId('departure_airport_id')->constrained('airports');
|
||||
$table->foreignId('arrival_airport_id')->constrained('airports');
|
||||
$table->foreignId('airline_id')->nullable()->constrained('airlines');
|
||||
$table->foreignId('aircraft_id')->nullable()->constrained('aircraft');
|
||||
$table->string('aircraft_registration')->nullable();
|
||||
$table->string('seat_number')->nullable();
|
||||
$table->unsignedTinyInteger('seat_type_id')->nullable();
|
||||
$table->unsignedTinyInteger('flight_reason_id')->nullable();
|
||||
$table->unsignedTinyInteger('flight_class_id')->nullable();
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('seat_type_id')->references('id')->on('seat_types');
|
||||
$table->foreign('flight_reason_id')->references('id')->on('flight_reasons');
|
||||
$table->foreign('flight_class_id')->references('id')->on('flight_classes');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_flights');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
|
||||
private array $countryCodes = [
|
||||
'Burma' => 'MM', // Myanmar
|
||||
'Ceylon' => 'LK', // Sri Lanka
|
||||
'congodr' => 'CD', // Congo, Dem. Republic
|
||||
'cotedivoire' => 'CI', // Côte d'Ivoire
|
||||
'dominicanrep' => 'DO', // Dominican Republic
|
||||
'Kenya, Uganda and Tanzania' => 'KE', // East African Airways — headquartered in Nairobi
|
||||
'Korea, Republic of' => 'KR', // South Korea
|
||||
'Malaysia / Singapore' => 'MY', // Malaysia-Singapore Airlines predecessor
|
||||
'Rhodesia' => 'ZW', // Zimbabwe
|
||||
'Saudia Arabia' => 'SA', // Typo for Saudi Arabia
|
||||
'St. Barthelemy' => 'BL', // Saint Barthélemy
|
||||
'St. Martin' => 'MF', // Saint Martin (French side — change to SX for Dutch)
|
||||
'Tanzania, United Republic of' => 'TZ',
|
||||
'uae' => 'AE', // United Arab Emirates
|
||||
'United States of America' => 'US',
|
||||
'West Samoa' => 'WS', // Samoa
|
||||
];
|
||||
public function up(): void
|
||||
{
|
||||
// 1. Insert Lufthansa
|
||||
DB::table('airlines')->insert([
|
||||
'IATA_code' => 'LH',
|
||||
'ICAO_code' => 'DLH',
|
||||
'name' => 'Lufthansa',
|
||||
'internal_name' => 'lufthansa',
|
||||
'country_code' => 'DE',
|
||||
'country_name' => 'Germany',
|
||||
'active' => true,
|
||||
'logo' => 'LH.png',
|
||||
]);
|
||||
|
||||
// 2. Add nullable country_id FK column
|
||||
Schema::table('airlines', function (Blueprint $table) {
|
||||
$table->foreignId('country_id')
|
||||
->nullable()
|
||||
->after('active')
|
||||
->constrained('countries', 'id')
|
||||
->onUpdate('cascade')
|
||||
->onDelete('set null');
|
||||
});
|
||||
|
||||
|
||||
foreach ($this->countryCodes as $countryName => $isoCode) {
|
||||
DB::statement("
|
||||
UPDATE airlines a
|
||||
SET country_id = c.id
|
||||
FROM countries c
|
||||
WHERE c.code = :code
|
||||
AND a.country_name = :name
|
||||
AND a.country_id IS NULL
|
||||
", [
|
||||
'code' => $isoCode,
|
||||
'name' => $countryName,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Remove the FK column
|
||||
Schema::table('airlines', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('country_id');
|
||||
});
|
||||
|
||||
// Remove Lufthansa (identified by unique ICAO code)
|
||||
DB::table('airlines')->where('ICAO_code', 'DLH')->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,748 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Each entry: id => ISO 3166-1 alpha-2 country code.
|
||||
* '??' means the country could not be determined with confidence.
|
||||
*
|
||||
* Logic used:
|
||||
* - Known airline name → country from public record
|
||||
* - ICAO callsign prefix (where visible in data)
|
||||
* - Organisation/government name (e.g. "Belgian Air Force" → BE)
|
||||
* - "Blocked" placeholder rows → left as ?? (no real airline)
|
||||
*/
|
||||
private array $map = [
|
||||
// ── A ──────────────────────────────────────────────────────────────
|
||||
1014 => '??', // Eastwest Airlines — multiple airlines use this name (AU/PH)
|
||||
1359 => '??', // ADV — no name, no data
|
||||
1383 => 'FR', // HOP!-AIRLINAIR — French regional carrier
|
||||
1388 => 'IT', // Air One Aviation — Italy
|
||||
1403 => 'VE', // Rutas Aereas de Venezuela RAV S.A — Venezuela
|
||||
1416 => 'BT', // Tashi Air — Bhutan
|
||||
1451 => 'CA', // Hawkair Aviation — Canada (BC)
|
||||
1473 => '??', // Air Inter Transport Co. Ltd. — unclear
|
||||
1507 => 'AR', // Tectimes Sudamericana S.A. — Argentina
|
||||
1510 => 'HR', // Trade Air Ltd. — Croatia
|
||||
1518 => 'US', // Conquest Air, Inc. — USA
|
||||
1524 => 'GB', // CHEP Aerospace Solutions Ltd. — UK
|
||||
1529 => 'LK', // Saffron Aviation (Pvt) Ltd dba Cinn Air — Sri Lanka
|
||||
1569 => 'CY', // Cobaltair Ltd — Cyprus
|
||||
1578 => 'TZ', // Coastal Travels Ltd. — Tanzania
|
||||
1581 => 'GB', // UBM Aviation - OAG — UK
|
||||
1632 => 'NO', // Norwegian Air Norway AS — Norway
|
||||
1654 => 'NG', // Dornier Aviation Nigeria AIEP Limit — Nigeria
|
||||
1658 => 'GB', // Air Direct Connect Ltd. — UK
|
||||
1676 => 'DO', // Dominican Wings. S.A. — Dominican Republic
|
||||
1691 => 'AT', // Eurowings Europe GmbH — Austria
|
||||
1696 => 'LA', // Sabaidee Airways dba New Gen — Laos
|
||||
1704 => 'CG', // Equaflight Service — Republic of Congo
|
||||
1749 => 'NL', // European Cargo Services BV — Netherlands
|
||||
1852 => 'NL', // Airshop B.V. — Netherlands
|
||||
1871 => '??', // S.A.C. — insufficient info
|
||||
1911 => 'GN', // Guinea Lineas Aereas — Guinea
|
||||
1935 => 'US', // Global Feeder Services, LLC — USA
|
||||
1951 => 'US', // Sky Lease I, Inc. — USA
|
||||
2023 => 'CA', // Harbour Air — Canada
|
||||
2029 => 'RU', // iFly Airlines — Russia
|
||||
2031 => 'HT', // Haiti Aviation — Haiti
|
||||
2035 => 'AM', // Taron Avia LLC — Armenia
|
||||
2041 => 'CL', // Chilejet S.A. — Chile
|
||||
2096 => 'SE', // Amapola Flyg AB — Sweden
|
||||
2137 => 'CI', // Ivoirienne de Transport Aerien — Côte d'Ivoire
|
||||
2159 => '??', // Airmax Airlines — unclear
|
||||
2171 => 'RU', // Sapsan Air / Irtysh Air — Russia
|
||||
2192 => 'US', // Intercontinental Airways (USA) — USA
|
||||
2200 => 'FR', // Michelin Air Services — France
|
||||
2219 => 'KH', // Apsara International — Cambodia
|
||||
2283 => 'JP', // Japan Air Commuter Co., Ltd. — Japan
|
||||
2327 => 'DK', // Jet Time A/S — Denmark
|
||||
2351 => 'KE', // DAC Aviation (EC) Ltd. — Kenya
|
||||
2353 => '??', // Skybus (KAL Aviation) — unclear
|
||||
2361 => 'ID', // Jatayu Air — Indonesia
|
||||
2491 => 'US', // American Capital Aviation — USA
|
||||
2494 => 'US', // Aviation Services & Support — USA
|
||||
2497 => 'VE', // Linea Aerea SAPSA — Venezuela
|
||||
2510 => 'US', // Bristow U.S. LLC — USA
|
||||
2525 => 'TZ', // Lion Air Cargo Tanzania Limited — Tanzania
|
||||
2527 => 'GY', // Laparkan Airways — Guyana
|
||||
2574 => 'US', // Pacific Wings — USA (Hawaii)
|
||||
2579 => 'CH', // Swiss Global Air Lines AG — Switzerland
|
||||
2583 => 'DE', // MHS Aviation GmbH — Germany
|
||||
2598 => 'PH', // Magnum Air dba Skyjet — Philippines
|
||||
2638 => 'US', // Mohawk Airlines — USA (historical)
|
||||
2648 => '??', // Mey-Air — insufficient info
|
||||
2680 => 'LB', // TMA Cargo — Lebanon (Trans Mediterranean Airways)
|
||||
2691 => 'ZA', // Nomad Aviation (PTY) Ltd. — South Africa
|
||||
2692 => 'FI', // Nordic Regional Airlines Oy — Finland
|
||||
2704 => 'NG', // Skypower Express Airways Ltd. — Nigeria
|
||||
2856 => 'AU', // FlyPelican — Australia
|
||||
2896 => 'PL', // Small Planet Airline Sp. Z.o.o — Poland
|
||||
2898 => 'PL', // Sprintair SA — Poland
|
||||
2929 => 'IN', // Vayudoot — India (historical)
|
||||
2947 => 'BS', // Southern Air Charter — Bahamas
|
||||
2965 => 'PH', // AirAsia Inc. — Philippines
|
||||
2989 => 'AI', // Anguilla Air Services, Ltd — Anguilla
|
||||
3019 => 'RU', // Dobrolet — Russia
|
||||
3033 => 'US', // Jet Airways Inc. — USA
|
||||
3038 => 'VE', // Linea Aerea De Servicio Ejecutivo Regional Laser — Venezuela
|
||||
3051 => '??', // Starlight Airline — insufficient info
|
||||
3055 => 'US', // North Eastern International — USA
|
||||
3057 => 'CZ', // SmartWings — Czech Republic
|
||||
3062 => 'CO', // Tampa Cargo S.A.S — Colombia
|
||||
3084 => '??', // Reliable Unique Services Aviation — insufficient info
|
||||
3094 => 'VE', // Sundance Air Venezuela S.A. — Venezuela
|
||||
3127 => '??', // Skyview Airways Company Limited — insufficient info
|
||||
3167 => 'CA', // 8165343 Canada Inc. dba Air Canada — Canada
|
||||
3187 => 'CR', // SANSA Regional — Costa Rica
|
||||
3191 => 'CR', // Servicios Aereos Nacionales S.A. SANSA — Costa Rica
|
||||
3198 => 'US', // Metro Air Northeast — USA
|
||||
3199 => 'US', // Brockway Air — USA
|
||||
3251 => 'TH', // Thai Lion Mentari Co., Ltd. — Thailand
|
||||
3257 => '??', // Sunshine Airlines — insufficient info
|
||||
3261 => 'TH', // Apex Airline Public Company Limited — Thailand
|
||||
3289 => 'GB', // AirTanker Services Limited — UK
|
||||
3291 => '??', // Fly Art — insufficient info
|
||||
3292 => 'CA', // Nakina Air Service — Canada (Ontario)
|
||||
3307 => 'MX', // MCS Aerocarga de Mexico — Mexico
|
||||
3317 => '??', // Master Aviation — insufficient info
|
||||
3358 => '??', // Jet 24 — insufficient info
|
||||
3458 => 'US', // Empire Airlines (1975) — USA
|
||||
3486 => 'VE', // Vensecar Internacional C.A. — Venezuela
|
||||
3499 => '??', // Clairmont Holdings dba VI Air — insufficient info
|
||||
3514 => '??', // Flylink Express — insufficient info
|
||||
3533 => 'IT', // CAI Second — Italy
|
||||
3564 => 'FR', // Air Vendee — France
|
||||
3576 => 'US', // Vintage Props & Jets — USA
|
||||
3638 => 'US', // Centurion Air Cargo, Inc. — USA
|
||||
3687 => 'US', // Midwest Airlines — USA
|
||||
3690 => 'PT', // Aero VIP Companhia Transportes Servicos Aere — Portugal
|
||||
3691 => 'US', // Midwest Airlines (duplicate) — USA
|
||||
3705 => 'RU', // Closed Joint Stock Company Red Wing — Russia
|
||||
3713 => 'US', // Air Excursion, LLC — USA
|
||||
3722 => 'US', // Exec Air, Inc. of Naples — USA (Florida)
|
||||
3726 => 'CL', // Airmax S.A. — Chile
|
||||
3732 => 'CA', // IATA - Montreal — Canada
|
||||
3737 => 'US', // Universal Air Travel Plan (UATP-Marketing) — USA
|
||||
3743 => 'US', // Delux Public Charter LLC — USA
|
||||
3763 => 'ID', // Xpressair — Indonesia
|
||||
3769 => 'US', // TEM Enterprises — USA
|
||||
3775 => 'ID', // Indonesia AirAsia Extra, PT — Indonesia
|
||||
3797 => 'CH', // Travel Technology Interactive SA — Switzerland
|
||||
3831 => 'CA', // Department of National Defence — Canada
|
||||
3832 => 'CN', // YTO Cargo Airlines Co. Ltd. — China
|
||||
3837 => 'CN', // Yunnan Ying'An Airlines — China
|
||||
3843 => 'CN', // Yunnan Yingan Airline Co., Ltd. — China
|
||||
3857 => 'MX', // Link Conexion Aerea S.A. de C.V — Mexico
|
||||
3878 => 'US', // PM Air LLC — USA
|
||||
3914 => 'US', // Air Routing International L.P. — USA
|
||||
3952 => 'RU', // Bashkortavia — Russia
|
||||
3953 => '??', // Sun Air — insufficient info (multiple carriers)
|
||||
|
||||
// ── ICAO-only rows (no IATA code) ──────────────────────────────────
|
||||
4197 => 'BE', // Abelag Aviation — Belgium
|
||||
4206 => 'UA', // Alpha Air (Ukraine) — Ukraine
|
||||
4208 => 'US', // Air Aurora — USA
|
||||
4235 => '??', // Aeolus Air — insufficient info
|
||||
4270 => 'CA', // Exploits Valley Air Services — Canada (Newfoundland)
|
||||
4295 => 'US', // Nantucket Airlines — USA
|
||||
4303 => 'MX', // Aero Comondu — Mexico
|
||||
4309 => '??', // ASSL — insufficient info
|
||||
4313 => 'US', // Flight Line — USA
|
||||
4322 => '??', // Blocked
|
||||
4327 => 'UA', // Antonov Airlines — Ukraine
|
||||
4344 => 'DE', // Aero Dienst — Germany
|
||||
4368 => 'EE', // Airest — Estonia
|
||||
4377 => '??', // AEROTEC — insufficient info
|
||||
4413 => '??', // Blocked
|
||||
4443 => 'AO', // Angola Air Charter — Angola
|
||||
4460 => 'AZ', // AZAL Avia Cargo — Azerbaijan
|
||||
4488 => 'FR', // Airbus Industrie — France (Toulouse HQ)
|
||||
4504 => 'RU', // AIS Airlines (Russia) — Russia
|
||||
4569 => '??', // International Association Of Latin American Air Carriers — regional body
|
||||
4572 => 'US', // American Linehaul Corporation — USA
|
||||
4627 => '??', // Aerom — insufficient info
|
||||
4639 => 'BR', // ASTA Linhas Aereas — Brazil
|
||||
4646 => '??', // Amiyi Airlines — insufficient info
|
||||
4663 => 'DE', // Antares Airtransport — Germany
|
||||
4678 => 'GB', // OAG Computer — UK
|
||||
4681 => '??', // Aerolion International — insufficient info
|
||||
4705 => 'US', // Aeros Flight Training — USA
|
||||
4780 => 'ZA', // Aeronexus — South Africa
|
||||
4846 => 'AU', // Royal Australian Air Force — Australia
|
||||
4862 => 'AU', // Par Avion Airlines — Australia (Tasmania)
|
||||
4880 => 'DE', // Avanti Air GmbH — Germany
|
||||
4966 => 'NZ', // Airwork (NZ) — New Zealand
|
||||
4970 => 'NE', // Air Niamey S.A. — Niger
|
||||
4976 => 'JO', // Arab Wings — Jordan
|
||||
5040 => 'RU', // Azov-Avia — Russia
|
||||
5054 => 'GB', // BAe Systems — UK
|
||||
5057 => 'BE', // Belgian Air Force — Belgium
|
||||
5082 => 'UA', // Bravo Airways — Ukraine
|
||||
5095 => 'KZ', // Beibars — Kazakhstan
|
||||
5123 => 'DK', // BenAir — Denmark
|
||||
5127 => 'GB', // QinetiQ — UK
|
||||
5148 => '??', // Airnow — insufficient info
|
||||
5161 => 'IN', // Kingfisher Air Service — India
|
||||
5178 => '??', // BFS International — insufficient info
|
||||
5197 => '??', // Air Inter Transport — insufficient info
|
||||
5203 => 'NO', // Bergen Air Transport — Norway
|
||||
5214 => 'GB', // Bristow Helicopters — UK
|
||||
5227 => 'DE', // BinAir — Germany
|
||||
5339 => '??', // Sundance Air — insufficient info
|
||||
5347 => 'CA', // Alberni Airways — Canada (BC)
|
||||
5377 => 'ID', // Merpati Intan — Indonesia
|
||||
5389 => 'HU', // Base Kft — Hungary
|
||||
5401 => 'AU', // Star Air Cargo Pty — Australia
|
||||
5418 => 'RU', // AeroBratsk JSC — Russia
|
||||
5485 => '??', // Blocked
|
||||
5534 => '??', // Blocked
|
||||
5552 => '??', // Blocked
|
||||
5569 => '??', // Blocked
|
||||
5629 => 'CN', // Air China Cargo — China
|
||||
5648 => '??', // Blocked
|
||||
5683 => 'US', // Chipola Aviation — USA (Florida)
|
||||
5694 => 'CN', // China Flying Dragon Aviation — China
|
||||
5716 => 'MX', // Aereo Calafia — Mexico
|
||||
5718 => 'CN', // Zhongfei Airlines — China
|
||||
5723 => 'US', // Charlotte NC Air National Guard — USA
|
||||
5751 => 'CA', // Cougar Helicopters — Canada
|
||||
5758 => 'US', // Channel Islands Aviation — USA (California)
|
||||
5813 => 'NG', // Caverton Helicopters Ltd. — Nigeria
|
||||
5821 => 'US', // Aviation Charter Services — USA
|
||||
5839 => 'US', // Colemill Air Charter — USA
|
||||
5844 => 'GB', // Cello Aviation — UK
|
||||
5859 => '??', // Challenge Air — insufficient info
|
||||
5916 => 'CA', // Sunwest Aviation — Canada
|
||||
5956 => 'US', // ConocoPhillips — USA
|
||||
5959 => '??', // Blocked
|
||||
5983 => 'US', // Corpjet — USA
|
||||
5996 => 'US', // Corporate Air — USA
|
||||
6043 => '??', // Avia Carriers — insufficient info
|
||||
6075 => 'US', // Aero Charter and Transport — USA
|
||||
6084 => '??', // CTI - Container Transport International — insufficient info
|
||||
6093 => 'UZ', // Tashkent Aircraft Production — Uzbekistan
|
||||
6102 => 'BR', // PanAir Cargo — Brazil
|
||||
6106 => 'AR', // CATA Linea Aerea — Argentina
|
||||
6113 => 'MX', // Aero Cuahonte — Mexico
|
||||
6116 => 'ZA', // Court Helicopters — South Africa
|
||||
6132 => 'US', // ChevronTexaco Aircraft Operations — USA
|
||||
6146 => 'MH', // Air Marshall Islands — Marshall Islands
|
||||
6179 => '??', // Blocked
|
||||
6180 => '??', // Blocked
|
||||
6181 => '??', // Blocked
|
||||
6182 => '??', // Blocked
|
||||
6183 => '??', // Blocked
|
||||
6184 => '??', // Blocked
|
||||
6185 => '??', // Blocked
|
||||
6186 => '??', // Blocked
|
||||
6187 => '??', // Blocked
|
||||
6188 => '??', // Blocked
|
||||
6189 => '??', // Blocked
|
||||
6190 => '??', // Blocked
|
||||
6191 => '??', // Blocked
|
||||
6192 => '??', // Blocked
|
||||
6193 => '??', // Blocked
|
||||
6194 => '??', // Blocked
|
||||
6195 => '??', // Blocked
|
||||
6196 => '??', // Blocked
|
||||
6197 => '??', // Blocked
|
||||
6198 => '??', // Blocked
|
||||
6199 => '??', // Blocked
|
||||
6200 => '??', // Blocked
|
||||
6201 => '??', // Blocked
|
||||
6202 => '??', // Blocked
|
||||
6203 => '??', // Blocked
|
||||
6204 => '??', // Blocked
|
||||
6222 => 'NG', // Dornier Aviation Nigeria — Nigeria
|
||||
6250 => 'DE', // DC Aviation — Germany
|
||||
6253 => 'GB', // Directflight Limited — UK
|
||||
6257 => 'US', // Pentastar Aviation — USA
|
||||
6259 => '??', // Blocked
|
||||
6294 => '??', // Dasnair — insufficient info
|
||||
6320 => 'US', // Encore Air Cargo — USA
|
||||
6355 => '??', // Blocked
|
||||
6377 => '??', // Blocked
|
||||
6429 => 'AE', // Dubai Air Wing — UAE
|
||||
6431 => '??', // Aerotecnica — insufficient info
|
||||
6442 => 'AW', // Divi Divi Air N.V. — Aruba
|
||||
6455 => '??', // Blocked
|
||||
6463 => 'NZ', // Eagle Airways — New Zealand
|
||||
6475 => 'AU', // Eastern Australia Airlines — Australia
|
||||
6522 => '??', // Blocked
|
||||
6539 => '??', // Blocked
|
||||
6549 => 'AT', // Avanti Airlines — Austria
|
||||
6570 => '??', // Excellent Glide — insufficient info
|
||||
6601 => 'US', // Executive Jet Management — USA
|
||||
6665 => '??', // Blocked
|
||||
6691 => 'SI', // Express Airways d.o.o — Slovenia
|
||||
6694 => 'US', // Epps Air Service — USA
|
||||
6702 => 'MX', // Aerotaxis Tucan — Mexico
|
||||
6787 => 'DE', // EFS European Flight Service — Germany
|
||||
6790 => 'IT', // Evin-Evoluzionhndustriali — Italy
|
||||
6791 => 'BR', // Everjets Aviacao Executiva S.A. — Brazil
|
||||
6804 => 'AT', // Eurowings Europe — Austria
|
||||
6827 => '??', // Blocked
|
||||
6833 => 'DE', // Nightexpress — Germany
|
||||
6839 => '??', // Elytra Charter — insufficient info
|
||||
6847 => '??', // Blocked
|
||||
6859 => 'HU', // ASL Airlines Hungary — Hungary
|
||||
6865 => 'SE', // Falcon Air — Sweden
|
||||
6913 => 'FR', // SEFA — France
|
||||
6920 => 'AU', // Nav Air Charter — Australia
|
||||
6923 => '??', // Blocked
|
||||
6985 => 'RU', // Test Flight Aerographical Center — Russia
|
||||
7013 => 'US', // CitationAir — USA
|
||||
7024 => 'ZW', // Fastjet Zimbabwe — Zimbabwe
|
||||
7042 => 'ES', // Prestige Jet Spain — Spain
|
||||
7064 => 'US', // Flight Express — USA
|
||||
7065 => 'RU', // I-Fly — Russia
|
||||
7078 => 'IS', // Norlandair — Iceland
|
||||
7092 => '??', // Blocked
|
||||
7133 => 'US', // Freight Runners Express — USA
|
||||
7142 => 'NG', // FirstNation Airways — Nigeria
|
||||
7150 => 'US', // Farwest Airlines — USA
|
||||
7233 => '??', // Foxair — insufficient info
|
||||
7244 => 'BE', // Flying Service — Belgium
|
||||
7248 => '??', // Comfort Air — insufficient info
|
||||
7253 => 'BR', // Flyways Linhas Aereas — Brazil
|
||||
7255 => '??', // Blocked
|
||||
7268 => 'DE', // Luftwaffe — Germany
|
||||
7271 => 'RU', // Gromov Air — Russia
|
||||
7274 => 'VE', // Alianza Glancelot C.A. — Venezuela
|
||||
7329 => '??', // Blocked
|
||||
7421 => 'LY', // Ghadames Air — Libya
|
||||
7425 => 'DE', // German Sky Airlines — Germany
|
||||
7460 => '??', // Grivco International — insufficient info
|
||||
7479 => '??', // Tranzglobal — insufficient info
|
||||
7492 => '??', // Gloria — insufficient info
|
||||
7496 => '??', // General Corporation For Light Air Transport — insufficient info
|
||||
7502 => 'GB', // Gama Aviation — UK
|
||||
7514 => 'MX', // Magnicharters — Mexico
|
||||
7525 => '??', // Blocked
|
||||
7531 => '??', // Genex — insufficient info
|
||||
7551 => '??', // Waltair Europe — insufficient info
|
||||
7552 => '??', // Blocked
|
||||
7572 => 'BR', // Agroar Carga Aerea — Brazil
|
||||
7627 => '??', // An-2 — insufficient info
|
||||
7654 => '??', // Blocked
|
||||
7655 => 'RU', // V. Grizodubova Air Company — Russia
|
||||
7717 => '??', // Blocked
|
||||
7722 => '??', // Blocked
|
||||
7749 => 'GB', // Highland European — UK (Scotland)
|
||||
7802 => 'US', // Superior Aviation — USA
|
||||
7809 => 'US', // Hawkaire — USA
|
||||
7813 => 'NO', // CHC Helikopter Service — Norway
|
||||
7824 => 'FR', // Heli Securite — France
|
||||
7863 => '??', // Blocked
|
||||
7903 => '??', // Helistar — insufficient info
|
||||
7944 => 'US', // Grossman Air Service — USA
|
||||
7993 => 'RU', // Private Sky — Russia
|
||||
7995 => '??', // Blocked
|
||||
8036 => 'ES', // Ibertrans Aerea — Spain
|
||||
8050 => 'CA', // Kalair — Canada
|
||||
8053 => 'CA', // ICAO — Canada (Montreal HQ)
|
||||
8066 => '??', // Blocked
|
||||
8091 => 'US', // IFL Group — USA
|
||||
8096 => 'DE', // Interflight — Germany
|
||||
8125 => 'US', // InterJet West — USA
|
||||
8148 => 'RU', // Ilavia — Russia
|
||||
8180 => 'GR', // InterJet Hellenic — Greece
|
||||
8223 => '??', // Blocked
|
||||
8226 => 'US', // CSA Air — USA
|
||||
8240 => 'IR', // Eram Air — Iran
|
||||
8294 => '??', // Chavia — insufficient info
|
||||
8295 => '??', // Blocked
|
||||
8296 => 'IR', // Zagros Airlines — Iran
|
||||
8302 => 'AT', // Jetalliance Flugbetriebs — Austria
|
||||
8320 => 'CN', // Sino Jet (Beijing) — China
|
||||
8326 => 'US', // Jetcraft Aviation — USA
|
||||
8339 => '??', // Blocked
|
||||
8353 => 'FI', // Jetflite — Finland
|
||||
8363 => '??', // JS Aviation — insufficient info
|
||||
8368 => 'JP', // JAL Express — Japan
|
||||
8440 => '??', // Blocked
|
||||
8445 => 'US', // Journey Aviation — USA
|
||||
8451 => 'SE', // Jonair Affarsflyg AB — Sweden
|
||||
8503 => 'FI', // Jet Time Finland — Finland
|
||||
8544 => '??', // Blocked
|
||||
8570 => 'GM', // Gamair — Gambia
|
||||
8584 => '??', // Blocked
|
||||
8586 => 'US', // Makani Kai Air — USA (Hawaii)
|
||||
8614 => 'EG', // Alexandria Airlines — Egypt
|
||||
8617 => 'UA', // Aircompany KHORS — Ukraine
|
||||
8673 => '??', // Blocked
|
||||
8707 => 'NZ', // Kiwi Regional Airlines — New Zealand
|
||||
8725 => 'RU', // Kosmos Airlines — Russia
|
||||
8732 => '??', // UN Humanitarian Relief Flights (Kosovo) — international
|
||||
8737 => 'LT', // TransAviaBaltika — Lithuania
|
||||
8744 => 'RU', // Kotlas Air — Russia
|
||||
8784 => '??', // Blocked
|
||||
8788 => 'KZ', // Zhezair — Kazakhstan
|
||||
8810 => 'CO', // Lineas Aereas Suramericanas — Colombia
|
||||
8826 => 'US', // Quest Diagnostics — USA
|
||||
8846 => 'MX', // TAR Aerolineas — Mexico
|
||||
8850 => '??', // Blocked
|
||||
8883 => 'MX', // Aerolineas Ejecutivas — Mexico
|
||||
8933 => 'SI', // Limitless Airways d.o.o — Slovenia
|
||||
8948 => 'RU', // Luk Aero — Russia
|
||||
8988 => 'DE', // Small Planet Airlines Germany — Germany
|
||||
8990 => '??', // Let's Fly — insufficient info
|
||||
9018 => '??', // Blocked
|
||||
9020 => '??', // Links Air — insufficient info
|
||||
9027 => 'GB', // London Executive Aviation — UK
|
||||
9068 => 'RU', // Alrosa-Avia — Russia
|
||||
9080 => '??', // Airailes — insufficient info
|
||||
9146 => 'LU', // Luxaviation S.A. — Luxembourg
|
||||
9151 => 'US', // Flexjet — USA
|
||||
9158 => 'GB', // LyddAir — UK
|
||||
9168 => '??', // Blocked
|
||||
9186 => 'CA', // Morningstar Air Express — Canada
|
||||
9199 => 'GR', // Minoan Air S.A. — Greece
|
||||
9202 => '??', // Max Aviation — insufficient info
|
||||
9251 => '??', // Blocked
|
||||
9268 => '??', // Shervbery — insufficient info
|
||||
9290 => 'GB', // Merlin Airways — UK
|
||||
9301 => 'RO', // ICAR — Romania
|
||||
9365 => '??', // Blocked
|
||||
9386 => '??', // Air Majoro — insufficient info
|
||||
9403 => 'TW', // Makung Airlines — Taiwan
|
||||
9428 => 'MT', // Maleth Aero — Malta
|
||||
9472 => 'IT', // MiniLiner s.r.l. — Italy
|
||||
9474 => '??', // Blocked
|
||||
9553 => 'CN', // Minsheng International Jet — China
|
||||
9561 => '??', // Aeromas — insufficient info
|
||||
9589 => 'LV', // RAF-Avia — Latvia
|
||||
9614 => 'GB', // Harrods Aviation — UK
|
||||
9658 => 'EE', // SmartLynx Airlines Estonia — Estonia
|
||||
9660 => '??', // Blocked
|
||||
9664 => '??', // Mahfooz Aviation — insufficient info
|
||||
9672 => 'SI', // North Adria Aviation — Slovenia
|
||||
9685 => 'US', // NASA — USA
|
||||
9704 => 'CA', // North Cariboo Air — Canada
|
||||
9709 => 'NG', // Chanchangi Airlines — Nigeria
|
||||
9724 => '??', // Blocked
|
||||
9742 => 'NO', // Barents AirLink — Norway
|
||||
9775 => 'US', // Angel Flight America — USA
|
||||
9809 => '??', // Blocked
|
||||
9823 => 'PT', // NetJets Europe — Portugal (EASA cert)
|
||||
9862 => '??', // Blocked
|
||||
9863 => '??', // Blocked
|
||||
9864 => '??', // Blocked
|
||||
9865 => '??', // Blocked
|
||||
9866 => '??', // Blocked
|
||||
9867 => '??', // Blocked
|
||||
9868 => '??', // Blocked
|
||||
9869 => '??', // Blocked
|
||||
9870 => '??', // Blocked
|
||||
9871 => '??', // Blocked
|
||||
9872 => '??', // Blocked
|
||||
9873 => '??', // Blocked
|
||||
9874 => '??', // Blocked
|
||||
9875 => '??', // Blocked
|
||||
9876 => '??', // Blocked
|
||||
9877 => '??', // Blocked
|
||||
9878 => '??', // Blocked
|
||||
9879 => '??', // Blocked
|
||||
9880 => '??', // Blocked
|
||||
9881 => '??', // Blocked
|
||||
9882 => '??', // Blocked
|
||||
9883 => '??', // Blocked
|
||||
9884 => '??', // Blocked
|
||||
9885 => '??', // Blocked
|
||||
9886 => '??', // Blocked
|
||||
9887 => '??', // Blocked
|
||||
9916 => 'GB', // Atlantic Airlines — UK
|
||||
9923 => 'NL', // North Sea Airways — Netherlands
|
||||
9948 => 'CN', // Nanshan Jet — China
|
||||
9961 => 'ES', // Canarias Airlines — Spain (Canary Islands)
|
||||
9972 => 'JP', // Hokkaido Air System — Japan
|
||||
10020 => 'NO', // AirWing — Norway
|
||||
10022 => 'AU', // Network Aviation — Australia
|
||||
10040 => 'MW', // Ulendo Airlink — Malawi
|
||||
10041 => 'NZ', // Vincent Aviation Ltd. — New Zealand
|
||||
10043 => '??', // Blocked
|
||||
10045 => 'RU', // Alliance Avia — Russia
|
||||
10087 => '??', // Blocked
|
||||
10146 => 'PE', // CM Airlines — Peru
|
||||
10149 => '??', // Blocked
|
||||
10168 => 'US', // Flight Options — USA
|
||||
10184 => 'CA', // Orca Airways — Canada
|
||||
10239 => 'ES', // Aeronova — Spain
|
||||
10259 => '??', // Blocked
|
||||
10274 => 'CA', // Perimeter Aviation — Canada
|
||||
10280 => 'BR', // MAP Linhas Aereas — Brazil
|
||||
10282 => '??', // Blocked
|
||||
10294 => 'US', // Presidential Airways — USA
|
||||
10308 => 'WS', // Virgin Samoa — Samoa
|
||||
10310 => 'NZ', // Virgin Australia (NZ) — New Zealand
|
||||
10335 => 'US', // West Air (USA) — USA
|
||||
10347 => '??', // Blocked
|
||||
10352 => '??', // Blocked
|
||||
10357 => 'FR', // Pan Europeenne Air Service — France
|
||||
10372 => 'EG', // Petroleum Air Services — Egypt
|
||||
10429 => 'AM', // Phoenix Avia — Armenia
|
||||
10525 => '??', // Blocked
|
||||
10589 => 'US', // PARS Systems — USA
|
||||
10620 => '??', // S.A.P. — insufficient info
|
||||
10622 => 'RU', // Pskovavia — Russia
|
||||
10635 => 'DE', // PrivatAir Germany — Germany
|
||||
10647 => 'JO', // Air Arabia Jordan — Jordan
|
||||
10675 => 'US', // Privaira / Sky One Holdings — USA
|
||||
10678 => '??', // Blocked
|
||||
10680 => 'US', // Priester Aviation — USA
|
||||
10703 => '??', // Blocked
|
||||
10708 => '??', // Blocked
|
||||
10709 => '??', // Blocked
|
||||
10712 => 'QA', // Qatar Amiri Flight — Qatar
|
||||
10724 => '??', // Blocked
|
||||
10730 => 'UA', // Windrose Air — Ukraine
|
||||
10739 => '??', // Blocked
|
||||
10742 => 'NZ', // Jetconnect — New Zealand
|
||||
10744 => '??', // Blocked
|
||||
10746 => '??', // Blocked
|
||||
10747 => '??', // Blocked
|
||||
10752 => 'CA', // Gouvernement Du Quebec — Canada
|
||||
10759 => '??', // Blocked
|
||||
10765 => 'JP', // Ryukyu Air Commuter — Japan
|
||||
10798 => 'US', // Rosenbalm Aviation — USA
|
||||
10799 => '??', // Royal Air Charter — insufficient info
|
||||
10862 => 'US', // Package Express, Inc. — USA
|
||||
10863 => '??', // Blocked
|
||||
10870 => 'RU', // Adygheya Avia — Russia
|
||||
10882 => '??', // Blocked
|
||||
10911 => '??', // Blocked
|
||||
10949 => 'ES', // Cygnus Air — Spain
|
||||
10968 => 'AR', // Ariella Airlines S.A. — Argentina
|
||||
10972 => 'LV', // Riga Airlines — Latvia
|
||||
10980 => 'BR', // Rio Linhas Aereas — Brazil
|
||||
11009 => 'NZ', // Air Nelson — New Zealand
|
||||
11017 => '??', // AirNow — insufficient info
|
||||
11019 => '??', // S-Air — insufficient info
|
||||
11056 => '??', // Blocked
|
||||
11090 => 'US', // Republic Airlines — USA
|
||||
11102 => '??', // Blocked
|
||||
11107 => '??', // Blocked
|
||||
11131 => 'ES', // Canarias Airlines — Spain
|
||||
11136 => 'AE', // SNAS Aviation — UAE
|
||||
11154 => '??', // Blocked
|
||||
11155 => '??', // Blocked
|
||||
11168 => '??', // Blocked
|
||||
11175 => '??', // Blocked
|
||||
11223 => 'AU', // Rex Regional Express — Australia
|
||||
11227 => 'MA', // RAM Express — Morocco
|
||||
11242 => 'ZW', // Royal Zimbabwe Airlines — Zimbabwe
|
||||
11243 => '??', // Blocked
|
||||
11250 => 'US', // Crossroads Aviation — USA
|
||||
11316 => 'FI', // Snowbird Airlines Oy — Finland
|
||||
11351 => '??', // Blocked
|
||||
11463 => 'TR', // Saga Airlines — Turkey
|
||||
11466 => 'GB', // Shell Aircraft — UK
|
||||
11481 => 'GB', // British Airways Shuttle — UK
|
||||
11491 => 'BR', // Sideral Linhas Aereas — Brazil
|
||||
11509 => 'IT', // Sirio — Italy
|
||||
11512 => 'RU', // Salair — Russia
|
||||
11530 => 'US', // Sun Jet International Airlines — USA
|
||||
11535 => 'CN', // Sino Jet — China
|
||||
11557 => 'CA', // Skycharter — Canada
|
||||
11563 => 'US', // LabCorp — USA
|
||||
11580 => 'CZ', // Silver Air — Czech Republic
|
||||
11590 => 'CA', // Skylink Aviation — Canada
|
||||
11610 => 'BR', // SETE Linhas Aereas — Brazil
|
||||
11648 => 'DE', // Senator Aviation Charter — Germany
|
||||
11664 => 'IT', // Servizi Aerei — Italy
|
||||
11666 => '??', // Blocked
|
||||
11700 => 'SI', // Solinair — Slovenia
|
||||
11701 => '??', // Blocked
|
||||
11739 => 'GB', // Speedwings — UK
|
||||
11748 => 'SG', // Singapore Airlines Cargo — Singapore
|
||||
11778 => 'PL', // Sprint Air — Poland
|
||||
11792 => '??', // Star Of Asia — insufficient info
|
||||
11801 => 'SK', // Slovak Government Flying Service — Slovakia
|
||||
11813 => 'AU', // Sunstate Airlines — Australia
|
||||
11839 => '??', // Starwings International — insufficient info
|
||||
11875 => 'PL', // Silesia Air — Poland
|
||||
11877 => 'US', // Suburban Air Freight — USA
|
||||
11909 => '??', // Blocked
|
||||
11914 => 'SE', // Swedish Air Force — Sweden
|
||||
11935 => 'LU', // Global Jet Luxembourg — Luxembourg
|
||||
11958 => 'CH', // Swiss Global Air Lines — Switzerland
|
||||
12010 => 'ZA', // South African Historic Flight — South Africa
|
||||
12016 => '??', // Blocked
|
||||
12032 => 'US', // TAG Aviation USA — USA
|
||||
12034 => 'PF', // Air Moorea — French Polynesia
|
||||
12054 => 'ES', // Air Iberia — Spain
|
||||
12072 => 'IR', // ATA Airlines — Iran
|
||||
12096 => '??', // Blocked
|
||||
12100 => '??', // Tadair — insufficient info
|
||||
12146 => 'KG', // Tez Jet — Kyrgyzstan
|
||||
12150 => '??', // Blocked
|
||||
12176 => 'CA', // Transport Canada — Canada
|
||||
12198 => 'CA', // Thunder Airlines — Canada
|
||||
12225 => 'AT', // Tyrolean Jet Service — Austria
|
||||
12234 => 'RU', // Tretyakovo Air Transport — Russia
|
||||
12259 => 'ES', // Top Fly — Spain
|
||||
12302 => '??', // Blocked
|
||||
12352 => 'MX', // TAPSA Aviacion — Mexico
|
||||
12419 => 'RU', // Trast — Russia
|
||||
12445 => 'BR', // Total Linhas Aereas — Brazil
|
||||
12456 => '??', // Blocked
|
||||
12463 => 'US', // Yute Air Alaska — USA
|
||||
12476 => 'RU', // Tupolev-Aerotrans — Russia
|
||||
12507 => 'FR', // Twinjet Aviation — France
|
||||
12536 => 'US', // Texstar Air Freight — USA
|
||||
12547 => 'AT', // Tyrol Air Ambulance — Austria
|
||||
12550 => '??', // Blocked
|
||||
12572 => 'UA', // Aviant — Ukraine
|
||||
12578 => 'UA', // Antonov Airtrack — Ukraine
|
||||
12601 => '??', // Blocked
|
||||
12621 => 'RU', // Shar Ink — Russia
|
||||
12637 => 'UA', // Ukraine Air Alliance — Ukraine
|
||||
12673 => '??', // Blocked
|
||||
12674 => '??', // United Nations — international body
|
||||
12689 => 'UA', // Ukrainian Pilot School — Ukraine
|
||||
12701 => 'AM', // Avia-Urartu — Armenia
|
||||
12710 => 'US', // AirNet — USA
|
||||
12740 => '??', // Blocked
|
||||
12751 => 'CA', // Voyageur Airways — Canada
|
||||
12755 => 'TH', // Phuket Air — Thailand
|
||||
12792 => '??', // Blocked
|
||||
12803 => 'VE', // Aeroejecutivos — Venezuela
|
||||
12831 => 'GB', // Virgin Atlantic International — UK
|
||||
12839 => 'RU', // Vologda Air Company — Russia
|
||||
12848 => 'DE', // Vibro Air Flugservice — Germany
|
||||
12911 => '??', // Blocked
|
||||
12926 => 'UA', // Veteran Airlines — Ukraine
|
||||
12929 => 'UA', // Veteran Air — Ukraine
|
||||
12949 => '??', // Vertir Airlines — insufficient info
|
||||
12970 => 'SI', // Aviostart — Slovenia
|
||||
12979 => 'RU', // Vostok Airlines — Russia
|
||||
12981 => 'MX', // Aeronaves TSM — Mexico
|
||||
13020 => '??', // Blocked
|
||||
13025 => 'GN', // Guinea Airlines — Guinea
|
||||
13041 => 'US', // World Atlantic Airlines — USA
|
||||
13069 => '??', // Blocked
|
||||
13074 => 'DE', // WDL Aviation — Germany
|
||||
13088 => '??', // Pronto Airways — insufficient info
|
||||
13098 => 'US', // Vacation Express Public Charter — USA
|
||||
13121 => 'US', // Wiggins Airways — USA
|
||||
13122 => 'NZ', // Waikato Aero Club — New Zealand
|
||||
744 => 'CA', // Hydro-Quebec — Canada
|
||||
749 => 'GB', // British Airways Cargo — UK
|
||||
750 => 'SG', // Sabre Asia Pacific Pte. Ltd. — Singapore
|
||||
759 => 'GB', // Travelport International Operations — UK
|
||||
760 => 'TW', // Eva Airways Cargo — Taiwan
|
||||
761 => 'KR', // Asiana Airways Cargo — South Korea
|
||||
762 => 'RU', // JSC Sirena-Travel — Russia
|
||||
772 => 'HK', // Dragonair Cargo — Hong Kong
|
||||
798 => '??', // GETS Marketing Company — insufficient info
|
||||
806 => 'AL', // Albawings — Albania
|
||||
808 => 'FR', // SNCF — France
|
||||
816 => 'US', // Smokey Bay Air — USA (Alaska)
|
||||
825 => 'BE', // Thalys International — Belgium
|
||||
832 => '??', // Karlog Air — insufficient info
|
||||
839 => '??', // Redemption Inc. — insufficient info
|
||||
840 => '??', // Redemption Inc. — insufficient info
|
||||
865 => 'US', // Amtrak — USA
|
||||
878 => 'US', // Kenosha Aero dba Alliance Air — USA
|
||||
879 => 'HK', // Chu Kong Passenger Transport — Hong Kong
|
||||
893 => 'DE', // Dokasch GmbH — Germany
|
||||
906 => 'MY', // Gading Sari Aviation Sdn Bhd — Malaysia
|
||||
928 => 'DE', // Conatact Air — Germany
|
||||
932 => 'MW', // Malawian Airlines — Malawi
|
||||
934 => '??', // Premier Trans Aire — insufficient info
|
||||
944 => 'US', // Aerodynamics, Inc. — USA
|
||||
967 => 'GE', // Georgian International Airlines — Georgia
|
||||
970 => '??', // Air Besit — insufficient info
|
||||
975 => 'US', // Island Air — USA (Hawaii)
|
||||
1005 => 'US', // Mercury Air Cargo, Inc. — USA
|
||||
1006 => 'US', // Mercury World Cargo — USA
|
||||
1025 => 'KE', // Five Fourty Aviation Limited — Kenya
|
||||
1027 => 'ES', // Alsa Grupo SLU — Spain
|
||||
1035 => '??', // ACM Air Charter — insufficient info
|
||||
1049 => 'VE', // Rutas Aereas, C.A. — Venezuela
|
||||
1066 => 'AT', // WESTbahn Management GmbH — Austria
|
||||
1080 => 'ID', // PT. Pelita Air — Indonesia
|
||||
1081 => 'SK', // Travel Service Slovensko s.r.o. — Slovakia
|
||||
1103 => '??', // Zacarias Moreno — insufficient info
|
||||
1111 => '??', // Gryphon Airlines — insufficient info
|
||||
1120 => 'RU', // Alrosa Air — Russia
|
||||
1122 => 'RU', // Open Joint Stock Company ALROSA — Russia
|
||||
1130 => 'TC', // Turks Air — Turks and Caicos
|
||||
1137 => 'ES', // Auto Res S.L.U. dba Avanza Group — Spain
|
||||
1156 => '??', // MGC Aviation — insufficient info
|
||||
1165 => 'US', // CSafe Global — USA
|
||||
1175 => 'HU', // Travel Service Legiforgalmi — Hungary
|
||||
1187 => 'EC', // Aero Express del Ecuador — Ecuador
|
||||
1190 => 'US', // Alpha Air Transport — USA
|
||||
1204 => 'US', // Ameristar Air Cargo, Inc. — USA
|
||||
1205 => '??', // Skylanes — insufficient info
|
||||
1208 => 'PA', // Panama Airways — Panama
|
||||
1237 => '??', // Regional Air Services — insufficient info
|
||||
1241 => 'CA', // West Coast Air — Canada (BC)
|
||||
1250 => 'CA', // Air Tindi — Canada (NWT)
|
||||
1254 => 'SR', // Fly All Ways — Suriname
|
||||
1258 => 'SR', // Fly Always N.V. — Suriname
|
||||
1269 => 'CA', // AccesRail and Partner Railways — Canada
|
||||
1281 => 'GB', // Eurostar International Limited — UK
|
||||
1284 => 'GB', // 9G Rail Limited — UK
|
||||
1299 => '??', // West Link Airlines — insufficient info
|
||||
1301 => '??', // West Link Airways — insufficient info
|
||||
1310 => '??', // The Cargo Flights Co. — insufficient info
|
||||
1323 => 'TR', // ACT Havayollari A.S. — Turkey
|
||||
1338 => '??', // National Airways — insufficient info (multiple carriers)
|
||||
1340 => 'AR', // Macair Jet S.A. — Argentina
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('countries')->insert([
|
||||
'code' => '??',
|
||||
'name' => '??',
|
||||
'continent_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('airlines')->where('country_code', 'AN')->update(['country_code' => 'CW']);
|
||||
DB::table('airlines')->where('country_code', 'AX')->update(['country_code' => 'FI']);
|
||||
DB::table('airlines')->where('country_code', 'CS')->update(['country_code' => 'RS']);
|
||||
DB::table('airlines')->where('country_code', 'UK')->update(['country_code' => 'GB']);
|
||||
DB::table('airlines')->where('country_code', 'YU')->update(['country_code' => 'RS']);
|
||||
DB::table('airlines')->where('country_code', 'ZR')->update(['country_code' => 'CD']);
|
||||
|
||||
foreach ($this->map as $id => $code) {
|
||||
|
||||
DB::table('airlines')
|
||||
->where('id', $id)
|
||||
->whereNull('country_id')
|
||||
->update(['country_code' => $code]);
|
||||
|
||||
}
|
||||
|
||||
DB::statement("
|
||||
UPDATE airlines a
|
||||
SET country_id = c.id
|
||||
FROM countries c
|
||||
WHERE c.code = a.country_code
|
||||
AND a.country_id IS NULL
|
||||
AND a.country_code != ''
|
||||
");
|
||||
|
||||
Schema::table('airlines', function (Blueprint $table) {
|
||||
$table->dropColumn(['country_code', 'country_name']);
|
||||
$table->foreignId('country_id')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$ids = array_keys(array_filter($this->map, fn($code) => $code !== '??'));
|
||||
|
||||
DB::table('airlines')
|
||||
->whereIn('id', $ids)
|
||||
->update(['country_code' => '']);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'gol-airlines')
|
||||
->update([
|
||||
'ICAO_code' => 'GLO',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'jetgo')
|
||||
->update([
|
||||
'logo' => 'JG_1.png',
|
||||
'active' => false,
|
||||
]);
|
||||
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'turpial')
|
||||
->update([
|
||||
'logo' => 'T9.png',
|
||||
'IATA_code' => 'T9',
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'gol-airlines')
|
||||
->update([
|
||||
'ICAO_code' => '',
|
||||
'active' => false,
|
||||
]);
|
||||
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'jetgo')
|
||||
->update([
|
||||
'logo' => 'JG.png',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
DB::table('airlines')
|
||||
->where('internal_name', 'turpial')
|
||||
->update([
|
||||
'logo' => '',
|
||||
'IATA_code' => '',
|
||||
]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private array $timezones = [
|
||||
176 => 'Antarctica/Macquarie',
|
||||
1012 => 'America/Iqaluit',
|
||||
1129 => 'America/Iqaluit',
|
||||
1153 => 'America/Iqaluit',
|
||||
9241 => 'Asia/Anadyr',
|
||||
9245 => 'Asia/Anadyr',
|
||||
9364 => 'Europe/Moscow',
|
||||
9365 => 'Asia/Krasnoyarsk',
|
||||
11018 => 'Asia/Ulaanbaatar',
|
||||
11019 => 'Asia/Ulaanbaatar',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
// 1. Populate timezones for the specific airports that were missing them
|
||||
foreach ($this->timezones as $id => $timezone) {
|
||||
DB::table('airports')
|
||||
->where('id', $id)
|
||||
->whereNull('timezone')
|
||||
->update(['timezone' => $timezone]);
|
||||
}
|
||||
|
||||
// 2. Make the timezone column non-nullable
|
||||
Schema::table('airports', function (Blueprint $table) {
|
||||
$table->string('timezone')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// 1. Revert the column back to nullable first
|
||||
Schema::table('airports', function (Blueprint $table) {
|
||||
$table->string('timezone')->nullable()->change();
|
||||
});
|
||||
|
||||
// 2. Null out only the rows we populated
|
||||
DB::table('airports')
|
||||
->whereIn('id', array_keys($this->timezones))
|
||||
->update(['timezone' => null]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Airline;
|
||||
use App\Models\Airport;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Airport::where('iata_code', 'LZR')->update(['municipality' => 'Lizard Island']);
|
||||
Airport::where('iata_code', 'SYD')->update(['municipality' => 'Sydney']);
|
||||
Airport::where('iata_code', 'ISB')->update(['municipality' => 'Islamabad']);
|
||||
Airport::where('iata_code', 'KUL')->update(['municipality' => 'Kuala Lumpur']);
|
||||
Airport::where('iata_code', 'USM')->update(['municipality' => 'Ko Samui']);
|
||||
Airport::where('iata_code', 'CDG')->update(['municipality' => 'Paris']);
|
||||
Airport::where('iata_code', 'ORY')->update(['municipality' => 'Paris']);
|
||||
Airport::where('iata_code', 'FRA')->update(['municipality' => 'Frankfurt']);
|
||||
Airport::where('iata_code', 'EZE')->update(['municipality' => 'Buenos Aires']);
|
||||
Airport::where('iata_code', 'PVG')->update(['municipality' => 'Shanghai']);
|
||||
Airport::where('iata_code', 'MRU')->update(['municipality' => 'Mauritius']);
|
||||
Airport::where('iata_code', 'HNL')->update(['municipality' => 'Honolulu']);
|
||||
Airport::where('iata_code', 'NCL')->update(['municipality' => 'Newcastle upon Tyne']);
|
||||
Airport::where('iata_code', 'KRK')->update(['municipality' => 'Krakow']);
|
||||
Airport::where('iata_code', 'OTP')->update(['municipality' => 'Bucharest']);
|
||||
Airport::where('iata_code', 'CAN')->update(['municipality' => 'Guangzhou']);
|
||||
Airport::where('iata_code', 'XIY')->update(['municipality' => "Xi'an"]);
|
||||
Airport::where('iata_code', 'ROR')->update(['municipality' => "Koror"]);
|
||||
Airport::where('iata_code', 'WUH')->update(['municipality' => "Wuhan"]);
|
||||
Airport::where('iata_code', 'CCS')->update(['municipality' => "Caracas"]);
|
||||
Airport::where('iata_code', 'TFU')->update(['municipality' => "Chengdu"]);
|
||||
Airport::where('iata_code', 'CTU')->update(['municipality' => "Chengdu"]);
|
||||
Airport::where('iata_code', 'DMM')->update(['municipality' => "Dammam"]);
|
||||
Airport::where('iata_code', 'VCE')->update(['municipality' => "Venice"]);
|
||||
Airport::where('iata_code', 'MXP')->update(['municipality' => "Milan"]);
|
||||
Airport::where('iata_code', 'DPS')->update(['municipality' => "Denpasar"]);
|
||||
Airport::where('iata_code', 'CAY')->update(['municipality' => "Cayenne"]);
|
||||
Airport::where('iata_code', 'LPA')->update(['municipality' => "Gran Canaria"]);
|
||||
|
||||
Airline::where('internal_name', 'thy-turkish-airlines')->update(['name' => 'Turkish Airlines']);
|
||||
Airline::where('internal_name', 'air-china')->update(['name' => 'Air China']);
|
||||
Airline::where('internal_name', 'jetstar-airways-pty')->update(['name' => 'Jetstar']);
|
||||
Airline::where('internal_name', 'easyjet')->update(['name' => 'Easyjet']);
|
||||
Airline::where('internal_name', 'china-west-air')->update(['name' => 'China West Air']);
|
||||
Airline::where('internal_name', 'aeroenlaces-nacionales-s-a-de-c-v')->update(['name' => 'Viva']);
|
||||
Airline::where('internal_name', 'aircompany-somon-air')->update(['name' => 'Somon Air']);
|
||||
Airline::where('internal_name', 'hinterland-aviation-pty')->update(['name' => 'Hinterland Aviation']);
|
||||
Airline::where('internal_name', 'mango-airlines-soc-trading-as-mango')->update(['name' => 'Mango']);
|
||||
Airline::where('internal_name', 'air-manas-dba-air-manas-air')->update(['name' => 'Air Manas']);
|
||||
Airline::where('internal_name', 'airasia-x-berhad-dba-airasia-x')->update(['name' => 'Air Asia X']);
|
||||
Airline::where('internal_name', 'mesa-airlines')->update(['name' => 'Mesa Airlines']);
|
||||
Airport::where('iata_code', 'MCT')->update(['municipality' => "Muscat"]);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Airport::where('iata_code', 'LZR')->update(['municipality' => '']);
|
||||
Airport::where('iata_code', 'SYD')->update(['municipality' => 'Sydney (Mascot)']);
|
||||
Airport::where('iata_code', 'ISB')->update(['municipality' => 'Attock']);
|
||||
Airport::where('iata_code', 'KUL')->update(['municipality' => 'Sepang']);
|
||||
Airport::where('iata_code', 'USM')->update(['municipality' => 'Na Thon (Ko Samui Island)']);
|
||||
Airport::where('iata_code', 'CDG')->update(['municipality' => "Paris (Roissy-en-France, Val-d'Oise)"]);
|
||||
Airport::where('iata_code', 'ORY')->update(['municipality' => "Paris (Orly, Val-de-Marne)"]);
|
||||
Airport::where('iata_code', 'FRA')->update(['municipality' => 'Frankfurt am Main']);
|
||||
Airport::where('iata_code', 'EZE')->update(['municipality' => 'Buenos Aires (Ezeiza)']);
|
||||
Airport::where('iata_code', 'PVG')->update(['municipality' => 'Shanghai (Pudong)']);
|
||||
Airport::where('iata_code', 'MRU')->update(['municipality' => 'Plaine Magnien']);
|
||||
Airport::where('iata_code', 'HNL')->update(['municipality' => 'Honolulu, Oahu']);
|
||||
Airport::where('iata_code', 'NCL')->update(['municipality' => 'Newcastle upon Tyne, Tyne and Wear']);
|
||||
Airport::where('iata_code', 'KRK')->update(['municipality' => 'Balice']);
|
||||
Airport::where('iata_code', 'OTP')->update(['municipality' => 'Otopeni']);
|
||||
Airport::where('iata_code', 'CAN')->update(['municipality' => 'Guangzhou (Huadu)']);
|
||||
Airport::where('iata_code', 'XIY')->update(['municipality' => "Xianyang (Weicheng)"]);
|
||||
Airport::where('iata_code', 'ROR')->update(['municipality' => "Babelthuap Island"]);
|
||||
Airport::where('iata_code', 'WUH')->update(['municipality' => "Wuhan (Huangpi)"]);
|
||||
Airport::where('iata_code', 'CCS')->update(['municipality' => "Maiquetía"]);
|
||||
Airport::where('iata_code', 'TFU')->update(['municipality' => "Chengdu (Jianyang)"]);
|
||||
Airport::where('iata_code', 'CTU')->update(['municipality' => "Chengdu (Shuangliu)"]);
|
||||
Airport::where('iata_code', 'DMM')->update(['municipality' => "Ad Dammam"]);
|
||||
Airport::where('iata_code', 'VCE')->update(['municipality' => "Venezia (VE)"]);
|
||||
Airport::where('iata_code', 'MXP')->update(['municipality' => "Ferno (VA)"]);
|
||||
Airport::where('iata_code', 'DPS')->update(['municipality' => "Kuta, Badung"]);
|
||||
Airport::where('iata_code', 'CAY')->update(['municipality' => "Matoury"]);
|
||||
Airport::where('iata_code', 'LPA')->update(['municipality' => "Gran Canaria Island"]);
|
||||
Airport::where('iata_code', 'MCT')->update(['municipality' => "Muscat/Seeb"]);
|
||||
|
||||
|
||||
Airline::where('internal_name', 'thy-turkish-airlines')->update(['name' => ' THY - Turkish Airlines']);
|
||||
Airline::where('internal_name', 'air-china')->update(['name' => 'Air China Limited']);
|
||||
Airline::where('internal_name', 'jetstar-airways-pty')->update(['name' => 'Jetstar Airways Pty Limited']);
|
||||
Airline::where('internal_name', 'easyjet')->update(['name' => 'Easyjet Airline Company Limited']);
|
||||
Airline::where('internal_name', 'china-west-air')->update(['name' => 'China West Air Co. Ltd.']);
|
||||
Airline::where('internal_name', 'aeroenlaces-nacionales-s-a-de-c-v')->update(['name' => 'Aeroenlaces Nacionales S.A. De C.V.']);
|
||||
Airline::where('internal_name', 'aircompany-somon-air')->update(['name' => 'Aircompany Somon Air LLC']);
|
||||
Airline::where('internal_name', 'hinterland-aviation-pty')->update(['name' => 'Hinterland Aviation Pty Ltd']);
|
||||
Airline::where('internal_name', 'mango-airlines-soc-trading-as-mango')->update(['name' => 'Mango Airlines (SOC) Ltd trading as MANGO']);
|
||||
Airline::where('internal_name', 'air-manas-dba-air-manas-air')->update(['name' => 'Air Manas dba Air Manas ltd. Air Company']);
|
||||
Airline::where('internal_name', 'airasia-x-berhad-dba-airasia-x')->update(['name' => 'Airasia X Berhad dba Airasia X']);
|
||||
Airline::where('internal_name', 'mesa-airlines')->update(['name' => 'Mesa Airlines, Inc']);
|
||||
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Airline;
|
||||
use App\Models\Country;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$northKorea = Country::where('code', 'KP')->first();
|
||||
|
||||
Airline::where('internal_name', 'air-koryo')->update(['country_id' => $northKorea->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$southKorea = Country::where('code', 'KR')->first();
|
||||
|
||||
Airline::where('internal_name', 'air-koryo')->update(['country_id' => $southKorea->id]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('user_flights', function (Blueprint $table) {
|
||||
$table->boolean('auto_update')->default(false)->after('note');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('user_flights', function (Blueprint $table) {
|
||||
$table->dropColumn('auto_update');
|
||||
});
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user