User can add/edit flights
This commit is contained in:
@@ -3,10 +3,39 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Airline;
|
||||
use App\Models\Airport;
|
||||
use App\Models\FlightClass;
|
||||
use App\Models\FlightReason;
|
||||
use App\Models\SeatType;
|
||||
use App\Models\UserFlight;
|
||||
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', 'after:departure_date'],
|
||||
'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' => ['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', 'max:5000'],
|
||||
'auto_update' => ['boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function lookup(Request $request)
|
||||
{
|
||||
$number = strtoupper(trim($request->query('number', '')));
|
||||
@@ -28,4 +57,106 @@ class FlightController extends Controller
|
||||
'aircraft_options' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
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 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'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate($this->rules());
|
||||
|
||||
auth()->user()->flights()->create($this->flightPayload($validated));
|
||||
|
||||
return redirect()->route('dashboard', Auth::user()->name);
|
||||
}
|
||||
|
||||
public function update(Request $request, UserFlight $flight)
|
||||
{
|
||||
$this->authorize('update', $flight);
|
||||
|
||||
$validated = $request->validate($this->rules());
|
||||
|
||||
$flight->update($this->flightPayload($validated));
|
||||
|
||||
return redirect()->route('dashboard', Auth::user()->name);
|
||||
}
|
||||
|
||||
public function add(){
|
||||
return Inertia::render('AddFlight', [
|
||||
'seat_types' => SeatType::all()->map(fn ($s) => ['value' => $s->id, 'title' => $s->name]),
|
||||
'flight_reasons' => FlightReason::all()->map(fn ($f) => ['value' => $f->id, 'title' => $f->name]),
|
||||
'flight_classes' => FlightClass::all()->map(fn ($f) => ['value' => $f->id, 'title' => $f->name]),
|
||||
]);
|
||||
}
|
||||
|
||||
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
|
||||
? ['value' => $flight->seatType->id, 'title' => $flight->seatType->name]
|
||||
: null,
|
||||
'flight_class' => $flight->flightClass
|
||||
? ['value' => $flight->flightClass->id, 'title' => $flight->flightClass->name]
|
||||
: null,
|
||||
'flight_reason' => $flight->flightReason
|
||||
? ['value' => $flight->flightReason->id, 'title' => $flight->flightReason->name]
|
||||
: null,
|
||||
'airline_options' => $flight->airline
|
||||
? [['value' => $flight->airline->id, 'title' => $flight->airline->name]]
|
||||
: [],
|
||||
'from_options' => [['value' => $flight->departureAirport->id, 'title' => $flight->departureAirport->name, 'country_code' => strtolower($flight->departureAirport->region->country->code)]],
|
||||
'to_options' => [['value' => $flight->arrivalAirport->id, 'title' => $flight->arrivalAirport->name, 'country_code' => strtolower($flight->arrivalAirport->region->country->code)]],
|
||||
'aircraft_options' => $flight->aircraft
|
||||
? [['value' => $flight->aircraft->id, 'title' => $flight->aircraft->name]]
|
||||
: [],
|
||||
];
|
||||
return Inertia::render('AddFlight', [
|
||||
'flight' => $flightData,
|
||||
'seat_types' => SeatType::all()->map(fn($t) => ['value' => $t->id, 'title' => $t->name]),
|
||||
'flight_classes' => FlightClass::all()->map(fn($t) => ['value' => $t->id, 'title' => $t->name]),
|
||||
'flight_reasons' => FlightReason::all()->map(fn($t) => ['value' => $t->id, 'title' => $t->name]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,20 @@ class FlightImportController extends Controller
|
||||
preg_match('/\((\w+)\)/', $aircraftQuery, $matches);
|
||||
$designator = $matches[1] ?? null;
|
||||
|
||||
$sortOverrides = [
|
||||
'B788' => "CASE WHEN model_full_name ILIKE '%BBJ%' THEN 1 ELSE 0 END",
|
||||
'B789' => "CASE WHEN model_full_name ILIKE '%BBJ%' THEN 1 ELSE 0 END",
|
||||
];
|
||||
|
||||
if(!$designator){
|
||||
$aircraft = [];
|
||||
} else {
|
||||
|
||||
$aircraft = Aircraft::when($designator, fn($query) => $query->where('designator', 'ilike', $designator))
|
||||
->when(
|
||||
isset($sortOverrides[$designator]),
|
||||
fn($q) => $q->orderByRaw($sortOverrides[$designator])
|
||||
)
|
||||
->orderBy('model_full_name')
|
||||
->limit(10)
|
||||
->get(['id', 'manufacturer_code', 'model_full_name', 'designator'])
|
||||
@@ -52,6 +61,7 @@ class FlightImportController extends Controller
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
}
|
||||
|
||||
return $aircraft;
|
||||
@@ -80,6 +90,7 @@ class FlightImportController extends Controller
|
||||
END
|
||||
", [$iata, $icao, $iata, $icao])
|
||||
->limit(10)
|
||||
->orderBy('id')
|
||||
->get(['id', 'name', 'municipality', 'iata_code', 'icao_code', 'region_id'])
|
||||
->map(fn($a) => [
|
||||
'value' => $a->id,
|
||||
|
||||
@@ -31,6 +31,7 @@ class FlightProfileController extends Controller
|
||||
|
||||
return Inertia::render('FlightProfile', [
|
||||
'user' => $user,
|
||||
'canEdit' => auth()->check() && auth()->id() === $user->id,
|
||||
'flights' => UserFlightResource::collection($flights)->resolve(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ class SearchController extends Controller
|
||||
return Aircraft::where('designator', 'ilike', "%{$q}%")
|
||||
->orWhereRaw("CONCAT(manufacturer_code, ' ', model_full_name) ilike ?", ["%{$q}%"])
|
||||
->limit(200)
|
||||
->orderBy('id', 'asc')
|
||||
->get(['id', 'manufacturer_code', 'model_full_name', 'designator'])
|
||||
->map(fn($a) => [
|
||||
'value' => $a->id,
|
||||
|
||||
@@ -32,6 +32,10 @@ class User extends Authenticatable
|
||||
];
|
||||
}
|
||||
|
||||
public function flights(): HasMany {
|
||||
return $this->hasMany(UserFlight::class);
|
||||
}
|
||||
|
||||
public function ImportedFlights(): HasMany
|
||||
{
|
||||
return $this->hasMany(ImportedFlight::class);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class UserFlightPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, UserFlight $userFlight): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, UserFlight $userFlight): bool
|
||||
{
|
||||
return $user->id === $userFlight->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, UserFlight $userFlight): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, UserFlight $userFlight): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, UserFlight $userFlight): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import AircraftToolTip from "@/Components/FlightsGoneBy/AircraftToolTip.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
flights: Flight[]
|
||||
canEdit: boolean
|
||||
}>()
|
||||
|
||||
const headers = [
|
||||
@@ -25,6 +26,7 @@ const headers = [
|
||||
{ title: 'DISTANCE', key: 'distance', sortable: true },
|
||||
{ title: 'AIRCRAFT', key: 'aircraft.designator', sortable: true },
|
||||
{ title: 'CLASS', key: 'flight_class', sortable: true },
|
||||
{ title: '', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
const CLASS_ORDER: Record<string, number> = {
|
||||
@@ -206,6 +208,30 @@ const tableItems = computed(() =>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="v-data-table__td actions-cell">
|
||||
<template v-if="canEdit">
|
||||
<v-menu>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
icon="mdi-dots-horizontal"
|
||||
variant="text"
|
||||
size="small"
|
||||
density="compact"
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact" bg-color="#1a1e2e">
|
||||
<v-list-item
|
||||
prepend-icon="mdi-pencil-outline"
|
||||
title="Edit"
|
||||
:href="route('flights.edit', { flight: (item as Flight).id })"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
@@ -462,4 +488,17 @@ const tableItems = computed(() =>
|
||||
letter-spacing: 0.2em;
|
||||
color: #445;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
width: 40px;
|
||||
padding: 0 0.5rem !important;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.v-list-item-title) {
|
||||
font-family: 'Share Tech Mono', monospace !important;
|
||||
font-size: 0.8rem !important;
|
||||
letter-spacing: 0.08em !important;
|
||||
color: #c8cdd8 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,7 +18,7 @@ const menuOpen = ref(false)
|
||||
<Link :href="route('login')" class="nav-link">Log In</Link>
|
||||
<Link :href="route('register')" class="nav-link nav-link--highlight">Register</Link>
|
||||
</template>
|
||||
<span v-else class="nav-link nav-link--greeting">Welcome, {{ props.auth.user.name }}</span>
|
||||
<span v-else class="nav-link nav-link--greeting"><Link :href="route('dashboard')">Welcome, {{ props.auth.user.name }}</Link></span>
|
||||
</nav>
|
||||
|
||||
<!-- Hamburger (mobile only) -->
|
||||
@@ -33,7 +33,7 @@ const menuOpen = ref(false)
|
||||
<Link :href="route('login')" class="nav-link">Log In</Link>
|
||||
<Link :href="route('register')" class="nav-link nav-link--highlight">Register</Link>
|
||||
</template>
|
||||
<span v-else class="nav-link nav-link--greeting">Welcome, {{ props.auth.user.name }}</span>
|
||||
<span v-else class="nav-link nav-link--greeting"><Link :href="route('dashboard')">Welcome, {{ props.auth.user.name }}</Link></span>
|
||||
</nav>
|
||||
</Transition>
|
||||
</header>
|
||||
|
||||
@@ -39,7 +39,7 @@ const isEdit = !!props.flight
|
||||
const flightNumber = ref(props.flight?.flight_number ?? '')
|
||||
const lookupLoading = ref(false)
|
||||
const lookupError = ref<string | null>(null)
|
||||
const lookupComplete = ref(isEdit)
|
||||
const lookupComplete = ref(true)
|
||||
|
||||
interface LookupResult {
|
||||
airline_options: { value: number; title: string }[]
|
||||
|
||||
@@ -19,15 +19,10 @@ const name = computed(() => page?.props?.auth?.user?.name || 'there');
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn size="large" block href="#" prepend-icon="mdi-plus">
|
||||
<v-btn size="large" @click="router.visit(route('flights.add'))" block href="#" prepend-icon="mdi-plus">
|
||||
Add a Flight
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn size="large" block href="#" prepend-icon="mdi-pencil-outline">
|
||||
Edit Flights
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn size="large" block @click="router.visit(route('import.fr24'))" prepend-icon="mdi-import">
|
||||
Import from FR24
|
||||
@@ -38,7 +33,7 @@ const name = computed(() => page?.props?.auth?.user?.name || 'there');
|
||||
View Profile
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn size="large" block @click="router.post(route('logout'))" prepend-icon="mdi-logout">
|
||||
Log Out
|
||||
</v-btn>
|
||||
|
||||
@@ -18,6 +18,7 @@ defineProps<{
|
||||
email: string
|
||||
}
|
||||
flights: Flight[]
|
||||
canEdit: boolean
|
||||
}>()
|
||||
|
||||
type View = 'board' | 'passes' | 'map'
|
||||
@@ -67,7 +68,7 @@ const activeView = ref<View>('map')
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DepartureBoard v-if="activeView === 'board'" :flights="flights" />
|
||||
<DepartureBoard v-if="activeView === 'board'" :flights="flights" :canEdit="canEdit" />
|
||||
<BoardingPasses v-else-if="activeView === 'passes'" :flights="flights" />
|
||||
<ProfileMap v-else-if="activeView === 'map'" :flights="flights" />
|
||||
</div>
|
||||
|
||||
+5
-8
@@ -37,13 +37,10 @@ Route::domain(config('app.domain'))->group(
|
||||
return Inertia::render('Fr24Import');
|
||||
})->name('import.fr24');
|
||||
|
||||
Route::get('/add-flight', function () {
|
||||
return Inertia::render('AddFlight', [
|
||||
'seat_types' => SeatType::all()->map(fn ($s) => ['value' => $s->id, 'title' => $s->name]),
|
||||
'flight_reasons' => FlightReason::all()->map(fn ($f) => ['value' => $f->id, 'title' => $f->name]),
|
||||
'flight_classes' => FlightClass::all()->map(fn ($f) => ['value' => $f->id, 'title' => $f->name]),
|
||||
]);
|
||||
});
|
||||
Route::post('/flights', [FlightController::class, 'store'])->name('flights.store');
|
||||
Route::get('/flights/add', [FlightController::class, 'add'])->name('flights.add');
|
||||
Route::get('/flights/{flight}/edit', [FlightController::class, 'edit'])->name('flights.edit');
|
||||
Route::put('/flights/{flight}', [FlightController::class, 'update'])->name('flights.update');
|
||||
|
||||
Route::get('/reconcile', function () {
|
||||
$flight = new FlightImportController()->reconcile(request());
|
||||
@@ -74,7 +71,7 @@ Route::domain(config('app.domain'))->group(
|
||||
Route::get('/search/airports', [SearchController::class, 'airports'])->name('search.airports');
|
||||
|
||||
|
||||
Route::get('/u/{username}', [FlightProfileController::class, 'view']);
|
||||
Route::get('/u/{username}', [FlightProfileController::class, 'view'])->name('profile.view');
|
||||
|
||||
require __DIR__.'/auth.php';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user