From a57775e141a89f8533bf0b430f845dde7549bb4b Mon Sep 17 00:00:00 2001 From: josh Date: Mon, 20 Apr 2026 22:30:34 +1000 Subject: [PATCH] Added Crew and General Aviation Filters --- app/Http/Controllers/FlightController.php | 19 ++- .../Controllers/FlightProfileController.php | 1 + app/Models/CrewType.php | 15 ++ app/Models/FlightClass.php | 5 +- app/Models/UserFlight.php | 7 + .../2026_04_20_093204_add_new_tables.php | 142 ++++++++++++++++++ resources/css/app.css | 16 ++ .../Components/FlightsGoneBy/BoardingPass.vue | 11 +- .../Charts/ChartTypes/DonutChart.vue | 2 +- .../ScrollingHorizontalBarChart.vue | 62 +++++--- .../FlightsGoneBy/Charts/CountriesChart.vue | 14 +- .../FlightsGoneBy/Charts/TopAirlinesChart.vue | 14 +- .../FlightsGoneBy/Charts/TopAirportsChart.vue | 17 ++- .../FlightsGoneBy/Charts/TopRoutesChart.vue | 48 +++++- .../Components/FlightsGoneBy/CrewTooltip.vue | 54 +++++++ .../FlightsGoneBy/DepartureBoard.vue | 11 +- .../Components/FlightsGoneBy/FlightCharts.vue | 4 +- .../FlightsGoneBy/FlightClassBadge.vue | 14 +- .../Components/FlightsGoneBy/FlightFilter.vue | 28 +++- .../js/Components/FlightsGoneBy/FlightMap.vue | 4 +- .../FlightsGoneBy/FlightMapAndCharts.vue | 1 + .../Components/FlightsGoneBy/InlineBadge.vue | 4 +- resources/js/Composables/useFlightStats.ts | 109 +++++++++++++- resources/js/Pages/AddFlight.vue | 31 +++- resources/js/Pages/UserProfile.vue | 15 +- resources/js/Types/types.d.ts | 9 ++ 26 files changed, 559 insertions(+), 98 deletions(-) create mode 100644 app/Models/CrewType.php create mode 100644 database/migrations/2026_04_20_093204_add_new_tables.php create mode 100644 resources/js/Components/FlightsGoneBy/CrewTooltip.vue diff --git a/app/Http/Controllers/FlightController.php b/app/Http/Controllers/FlightController.php index 5950df7..892e226 100644 --- a/app/Http/Controllers/FlightController.php +++ b/app/Http/Controllers/FlightController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Models\Airline; use App\Models\Airport; +use App\Models\CrewType; use App\Models\FlightClass; use App\Models\FlightReason; use App\Models\SeatType; @@ -33,6 +34,7 @@ class FlightController extends Controller 'flight_reason_id' => ['integer', 'exists:flight_reasons,id'], 'note' => ['nullable', 'string', 'max:5000'], 'auto_update' => ['boolean'], + 'crew_type_id' => ['nullable', 'exists:crew_types,id'], ]; } @@ -88,6 +90,7 @@ class FlightController extends Controller 'flight_reason_id' => $validated['flight_reason_id'], 'note' => $validated['note'], 'auto_update' => $validated['auto_update'], + 'crew_type_id' => $validated['crew_type_id'], ]; } @@ -113,12 +116,17 @@ class FlightController extends Controller return redirect()->route('profile.departure-board', [Auth::user()->name, $flight->id]); } - public function add(){ - return Inertia::render('AddFlight', [ + 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) @@ -136,6 +144,7 @@ class FlightController extends Controller '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]] @@ -148,9 +157,7 @@ class FlightController extends Controller ]; return Inertia::render('AddFlight', [ 'flight' => $flightData, - 'seat_types' => SeatType::orderBy('id')->get()->toArray(), - 'flight_reasons' => FlightReason::orderBy('id')->get()->toArray(), - 'flight_classes' => FlightClass::orderBy('id')->get()->toArray(), + ...$this->staticData(), ]); } } diff --git a/app/Http/Controllers/FlightProfileController.php b/app/Http/Controllers/FlightProfileController.php index 8d24533..4e7a9d8 100644 --- a/app/Http/Controllers/FlightProfileController.php +++ b/app/Http/Controllers/FlightProfileController.php @@ -24,6 +24,7 @@ class FlightProfileController extends Controller 'seatType', 'flightReason', 'flightClass', + 'crewType' ]) ->orderBy('departure_date', 'desc') ->get(); diff --git a/app/Models/CrewType.php b/app/Models/CrewType.php new file mode 100644 index 0000000..054060e --- /dev/null +++ b/app/Models/CrewType.php @@ -0,0 +1,15 @@ +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); diff --git a/database/migrations/2026_04_20_093204_add_new_tables.php b/database/migrations/2026_04_20_093204_add_new_tables.php new file mode 100644 index 0000000..e8fed00 --- /dev/null +++ b/database/migrations/2026_04_20_093204_add_new_tables.php @@ -0,0 +1,142 @@ +id(); + $table->string('name'); + $table->string('internal_name')->unique(); + }); + + DB::table('crew_types')->insert([ + ['name' => 'Unspecified', 'internal_name' => 'unspecified'], + ['name' => 'Cabin Crew', 'internal_name' => 'cabin_crew'], + ['name' => 'Purser / CSM', 'internal_name' => 'purser'], + ['name' => 'Captain', 'internal_name' => 'captain'], + ['name' => 'First Officer', 'internal_name' => 'first_officer'], + ['name' => 'Second Officer', 'internal_name' => 'second_officer'], + ['name' => 'Deadhead', 'internal_name' => 'deadhead'], + ['name' => 'Marshal / Security', 'internal_name' => 'marshal'], + ]); + + // --------------------------------------------------------------------- + // Followees + // --------------------------------------------------------------------- + Schema::create('followees', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('followee_id')->constrained('users')->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['user_id', 'followee_id']); + }); + + // --------------------------------------------------------------------- + // User Actions + // --------------------------------------------------------------------- + Schema::create('user_actions', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->unsignedBigInteger('user_flight_id'); + $table->text('message'); + $table->timestamps(); + }); + + // --------------------------------------------------------------------- + // Achievements + // --------------------------------------------------------------------- + Schema::create('achievements', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('internal_name')->unique(); + $table->text('description'); + $table->string('icon'); + }); + + // --------------------------------------------------------------------- + // User Achievements + // --------------------------------------------------------------------- + Schema::create('user_achievements', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('achievement_id')->constrained('achievements')->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['user_id', 'achievement_id']); + }); + + DB::statement('ALTER TABLE flight_classes ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY'); + DB::statement('ALTER TABLE flight_reasons ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY'); + + DB::statement(" + SELECT setval( + pg_get_serial_sequence('flight_classes', 'id'), + (SELECT MAX(id) FROM flight_classes) + ) + "); + + DB::statement(" + SELECT setval( + pg_get_serial_sequence('flight_reasons', 'id'), + (SELECT MAX(id) FROM flight_reasons) + ) + "); + + DB::table('flight_classes')->insert([ + ['name' => 'General Aviation'], + ['name' => 'Crew'], + ]); + + DB::table('flight_reasons')->insert([ + ['name' => 'Visiting Friends / Relatives'], + ]); + + Schema::table('flight_classes', function (Blueprint $table) { + $table->string('internal_name')->nullable(); + }); + + DB::table('flight_classes')->get()->each(function ($row) { + DB::table('flight_classes') + ->where('id', $row->id) + ->update(['internal_name' => Str::slug($row->name, '_')]); + }); + + Schema::table('flight_classes', function (Blueprint $table) { + $table->string('internal_name')->nullable(false)->unique()->change(); + }); + + Schema::table('user_flights', function (Blueprint $table) { + $table->foreignId('crew_type_id')->nullable()->constrained('crew_types')->nullOnDelete(); + }); + + $economy = FlightClass::where('internal_name', 'economy')->first(); + $unspecified = FlightClass::where('internal_name', 'unspecified')->first(); + UserFlight::where('flight_class_id', $unspecified->id)->update(['flight_class_id' => $economy->id]); + + FlightReason::where('name', 'Other')->update(['id' => 999]); + } + + public function down(): void + { + Schema::dropIfExists('user_achievements'); + Schema::dropIfExists('achievements'); + Schema::dropIfExists('user_actions'); + Schema::dropIfExists('followees'); + Schema::dropIfExists('crew_types'); + DB::table('flight_classes')->whereIn('name', ['General Aviation', 'Crew'])->delete(); + DB::table('flight_reasons')->where('name', 'Crew')->delete(); + } +}; diff --git a/resources/css/app.css b/resources/css/app.css index e203720..6c1185f 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -119,6 +119,22 @@ body { overflow: hidden; } +.class-general_aviation-global { + background: rgba(34, 197, 94, 0.15); + border: 1px solid rgba(34, 197, 94, 0.3); + color: #22c55e; + position: relative; + overflow: hidden; +} + +.class-crew-global { + background: rgba(198, 120, 110, 0.15); + border: 1px solid rgba(198, 120, 110, 0.3); + color: #c6786e; + position: relative; + overflow: hidden; +} + /* ── First: Platinum ── */ .class-first-global { background: linear-gradient( diff --git a/resources/js/Components/FlightsGoneBy/BoardingPass.vue b/resources/js/Components/FlightsGoneBy/BoardingPass.vue index 3e4c2a5..a8549eb 100644 --- a/resources/js/Components/FlightsGoneBy/BoardingPass.vue +++ b/resources/js/Components/FlightsGoneBy/BoardingPass.vue @@ -8,19 +8,12 @@ defineProps<{ flight: Flight }>() -function classKey(flight: Flight): string { - const n = flight.flight_class?.name?.toLowerCase() ?? '' - if (n.includes('private')) return 'private' - if (n.includes('first')) return 'first' - if (n.includes('business')) return 'business' - if (n.includes('premium')) return 'premium' - return 'economy' -} +