diff --git a/app/Http/Controllers/FlightImportController.php b/app/Http/Controllers/FlightImportController.php index 5c1d85d..cc4b0ff 100644 --- a/app/Http/Controllers/FlightImportController.php +++ b/app/Http/Controllers/FlightImportController.php @@ -5,38 +5,17 @@ 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\UserFlight; use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class FlightImportController extends Controller { - - const array FLIGHT_CLASSES = [ - 0 => 'Please Select', - 1 => 'Economy', - 2 => 'Business', - 3 => 'First', - 4 => 'Premium Economy', - 5 => 'Private', - ]; - - const array SEAT_TYPES = [ - 0 => 'Please Select', - 1 => 'Window', - 2 => 'Middle', - 3 => 'Aisle', - ]; - - const array FLIGHT_REASONS = [ - 0 => 'Please Select', - 1 => 'Pleasure', - 2 => 'Business', - 3 => 'Crew', - 4 => 'Other', - ]; - private function formatTime(?string $time): string { if (!$time) return '00:00'; @@ -45,6 +24,15 @@ class FlightImportController extends Controller 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; @@ -153,19 +141,10 @@ class FlightImportController extends Controller return [ - 'flight_classes' => collect(self::FLIGHT_CLASSES)->map(fn($title, $value) => [ - 'value' => $value, - 'title' => $title, - ])->values(), - 'flight_reasons' => collect(self::FLIGHT_REASONS)->map(fn($title, $value) => [ - 'value' => $value, - 'title' => $title, - ])->values(), - 'seat_types' => collect(self::SEAT_TYPES)->map(fn($title, $value) => [ - 'value' => $value, - 'title' => $title, - ])->values(), - + '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), @@ -184,32 +163,90 @@ class FlightImportController extends Controller ]; } - public function save(Request $request){ + public function save(Request $request) + { $validated = $request->validate([ - 'date' => 'required|date', - 'flight_number' => 'required|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' => 'nullable|integer', - 'flight_class_id' => 'nullable|integer', - 'flight_reason_id' => 'nullable|integer', - 'note' => 'nullable|string', - ],[ - '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', + 'date' => 'required|date', + 'imported_flight_id' => 'required|exists:imported_flights,id', + 'flight_number' => 'required|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' => 'nullable|integer|exists:seat_types,id', + 'flight_class_id' => 'nullable|integer|exists:flight_classes,id', + 'flight_reason_id' => 'nullable|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; + } + + 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'] ?? null, + 'flight_class_id' => $validated['flight_class_id'] ?? null, + 'flight_reason_id' => $validated['flight_reason_id'] ?? null, + 'note' => $validated['note'] ?? null, + ]); + + ImportedFlight::destroy($validated['imported_flight_id']); + + return redirect()->route('reconcile'); } diff --git a/app/Models/FlightClass.php b/app/Models/FlightClass.php new file mode 100644 index 0000000..3de3efa --- /dev/null +++ b/app/Models/FlightClass.php @@ -0,0 +1,10 @@ + 'immutable_datetime', + 'arrival_date' => 'immutable_datetime', + ]; + + 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 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); + } +} diff --git a/database/migrations/2026_04_05_025209_create_extra_tables.php b/database/migrations/2026_04_05_025209_create_extra_tables.php new file mode 100644 index 0000000..cf0b9d3 --- /dev/null +++ b/database/migrations/2026_04_05_025209_create_extra_tables.php @@ -0,0 +1,58 @@ +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'); + } +}; diff --git a/database/migrations/2026_04_05_030407_create_user_flights_table.php b/database/migrations/2026_04_05_030407_create_user_flights_table.php new file mode 100644 index 0000000..616ae74 --- /dev/null +++ b/database/migrations/2026_04_05_030407_create_user_flights_table.php @@ -0,0 +1,39 @@ +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'); + } +}; diff --git a/resources/js/Pages/ReconcileFlight.vue b/resources/js/Pages/ReconcileFlight.vue index 3346178..fcaf173 100644 --- a/resources/js/Pages/ReconcileFlight.vue +++ b/resources/js/Pages/ReconcileFlight.vue @@ -11,6 +11,7 @@ defineOptions({ layout: MainLayout }); const props = defineProps<{ flight: { + imported_flight_id: number, flight_reasons: { value: number, title: string }[] flight_classes:{ value: number, title: string }[] seat_types: { value: number, title: string }[] @@ -34,6 +35,7 @@ const props = defineProps<{ const flight = props.flight; const form = useForm({ + imported_flight_id: flight.imported_flight_id, date: flight.date, flight_number: flight.flight_number, from: flight.from_options[0] ?? null, @@ -53,6 +55,7 @@ const form = useForm({ }); const submitForm = useForm({ + imported_flight_id: flight.imported_flight_id, date: '' as string | null, flight_number: '' as string | null, from_id: null as number | null, @@ -71,6 +74,7 @@ const submitForm = useForm({ }); function submit() { + submitForm.imported_flight_id = form.imported_flight_id; submitForm.date = form.date; submitForm.flight_number = form.flight_number; submitForm.from_id = form.from?.value ?? null; @@ -139,10 +143,10 @@ function submit() { - + - + diff --git a/routes/web.php b/routes/web.php index 6dd2c52..1242464 100644 --- a/routes/web.php +++ b/routes/web.php @@ -35,7 +35,7 @@ Route::domain(config('app.domain'))->group( $flight = new FlightImportController()->reconcile(request()); if (!$flight) { - return redirect('/'); + return redirect('/import/fr24'); } return Inertia::render('ReconcileFlight', [