Added Notifications
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\DTOs;
|
||||||
|
|
||||||
|
readonly class MissingLivery
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $airline_name,
|
||||||
|
public readonly string $aircraft_display_name,
|
||||||
|
public readonly string $filename,
|
||||||
|
public readonly string $clipboard_text,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
|
||||||
|
use App\Models\IgnoredMissingLivery;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\UserFlight;
|
||||||
|
use App\Services\AdminService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class AdminController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct(private readonly AdminService $adminService){}
|
||||||
|
|
||||||
|
function staticAdminData(){
|
||||||
|
return [
|
||||||
|
'missingLiveryCount' => $this->adminService->getMissingLiveries()->count(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard(){
|
||||||
|
return inertia('Admin/Dashboard', [
|
||||||
|
'title' => 'Admin Control Panel',
|
||||||
|
'userCount' => User::count(),
|
||||||
|
'oneWeekUserGrowth' => User::where('created_at', '>=', Carbon::now()->subWeek())->count(),
|
||||||
|
'flightCount' => UserFlight::count(),
|
||||||
|
'oneWeekFlightGrowth' => UserFlight::where('created_at', '>=', Carbon::now()->subWeek())->count(),
|
||||||
|
'latestUser' => User::latest()->first()->name,
|
||||||
|
...$this->staticAdminData(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileMissingLiveries(){
|
||||||
|
$missingLiveries = $this->adminService->getMissingLiveries();
|
||||||
|
|
||||||
|
return Inertia::render('Admin/MissingLiveries', [
|
||||||
|
'title' => $missingLiveries->count() . ' Missing Liveries',
|
||||||
|
'missingLiveries' => $missingLiveries,
|
||||||
|
...$this->staticAdminData(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ignoreMissingLivery(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'filename' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
IgnoredMissingLivery::createOrFirst($validated);
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -126,6 +126,18 @@ class FlightImportController extends Controller
|
|||||||
return $airlines;
|
return $airlines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function showFr24Import()
|
||||||
|
{
|
||||||
|
if (Auth::user()->importedFlights()->exists()) {
|
||||||
|
return to_route('reconcile');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Inertia::render('Fr24Import');
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlightImportController.php
|
||||||
|
|
||||||
public function reconcile(Request $request)
|
public function reconcile(Request $request)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
@@ -133,35 +145,38 @@ class FlightImportController extends Controller
|
|||||||
$flightToReconcile = ImportedFlight::where('user_id', $user->id)->orderBy('date', 'asc')->first();
|
$flightToReconcile = ImportedFlight::where('user_id', $user->id)->orderBy('date', 'asc')->first();
|
||||||
|
|
||||||
if (!$flightToReconcile) {
|
if (!$flightToReconcile) {
|
||||||
return null;
|
return to_route('import.fr24');
|
||||||
}
|
}
|
||||||
|
|
||||||
$date = null;
|
$date = $flightToReconcile->date
|
||||||
if ($flightToReconcile->date) {
|
? Carbon::createFromFormat('Y-m-d', $flightToReconcile->date)->format('Y-m-d')
|
||||||
$date = Carbon::createFromFormat('Y-m-d', $flightToReconcile->date)->format('Y-m-d');
|
: null;
|
||||||
}
|
|
||||||
|
|
||||||
|
$flight = [
|
||||||
return [
|
'imported_flight_id' => $flightToReconcile->id,
|
||||||
'imported_flight_id' => $flightToReconcile->id,
|
'flight_classes' => $this->selectOptions(FlightClass::class),
|
||||||
'flight_classes' => $this->selectOptions(FlightClass::class),
|
'flight_reasons' => $this->selectOptions(FlightReason::class),
|
||||||
'flight_reasons' => $this->selectOptions(FlightReason::class),
|
'seat_types' => $this->selectOptions(SeatType::class),
|
||||||
'seat_types' => $this->selectOptions(SeatType::class),
|
'flight_number' => $flightToReconcile->flight_number ?? '',
|
||||||
'flight_number' => $flightToReconcile->flight_number ?? '',
|
'date' => $date ?? '',
|
||||||
'date' => $date ?? '',
|
'dep_time' => $this->formatTime($flightToReconcile->dep_time),
|
||||||
'dep_time' => $this->formatTime($flightToReconcile->dep_time),
|
'arr_time' => $this->formatTime($flightToReconcile->arr_time),
|
||||||
'arr_time' => $this->formatTime($flightToReconcile->arr_time),
|
'duration' => $this->formatTime($flightToReconcile->duration),
|
||||||
'duration' => $this->formatTime($flightToReconcile->duration),
|
'registration' => $flightToReconcile->registration ?? '',
|
||||||
'registration' => $flightToReconcile->registration ?? '',
|
'note' => $flightToReconcile->note ?? '',
|
||||||
'note' => $flightToReconcile->note ?? '',
|
'flight_class' => $flightToReconcile->flight_class !== null ? (int) $flightToReconcile->flight_class : null,
|
||||||
'flight_class' => $flightToReconcile->flight_class !== null ? (int) $flightToReconcile->flight_class : null,
|
'seat_type' => $flightToReconcile->seat_type !== null ? (int) $flightToReconcile->seat_type : null,
|
||||||
'seat_type' => $flightToReconcile->seat_type !== null ? (int) $flightToReconcile->seat_type : null,
|
'flight_reason' => $flightToReconcile->flight_reason !== null ? (int) $flightToReconcile->flight_reason : null,
|
||||||
'flight_reason' => $flightToReconcile->flight_reason !== null ? (int) $flightToReconcile->flight_reason : null,
|
'airline_options' => $this->getPossibleAirlines($flightToReconcile->airline ?? ''),
|
||||||
'airline_options' => $this->getPossibleAirlines($flightToReconcile->airline ?? ''),
|
'to_options' => $this->getPossibleAirports($flightToReconcile->to ?? ''),
|
||||||
'to_options' => $this->getPossibleAirports($flightToReconcile->to ?? ''),
|
'from_options' => $this->getPossibleAirports($flightToReconcile->from ?? ''),
|
||||||
'from_options' => $this->getPossibleAirports($flightToReconcile->from ?? ''),
|
'aircraft_options' => $this->getPossibleAircraft($flightToReconcile->aircraft ?? ''),
|
||||||
'aircraft_options' => $this->getPossibleAircraft($flightToReconcile->aircraft ?? ''),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return Inertia::render('ReconcileFlight', [
|
||||||
|
'flight' => $flight,
|
||||||
|
'key' => $flight['imported_flight_id'],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save(Request $request)
|
public function save(Request $request)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class FlightProfileController extends Controller
|
|||||||
public function profileData(User $user, string $view, ?int $selectedFlightId = null) : array {
|
public function profileData(User $user, string $view, ?int $selectedFlightId = null) : array {
|
||||||
return [
|
return [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'canEdit' => auth()->check() && auth()->id() === $user->id,
|
'canEdit' => (auth()->check() && auth()->id() === $user->id) || auth()->user()->hasRole('admin'),
|
||||||
'initialView' => $view,
|
'initialView' => $view,
|
||||||
'selectedFlightId' => $selectedFlightId,
|
'selectedFlightId' => $selectedFlightId,
|
||||||
'flight_api_url' => self::getUserFlightApiURL($user),
|
'flight_api_url' => self::getUserFlightApiURL($user),
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class HandleInertiaRequests extends Middleware
|
|||||||
'logo_api_url' => config('app.logo_api_url'),
|
'logo_api_url' => config('app.logo_api_url'),
|
||||||
'auth' => [
|
'auth' => [
|
||||||
'user' => $request->user(),
|
'user' => $request->user(),
|
||||||
|
'roles' => $request->user()?->getRoleNames() ?? [],
|
||||||
|
'permissions' => $request->user()?->getAllPermissions()->pluck('name') ?? [],
|
||||||
],
|
],
|
||||||
'achievement_notifications' => fn() => $request->user()
|
'achievement_notifications' => fn() => $request->user()
|
||||||
? $request->user()
|
? $request->user()
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class IgnoredMissingLivery extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'ignored_missing_liveries';
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $fillable = ['filename'];
|
||||||
|
|
||||||
|
}
|
||||||
+2
-4
@@ -10,8 +10,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use App\Traits\HasAchievements;
|
use App\Traits\HasAchievements;
|
||||||
use App\Models\Notification;
|
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
|
||||||
#[Fillable(['name', 'email', 'password', 'distance_unit'])]
|
#[Fillable(['name', 'email', 'password', 'distance_unit'])]
|
||||||
#[Hidden(['password', 'remember_token'])]
|
#[Hidden(['password', 'remember_token'])]
|
||||||
@@ -19,9 +19,7 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
|
|
||||||
/** @use HasFactory<UserFactory> */
|
/** @use HasFactory<UserFactory> */
|
||||||
use HasFactory, HasAchievements, HasApiTokens;
|
use HasFactory, HasAchievements, HasApiTokens, HasRoles;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the attributes that should be cast.
|
* Get the attributes that should be cast.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class UserFlightPolicy
|
|||||||
*/
|
*/
|
||||||
public function update(User $user, UserFlight $userFlight): bool
|
public function update(User $user, UserFlight $userFlight): bool
|
||||||
{
|
{
|
||||||
return $user->id === $userFlight->user_id;
|
return $user->id === $userFlight->user_id || $user->hasRole('admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Services;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use App\DTOs\MissingLivery;
|
||||||
|
use App\Models\IgnoredMissingLivery;
|
||||||
|
use App\Models\UserFlight;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class AdminToolsController extends Controller
|
class AdminService
|
||||||
{
|
{
|
||||||
function missingLiveries(){
|
/** @return Collection<int, MissingLivery> */
|
||||||
|
function getMissingLiveries(): Collection
|
||||||
|
{
|
||||||
|
|
||||||
|
/* $existingFiles = collect(glob(public_path('img/liveries/generated/*')))
|
||||||
|
->map(fn ($path) => pathinfo($path, PATHINFO_FILENAME))
|
||||||
|
->toArray();*/
|
||||||
|
|
||||||
/* $existingFiles = collect(glob(public_path('img/liveries/generated/*')))
|
|
||||||
->map(fn ($path) => pathinfo($path, PATHINFO_FILENAME))
|
|
||||||
->toArray();*/
|
|
||||||
|
|
||||||
$existingFiles = collect(glob(Storage::disk('local')->path('images/liveries').'/*.png'))
|
$existingFiles = collect(glob(Storage::disk('local')->path('images/liveries').'/*.png'))
|
||||||
->map(fn ($path) => pathinfo($path, PATHINFO_FILENAME))
|
->map(fn ($path) => pathinfo($path, PATHINFO_FILENAME))
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
|
$combos = UserFlight::with(['aircraft', 'airline'])
|
||||||
|
|
||||||
$combos = \App\Models\UserFlight::with(['aircraft', 'airline'])
|
|
||||||
->select('airline_id', 'aircraft_id')
|
->select('airline_id', 'aircraft_id')
|
||||||
->whereNotNull('airline_id')
|
->whereNotNull('airline_id')
|
||||||
->whereNotNull('aircraft_id')
|
->whereNotNull('aircraft_id')
|
||||||
@@ -31,14 +34,15 @@ class AdminToolsController extends Controller
|
|||||||
'airline_name' => $flight->airline->name,
|
'airline_name' => $flight->airline->name,
|
||||||
'aircraft_display_name' => $flight->aircraft->display_name,
|
'aircraft_display_name' => $flight->aircraft->display_name,
|
||||||
'filename' => $flight->airline->internal_name . '_' . $flight->aircraft->designator,
|
'filename' => $flight->airline->internal_name . '_' . $flight->aircraft->designator,
|
||||||
|
'clipboard_text' => $flight->airline->name . ' ' . $flight->aircraft->display_name_short,
|
||||||
])
|
])
|
||||||
->filter(fn ($combo) => !in_array($combo['filename'], $existingFiles))
|
->filter(fn ($combo) => !in_array($combo['filename'], $existingFiles));
|
||||||
|
|
||||||
|
$ignoredFiles = IgnoredMissingLivery::whereIn('filename', $combos->pluck('filename'))->pluck('filename')->toArray();
|
||||||
|
|
||||||
|
return $combos
|
||||||
|
->filter(fn ($combo) => !in_array($combo['filename'], $ignoredFiles))
|
||||||
->sortBy('airline_name')
|
->sortBy('airline_name')
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'count' => $combos->count(),
|
|
||||||
'liveries' => $combos,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-1
@@ -16,7 +16,11 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||||
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
||||||
]);
|
]);
|
||||||
|
$middleware->alias([
|
||||||
|
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
|
||||||
|
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
|
||||||
|
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
|
||||||
|
]);
|
||||||
//
|
//
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"laravel/framework": "^13.0",
|
"laravel/framework": "^13.0",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
|
"spatie/laravel-permission": "^8.0",
|
||||||
"tightenco/ziggy": "^2.0"
|
"tightenco/ziggy": "^2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|||||||
Generated
+149
-1
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "0e560320885031dd36bb08bb44fe05d4",
|
"content-hash": "2fab3a0703fff56cedb9d4c9b650e2fc",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -3433,6 +3433,154 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-12-14T04:43:48+00:00"
|
"time": "2025-12-14T04:43:48+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/laravel-package-tools",
|
||||||
|
"version": "1.93.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/laravel-package-tools.git",
|
||||||
|
"reference": "d5552849801f2642aea710557463234b59ef65eb"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d5552849801f2642aea710557463234b59ef65eb",
|
||||||
|
"reference": "d5552849801f2642aea710557463234b59ef65eb",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.5",
|
||||||
|
"orchestra/testbench": "^8.0|^9.2|^10.0|^11.0",
|
||||||
|
"pestphp/pest": "^2.1|^3.1|^4.0",
|
||||||
|
"phpunit/php-code-coverage": "^10.0|^11.0|^12.0",
|
||||||
|
"phpunit/phpunit": "^10.5|^11.5|^12.5",
|
||||||
|
"spatie/pest-plugin-test-time": "^2.2|^3.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\LaravelPackageTools\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Freek Van der Herten",
|
||||||
|
"email": "freek@spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Tools for creating Laravel packages",
|
||||||
|
"homepage": "https://github.com/spatie/laravel-package-tools",
|
||||||
|
"keywords": [
|
||||||
|
"laravel-package-tools",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/laravel-package-tools/issues",
|
||||||
|
"source": "https://github.com/spatie/laravel-package-tools/tree/1.93.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/spatie",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-19T14:06:37+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/laravel-permission",
|
||||||
|
"version": "8.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/laravel-permission.git",
|
||||||
|
"reference": "70a6ab04108616b438e0839598f473b513281644"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/70a6ab04108616b438e0839598f473b513281644",
|
||||||
|
"reference": "70a6ab04108616b438e0839598f473b513281644",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/auth": "^12.0|^13.0",
|
||||||
|
"illuminate/container": "^12.0|^13.0",
|
||||||
|
"illuminate/contracts": "^12.0|^13.0",
|
||||||
|
"illuminate/database": "^12.0|^13.0",
|
||||||
|
"php": "^8.3",
|
||||||
|
"spatie/laravel-package-tools": "^1.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"larastan/larastan": "^3.9",
|
||||||
|
"laravel/passport": "^13.0",
|
||||||
|
"laravel/pint": "^1.0",
|
||||||
|
"orchestra/testbench": "^10.0|^11.0",
|
||||||
|
"pestphp/pest": "^3.0|^4.0",
|
||||||
|
"pestphp/pest-plugin-laravel": "^3.0|^4.1",
|
||||||
|
"phpstan/phpstan": "^2.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Spatie\\Permission\\PermissionServiceProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "8.x-dev",
|
||||||
|
"dev-master": "8.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/helpers.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\Permission\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Freek Van der Herten",
|
||||||
|
"email": "freek@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Permission handling for Laravel 12 and up",
|
||||||
|
"homepage": "https://github.com/spatie/laravel-permission",
|
||||||
|
"keywords": [
|
||||||
|
"acl",
|
||||||
|
"laravel",
|
||||||
|
"permission",
|
||||||
|
"permissions",
|
||||||
|
"rbac",
|
||||||
|
"roles",
|
||||||
|
"security",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/laravel-permission/issues",
|
||||||
|
"source": "https://github.com/spatie/laravel-permission/tree/8.0.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/spatie",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-30T19:30:22+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/clock",
|
"name": "symfony/clock",
|
||||||
"version": "v8.0.0",
|
"version": "v8.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Spatie\Permission\DefaultTeamResolver;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'models' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your permissions. Of course, it
|
||||||
|
* is often just the "Permission" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Permission model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Permission` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permission' => Permission::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your roles. Of course, it
|
||||||
|
* is often just the "Role" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Role model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Role` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role' => Role::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "Teams" feature from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your teams. Of course, it
|
||||||
|
* is often just the "Team" model but you may use whatever you like.
|
||||||
|
*/
|
||||||
|
'team' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasModels" trait and passing raw IDs to syncModels,
|
||||||
|
* attachModels, or detachModels, this model class will be used to
|
||||||
|
* resolve those IDs. If null, defaults to the guard's model.
|
||||||
|
*/
|
||||||
|
'default_model' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
'table_names' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'roles' => 'roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your permissions. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permissions' => 'permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_permissions' => 'model_has_permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models roles. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_roles' => 'model_has_roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role_has_permissions' => 'role_has_permissions',
|
||||||
|
],
|
||||||
|
|
||||||
|
'column_names' => [
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related pivots other than defaults
|
||||||
|
*/
|
||||||
|
'role_pivot_key' => null, // default 'role_id',
|
||||||
|
'permission_pivot_key' => null, // default 'permission_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related model primary key other than
|
||||||
|
* `model_id`.
|
||||||
|
*
|
||||||
|
* For example, this would be nice if your primary keys are all UUIDs. In
|
||||||
|
* that case, name this `model_uuid`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_morph_key' => 'model_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to use the teams feature and your related model's
|
||||||
|
* foreign key is other than `team_id`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'team_foreign_key' => 'team_id',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the method for checking permissions will be registered on the gate.
|
||||||
|
* Set this to false if you want to implement custom logic for checking permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'register_permission_check_method' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
|
||||||
|
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
|
||||||
|
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
|
||||||
|
*/
|
||||||
|
'register_octane_reset_listener' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Events will fire when a role or permission is assigned/unassigned:
|
||||||
|
* \Spatie\Permission\Events\RoleAttachedEvent
|
||||||
|
* \Spatie\Permission\Events\RoleDetachedEvent
|
||||||
|
* \Spatie\Permission\Events\PermissionAttachedEvent
|
||||||
|
* \Spatie\Permission\Events\PermissionDetachedEvent
|
||||||
|
*
|
||||||
|
* To enable, set to true, and then create listeners to watch these events.
|
||||||
|
*/
|
||||||
|
'events_enabled' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Teams Feature.
|
||||||
|
* When set to true the package implements teams using the 'team_foreign_key'.
|
||||||
|
* If you want the migrations to register the 'team_foreign_key', you must
|
||||||
|
* set this to true before doing the migration.
|
||||||
|
* If you already did the migration then you must make a new migration to also
|
||||||
|
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
|
||||||
|
* (view the latest version of this package's migration file)
|
||||||
|
*/
|
||||||
|
|
||||||
|
'teams' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class to use to resolve the permissions team id
|
||||||
|
*/
|
||||||
|
'team_resolver' => DefaultTeamResolver::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Passport Client Credentials Grant
|
||||||
|
* When set to true the package will use Passports Client to check permissions
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use_passport_client_credentials' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required permission names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_permission_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required role names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_role_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default wildcard permission lookups are disabled.
|
||||||
|
* See documentation to understand supported syntax.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'enable_wildcard_permission' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class to use for interpreting wildcard permissions.
|
||||||
|
* If you need to modify delimiters, override the class and specify its name here.
|
||||||
|
*/
|
||||||
|
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
|
||||||
|
|
||||||
|
/* Cache-specific settings */
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default all permissions are cached for 24 hours to speed up performance.
|
||||||
|
* When permissions or roles are updated the cache is flushed automatically.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration_time' => DateInterval::createFromDateString('24 hours'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The cache key used to store all permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'key' => 'spatie.permission.cache',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* You may optionally indicate a specific cache driver to use for permission and
|
||||||
|
* role caching using any of the `store` drivers listed in the cache.php config
|
||||||
|
* file. Using 'default' here means to use the `default` set in cache.php.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => 'default',
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$teams = config('permission.teams');
|
||||||
|
$tableNames = config('permission.table_names');
|
||||||
|
$columnNames = config('permission.column_names');
|
||||||
|
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
|
||||||
|
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
|
||||||
|
|
||||||
|
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
||||||
|
*/
|
||||||
|
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||||
|
$table->id(); // permission id
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('guard_name');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['name', 'guard_name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
||||||
|
*/
|
||||||
|
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
||||||
|
$table->id(); // role id
|
||||||
|
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
||||||
|
}
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('guard_name');
|
||||||
|
$table->timestamps();
|
||||||
|
if ($teams || config('permission.testing')) {
|
||||||
|
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
||||||
|
} else {
|
||||||
|
$table->unique(['name', 'guard_name']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||||
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
|
|
||||||
|
$table->string('model_type');
|
||||||
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||||
|
|
||||||
|
$table->foreign($pivotPermission)
|
||||||
|
->references('id') // permission id
|
||||||
|
->on($tableNames['permissions'])
|
||||||
|
->cascadeOnDelete();
|
||||||
|
if ($teams) {
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
||||||
|
|
||||||
|
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_permissions_permission_model_type_primary');
|
||||||
|
} else {
|
||||||
|
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_permissions_permission_model_type_primary');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
|
|
||||||
|
$table->string('model_type');
|
||||||
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||||
|
|
||||||
|
$table->foreign($pivotRole)
|
||||||
|
->references('id') // role id
|
||||||
|
->on($tableNames['roles'])
|
||||||
|
->cascadeOnDelete();
|
||||||
|
if ($teams) {
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
||||||
|
|
||||||
|
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_roles_role_model_type_primary');
|
||||||
|
} else {
|
||||||
|
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_roles_role_model_type_primary');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||||
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
|
|
||||||
|
$table->foreign($pivotPermission)
|
||||||
|
->references('id') // permission id
|
||||||
|
->on($tableNames['permissions'])
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->foreign($pivotRole)
|
||||||
|
->references('id') // role id
|
||||||
|
->on($tableNames['roles'])
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||||
|
});
|
||||||
|
|
||||||
|
app('cache')
|
||||||
|
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||||
|
->forget(config('permission.cache.key'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$tableNames = config('permission.table_names');
|
||||||
|
|
||||||
|
throw_if(empty($tableNames), 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
|
||||||
|
|
||||||
|
Schema::dropIfExists($tableNames['role_has_permissions']);
|
||||||
|
Schema::dropIfExists($tableNames['model_has_roles']);
|
||||||
|
Schema::dropIfExists($tableNames['model_has_permissions']);
|
||||||
|
Schema::dropIfExists($tableNames['roles']);
|
||||||
|
Schema::dropIfExists($tableNames['permissions']);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$role = Role::create(['name' => 'admin']);
|
||||||
|
$permission = Permission::create(['name' => 'reconcile_missing_liveries']);
|
||||||
|
|
||||||
|
$role->givePermissionTo($permission);
|
||||||
|
|
||||||
|
$user = User::whereName('Josh')->first();
|
||||||
|
|
||||||
|
$user->assignRole($role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('ignored_missing_liveries', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('filename')->unique()->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('ignored_missing_liveries');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
label: string;
|
||||||
|
count: number;
|
||||||
|
weeklyGrowth: number;
|
||||||
|
icon: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="pa-5 d-flex align-center justify-space-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ label }}</div>
|
||||||
|
<div class="text-h5 font-weight-medium">{{ count }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption "><slot/></div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="d-flex align-center justify-end ga-1">
|
||||||
|
<v-icon :icon="icon" color="success" size="20" />
|
||||||
|
<span class="text-h6 font-weight-medium text-success">+{{ weeklyGrowth }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-disabled">new this week </div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { MissingLivery } from "@/Types/types";
|
||||||
|
import {router} from "@inertiajs/vue3";
|
||||||
|
import CopyButton from "@/Components/FlightsGoneBy/CopyButton.vue";
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
livery: MissingLivery;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const ignore = () => {
|
||||||
|
router.post(route('admin.ignore-missing-livery'), { filename: props.livery.filename });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card flat rounded="lg" class="mb-2 px-4 py-3">
|
||||||
|
<v-row align="center" no-gutters>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<div class="d-flex align-center ga-1 text-medium-emphasis" style="font-size: 11px;">
|
||||||
|
Airline
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px;" class="font-weight-medium">{{ livery.airline_name }}</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<div class="d-flex align-center ga-1 text-medium-emphasis" style="font-size: 11px;">
|
||||||
|
Aircraft
|
||||||
|
<CopyButton :text="livery.clipboard_text" />
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px;" class="font-weight-medium">{{ livery.aircraft_display_name }}</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<div class="d-flex align-center ga-1 text-medium-emphasis" style="font-size: 11px;">
|
||||||
|
Filename
|
||||||
|
<CopyButton :text="livery.filename" />
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px;" class="font-weight-medium">{{ livery.filename }}</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row no-gutters class="mt-2" justify="end">
|
||||||
|
<v-btn
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
color="error"
|
||||||
|
prepend-icon="mdi-eye-off"
|
||||||
|
@click="ignore"
|
||||||
|
>
|
||||||
|
Ignore
|
||||||
|
</v-btn>
|
||||||
|
</v-row>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@@ -4,6 +4,7 @@ import type { CodeType } from '@/Composables/useAlphabetAirlines'
|
|||||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||||
import InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
|
import InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
|
||||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||||
|
import {computed} from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
letters: string[]
|
letters: string[]
|
||||||
@@ -50,8 +51,8 @@ function isHighlighted({ firstYear }: AirlineEntry): boolean {
|
|||||||
return props.selectedYear !== null && firstYear === props.selectedYear
|
return props.selectedYear !== null && firstYear === props.selectedYear
|
||||||
}
|
}
|
||||||
|
|
||||||
function toBBCode(): string {
|
const bbCode = computed(() =>
|
||||||
return props.letters
|
props.letters
|
||||||
.map(letter => {
|
.map(letter => {
|
||||||
const entries = airlineEntriesForLetter(letter)
|
const entries = airlineEntriesForLetter(letter)
|
||||||
if (!entries.length) return letter
|
if (!entries.length) return letter
|
||||||
@@ -65,9 +66,9 @@ function toBBCode(): string {
|
|||||||
.join(', ')
|
.join(', ')
|
||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
}
|
)
|
||||||
|
|
||||||
defineExpose({ toBBCode })
|
defineExpose({ bbCode })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {Airport, Flight} from '@/Types/types'
|
|||||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||||
import InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
|
import InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
|
||||||
import AirportToolTip from '@/Components/FlightsGoneBy/AirportToolTip.vue'
|
import AirportToolTip from '@/Components/FlightsGoneBy/AirportToolTip.vue'
|
||||||
|
import {computed} from "vue";
|
||||||
|
|
||||||
type CodeType = 'iata' | 'icao'
|
type CodeType = 'iata' | 'icao'
|
||||||
|
|
||||||
@@ -50,8 +51,8 @@ function isHighlighted({ firstYear }: AirportEntry): boolean {
|
|||||||
return props.selectedYear !== null && firstYear === props.selectedYear
|
return props.selectedYear !== null && firstYear === props.selectedYear
|
||||||
}
|
}
|
||||||
|
|
||||||
function toBBCode(): string {
|
const bbCode = computed(() =>
|
||||||
return props.letters
|
props.letters
|
||||||
.map(letter => {
|
.map(letter => {
|
||||||
const entries = airportEntriesForLetter(letter)
|
const entries = airportEntriesForLetter(letter)
|
||||||
if (!entries.length) return letter
|
if (!entries.length) return letter
|
||||||
@@ -64,9 +65,9 @@ function toBBCode(): string {
|
|||||||
.join(', ')
|
.join(', ')
|
||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
}
|
)
|
||||||
|
|
||||||
defineExpose({ toBBCode })
|
defineExpose({ bbCode })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { copyToClipboard } from "@/Composables/useClipboard";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
text: string;
|
||||||
|
title?: string;
|
||||||
|
size?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const copied = ref(false);
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
await copyToClipboard(props.text);
|
||||||
|
copied.value = true;
|
||||||
|
setTimeout(() => copied.value = false, 1500);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-btn
|
||||||
|
:title="title"
|
||||||
|
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
:size="size ?? 'x-small'"
|
||||||
|
:variant="copied ? 'tonal' : 'plain'"
|
||||||
|
:color="copied ? 'success' : undefined"
|
||||||
|
density="compact"
|
||||||
|
@click="handleCopy"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -38,7 +38,8 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<Link :href="route('flights.add')" class="nav-link">Add Flight</Link>
|
<Link :href="route('flights.add')" class="nav-link">Add Flight</Link>
|
||||||
<Link :href="route('profile.view', { user: props.auth.user.name })" class="nav-link">Profile</Link>
|
<Link :href="route('profile.view', { user: props.auth.user.name })" class="nav-link">Profile</Link>
|
||||||
<Link href="/feed" class="nav-link">Feed</Link>
|
<Link :href="route('feed')" class="nav-link">Feed</Link>
|
||||||
|
<Link v-if="props.auth.roles.includes('admin')" :href="route('admin.dashboard')" class="nav-link">Admin</Link>
|
||||||
|
|
||||||
<div class="dropdown" ref="dropdownRef">
|
<div class="dropdown" ref="dropdownRef">
|
||||||
<button class="nav-link dropdown-trigger" @click.stop="dropdownOpen = !dropdownOpen">
|
<button class="nav-link dropdown-trigger" @click.stop="dropdownOpen = !dropdownOpen">
|
||||||
@@ -73,7 +74,7 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
|||||||
<span class="nav-greeting">Welcome, {{ props.auth.user.name }}</span>
|
<span class="nav-greeting">Welcome, {{ props.auth.user.name }}</span>
|
||||||
<Link :href="route('flights.add')" class="nav-link" @click="menuOpen = false">Add Flight</Link>
|
<Link :href="route('flights.add')" class="nav-link" @click="menuOpen = false">Add Flight</Link>
|
||||||
<Link :href="route('profile.view', { user: props.auth.user.name })" class="nav-link" @click="menuOpen = false">Profile</Link>
|
<Link :href="route('profile.view', { user: props.auth.user.name })" class="nav-link" @click="menuOpen = false">Profile</Link>
|
||||||
<Link href="/feed" class="nav-link nav-link--highlight" @click="menuOpen = false">Feed</Link>
|
<Link :href="route('feed')" class="nav-link nav-link" @click="menuOpen = false">Feed</Link>
|
||||||
<Link :href="route('import.fr24')" class="nav-link" @click="menuOpen = false">Import from FR24</Link>
|
<Link :href="route('import.fr24')" class="nav-link" @click="menuOpen = false">Import from FR24</Link>
|
||||||
<div class="dropdown-divider" />
|
<div class="dropdown-divider" />
|
||||||
<button class="nav-link nav-link--danger" @click="logout">Log Out</button>
|
<button class="nav-link nav-link--danger" @click="logout">Log Out</button>
|
||||||
@@ -115,9 +116,10 @@ header {
|
|||||||
|
|
||||||
/* Shared nav link base */
|
/* Shared nav link base */
|
||||||
.nav-link {
|
.nav-link {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3em;
|
gap: 0.3em;
|
||||||
|
height: 100%;
|
||||||
padding: 0.3em 0.75em;
|
padding: 0.3em 0.75em;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export const copyToClipboard = (text: string): Promise<void> => {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
return navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const el = document.createElement('textarea');
|
||||||
|
el.value = text;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(el);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
|
|
||||||
import Dropdown from '@/Components/Dropdown.vue';
|
|
||||||
import DropdownLink from '@/Components/DropdownLink.vue';
|
|
||||||
import NavLink from '@/Components/NavLink.vue';
|
|
||||||
import ResponsiveNavLink from '@/Components/ResponsiveNavLink.vue';
|
|
||||||
import { Link } from '@inertiajs/vue3';
|
|
||||||
|
|
||||||
const showingNavigationDropdown = ref(false);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="min-h-screen bg-gray-100">
|
|
||||||
<nav
|
|
||||||
class="border-b border-gray-100 bg-white"
|
|
||||||
>
|
|
||||||
<!-- Primary Navigation Menu -->
|
|
||||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="flex h-16 justify-between">
|
|
||||||
<div class="flex">
|
|
||||||
<!-- Logo -->
|
|
||||||
<div class="flex shrink-0 items-center">
|
|
||||||
<Link :href="route('dashboard')">
|
|
||||||
<ApplicationLogo
|
|
||||||
class="block h-9 w-auto fill-current text-gray-800"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation Links -->
|
|
||||||
<div
|
|
||||||
class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"
|
|
||||||
>
|
|
||||||
<NavLink
|
|
||||||
:href="route('dashboard')"
|
|
||||||
:active="route().current('dashboard')"
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hidden sm:ms-6 sm:flex sm:items-center">
|
|
||||||
<!-- Settings Dropdown -->
|
|
||||||
<div class="relative ms-3">
|
|
||||||
<Dropdown align="right" width="48">
|
|
||||||
<template #trigger>
|
|
||||||
<span class="inline-flex rounded-md">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center rounded-md border border-transparent bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-500 transition duration-150 ease-in-out hover:text-gray-700 focus:outline-none"
|
|
||||||
>
|
|
||||||
{{ $page.props.auth.user.name }}
|
|
||||||
|
|
||||||
<svg
|
|
||||||
class="-me-0.5 ms-2 h-4 w-4"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #content>
|
|
||||||
<DropdownLink
|
|
||||||
:href="route('profile.edit')"
|
|
||||||
>
|
|
||||||
Profile
|
|
||||||
</DropdownLink>
|
|
||||||
<DropdownLink
|
|
||||||
:href="route('logout')"
|
|
||||||
method="post"
|
|
||||||
as="button"
|
|
||||||
>
|
|
||||||
Log Out
|
|
||||||
</DropdownLink>
|
|
||||||
</template>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hamburger -->
|
|
||||||
<div class="-me-2 flex items-center sm:hidden">
|
|
||||||
<button
|
|
||||||
@click="
|
|
||||||
showingNavigationDropdown =
|
|
||||||
!showingNavigationDropdown
|
|
||||||
"
|
|
||||||
class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 transition duration-150 ease-in-out hover:bg-gray-100 hover:text-gray-500 focus:bg-gray-100 focus:text-gray-500 focus:outline-none"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="h-6 w-6"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
:class="{
|
|
||||||
hidden: showingNavigationDropdown,
|
|
||||||
'inline-flex':
|
|
||||||
!showingNavigationDropdown,
|
|
||||||
}"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M4 6h16M4 12h16M4 18h16"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
:class="{
|
|
||||||
hidden: !showingNavigationDropdown,
|
|
||||||
'inline-flex':
|
|
||||||
showingNavigationDropdown,
|
|
||||||
}"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Responsive Navigation Menu -->
|
|
||||||
<div
|
|
||||||
:class="{
|
|
||||||
block: showingNavigationDropdown,
|
|
||||||
hidden: !showingNavigationDropdown,
|
|
||||||
}"
|
|
||||||
class="sm:hidden"
|
|
||||||
>
|
|
||||||
<div class="space-y-1 pb-3 pt-2">
|
|
||||||
<ResponsiveNavLink
|
|
||||||
:href="route('dashboard')"
|
|
||||||
:active="route().current('dashboard')"
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</ResponsiveNavLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Responsive Settings Options -->
|
|
||||||
<div
|
|
||||||
class="border-t border-gray-200 pb-1 pt-4"
|
|
||||||
>
|
|
||||||
<div class="px-4">
|
|
||||||
<div
|
|
||||||
class="text-base font-medium text-gray-800"
|
|
||||||
>
|
|
||||||
{{ $page.props.auth.user.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm font-medium text-gray-500">
|
|
||||||
{{ $page.props.auth.user.email }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3 space-y-1">
|
|
||||||
<ResponsiveNavLink :href="route('profile.edit')">
|
|
||||||
Profile
|
|
||||||
</ResponsiveNavLink>
|
|
||||||
<ResponsiveNavLink
|
|
||||||
:href="route('logout')"
|
|
||||||
method="post"
|
|
||||||
as="button"
|
|
||||||
>
|
|
||||||
Log Out
|
|
||||||
</ResponsiveNavLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Page Heading -->
|
|
||||||
<header
|
|
||||||
class="bg-white shadow"
|
|
||||||
v-if="$slots.header"
|
|
||||||
>
|
|
||||||
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
||||||
<slot name="header" />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Page Content -->
|
|
||||||
<main>
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
|
|
||||||
import { Link } from '@inertiajs/vue3';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex min-h-screen flex-col items-center bg-gray-100 pt-6 sm:justify-center sm:pt-0"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Link href="/">
|
|
||||||
<ApplicationLogo class="h-20 w-20 fill-current text-gray-500" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="mt-6 w-full overflow-hidden bg-white px-6 py-4 shadow-md sm:max-w-md sm:rounded-lg"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -16,15 +16,21 @@ const achievementSound = new Audio('/sounds/seatBelt.wav')
|
|||||||
|
|
||||||
function handleNewNotifications(notifications: Notification[]) {
|
function handleNewNotifications(notifications: Notification[]) {
|
||||||
if (!notifications?.length) return
|
if (!notifications?.length) return
|
||||||
const unseen = notifications.filter(n => !seenNotificationIds.value.has(n.id))
|
|
||||||
if (!unseen.length) return
|
const newToasts: Notification[] = []
|
||||||
unseen.forEach(n => seenNotificationIds.value.add(n.id))
|
for (const n of notifications) {
|
||||||
activeToasts.value.push(...unseen)
|
if (!seenNotificationIds.value.has(n.id)) {
|
||||||
|
seenNotificationIds.value.add(n.id)
|
||||||
|
newToasts.push(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newToasts.length) return
|
||||||
|
activeToasts.value.push(...newToasts)
|
||||||
achievementSound.play().catch(() => {})
|
achievementSound.play().catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ── Toasts ────────────────────────────────────────────────────────────────────
|
// ── Toasts ────────────────────────────────────────────────────────────────────
|
||||||
const activeToasts = ref<Notification[]>([])
|
const activeToasts = ref<Notification[]>([])
|
||||||
|
|
||||||
@@ -47,13 +53,13 @@ router.on('success', (event) => {
|
|||||||
<template>
|
<template>
|
||||||
<Radar>
|
<Radar>
|
||||||
<div class="layoutContainer">
|
<div class="layoutContainer">
|
||||||
<MainHeader :key="transitionKey" />
|
<MainHeader :key="`header-${transitionKey}`" />
|
||||||
<Transition name="fade" mode="out-in">
|
<Transition name="fade" mode="out-in">
|
||||||
<main id="pageContainer" :key="transitionKey">
|
<main id="pageContainer" :key="`main-${transitionKey}`">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</Transition>
|
</Transition>
|
||||||
<MainFooter :key="transitionKey" />
|
<MainFooter :key="`footer-${transitionKey}`" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toast-stack">
|
<div class="toast-stack">
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||||
|
import AdminSidebar from "@/Pages/Admin/AdminSidebar.vue";
|
||||||
|
import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue";
|
||||||
|
import {Head} from "@inertiajs/vue3";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string;
|
||||||
|
missingLiveryCount: number;
|
||||||
|
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MainLayout>
|
||||||
|
<Head :title="title" />
|
||||||
|
<div class="admin-container">
|
||||||
|
<AdminSidebar :missingLiveryCount="missingLiveryCount" />
|
||||||
|
<div class="admin-content">
|
||||||
|
<GlassBox class="admin-page" :title="title">
|
||||||
|
<slot />
|
||||||
|
</GlassBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 90dvh;
|
||||||
|
/* Override MainLayout's centred main — fill the full height */
|
||||||
|
align-self: stretch;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-page {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
min-height: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Link} from "@inertiajs/vue3";
|
||||||
|
import {number} from "echarts";
|
||||||
|
defineProps<{
|
||||||
|
missingLiveryCount: number;
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<aside class="glass admin-sidebar">
|
||||||
|
<div class="sidebar-title">Admin</div>
|
||||||
|
<Link :href="route('admin.dashboard')" class="sidebar-link">
|
||||||
|
<v-icon icon="mdi-chart-line" size="18" />
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
<Link :href="route('admin.reconcile-missing-liveries')" class="sidebar-link">
|
||||||
|
<v-icon icon="mdi-airplane-takeoff" size="18" />
|
||||||
|
Reconcile Missing Liveries
|
||||||
|
<v-chip size="x-small" color="error" class="ml-1">{{ missingLiveryCount }}</v-chip>
|
||||||
|
</Link>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-sidebar {
|
||||||
|
width: 400px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.25rem;
|
||||||
|
/* no height here */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--text);
|
||||||
|
opacity: 0.5;
|
||||||
|
padding: 0 0.5rem 0.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
background: rgba(56, 189, 248, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link.active {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import AdminLayout from "@/Pages/Admin/AdminLayout.vue";
|
||||||
|
import GrowthCard from "@/Components/Admin/GrowthCard.vue";
|
||||||
|
|
||||||
|
defineOptions({ layout: AdminLayout });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
userCount: number,
|
||||||
|
oneWeekUserGrowth: number,
|
||||||
|
flightCount: number,
|
||||||
|
oneWeekFlightGrowth: number,
|
||||||
|
latestUser:string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<GrowthCard label="Total Users" :count="userCount" :weekly-growth="oneWeekUserGrowth" icon="mdi-account">
|
||||||
|
Latest User: {{ latestUser }}
|
||||||
|
</GrowthCard>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<GrowthCard label="Total Flights" :count="flightCount" :weekly-growth="oneWeekFlightGrowth" icon="mdi-airplane" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue";
|
||||||
|
import { Head } from "@inertiajs/vue3";
|
||||||
|
import {MissingLivery} from "@/Types/types";
|
||||||
|
import MissingLiveryCard from "@/Components/Admin/MissingLiveryCard.vue";
|
||||||
|
import AdminLayout from "@/Pages/Admin/AdminLayout.vue";
|
||||||
|
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
|
||||||
|
|
||||||
|
defineOptions({ layout: AdminLayout });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
missingLiveries: MissingLivery[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MissingLiveryCard v-for="livery in missingLiveries" :key="livery.filename" :livery="livery" />
|
||||||
|
</template>
|
||||||
@@ -10,7 +10,7 @@ import { router } from "@inertiajs/vue3";
|
|||||||
defineOptions({ layout: MainLayout });
|
defineOptions({ layout: MainLayout });
|
||||||
|
|
||||||
const page = usePage<SharedProps>();
|
const page = usePage<SharedProps>();
|
||||||
const name = computed(() => page?.props?.auth?.user?.name || 'there');
|
const name = computed(() => page?.props?.auth?.user?.name || 'mate');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
|
|||||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
|
|||||||
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
|
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
|
||||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
defineProps<{
|
defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
defineProps<{
|
defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
defineProps<{
|
defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
defineProps<{
|
defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
|
|||||||
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
|
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
|
||||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import Panel from '@/Components/FlightsGoneBy/Panels/Panel.vue'
|
|||||||
import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
|
import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
|
||||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue
|
|||||||
import AirlineAlphabetTable from '@/Components/FlightsGoneBy/AirlineAlphabetTable.vue'
|
import AirlineAlphabetTable from '@/Components/FlightsGoneBy/AirlineAlphabetTable.vue'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useAlphabetAirlines, type CodeType } from '@/Composables/useAlphabetAirlines'
|
import { useAlphabetAirlines, type CodeType } from '@/Composables/useAlphabetAirlines'
|
||||||
|
import {copyToClipboard} from "@/Composables/useClipboard";
|
||||||
|
import CopyButton from "@/Components/FlightsGoneBy/CopyButton.vue";
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
@@ -47,28 +49,7 @@ const selectedYear = ref<number | null>(
|
|||||||
|
|
||||||
// Copy BBCode
|
// Copy BBCode
|
||||||
const airlineTable = ref<InstanceType<typeof AirlineAlphabetTable> | null>(null)
|
const airlineTable = ref<InstanceType<typeof AirlineAlphabetTable> | null>(null)
|
||||||
const copied = ref(false)
|
|
||||||
|
|
||||||
async function copyBBCode() {
|
|
||||||
const text = airlineTable.value?.toBBCode()
|
|
||||||
if (text == null) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
} catch {
|
|
||||||
const el = document.createElement('textarea')
|
|
||||||
el.value = text
|
|
||||||
el.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0'
|
|
||||||
document.body.appendChild(el)
|
|
||||||
el.focus()
|
|
||||||
el.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
document.body.removeChild(el)
|
|
||||||
}
|
|
||||||
|
|
||||||
copied.value = true
|
|
||||||
setTimeout(() => (copied.value = false), 2000)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -133,13 +114,10 @@ async function copyBBCode() {
|
|||||||
class="year-select"
|
class="year-select"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<v-btn
|
<CopyButton
|
||||||
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
|
|
||||||
:color="copied ? 'success' : undefined"
|
|
||||||
density="compact"
|
|
||||||
variant="text"
|
|
||||||
title="Copy as BBCode"
|
title="Copy as BBCode"
|
||||||
@click="copyBBCode"
|
size="large"
|
||||||
|
:text="airlineTable?.bbCode ?? ''"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import AlphabetTable from '@/Components/FlightsGoneBy/AlphabetTable.vue'
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useAlphabetFlights, type CodeType } from '@/Composables/useAlphabetFlights'
|
import { useAlphabetFlights, type CodeType } from '@/Composables/useAlphabetFlights'
|
||||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||||
|
import CopyButton from "@/Components/FlightsGoneBy/CopyButton.vue";
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
@@ -45,27 +46,6 @@ const selectedYear = ref<number | null>(
|
|||||||
const alphabetTable = ref<InstanceType<typeof AlphabetTable> | null>(null)
|
const alphabetTable = ref<InstanceType<typeof AlphabetTable> | null>(null)
|
||||||
const copied = ref(false)
|
const copied = ref(false)
|
||||||
|
|
||||||
async function copyBBCode() {
|
|
||||||
const text = alphabetTable.value?.toBBCode()
|
|
||||||
if (text == null) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
} catch {
|
|
||||||
// Fallback for non-HTTPS or browsers that block clipboard API
|
|
||||||
const el = document.createElement('textarea')
|
|
||||||
el.value = text
|
|
||||||
el.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0'
|
|
||||||
document.body.appendChild(el)
|
|
||||||
el.focus()
|
|
||||||
el.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
document.body.removeChild(el)
|
|
||||||
}
|
|
||||||
|
|
||||||
copied.value = true
|
|
||||||
setTimeout(() => (copied.value = false), 2000)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -124,13 +104,10 @@ async function copyBBCode() {
|
|||||||
class="year-select"
|
class="year-select"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<v-btn
|
<CopyButton
|
||||||
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
|
|
||||||
:color="copied ? 'success' : undefined"
|
|
||||||
density="compact"
|
|
||||||
variant="text"
|
|
||||||
title="Copy as BBCode"
|
title="Copy as BBCode"
|
||||||
@click="copyBBCode"
|
size="large"
|
||||||
|
:text="alphabetTable?.bbCode ?? ''"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {useRegionFlights} from "@/Composables/useRegionFlights";
|
|||||||
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||||
|
|
||||||
|
|
||||||
defineOptions({ layout: MainLayout })
|
defineOptions({ inheritAttrs: false })
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
|||||||
import Brazil from "@/Components/Maps/Brazil.vue";
|
import Brazil from "@/Components/Maps/Brazil.vue";
|
||||||
|
|
||||||
|
|
||||||
defineOptions({ layout: MainLayout })
|
defineOptions({ inheritAttrs: false })
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
|||||||
import Canada from "@/Components/Maps/Canada.vue";
|
import Canada from "@/Components/Maps/Canada.vue";
|
||||||
|
|
||||||
|
|
||||||
defineOptions({ layout: MainLayout })
|
defineOptions({ inheritAttrs: false })
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import China from "@/Components/Maps/China.vue";
|
|||||||
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||||
|
|
||||||
|
|
||||||
defineOptions({ layout: MainLayout })
|
defineOptions({ inheritAttrs: false })
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ import FlightRegionTable from "@/Components/FlightsGoneBy/FlightRegionTable.vue"
|
|||||||
import {useRegionFlights} from "@/Composables/useRegionFlights";
|
import {useRegionFlights} from "@/Composables/useRegionFlights";
|
||||||
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||||
import USA from "@/Components/Maps/USA.vue";
|
import USA from "@/Components/Maps/USA.vue";
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
|
||||||
defineOptions({ layout: MainLayout })
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
achievement: Achievement
|
achievement: Achievement
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
Vendored
+9
@@ -62,6 +62,8 @@ export type SharedProps = import('@inertiajs/core').PageProps & {
|
|||||||
auth: {
|
auth: {
|
||||||
user: User | null
|
user: User | null
|
||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
|
roles: string[];
|
||||||
|
permissions: string[];
|
||||||
},
|
},
|
||||||
logo_api_url: string
|
logo_api_url: string
|
||||||
achievement_notifications: Notification[]
|
achievement_notifications: Notification[]
|
||||||
@@ -259,6 +261,13 @@ export interface Flight {
|
|||||||
livery_url?: string
|
livery_url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MissingLivery {
|
||||||
|
airline_name: string;
|
||||||
|
aircraft_display_name: string;
|
||||||
|
filename: string;
|
||||||
|
clipboard_text: string;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@inertiajs/vue3' {
|
declare module '@inertiajs/vue3' {
|
||||||
interface PageProps extends SharedProps {}
|
interface PageProps extends SharedProps {}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-23
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\AchievementController;
|
use App\Http\Controllers\AchievementController;
|
||||||
|
use App\Http\Controllers\AdminController;
|
||||||
use App\Http\Controllers\AdminToolsController;
|
use App\Http\Controllers\AdminToolsController;
|
||||||
use App\Http\Controllers\Api\AircraftApiController;
|
use App\Http\Controllers\Api\AircraftApiController;
|
||||||
use App\Http\Controllers\Api\AirlineApiController;
|
use App\Http\Controllers\Api\AirlineApiController;
|
||||||
@@ -40,14 +41,13 @@ Route::domain(config('app.domain'))->group(
|
|||||||
return Inertia::render('Dashboard');
|
return Inertia::render('Dashboard');
|
||||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
})->middleware(['auth', 'verified'])->name('dashboard');
|
||||||
|
|
||||||
Route::middleware('auth')->group(function () {
|
Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->group(function () {
|
||||||
Route::get('/import/fr24', function () {
|
Route::get('/', [AdminController::class, 'dashboard'])->name('dashboard');
|
||||||
if (Auth::user()->importedFlights()->exists()) {
|
Route::get('/missing-liveries', [AdminController::class, 'reconcileMissingLiveries'])->name('reconcile-missing-liveries')->middleware('permission:reconcile_missing_liveries');
|
||||||
return redirect()->route('reconcile');
|
Route::post('/ignore-missing-livery', [AdminController::class, 'ignoreMissingLivery'])->name('ignore-missing-livery');
|
||||||
}
|
});
|
||||||
return Inertia::render('Fr24Import');
|
|
||||||
})->name('import.fr24');
|
|
||||||
|
|
||||||
|
Route::middleware('auth')->group(function () {
|
||||||
Route::post('/flights', [FlightController::class, 'store'])->name('flights.store');
|
Route::post('/flights', [FlightController::class, 'store'])->name('flights.store');
|
||||||
Route::get('/flights/add', [FlightController::class, 'add'])->name('flights.add');
|
Route::get('/flights/add', [FlightController::class, 'add'])->name('flights.add');
|
||||||
Route::get('/flights/{flight}/edit', [FlightController::class, 'edit'])->name('flights.edit');
|
Route::get('/flights/{flight}/edit', [FlightController::class, 'edit'])->name('flights.edit');
|
||||||
@@ -55,19 +55,8 @@ Route::domain(config('app.domain'))->group(
|
|||||||
Route::delete('/flights/{flight}/{referrer?}', [FlightController::class, 'delete'])->name('flights.delete');
|
Route::delete('/flights/{flight}/{referrer?}', [FlightController::class, 'delete'])->name('flights.delete');
|
||||||
|
|
||||||
|
|
||||||
|
Route::get('/import/fr24', [FlightImportController::class, 'showFr24Import'])->name('import.fr24');
|
||||||
Route::get('/reconcile', function () {
|
Route::get('/reconcile', [FlightImportController::class, 'reconcile'])->name('reconcile');;
|
||||||
$flight = new FlightImportController()->reconcile(request());
|
|
||||||
|
|
||||||
if (!$flight) {
|
|
||||||
return to_route('import.fr24');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Inertia::render('ReconcileFlight', [
|
|
||||||
'flight' => $flight,
|
|
||||||
'key' => $flight['imported_flight_id'],
|
|
||||||
]);
|
|
||||||
})->name('reconcile');
|
|
||||||
|
|
||||||
Route::get('/flights/lookup', [FlightController::class, 'lookup'])->name('flights.lookup');
|
Route::get('/flights/lookup', [FlightController::class, 'lookup'])->name('flights.lookup');
|
||||||
Route::post('/flights/import', [FlightImportController::class, 'store'])->name('flights.import.store');
|
Route::post('/flights/import', [FlightImportController::class, 'store'])->name('flights.import.store');
|
||||||
@@ -88,13 +77,12 @@ Route::domain(config('app.domain'))->group(
|
|||||||
Route::get('/search/aircraft', [SearchController::class, 'aircraft'])->name('search.aircraft');
|
Route::get('/search/aircraft', [SearchController::class, 'aircraft'])->name('search.aircraft');
|
||||||
Route::get('/search/airports', [SearchController::class, 'airports'])->name('search.airports');
|
Route::get('/search/airports', [SearchController::class, 'airports'])->name('search.airports');
|
||||||
|
|
||||||
|
//@Todo: Move to API
|
||||||
Route::get('/data/user/{username}/flights', [UserApiController::class, 'flights']);
|
Route::get('/data/user/{username}/flights', [UserApiController::class, 'flights']);
|
||||||
Route::get('/missing-liveries', [AdminToolsController::class, 'missingLiveries'])->name('admin.tools.missing-liveries');
|
|
||||||
|
|
||||||
Route::get('/u/{user}', [FlightProfileController::class, 'view'])->name('profile.view');
|
Route::get('/u/{user}', [FlightProfileController::class, 'view'])->name('profile.view');
|
||||||
Route::get('/u/{user}/map', [FlightProfileController::class, 'map'])->name('profile.map');
|
Route::get('/u/{user}/map', [FlightProfileController::class, 'map'])->name('profile.map');
|
||||||
Route::get('/u/{user}/departure-board/{flight?}', [FlightProfileController::class, 'departureBoard'])
|
Route::get('/u/{user}/departure-board/{flight?}', [FlightProfileController::class, 'departureBoard'])->name('profile.departure-board');
|
||||||
->name('profile.departure-board');
|
|
||||||
Route::get('/u/{user}/boarding-passes', [FlightProfileController::class, 'boardingPasses'])->name('profile.boarding-passes');
|
Route::get('/u/{user}/boarding-passes', [FlightProfileController::class, 'boardingPasses'])->name('profile.boarding-passes');
|
||||||
Route::get('/u/{user}/achievements', [AchievementController::class, 'index'])->name('profile.achievements');
|
Route::get('/u/{user}/achievements', [AchievementController::class, 'index'])->name('profile.achievements');
|
||||||
Route::get('/u/{user}/achievement/{achievement}', [AchievementController::class, 'specific'])->name('profile.achievement');
|
Route::get('/u/{user}/achievement/{achievement}', [AchievementController::class, 'specific'])->name('profile.achievement');
|
||||||
|
|||||||
Reference in New Issue
Block a user