Ui fixes #16

Merged
dredgy merged 9 commits from ui-fixes into main 2026-08-21 22:54:14 +10:00
9 changed files with 132 additions and 25 deletions
Showing only changes of commit 4a296705a6 - Show all commits
+14
View File
@@ -7,6 +7,7 @@ use App\Models\IgnoredMissingLivery;
use App\Models\User; use App\Models\User;
use App\Models\UserFlight; use App\Models\UserFlight;
use App\Services\AdminService; use App\Services\AdminService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Inertia\Inertia; use Inertia\Inertia;
@@ -66,4 +67,17 @@ class AdminController extends Controller
return back(); return back();
} }
public function ignoreMissingAirline(Request $request): JsonResponse
{
$validated = $request->validate([
'flight_id' => ['required', 'integer', 'exists:user_flights,id'],
]);
$flight = UserFlight::findOrFail($validated['flight_id']);
$flight->ignoreMissingAirline()->firstOrCreate([]);
return response()->json(['status' => 'ok']);
}
} }
+11
View File
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@@ -271,6 +272,16 @@ class UserFlight extends Model
} }
public function ignoreMissingAirline(): HasOne
{
return $this->hasOne(UserFlightIgnoreMissingAirline::class);
}
public function isIgnoringMissingAirline(): bool
{
return $this->ignoreMissingAirline()->exists();
}
public function aircraft(): BelongsTo public function aircraft(): BelongsTo
{ {
return $this->belongsTo(Aircraft::class); return $this->belongsTo(Aircraft::class);
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserFlightIgnoreMissingAirline extends Model
{
protected $table = 'user_flights_ignore_missing_airlines';
protected $fillable = [
'user_flight_id',
];
public function userFlight(): BelongsTo
{
return $this->belongsTo(UserFlight::class);
}
}
+5 -3
View File
@@ -51,26 +51,28 @@ class AdminService
{ {
$flights = UserFlight::whereNotNull('flight_number') $flights = UserFlight::whereNotNull('flight_number')
->whereNull('airline_id') ->whereNull('airline_id')
->with('user:id,name') // adjust to your actual user relation/columns ->whereDoesntHave('ignoreMissingAirline')
->with('user:id,name')
->get(['id', 'flight_number', 'user_id']); ->get(['id', 'flight_number', 'user_id']);
return $flights return $flights
->map(function ($flight) { ->map(function ($flight) {
// Extract the 2-character IATA-style prefix (letters and/or digits)
preg_match('/^([A-Za-z0-9]{2})\d+/', trim($flight->flight_number), $matches); preg_match('/^([A-Za-z0-9]{2})\d+/', trim($flight->flight_number), $matches);
return [ return [
'id' => $flight->id,
'code' => $matches[1] ?? null, 'code' => $matches[1] ?? null,
'flight_number' => $flight->flight_number, 'flight_number' => $flight->flight_number,
'link' => "/u/{$flight->user->name}/flight/{$flight->id}", 'link' => "/u/{$flight->user->name}/flight/{$flight->id}",
]; ];
}) })
->filter(fn ($f) => $f['code'] !== null) // drop any that didn't match the pattern ->filter(fn ($f) => $f['code'] !== null)
->groupBy('code') ->groupBy('code')
->map(function ($group, $code) { ->map(function ($group, $code) {
return [ return [
'code' => $code, 'code' => $code,
'flights' => $group->map(fn ($f) => [ 'flights' => $group->map(fn ($f) => [
'id' => $f['id'],
'flight_number' => $f['flight_number'], 'flight_number' => $f['flight_number'],
'link' => $f['link'], 'link' => $f['link'],
])->values()->all(), ])->values()->all(),
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('user_flights_ignore_missing_airlines', function (Blueprint $table) {
$table->id();
$table->foreignId('user_flight_id')
->constrained('user_flights')
->cascadeOnDelete();
$table->timestamps();
$table->unique('user_flight_id');
});
}
public function down(): void
{
Schema::dropIfExists('user_flights_ignore_missing_airlines');
}
};
@@ -1,25 +1,63 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"
import axios from "axios"
import { MissingAirline } from "@/Types/types" import { MissingAirline } from "@/Types/types"
defineProps<{ const props = defineProps<{
airline: MissingAirline airline: MissingAirline
}>() }>()
// Track ids we've dismissed locally (optimistic UI) and ones currently in-flight
const dismissedIds = ref<Set<number>>(new Set())
const pendingIds = ref<Set<number>>(new Set())
const visibleFlights = computed(() =>
props.airline.flights.filter(flight => !dismissedIds.value.has(flight.id))
)
async function dismissFlight(flightId: number) {
if (pendingIds.value.has(flightId)) return
pendingIds.value.add(flightId)
// Optimistically remove the pill
dismissedIds.value.add(flightId)
try {
await axios.post('/admin/ignore-missing-airline', {
flight_id: flightId,
})
} catch (error) {
// Revert on failure so the flight isn't silently lost from view
dismissedIds.value.delete(flightId)
console.error('Failed to ignore missing airline for flight', flightId, error)
} finally {
pendingIds.value.delete(flightId)
}
}
</script> </script>
<template> <template>
<div class="missing-airline-card"> <div class="missing-airline-card">
<div class="card-header"> <div class="card-header">
<span class="airline-code">{{ airline.code }}</span> <span class="airline-code">{{ airline.code }}</span>
<span class="flight-count">{{ airline.flights.length }} flight{{ airline.flights.length === 1 ? '' : 's' }}</span> <span class="flight-count">{{ visibleFlights.length }} flight{{ visibleFlights.length === 1 ? '' : 's' }}</span>
</div> </div>
<ul class="flight-list"> <div class="flight-list">
<li v-for="flight in airline.flights" :key="flight.link" class="flight-item"> <v-chip
<a :href="flight.link" class="flight-link" target="_blank" rel="noopener"> v-for="flight in visibleFlights"
:key="flight.link"
class="flight-chip"
size="small"
closable
:disabled="pendingIds.has(flight.id)"
@click:close="dismissFlight(flight.id)"
>
<a :href="flight.link" class="flight-link" target="_blank" rel="noopener" @click.stop>
{{ flight.flight_number }} {{ flight.flight_number }}
</a> </a>
</li> </v-chip>
</ul> </div>
</div> </div>
</template> </template>
@@ -62,30 +100,23 @@ defineProps<{
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5rem; gap: 0.5rem;
list-style: none;
padding: 0;
margin: 0;
} }
.flight-item { .flight-chip {
display: flex;
}
.flight-link {
font-family: 'Share Tech Mono', monospace; font-family: 'Share Tech Mono', monospace;
font-size: 0.72rem; font-size: 0.72rem;
letter-spacing: 0.06em; letter-spacing: 0.06em;
background: rgba(255, 255, 255, 0.04) !important;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.flight-link {
color: #9fb3c8; color: #9fb3c8;
text-decoration: none; text-decoration: none;
background: rgba(255, 255, 255, 0.04); transition: color 0.15s ease;
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 4px;
padding: 0.25rem 0.5rem;
transition: color 0.15s ease, border-color 0.15s ease;
} }
.flight-link:hover { .flight-link:hover {
color: #ffc107; color: #ffc107;
border-color: rgba(255, 193, 7, 0.4);
} }
</style> </style>
+1 -1
View File
@@ -19,7 +19,7 @@ defineProps<{
Reconcile Missing Liveries Reconcile Missing Liveries
<v-chip size="x-small" color="error" class="ml-1">{{ missingLiveryCount }}</v-chip> <v-chip size="x-small" color="error" class="ml-1">{{ missingLiveryCount }}</v-chip>
</Link> </Link>
<Link :href="route('admin.reconcile-missing-airlines')" class="sidebar-link"> <Link v-if="missingAirlineCount" :href="route('admin.reconcile-missing-airlines')" class="sidebar-link">
<v-icon icon="mdi-airplane-takeoff" size="18" /> <v-icon icon="mdi-airplane-takeoff" size="18" />
Reconcile Missing Airlines Reconcile Missing Airlines
<v-chip size="x-small" color="error" class="ml-1">{{ missingAirlineCount }}</v-chip> <v-chip size="x-small" color="error" class="ml-1">{{ missingAirlineCount }}</v-chip>
+1
View File
@@ -334,6 +334,7 @@ export interface MissingAirline{
flights: { flights: {
flight_number: string; flight_number: string;
link: string; link: string;
id: number;
}[] }[]
} }
+2
View File
@@ -42,6 +42,8 @@ use Inertia\Inertia;
Route::post('/ignore-missing-livery', [AdminController::class, 'ignoreMissingLivery'])->name('ignore-missing-livery'); Route::post('/ignore-missing-livery', [AdminController::class, 'ignoreMissingLivery'])->name('ignore-missing-livery');
Route::get('/missing-airlines', [AdminController::class, 'reconcileMissingAirlines'])->name('reconcile-missing-airlines')->middleware('permission:reconcile_missing_liveries'); Route::get('/missing-airlines', [AdminController::class, 'reconcileMissingAirlines'])->name('reconcile-missing-airlines')->middleware('permission:reconcile_missing_liveries');
Route::post('/ignore-missing-airline', [AdminController::class, 'ignoreMissingAirline'])->name('admin.ignore-missing-airline');
}); });