['nullable', 'string', 'max:10'], 'departure_date' => ['required', 'date'], 'arrival_date' => ['required', 'date', function ($attribute, $value, $fail) { $from = request()->input('from_id'); $to = request()->input('to_id'); if (!$from || !$to) return; $departureAirport = Airport::find($from); $arrivalAirport = Airport::find($to); if (!$departureAirport || !$arrivalAirport) return; $departureUtc = Carbon::createFromFormat('Y-m-d\TH:i', request()->input('departure_date'), $departureAirport->timezone)->utc(); $arrivalUtc = Carbon::createFromFormat('Y-m-d\TH:i', $value, $arrivalAirport->timezone)->utc(); if ($arrivalUtc->lessThanOrEqualTo($departureUtc)) { $fail('The arrival time must be after the departure time, accounting for time zones.'); } }], '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', ''))); preg_match('/^([A-Z]{2,3})(\d+)/', $number, $matches); $code = $matches[1] ?? null; $flightNumber = $matches[2] ?? null; $isIata = strlen($code) === 2; $codeColumn = $isIata ? 'IATA_code' : 'ICAO_code'; $apiAirlineCodes = []; $fromOptions = []; $toOptions = []; $aircraftOptions = []; if (strlen($number) >= 3 && $isIata) { $flightStatsApi = new FlightStatsService(); $flightData = $flightStatsApi->fetchFlightData($code, $flightNumber); if ($flightData) { $apiAirlineCodes = $flightData->airline_fs_codes; $fromOptions = Airport::where('iata_code', $flightData->departure_iata) ->get() ->map(fn($a) => ['value' => $a->id, 'title' => $a->display_name, 'country_code' => strtolower($a->region->country->code)]) ->values() ->toArray(); $toOptions = Airport::where('iata_code', $flightData->arrival_iata) ->get() ->map(fn($a) => ['value' => $a->id, 'title' => $a->display_name, 'country_code' => strtolower($a->region->country->code)]) ->values() ->toArray(); if($flightData->equipment_iata){ $equipment = IataEquipmentCode::where('iata_code', $flightData->equipment_iata)->first(); if ($equipment) { $bestGuess = $flightStatsApi->guessAircraftFromIata($flightData->equipment_iata); $aircraftOptions = Aircraft::where('designator', $equipment->icao_code) ->get() ->sortBy(fn($a) => $a->id === $bestGuess?->id ? 0 : 1) ->map(fn($a) => ['value' => $a->id, 'title' => $a->display_name]) ->values() ->toArray(); } } } } // Airlines from the typed code + any additional codes from the API, merged and deduped by id $allCodes = array_unique(array_filter([$code, ...$apiAirlineCodes])); $airlines = Airline::where(function ($q) use ($codeColumn, $code, $allCodes) { $q->whereIn($codeColumn, $allCodes); }) ->get() ->unique('id') ->sortBy(function ($a) use ($code, $flightData) { if ($a->IATA_code === $flightData?->operating_fs) return 0; if ($a->IATA_code === $code) return 1; return 2; }) ->map(fn($a) => ['value' => $a->id, 'title' => $a->display_name, 'logo_url' => $a->logo_url]) ->values() ->toArray(); return response()->json([ 'airline_options' => $airlines, 'from_options' => $fromOptions, 'to_options' => $toOptions, 'aircraft_options' => $aircraftOptions, ]); } 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, ?string $referrer = 'departure-board') { $this->authorize('delete', $flight); $snapshot = $flight->snapshot($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.'.$referrer, [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(), ]); } }