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;
|
||||
}
|
||||
|
||||
|
||||
public function showFr24Import()
|
||||
{
|
||||
if (Auth::user()->importedFlights()->exists()) {
|
||||
return to_route('reconcile');
|
||||
}
|
||||
|
||||
return Inertia::render('Fr24Import');
|
||||
}
|
||||
|
||||
// FlightImportController.php
|
||||
|
||||
public function reconcile(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
@@ -133,16 +145,14 @@ class FlightImportController extends Controller
|
||||
$flightToReconcile = ImportedFlight::where('user_id', $user->id)->orderBy('date', 'asc')->first();
|
||||
|
||||
if (!$flightToReconcile) {
|
||||
return null;
|
||||
return to_route('import.fr24');
|
||||
}
|
||||
|
||||
$date = null;
|
||||
if ($flightToReconcile->date) {
|
||||
$date = Carbon::createFromFormat('Y-m-d', $flightToReconcile->date)->format('Y-m-d');
|
||||
}
|
||||
$date = $flightToReconcile->date
|
||||
? Carbon::createFromFormat('Y-m-d', $flightToReconcile->date)->format('Y-m-d')
|
||||
: null;
|
||||
|
||||
|
||||
return [
|
||||
$flight = [
|
||||
'imported_flight_id' => $flightToReconcile->id,
|
||||
'flight_classes' => $this->selectOptions(FlightClass::class),
|
||||
'flight_reasons' => $this->selectOptions(FlightReason::class),
|
||||
@@ -162,6 +172,11 @@ class FlightImportController extends Controller
|
||||
'from_options' => $this->getPossibleAirports($flightToReconcile->from ?? ''),
|
||||
'aircraft_options' => $this->getPossibleAircraft($flightToReconcile->aircraft ?? ''),
|
||||
];
|
||||
|
||||
return Inertia::render('ReconcileFlight', [
|
||||
'flight' => $flight,
|
||||
'key' => $flight['imported_flight_id'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
|
||||
@@ -17,7 +17,7 @@ class FlightProfileController extends Controller
|
||||
public function profileData(User $user, string $view, ?int $selectedFlightId = null) : array {
|
||||
return [
|
||||
'user' => $user,
|
||||
'canEdit' => auth()->check() && auth()->id() === $user->id,
|
||||
'canEdit' => (auth()->check() && auth()->id() === $user->id) || auth()->user()->hasRole('admin'),
|
||||
'initialView' => $view,
|
||||
'selectedFlightId' => $selectedFlightId,
|
||||
'flight_api_url' => self::getUserFlightApiURL($user),
|
||||
|
||||
@@ -35,6 +35,8 @@ class HandleInertiaRequests extends Middleware
|
||||
'logo_api_url' => config('app.logo_api_url'),
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
'roles' => $request->user()?->getRoleNames() ?? [],
|
||||
'permissions' => $request->user()?->getAllPermissions()->pluck('name') ?? [],
|
||||
],
|
||||
'achievement_notifications' => fn() => $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\Foundation\Auth\User as Authenticatable;
|
||||
use App\Traits\HasAchievements;
|
||||
use App\Models\Notification;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
#[Fillable(['name', 'email', 'password', 'distance_unit'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
@@ -19,9 +19,7 @@ class User extends Authenticatable
|
||||
{
|
||||
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, HasAchievements, HasApiTokens;
|
||||
|
||||
|
||||
use HasFactory, HasAchievements, HasApiTokens, HasRoles;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
|
||||
@@ -37,7 +37,7 @@ class UserFlightPolicy
|
||||
*/
|
||||
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
|
||||
|
||||
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\Collection;
|
||||
|
||||
class AdminToolsController extends Controller
|
||||
class AdminService
|
||||
{
|
||||
function missingLiveries(){
|
||||
/** @return Collection<int, MissingLivery> */
|
||||
function getMissingLiveries(): Collection
|
||||
{
|
||||
|
||||
|
||||
/* $existingFiles = collect(glob(public_path('img/liveries/generated/*')))
|
||||
/* $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'))
|
||||
->map(fn ($path) => pathinfo($path, PATHINFO_FILENAME))
|
||||
->toArray();
|
||||
|
||||
|
||||
|
||||
$combos = \App\Models\UserFlight::with(['aircraft', 'airline'])
|
||||
$combos = UserFlight::with(['aircraft', 'airline'])
|
||||
->select('airline_id', 'aircraft_id')
|
||||
->whereNotNull('airline_id')
|
||||
->whereNotNull('aircraft_id')
|
||||
@@ -31,14 +34,15 @@ class AdminToolsController extends Controller
|
||||
'airline_name' => $flight->airline->name,
|
||||
'aircraft_display_name' => $flight->aircraft->display_name,
|
||||
'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')
|
||||
->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,
|
||||
\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 {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^3.0",
|
||||
"spatie/laravel-permission": "^8.0",
|
||||
"tightenco/ziggy": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
Generated
+149
-1
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "0e560320885031dd36bb08bb44fe05d4",
|
||||
"content-hash": "2fab3a0703fff56cedb9d4c9b650e2fc",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -3433,6 +3433,154 @@
|
||||
},
|
||||
"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",
|
||||
"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 InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
letters: string[]
|
||||
@@ -50,8 +51,8 @@ function isHighlighted({ firstYear }: AirlineEntry): boolean {
|
||||
return props.selectedYear !== null && firstYear === props.selectedYear
|
||||
}
|
||||
|
||||
function toBBCode(): string {
|
||||
return props.letters
|
||||
const bbCode = computed(() =>
|
||||
props.letters
|
||||
.map(letter => {
|
||||
const entries = airlineEntriesForLetter(letter)
|
||||
if (!entries.length) return letter
|
||||
@@ -65,9 +66,9 @@ function toBBCode(): string {
|
||||
.join(', ')
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
)
|
||||
|
||||
defineExpose({ toBBCode })
|
||||
defineExpose({ bbCode })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {Airport, Flight} from '@/Types/types'
|
||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||
import InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
|
||||
import AirportToolTip from '@/Components/FlightsGoneBy/AirportToolTip.vue'
|
||||
import {computed} from "vue";
|
||||
|
||||
type CodeType = 'iata' | 'icao'
|
||||
|
||||
@@ -50,8 +51,8 @@ function isHighlighted({ firstYear }: AirportEntry): boolean {
|
||||
return props.selectedYear !== null && firstYear === props.selectedYear
|
||||
}
|
||||
|
||||
function toBBCode(): string {
|
||||
return props.letters
|
||||
const bbCode = computed(() =>
|
||||
props.letters
|
||||
.map(letter => {
|
||||
const entries = airportEntriesForLetter(letter)
|
||||
if (!entries.length) return letter
|
||||
@@ -64,9 +65,9 @@ function toBBCode(): string {
|
||||
.join(', ')
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
)
|
||||
|
||||
defineExpose({ toBBCode })
|
||||
defineExpose({ bbCode })
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<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="/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">
|
||||
<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>
|
||||
<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="/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>
|
||||
<div class="dropdown-divider" />
|
||||
<button class="nav-link nav-link--danger" @click="logout">Log Out</button>
|
||||
@@ -115,9 +116,10 @@ header {
|
||||
|
||||
/* Shared nav link base */
|
||||
.nav-link {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
height: 100%;
|
||||
padding: 0.3em 0.75em;
|
||||
font-size: 0.875rem;
|
||||
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[]) {
|
||||
if (!notifications?.length) return
|
||||
const unseen = notifications.filter(n => !seenNotificationIds.value.has(n.id))
|
||||
if (!unseen.length) return
|
||||
unseen.forEach(n => seenNotificationIds.value.add(n.id))
|
||||
activeToasts.value.push(...unseen)
|
||||
|
||||
const newToasts: Notification[] = []
|
||||
for (const n of notifications) {
|
||||
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(() => {})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── Toasts ────────────────────────────────────────────────────────────────────
|
||||
const activeToasts = ref<Notification[]>([])
|
||||
|
||||
@@ -47,13 +53,13 @@ router.on('success', (event) => {
|
||||
<template>
|
||||
<Radar>
|
||||
<div class="layoutContainer">
|
||||
<MainHeader :key="transitionKey" />
|
||||
<MainHeader :key="`header-${transitionKey}`" />
|
||||
<Transition name="fade" mode="out-in">
|
||||
<main id="pageContainer" :key="transitionKey">
|
||||
<main id="pageContainer" :key="`main-${transitionKey}`">
|
||||
<slot />
|
||||
</main>
|
||||
</Transition>
|
||||
<MainFooter :key="transitionKey" />
|
||||
<MainFooter :key="`footer-${transitionKey}`" />
|
||||
</div>
|
||||
|
||||
<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 });
|
||||
|
||||
const page = usePage<SharedProps>();
|
||||
const name = computed(() => page?.props?.auth?.user?.name || 'there');
|
||||
const name = computed(() => page?.props?.auth?.user?.name || 'mate');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -7,6 +7,8 @@ import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
|
||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -7,7 +7,7 @@ import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
|
||||
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
|
||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
defineProps<{
|
||||
achievement: Achievement
|
||||
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 BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
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 BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -6,7 +6,9 @@ import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue
|
||||
import AirlineAlphabetTable from '@/Components/FlightsGoneBy/AirlineAlphabetTable.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
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<{
|
||||
achievement: Achievement
|
||||
@@ -47,28 +49,7 @@ const selectedYear = ref<number | null>(
|
||||
|
||||
// Copy BBCode
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -133,13 +114,10 @@ async function copyBBCode() {
|
||||
class="year-select"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
|
||||
:color="copied ? 'success' : undefined"
|
||||
density="compact"
|
||||
variant="text"
|
||||
<CopyButton
|
||||
title="Copy as BBCode"
|
||||
@click="copyBBCode"
|
||||
size="large"
|
||||
:text="airlineTable?.bbCode ?? ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,8 @@ import AlphabetTable from '@/Components/FlightsGoneBy/AlphabetTable.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAlphabetFlights, type CodeType } from '@/Composables/useAlphabetFlights'
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
|
||||
import CopyButton from "@/Components/FlightsGoneBy/CopyButton.vue";
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
@@ -45,27 +46,6 @@ const selectedYear = ref<number | null>(
|
||||
const alphabetTable = ref<InstanceType<typeof AlphabetTable> | null>(null)
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -124,13 +104,10 @@ async function copyBBCode() {
|
||||
class="year-select"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
|
||||
:color="copied ? 'success' : undefined"
|
||||
density="compact"
|
||||
variant="text"
|
||||
<CopyButton
|
||||
title="Copy as BBCode"
|
||||
@click="copyBBCode"
|
||||
size="large"
|
||||
:text="alphabetTable?.bbCode ?? ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {useRegionFlights} from "@/Composables/useRegionFlights";
|
||||
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -12,7 +12,7 @@ import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||
import Brazil from "@/Components/Maps/Brazil.vue";
|
||||
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -13,7 +13,7 @@ import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||
import Canada from "@/Components/Maps/Canada.vue";
|
||||
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -12,7 +12,7 @@ import China from "@/Components/Maps/China.vue";
|
||||
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -11,9 +11,8 @@ import FlightRegionTable from "@/Components/FlightsGoneBy/FlightRegionTable.vue"
|
||||
import {useRegionFlights} from "@/Composables/useRegionFlights";
|
||||
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||
import USA from "@/Components/Maps/USA.vue";
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
Vendored
+9
@@ -62,6 +62,8 @@ export type SharedProps = import('@inertiajs/core').PageProps & {
|
||||
auth: {
|
||||
user: User | null
|
||||
isLoggedIn: boolean
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
},
|
||||
logo_api_url: string
|
||||
achievement_notifications: Notification[]
|
||||
@@ -259,6 +261,13 @@ export interface Flight {
|
||||
livery_url?: string
|
||||
}
|
||||
|
||||
export interface MissingLivery {
|
||||
airline_name: string;
|
||||
aircraft_display_name: string;
|
||||
filename: string;
|
||||
clipboard_text: string;
|
||||
}
|
||||
|
||||
declare module '@inertiajs/vue3' {
|
||||
interface PageProps extends SharedProps {}
|
||||
}
|
||||
|
||||
+11
-23
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\AchievementController;
|
||||
use App\Http\Controllers\AdminController;
|
||||
use App\Http\Controllers\AdminToolsController;
|
||||
use App\Http\Controllers\Api\AircraftApiController;
|
||||
use App\Http\Controllers\Api\AirlineApiController;
|
||||
@@ -40,14 +41,13 @@ Route::domain(config('app.domain'))->group(
|
||||
return Inertia::render('Dashboard');
|
||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::get('/import/fr24', function () {
|
||||
if (Auth::user()->importedFlights()->exists()) {
|
||||
return redirect()->route('reconcile');
|
||||
}
|
||||
return Inertia::render('Fr24Import');
|
||||
})->name('import.fr24');
|
||||
Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/', [AdminController::class, 'dashboard'])->name('dashboard');
|
||||
Route::get('/missing-liveries', [AdminController::class, 'reconcileMissingLiveries'])->name('reconcile-missing-liveries')->middleware('permission:reconcile_missing_liveries');
|
||||
Route::post('/ignore-missing-livery', [AdminController::class, 'ignoreMissingLivery'])->name('ignore-missing-livery');
|
||||
});
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::post('/flights', [FlightController::class, 'store'])->name('flights.store');
|
||||
Route::get('/flights/add', [FlightController::class, 'add'])->name('flights.add');
|
||||
Route::get('/flights/{flight}/edit', [FlightController::class, 'edit'])->name('flights.edit');
|
||||
@@ -55,19 +55,8 @@ Route::domain(config('app.domain'))->group(
|
||||
Route::delete('/flights/{flight}/{referrer?}', [FlightController::class, 'delete'])->name('flights.delete');
|
||||
|
||||
|
||||
|
||||
Route::get('/reconcile', function () {
|
||||
$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('/import/fr24', [FlightImportController::class, 'showFr24Import'])->name('import.fr24');
|
||||
Route::get('/reconcile', [FlightImportController::class, 'reconcile'])->name('reconcile');;
|
||||
|
||||
Route::get('/flights/lookup', [FlightController::class, 'lookup'])->name('flights.lookup');
|
||||
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/airports', [SearchController::class, 'airports'])->name('search.airports');
|
||||
|
||||
//@Todo: Move to API
|
||||
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}/map', [FlightProfileController::class, 'map'])->name('profile.map');
|
||||
Route::get('/u/{user}/departure-board/{flight?}', [FlightProfileController::class, 'departureBoard'])
|
||||
->name('profile.departure-board');
|
||||
Route::get('/u/{user}/departure-board/{flight?}', [FlightProfileController::class, 'departureBoard'])->name('profile.departure-board');
|
||||
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}/achievement/{achievement}', [AchievementController::class, 'specific'])->name('profile.achievement');
|
||||
|
||||
Reference in New Issue
Block a user