Display applications in admin interface
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
weeb 2025-05-01 23:07:38 +02:00
parent a66c65e38a
commit 5a463656a7
9 changed files with 194 additions and 69 deletions

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Guard;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin
{
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (!$this->auth->user()->is_admin) {
session()->flash('error_msg', 'This resource is restricted to Administrators!');
return redirect()->route('home');
}
return $next($request);
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Livewire;
use App\Models\Application;
use Livewire\Component;
use Livewire\WithPagination;
class ApplicationList extends Component
{
use WithPagination;
public $hideApproved = true;
public function updatingHideApproved()
{
$this->resetPage();
}
public function approveApplication($uuid)
{
Application::where('uuid', $uuid)->update(['status' => 1]);
}
public function rejectApplication($uuid)
{
Application::where('uuid', $uuid)->update(['status' => 2]);
}
public function render()
{
$applications = Application::with('uploads')
->when($this->hideApproved !== false, fn($query) => $query->where('status', 0))
->latest()
->paginate(10);
return view('livewire.application-list', compact('applications'));
}
}

View File

@ -17,6 +17,7 @@ return new class extends Migration
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->boolean('is_admin')->default(false);
$table->rememberToken();
$table->timestamps();
});

View File

@ -7,28 +7,14 @@
<flux:sidebar sticky stashable class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<a href="{{ route('dashboard') }}" class="me-5 flex items-center space-x-2 rtl:space-x-reverse" wire:navigate>
<x-app-logo />
</a>
<flux:navlist variant="outline">
<flux:navlist.group :heading="__('Platform')" class="grid">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
<flux:navlist.item icon="document-text" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Applications') }}</flux:navlist.item>
</flux:navlist.group>
</flux:navlist>
<flux:spacer />
<flux:navlist variant="outline">
<flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:navlist.item>
<flux:navlist.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
{{ __('Documentation') }}
</flux:navlist.item>
</flux:navlist>
<!-- Desktop User Menu -->
<flux:dropdown position="bottom" align="start">
<flux:profile

View File

@ -1,24 +1,3 @@
<x-layouts.app :title="__('Applications')">
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div
class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern
class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
<div
class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern
class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
<div
class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern
class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
</div>
<div class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
</div>
@livewire('application-list')
</x-layouts.app>

View File

@ -5,7 +5,7 @@
<flux:heading>Your application has been submitted</flux:heading>
<flux:text class="mt-2">
You can check its status here: <flux:link
href="{{ route('application.status', ['uuid' => $applicationUuid]) }}">View Application Status
href="{{ route('application', ['uuid' => $applicationUuid]) }}">View Application Status
</flux:link>
</flux:text>
<flux:text class="mt-2">

View File

@ -0,0 +1,69 @@
<div>
<div class="max-w-5xl mx-auto p-6">
<h1 class="text-2xl font-bold mb-4">Application Management</h1>
<div class="mb-4">
<label class="inline-flex items-center">
<input type="checkbox" wire:model.live="hideApproved" class="mr-2">
Hide approved applications
</label>
</div>
<table class="w-full border-collapse border border-gray-300 dark:border-neutral-950 rounded-lg">
<thead class="bg-gray-100 dark:bg-neutral-900">
<tr>
<th class="px-4 py-2 text-left">UUID</th>
<th class="px-4 py-2">Status</th>
<th class="px-4 py-2">Uploads</th>
<th class="px-4 py-2">Submitted</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>
@forelse ($applications as $app)
<tr>
<td class="px-4 py-2 font-mono text-sm">{{ $app->uuid }}</td>
<td class="px-4 py-2">
@if ($app->status === 0)
<span class="text-yellow-600 font-semibold">Pending</span>
@elseif ($app->status === 1)
<span class="text-green-600 font-semibold">Approved</span>
@elseif ($app->status === 2)
<span class="text-red-600 font-semibold">Denied</span>
@endif
</td>
<td class="px-4 py-2 text-center">{{ $app->uploads->count() }}</td>
<td class="px-4 py-2 text-sm">{{ $app->created_at->diffForHumans() }}</td>
<td class="px-4 py-2">
<flux:button size="sm" variant="primary"
href="{{ route('application', ['uuid' => $app->uuid]) }}">
View
</flux:button>
@if ($app->status === 0)
<flux:button class="cursor-pointer" size="sm"
wire:click="approveApplication('{{ (string) $app->uuid }}')">
Approve
</flux:button>
<flux:button class="cursor-pointer" size="sm" variant="danger"
wire:click="rejectApplication('{{ (string) $app->uuid }}')">
Reject
</flux:button>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="5" class="text-center py-4 text-gray-500">No applications found.</td>
</tr>
@endforelse
</tbody>
</table>
<div class="mt-4">
{{ $applications->links() }}
</div>
</div>
</div>

