347 lines
14 KiB
PHP
347 lines
14 KiB
PHP
<?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\IataEquipmentCode;
|
|
use App\Models\SeatType;
|
|
use App\Models\UserAction;
|
|
use App\Models\UserFlight;
|
|
use App\Services\FlightStatsService;
|
|
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', 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(),
|
|
]);
|
|
}
|
|
}
|