diff --git a/app/Http/Controllers/FlightImportController.php b/app/Http/Controllers/FlightImportController.php index d95812e..33ee42e 100644 --- a/app/Http/Controllers/FlightImportController.php +++ b/app/Http/Controllers/FlightImportController.php @@ -2,7 +2,9 @@ namespace App\Http\Controllers; +use App\Models\Aircraft; use App\Models\Airline; +use App\Models\Airport; use App\Models\ImportedFlight; use Carbon\Carbon; use Illuminate\Http\Request; @@ -43,6 +45,64 @@ class FlightImportController extends Controller return str_pad($hours, 2, '0', STR_PAD_LEFT) . ':' . $minutes; } + public function getPossibleAircraft(string $aircraftQuery) { + preg_match('/\((\w+)\)/', $aircraftQuery, $matches); + $designator = $matches[1] ?? null; + + if(!$designator){ + $aircraft = []; + } else { + + $aircraft = Aircraft::when($designator, fn($query) => $query->where('designator', 'ilike', $designator)) + ->orderBy('model_full_name') + ->limit(10) + ->get(['id', 'manufacturer_code', 'model_full_name', 'designator']) + ->map(fn($a) => [ + 'value' => $a->id, + 'title' => "{$a->manufacturer_code} {$a->model_full_name} ({$a->designator})", + ]) + ->values() + ->toArray(); + } + + return $aircraft; + } + + public function getPossibleAirports(string $airportQuery) { + preg_match('/\((\w{3})\/(\w{4})\)/', $airportQuery, $matches); + $iata = $matches[1] ?? null; + $icao = $matches[2] ?? null; + + if (!$iata && !$icao) { + return []; + } + + $airports = Airport::with('region.country') + ->where(function ($q) use ($iata, $icao) { + $q->where('iata_code', 'ilike', $iata) + ->orWhere('icao_code', 'ilike', $icao); + }) + ->orderByRaw(" + CASE + WHEN iata_code = ? AND icao_code = ? THEN 0 + WHEN iata_code = ? THEN 1 + WHEN icao_code = ? THEN 2 + ELSE 3 + END + ", [$iata, $icao, $iata, $icao]) + ->limit(10) + ->get(['id', 'name', 'municipality', 'iata_code', 'icao_code', 'region_id']) + ->map(fn($a) => [ + 'value' => $a->id, + 'title' => "{$a->name} ({$a->iata_code}/{$a->icao_code})", + 'country_code' => strtolower($a?->region->country->code ?? ''), + ]) + ->values() + ->toArray(); + + return $airports; + } + public function getPossibleAirlines(string $airlineQuery) { preg_match('/\((\w{2,3})\/(\w{3,4})\)/', $airlineQuery, $matches); $iata = $matches[1] ?? null; @@ -72,10 +132,8 @@ class FlightImportController extends Controller ->values() ->toArray(); - return [ - 'airline_options' => $airlines, - 'raw_airline' => $airlineQuery, - ]; + + return $airlines; } public function reconcile(Request $request) @@ -119,9 +177,40 @@ class FlightImportController extends Controller 'flight_class' => $flightToReconcile->flight_class ?? '', 'seat_type' => $flightToReconcile->seat_type ?? '', 'flight_reason' => $flightToReconcile->flight_reason ?? '', - 'airline_options' => $this->getPossibleAirlines($flightToReconcile->airline ?? '')['airline_options'], + 'airline_options' => $this->getPossibleAirlines($flightToReconcile->airline ?? ''), + 'to_options' => $this->getPossibleAirports($flightToReconcile->to ?? ''), + 'from_options' => $this->getPossibleAirports($flightToReconcile->from ?? ''), + 'aircraft_options' => $this->getPossibleAircraft($flightToReconcile->aircraft ?? ''), ]; } + + 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' => 'nullable|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', + ]); + + } + public function store(Request $request) { try { diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php new file mode 100644 index 0000000..3edc6a5 --- /dev/null +++ b/app/Http/Controllers/SearchController.php @@ -0,0 +1,74 @@ +where(function ($query) use ($q) { + $query->where('name', 'ilike', "%{$q}%") + ->orWhere('IATA_code', 'ilike', "%{$q}%") + ->orWhere('ICAO_code', 'ilike', "%{$q}%"); + }) + ->limit(15) + ->get(['id', 'name', 'IATA_code', 'ICAO_code', 'logo']) + ->map(fn($a) => [ + 'value' => $a->id, + 'title' => "{$a->name} ({$a->IATA_code}/{$a->ICAO_code})", + ]) + ->values(); + } + + public function aircraft() + { + $q = request('q', ''); + + return Aircraft::where('designator', 'ilike', "%{$q}%") + ->orWhereRaw("CONCAT(manufacturer_code, ' ', model_full_name) ilike ?", ["%{$q}%"]) + ->limit(15) + ->get(['id', 'manufacturer_code', 'model_full_name', 'designator']) + ->map(fn($a) => [ + 'value' => $a->id, + 'title' => "{$a->manufacturer_code} {$a->model_full_name} ({$a->designator})", + ]) + ->values(); + } + + public function airports() + { + $q = request('q', ''); + $len = strlen($q); + + if ($len < 3) return []; + + return Airport::with('region.country') + ->when($len === 3, fn($query) => $query->where('iata_code', 'ilike', $q)) + ->when($len >= 4, fn($query) => $query->where(function ($sub) use ($q, $len) { + $sub->when($len === 4, fn($s) => $s->where('icao_code', 'ilike', $q)) + ->orWhere('name', 'ilike', "%{$q}%") + ->orWhere('municipality', 'ilike', "%{$q}%"); + })->orderByRaw(" + CASE + WHEN icao_code = ? THEN 0 + WHEN iata_code = ? THEN 1 + ELSE 2 + END + ", [$q, $q])) + ->limit(15) + ->get(['id', 'name', 'municipality', 'iata_code', 'icao_code', 'region_id']) + ->map(fn($a) => [ + 'value' => $a->id, + 'title' => "{$a->name} ({$a->iata_code}/{$a->icao_code})", + 'country_code' => strtolower($a->region->country->code), + ]) + ->values(); + } +} diff --git a/app/Models/Airline.php b/app/Models/Airline.php index 8b71ed9..99e5b05 100644 --- a/app/Models/Airline.php +++ b/app/Models/Airline.php @@ -24,5 +24,6 @@ class Airline extends Model 'active' => 'boolean', ]; + public $timestamps = false; } diff --git a/package-lock.json b/package-lock.json index a09c094..21c6e11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "@mdi/font": "^7.4.47", + "flag-icons": "^7.5.0", "vuetify": "^4.0.5" }, "devDependencies": { @@ -2703,6 +2704,12 @@ "node": ">=8" } }, + "node_modules/flag-icons": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.5.0.tgz", + "integrity": "sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", diff --git a/package.json b/package.json index 2790603..32e267e 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "dependencies": { "@mdi/font": "^7.4.47", + "flag-icons": "^7.5.0", "vuetify": "^4.0.5" } } diff --git a/resources/css/app.css b/resources/css/app.css index e899ff5..9c59507 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -15,8 +15,8 @@ .glass { background: rgba(17, 24, 39, 0.2); /* --surface at 60% */ - backdrop-filter: blur(12px) saturate(180%); -webkit-backdrop-filter: blur(12px) saturate(180%); + backdrop-filter: blur(12px) saturate(180%); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(56, 189, 248, 0.05); } diff --git a/resources/js/Components/FlightsGoneBy/AircraftSearchBox.vue b/resources/js/Components/FlightsGoneBy/AircraftSearchBox.vue new file mode 100644 index 0000000..ce530da --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/AircraftSearchBox.vue @@ -0,0 +1,51 @@ + + + diff --git a/resources/js/Components/FlightsGoneBy/AirlineSearchBox.vue b/resources/js/Components/FlightsGoneBy/AirlineSearchBox.vue index 801b0fc..7e58106 100644 --- a/resources/js/Components/FlightsGoneBy/AirlineSearchBox.vue +++ b/resources/js/Components/FlightsGoneBy/AirlineSearchBox.vue @@ -31,7 +31,7 @@ const searchAirlines = async (query: string) => { if (query === model.value?.title) return - const { data } = await axios.get('/airlines/search', { params: { q: query } }) + const { data } = await axios.get('/search/airlines', { params: { q: query } }) airlineOptions.value = data } diff --git a/resources/js/Components/FlightsGoneBy/AirportSearchBox.vue b/resources/js/Components/FlightsGoneBy/AirportSearchBox.vue new file mode 100644 index 0000000..1894328 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/AirportSearchBox.vue @@ -0,0 +1,69 @@ + + + diff --git a/resources/js/Pages/ReconcileFlight.vue b/resources/js/Pages/ReconcileFlight.vue index fc66049..8749578 100644 --- a/resources/js/Pages/ReconcileFlight.vue +++ b/resources/js/Pages/ReconcileFlight.vue @@ -3,6 +3,8 @@ import MainLayout from "@/Layouts/MainLayout.vue"; import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue"; import {Head, useForm} from "@inertiajs/vue3"; import AirlineSearchBox from "@/Components/FlightsGoneBy/AirlineSearchBox.vue"; +import AircraftSearchBox from "@/Components/FlightsGoneBy/AircraftSearchBox.vue"; +import AirportSearchBox from "@/Components/FlightsGoneBy/AirportSearchBox.vue"; defineOptions({ layout: MainLayout }); @@ -24,6 +26,9 @@ const props = defineProps<{ arr_time: string duration: string airline_options: { value: number, title: string }[] + from_options: { value: number, title: string, country_code: string}[] + to_options: { value: number, title: string, country_code: string }[] + aircraft_options: { value: number, title: string }[] } }>() const flight = props.flight; @@ -31,13 +36,13 @@ const flight = props.flight; const form = useForm({ date: flight.date, flight_number: flight.flight_number, - from: '', - to: '', + from: flight.from_options[0] ?? null, + to: flight.to_options[0] ?? null, dep_time: flight.dep_time, arr_time: flight.arr_time, duration: flight.duration, airline_id: flight.airline_options[0] ?? null, - aircraft: '', + aircraft: flight.aircraft_options[0] ?? null, registration: flight.registration, seat_number: flight.seat_number, seat_type: flight.seat_types[flight.seat_type], @@ -47,8 +52,42 @@ const form = useForm({ }); +const submitForm = useForm({ + date: '' as string | null, + flight_number: '' as string | null, + from_id: null as number | null, + to_id: null as number | null, + dep_time: '' as string | null, + arr_time: '' as string | null, + duration: '' as string | null, + airline_id: null as number | null, + aircraft_id: null as number | null, + registration: '' as string | null, + seat_number: '' as string | null, + seat_type_id: null as number | null, + flight_class_id: null as number | null, + flight_reason_id: null as number | null, + note: '' as string | null, +}); + function submit() { - form.post(route('reconcile.store')); + submitForm.date = form.date; + submitForm.flight_number = form.flight_number; + submitForm.from_id = form.from?.value ?? null; + submitForm.to_id = form.to?.value ?? null; + submitForm.dep_time = form.dep_time; + submitForm.arr_time = form.arr_time; + submitForm.duration = form.duration; + submitForm.airline_id = form.airline_id?.value ?? null; + submitForm.aircraft_id = form.aircraft?.value ?? null; + submitForm.registration = form.registration; + submitForm.seat_number = form.seat_number; + submitForm.seat_type_id = form.seat_type?.value ?? null; + submitForm.flight_class_id = form.flight_class?.value ?? null; + submitForm.flight_reason_id = form.flight_reason?.value ?? null; + submitForm.note = form.note; + + submitForm.post(route('import.save')); } @@ -65,36 +104,47 @@ function submit() { - + - + - + - - - + + - - + + + + - + - + - + @@ -104,35 +154,39 @@ function submit() { - + - + - + - + - + - + diff --git a/resources/js/app.ts b/resources/js/app.ts index 6f7ddcc..35e228e 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -7,6 +7,7 @@ import { ZiggyVue } from '../../vendor/tightenco/ziggy'; import { createApp, h, DefineComponent } from 'vue'; import vuetify from './plugins/vuetify'; import '@mdi/font/css/materialdesignicons.css' +import 'flag-icons/css/flag-icons.min.css' const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; diff --git a/routes/web.php b/routes/web.php index 0e3c7e7..6dd2c52 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,6 +3,7 @@ use App\Http\Controllers\FlightImportController; use App\Http\Controllers\LogoController; use App\Http\Controllers\ProfileController; +use App\Http\Controllers\SearchController; use App\Models\Airline; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; @@ -43,24 +44,6 @@ Route::domain(config('app.domain'))->group( })->name('reconcile'); - Route::get('/airlines/search', function () { - $q = request('q', ''); - - return Airline::orderByDesc('active') - ->where(function ($query) use ($q) { - $query->where('name', 'ilike', "%{$q}%") - ->orWhere('IATA_code', 'ilike', "%{$q}%") - ->orWhere('ICAO_code', 'ilike', "%{$q}%"); - }) - ->limit(15) - ->get(['id', 'name', 'IATA_code', 'ICAO_code', 'logo']) - ->map(fn($a) => [ - 'value' => $a->id, - 'title' => "{$a->name} ({$a->IATA_code}/{$a->ICAO_code})", - ]) - ->values(); - })->name('airlines.search'); - Route::post('/flights/import', [FlightImportController::class, 'store'])->name('flights.import.store'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); @@ -68,6 +51,13 @@ Route::domain(config('app.domain'))->group( Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); }); + Route::post('/import/save', [FlightImportController::class, 'save'])->name('import.save'); + + //Search Routes + Route::get('/search/airlines', [SearchController::class, 'airlines'])->name('search.airlines'); + Route::get('/search/aircraft', [SearchController::class, 'aircraft'])->name('search.aircraft'); + Route::get('/search/airports', [SearchController::class, 'airports'])->name('search.airports'); + require __DIR__.'/auth.php'; }