View File

@ -1,34 +1,41 @@
<div wire:poll.10s="loadApplication" class="max-w-2xl mx-auto mt-10 flex flex-col gap-6">
<div class="max-w-2xl mx-auto mt-10">
<div class="p-5 mb-4 dark:bg-pink-950/50 rounded-lg">
<flux:heading size="xl">Application Status</flux:heading>
@php
$status = [
0 => 'Pending Review',
1 => 'Approved',
2 => 'Denied',
];
@endphp
<flux:text class="mt-2 text-lg">
<p>
<strong>Status:</strong>
{{ $status[$application->status] }}
</p>
<p>
<strong>Created:</strong>
{{ Carbon\Carbon::parse($application->created_at)->diffForHumans(['parts' => 2]) }}
</p>
</flux:text>
</div>
<div class="max-w-2xl md:w-2xl mx-auto mt-10">
@if ($application->status === 0)
<div class="p-5 mb-4 dark:bg-pink-950/50 rounded-lg">
@elseif ($application->status === 1)
<div class="p-5 mb-4 dark:bg-green-950/50 rounded-lg">
@elseif ($application->status === 2)
<div class="p-5 mb-4 dark:bg-red-950/50 rounded-lg">
@endif
<h2 class="mt-4 font-semibold">Uploaded Photos:</h2>
<div class="flex flex-wrap gap-4 mt-4">
@foreach ($application->uploads as $upload)
<div>
<img class="h-48 w-fit object-cover rounded-2xl hover:-translate-y-1 hover:scale-110 transition duration-300 ease-in-out"
src="{{ asset('storage/' . $upload->file_path) }}">
</div>
@endforeach
</div>
<flux:heading size="xl">Application Status</flux:heading>
@php
$status = [
0 => 'Pending Review',
1 => 'Approved',
2 => 'Denied',
];
@endphp
<flux:text class="mt-2">
<p>
<strong>Status:</strong>
{{ $status[$application->status] }}
</p>
<p>
<strong>Created:</strong>
{{ Carbon\Carbon::parse($application->created_at)->diffForHumans(['parts' => 2]) }}
</p>
</flux:text>
</div>
<h2 class="mt-4 font-semibold">Uploaded Photos:</h2>
<div class="flex flex-wrap gap-4 mt-4">
@foreach ($application->uploads as $upload)
<div>
<img class="h-48 w-fit object-cover rounded-2xl hover:-translate-y-1 hover:scale-110 transition duration-300 ease-in-out"
src="{{ asset('storage/' . $upload->file_path) }}">
</div>
@endforeach
</div>
</div>
</div>

View File

@ -9,7 +9,7 @@ Route::get('/', App\Livewire\ApplicationForm::class)->name('home');
Route::get('/application-status/{uuid}', App\Livewire\ApplicationStatus::class)->name('application');
Route::view('dashboard', 'dashboard')
->middleware(['auth', 'verified'])
->middleware(['auth', 'verified', App\Http\Middleware\IsAdmin::class])
->name('dashboard');
Route::middleware(['auth'])->group(function () {