From 678096b46305f8526b344ea64480fee508604ca1 Mon Sep 17 00:00:00 2001 From: josh Date: Thu, 23 Apr 2026 21:32:25 +1000 Subject: [PATCH] Updated logo API --- ...ontroller.php => AirlineApiController.php} | 28 ++- .../Controllers/Api/UserApiController.php | 60 ++++++ app/Http/Controllers/Api/UserController.php | 50 ----- app/Http/Controllers/FlightController.php | 72 ++++++- .../Controllers/FlightProfileController.php | 38 ++-- app/Http/Controllers/UserController.php | 31 +++ app/Http/Controllers/UserFlightController.php | 35 ++++ app/Models/Airline.php | 2 +- app/Models/Followee.php | 24 +++ app/Models/User.php | 26 +++ app/Models/UserAction.php | 20 ++ .../Components/FlightsGoneBy/AirlineLogo.vue | 4 +- .../Components/FlightsGoneBy/CrewTooltip.vue | 4 +- .../FlightsGoneBy/DepartureBoard.vue | 8 +- .../FlightsGoneBy/FlightStatsBar.vue | 24 +++ .../Components/FlightsGoneBy/MainHeader.vue | 179 +++++++++++++++++- .../FlightsGoneBy/ProfileHeader.vue | 93 ++++++++- .../FlightsGoneBy/ProfileLayout.vue | 9 +- resources/js/Pages/AddFlight.vue | 6 +- resources/js/Pages/ProfileMap.vue | 30 --- resources/js/Pages/UserProfile.vue | 15 +- routes/web.php | 26 ++- 22 files changed, 638 insertions(+), 146 deletions(-) rename app/Http/Controllers/Api/{LogoController.php => AirlineApiController.php} (60%) create mode 100644 app/Http/Controllers/Api/UserApiController.php delete mode 100644 app/Http/Controllers/Api/UserController.php create mode 100644 app/Http/Controllers/UserController.php create mode 100644 app/Http/Controllers/UserFlightController.php create mode 100644 app/Models/Followee.php create mode 100644 app/Models/UserAction.php delete mode 100644 resources/js/Pages/ProfileMap.vue diff --git a/app/Http/Controllers/Api/LogoController.php b/app/Http/Controllers/Api/AirlineApiController.php similarity index 60% rename from app/Http/Controllers/Api/LogoController.php rename to app/Http/Controllers/Api/AirlineApiController.php index 0c4518a..0b9b71b 100644 --- a/app/Http/Controllers/Api/LogoController.php +++ b/app/Http/Controllers/Api/AirlineApiController.php @@ -8,7 +8,7 @@ use App\Models\Airline; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; -class LogoController extends ApiController +class AirlineApiController extends ApiController { const array CONDOR_LOGOS = ['BEACH', 'ISLAND', 'PASSION', 'SEA', 'SUNSHINE']; @@ -47,4 +47,30 @@ class LogoController extends ApiController 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)); + } + } diff --git a/app/Http/Controllers/Api/UserApiController.php b/app/Http/Controllers/Api/UserApiController.php new file mode 100644 index 0000000..28ecfda --- /dev/null +++ b/app/Http/Controllers/Api/UserApiController.php @@ -0,0 +1,60 @@ +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()); + } +} diff --git a/app/Http/Controllers/Api/UserController.php b/app/Http/Controllers/Api/UserController.php deleted file mode 100644 index f9718ac..0000000 --- a/app/Http/Controllers/Api/UserController.php +++ /dev/null @@ -1,50 +0,0 @@ -first(); - if(!$user) return [ - 'message' => 'User not found', - ]; - - $flight = UserFlight::with(['departureAirport', 'arrivalAirport', 'airline', 'aircraft']) - ->where('user_id', $user->id) - ->where('departure_date', '>', now()->utc()) - ->orderBy('departure_date', 'asc') - ->first(); - - $departure = Carbon::parse($flight->departure_date)->setTimezone($flight->departureAirport->timezone); - $arrival = Carbon::parse($flight->arrival_date)->setTimezone($flight->arrivalAirport->timezone); - - return [ - '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, - ]; - } - - function flights(string $username){ - - } -} diff --git a/app/Http/Controllers/FlightController.php b/app/Http/Controllers/FlightController.php index 892e226..58fe7cc 100644 --- a/app/Http/Controllers/FlightController.php +++ b/app/Http/Controllers/FlightController.php @@ -2,12 +2,14 @@ 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; @@ -72,6 +74,70 @@ class FlightController extends Controller ]; } + private function recordChanges(UserFlight $flight): void + { + $dirty = $flight->getDirty(); + + if (empty($dirty)) { + return; + } + + $actions = []; + foreach ($dirty as $field => $newValue) { + $original = $flight->getOriginal($field); + $actions[] = [ + 'user_id' => $flight->user_id, + 'user_flight_id' => $flight->id, + 'message' => $this->formatChange($field, $original, $newValue), + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + + UserAction::insert($actions); + } + + 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): string + { + $label = str($field)->replace('_id', '')->replace('_', ' ')->title(); + $fromLabel = $this->resolveLabel($field, $from); + $toLabel = $this->resolveLabel($field, $to); + + return "{$label} changed from {$fromLabel} to {$toLabel}"; + } + private function flightPayload(array $validated): array { [$departureUtc, $arrivalUtc] = $this->convertedDates($validated); @@ -111,7 +177,9 @@ class FlightController extends Controller $validated = $request->validate($this->rules()); - $flight->update($this->flightPayload($validated)); + $flight->fill($this->flightPayload($validated)); + $this->recordChanges($flight); + $flight->save(); return redirect()->route('profile.departure-board', [Auth::user()->name, $flight->id]); } @@ -147,7 +215,7 @@ class FlightController extends Controller 'crew_type' => $flight->crewType?->toArray() ?? [], 'flight_reason' => $flight->flightReason->toArray(), 'airline_options' => $flight->airline - ? [['value' => $flight->airline->id, 'title' => $flight->airline->display_name]] + ? [['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)]], diff --git a/app/Http/Controllers/FlightProfileController.php b/app/Http/Controllers/FlightProfileController.php index 4e7a9d8..3536296 100644 --- a/app/Http/Controllers/FlightProfileController.php +++ b/app/Http/Controllers/FlightProfileController.php @@ -10,51 +10,35 @@ use Inertia\Inertia; class FlightProfileController extends Controller { - public function profileData(string $username, string $view, ?int $selectedFlightId = null) : array { - $user = User::whereRaw(DB::raw('LOWER(name) = ?'), [strtolower($username)])->firstOrFail(); - - $flights = UserFlight::where('user_id', $user->id) - ->with([ - 'departureAirport.region.country', - 'departureAirport.region.continent', - 'arrivalAirport.region.country', - 'arrivalAirport.region.continent', - 'airline.country', - 'aircraft', - 'seatType', - 'flightReason', - 'flightClass', - 'crewType' - ]) - ->orderBy('departure_date', 'desc') - ->get(); - + public function profileData(User $user, string $view, ?int $selectedFlightId = null) : array { + $flights = $user->FlightController()->flights(); return [ 'user' => $user, 'canEdit' => auth()->check() && auth()->id() === $user->id, 'flights' => UserFlightResource::collection($flights)->resolve(), 'initialView' => $view, 'selectedFlightId' => $selectedFlightId, + 'isFollowing' => auth()->check() && auth()->user()->isFollowing($user), ]; } - public function departureBoard(string $username, ?UserFlight $flight = null){ - $profileData = $this->profileData($username, 'board', $flight?->id); + public function departureBoard(User $user, ?UserFlight $flight = null){ + $profileData = $this->profileData($user, 'board', $flight?->id); return Inertia::render('UserProfile', $profileData); } - public function map(string $username){ - $profileData = $this->profileData($username, 'map'); + public function map(User $user){ + $profileData = $this->profileData($user, 'map'); return Inertia::render('UserProfile', $profileData); } - public function boardingPasses(string $username){ - $profileData = $this->profileData($username, 'passes'); + public function boardingPasses(User $user){ + $profileData = $this->profileData($user, 'passes'); return Inertia::render('UserProfile', $profileData); } - public function view(string $username) + public function view(User $user) { - return $this->departureBoard($username); + return $this->departureBoard($user); } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..4866d89 --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,31 @@ +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]); + } +} diff --git a/app/Http/Controllers/UserFlightController.php b/app/Http/Controllers/UserFlightController.php new file mode 100644 index 0000000..538c665 --- /dev/null +++ b/app/Http/Controllers/UserFlightController.php @@ -0,0 +1,35 @@ +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', + 'aircraft', + 'seatType', + 'flightReason', + 'flightClass', + 'crewType' + ]) + ->orderBy('departure_date', 'desc') + ->get(); + } +} diff --git a/app/Models/Airline.php b/app/Models/Airline.php index e591f1f..84ba944 100644 --- a/app/Models/Airline.php +++ b/app/Models/Airline.php @@ -45,7 +45,7 @@ class Airline extends Model protected function logoUrl() : Attribute{ return Attribute::make( get: function () { - return config('app.logo_api_url') . "/airline/$this->internal_name/logo/tail/"; + return config('app.logo_api_url') . "/airline/$this->internal_name/logo/tail"; } ); } diff --git a/app/Models/Followee.php b/app/Models/Followee.php new file mode 100644 index 0000000..6527d25 --- /dev/null +++ b/app/Models/Followee.php @@ -0,0 +1,24 @@ +belongsTo(User::class, 'user_id'); + } + + public function followee(): BelongsTo + { + return $this->belongsTo(User::class, 'followee_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 308c0ee..f573292 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,6 +3,7 @@ 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; @@ -32,6 +33,16 @@ class User extends Authenticatable ]; } + 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); } @@ -40,4 +51,19 @@ class User extends Authenticatable { 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(); + } } diff --git a/app/Models/UserAction.php b/app/Models/UserAction.php new file mode 100644 index 0000000..b037e3f --- /dev/null +++ b/app/Models/UserAction.php @@ -0,0 +1,20 @@ + 'integer', + 'user_id' => 'integer', + 'message' => 'string', + ]; +} diff --git a/resources/js/Components/FlightsGoneBy/AirlineLogo.vue b/resources/js/Components/FlightsGoneBy/AirlineLogo.vue index 3e018dc..64396f7 100644 --- a/resources/js/Components/FlightsGoneBy/AirlineLogo.vue +++ b/resources/js/Components/FlightsGoneBy/AirlineLogo.vue @@ -5,12 +5,14 @@ import {usePage} from "@inertiajs/vue3"; import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue"; import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue"; +const page = usePage().props; + const props = defineProps<{ airline: Airline | null; size?: number | string; }>(); -const logoUrl = computed(() => `url('${props.airline?.logo_url}')`); +const logoUrl = computed(() => `url('${props.airline?.logo_url ?? page.logo_api_url+'/airline/undefined/logo/tail'}')`); const logoStyle = computed(() => ({ width: size.value, height: size.value, diff --git a/resources/js/Components/FlightsGoneBy/CrewTooltip.vue b/resources/js/Components/FlightsGoneBy/CrewTooltip.vue index 3c0ca69..885afa9 100644 --- a/resources/js/Components/FlightsGoneBy/CrewTooltip.vue +++ b/resources/js/Components/FlightsGoneBy/CrewTooltip.vue @@ -10,9 +10,9 @@ defineProps<{