refactor: implement verification action concerns and enhance admin verification page functionality

This commit is contained in:
Yoga Pangestu 2026-04-08 10:11:06 +07:00
parent 2d2ef77a94
commit e5cbbe7e96
12 changed files with 460 additions and 388 deletions

View File

@ -3,13 +3,13 @@
namespace App\Filament\Pages\AdminVerification\Actions; namespace App\Filament\Pages\AdminVerification\Actions;
use App\Enums\DecisionAdmin; use App\Enums\DecisionAdmin;
use App\Filament\Pages\AdminVerification\Actions\Concerns\CanProcessReview; use App\Filament\Pages\AdminVerification\Concerns\CanProcessVerificationReview;
use App\Filament\Support\CheerfulNotification; use App\Filament\Support\CheerfulNotification;
use Filament\Actions\Action; use Filament\Actions\Action;
class ApproveAction extends Action class ApproveAction extends Action
{ {
use CanProcessReview; use CanProcessVerificationReview;
public static function getDefaultName(): ?string public static function getDefaultName(): ?string
{ {
@ -21,9 +21,11 @@ protected function setUp(): void
parent::setUp(); parent::setUp();
$this->label('Setujui') $this->label('Setujui')
->authorize(fn (): bool => auth()->user()?->can('Approve:AdminVerification'))
->color('success') ->color('success')
->icon('heroicon-o-check') ->icon('heroicon-o-check')
->size('sm') ->size('sm')
->link()
->requiresConfirmation() ->requiresConfirmation()
->modalHeading(fn () => CheerfulNotification::getByKey('actions.approve.title')) ->modalHeading(fn () => CheerfulNotification::getByKey('actions.approve.title'))
->modalDescription(fn () => CheerfulNotification::getByKey('actions.approve.description')) ->modalDescription(fn () => CheerfulNotification::getByKey('actions.approve.description'))

View File

@ -3,14 +3,14 @@
namespace App\Filament\Pages\AdminVerification\Actions; namespace App\Filament\Pages\AdminVerification\Actions;
use App\Enums\DecisionAdmin; use App\Enums\DecisionAdmin;
use App\Filament\Pages\AdminVerification\Actions\Concerns\CanProcessReview; use App\Filament\Pages\AdminVerification\Concerns\CanProcessVerificationReview;
use App\Filament\Support\CheerfulNotification; use App\Filament\Support\CheerfulNotification;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
class RejectAction extends Action class RejectAction extends Action
{ {
use CanProcessReview; use CanProcessVerificationReview;
public static function getDefaultName(): ?string public static function getDefaultName(): ?string
{ {
@ -22,9 +22,11 @@ protected function setUp(): void
parent::setUp(); parent::setUp();
$this->label('Tolak') $this->label('Tolak')
->authorize(fn (): bool => auth()->user()?->can('Reject:AdminVerification'))
->color('danger') ->color('danger')
->icon('heroicon-o-x-mark') ->icon('heroicon-o-x-mark')
->size('sm') ->size('sm')
->link()
->requiresConfirmation() ->requiresConfirmation()
->modalHeading(fn () => CheerfulNotification::getByKey('actions.reject.title')) ->modalHeading(fn () => CheerfulNotification::getByKey('actions.reject.title'))
->modalDescription(fn () => CheerfulNotification::getByKey('actions.reject.description')) ->modalDescription(fn () => CheerfulNotification::getByKey('actions.reject.description'))

View File

@ -3,7 +3,7 @@
namespace App\Filament\Pages\AdminVerification\Actions; namespace App\Filament\Pages\AdminVerification\Actions;
use App\Enums\DecisionAdmin; use App\Enums\DecisionAdmin;
use App\Filament\Pages\AdminVerification\Actions\Concerns\CanProcessReview; use App\Filament\Pages\AdminVerification\Concerns\CanProcessVerificationReview;
use App\Filament\Support\CheerfulNotification; use App\Filament\Support\CheerfulNotification;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
@ -11,7 +11,7 @@
class RevisionAction extends Action class RevisionAction extends Action
{ {
use CanProcessReview; use CanProcessVerificationReview;
public static function getDefaultName(): ?string public static function getDefaultName(): ?string
{ {
@ -23,9 +23,11 @@ protected function setUp(): void
parent::setUp(); parent::setUp();
$this->label('Minta Revisi') $this->label('Minta Revisi')
->authorize(fn (): bool => auth()->user()?->can('Revision:AdminVerification'))
->color('warning') ->color('warning')
->icon('heroicon-o-pencil-square') ->icon('heroicon-o-pencil-square')
->size('sm') ->size('sm')
->link()
->modalHeading(fn () => CheerfulNotification::getByKey('actions.revision.title')) ->modalHeading(fn () => CheerfulNotification::getByKey('actions.revision.title'))
->modalDescription(fn () => CheerfulNotification::getByKey('actions.revision.description')) ->modalDescription(fn () => CheerfulNotification::getByKey('actions.revision.description'))
->schema([ ->schema([

View File

@ -0,0 +1,29 @@
<?php
namespace App\Filament\Pages\AdminVerification\Actions;
use App\Filament\Pages\AdminVerification\Concerns\CanProcessVerificationReview;
use App\Filament\Resources\Manage\Partners\Pages\ViewPartner;
use Filament\Actions\Action;
class ViewAction extends Action
{
use CanProcessVerificationReview;
public static function getDefaultName(): ?string
{
return 'view';
}
protected function setUp(): void
{
parent::setUp();
$this->label('Lihat')
->color('success')
->icon('heroicon-o-eye')
->size('sm')
->link()
->url(fn (array $arguments) => ViewPartner::getUrl(['record' => $arguments['id']]));
}
}

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Filament\Pages\AdminVerification\Actions\Concerns; namespace App\Filament\Pages\AdminVerification\Concerns;
use App\Enums\DecisionAdmin; use App\Enums\DecisionAdmin;
use App\Enums\VerificationStatus; use App\Enums\VerificationStatus;
@ -13,45 +13,33 @@
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
trait CanProcessReview trait CanProcessVerificationReview
{ {
protected function processReview(int $requestId, DecisionAdmin $decision, ?string $note): void protected function processReview(int $requestId, DecisionAdmin $decision, ?string $note): void
{ {
$verificationRequest = VerificationRequest::with(['company.partnerMedia.journalists'])->find($requestId); $verificationRequest = VerificationRequest::with(['company.partnerMedia.journalists'])->findOrFail($requestId);
if (! $verificationRequest) {
CheerfulNotification::danger(
CheerfulNotification::getByKey('verification.admin.process.not_found.title'),
CheerfulNotification::getByKey('verification.admin.process.not_found.body')
)
->send();
return;
}
try { try {
DB::beginTransaction(); DB::transaction(function () use ($verificationRequest, $decision, $note) {
// Create review record
VerificationReview::create([
'verification_request_id' => $verificationRequest->id,
'reviewer_id' => auth()->id(),
'decision' => $decision,
'note' => $note,
]);
// Create review record // Update verification request status
VerificationReview::create([ $newStatus = match ($decision) {
'verification_request_id' => $verificationRequest->id, DecisionAdmin::APPROVED => VerificationStatus::APPROVED,
'reviewer_id' => auth()->id(), DecisionAdmin::NEED_REVISION => VerificationStatus::NEED_REVISION,
'decision' => $decision, DecisionAdmin::REJECTED => VerificationStatus::REJECTED,
'note' => $note, };
]);
// Update verification request status $verificationRequest->update([
$newStatus = match ($decision) { 'status' => $newStatus,
DecisionAdmin::APPROVED => VerificationStatus::APPROVED, ]);
DecisionAdmin::NEED_REVISION => VerificationStatus::NEED_REVISION, });
DecisionAdmin::REJECTED => VerificationStatus::REJECTED,
};
$verificationRequest->update([
'status' => $newStatus,
]);
DB::commit();
$actionLabel = match ($decision) { $actionLabel = match ($decision) {
DecisionAdmin::APPROVED => 'disetujui', DecisionAdmin::APPROVED => 'disetujui',
@ -95,7 +83,6 @@ protected function processReview(int $requestId, DecisionAdmin $decision, ?strin
])); ]));
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
DB::rollBack();
Log::error('Verification processing failed', [ Log::error('Verification processing failed', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),

View File

@ -0,0 +1,32 @@
<?php
namespace App\Filament\Pages\AdminVerification\Concerns;
use App\Filament\Pages\AdminVerification\Actions\ApproveAction;
use App\Filament\Pages\AdminVerification\Actions\RejectAction;
use App\Filament\Pages\AdminVerification\Actions\RevisionAction;
use App\Filament\Pages\AdminVerification\Actions\ViewAction;
use Filament\Actions\Action;
trait HasVerificationActions
{
public function approveAction(): Action
{
return ApproveAction::make();
}
public function revisionAction(): Action
{
return RevisionAction::make();
}
public function rejectAction(): Action
{
return RejectAction::make();
}
public function viewAction(): Action
{
return ViewAction::make();
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Filament\Pages\AdminVerification\Concerns;
use App\Enums\VerificationStatus;
use App\Models\VerificationRequest;
use Livewire\Attributes\Computed;
trait HasVerificationFilters
{
public string $filterStatus = '1';
public int $rowsToDisplay = 12;
public function loadMore(): void
{
$this->rowsToDisplay += 12;
}
public function setFilter(string $status): void
{
$this->filterStatus = $status;
$this->rowsToDisplay = 12;
}
#[Computed]
public function getFilters(): array
{
return [
[
'id' => 'all',
'label' => 'Semua',
'icon' => 'heroicon-s-squares-2x2',
'color' => 'primary',
'count' => VerificationRequest::count(),
],
[
'id' => (string) VerificationStatus::PENDING->value,
'label' => 'Menunggu',
'icon' => 'heroicon-s-clock',
'color' => 'warning',
'count' => VerificationRequest::query()->pending()->count(),
],
[
'id' => (string) VerificationStatus::NEED_REVISION->value,
'label' => 'Revisi',
'icon' => 'heroicon-s-pencil-square',
'color' => 'info',
'count' => VerificationRequest::query()->needRevision()->count(),
],
[
'id' => (string) VerificationStatus::APPROVED->value,
'label' => 'Disetujui',
'icon' => 'heroicon-s-check-circle',
'color' => 'success',
'count' => VerificationRequest::query()->approved()->count(),
],
[
'id' => (string) VerificationStatus::REJECTED->value,
'label' => 'Ditolak',
'icon' => 'heroicon-s-x-circle',
'color' => 'danger',
'count' => VerificationRequest::query()->rejected()->count(),
],
];
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Filament\Pages\AdminVerification\Concerns;
use App\Filament\Support\CheerfulNotification;
trait HasVerificationNotifications
{
public function getEmptyStateHeading(): string
{
return CheerfulNotification::getByKey('verification.admin.empty_state.heading');
}
public function getEmptyStateDescription(): string
{
return CheerfulNotification::getByKey('verification.admin.empty_state.description');
}
}

View File

@ -3,15 +3,14 @@
namespace App\Filament\Pages\AdminVerification; namespace App\Filament\Pages\AdminVerification;
use App\Enums\VerificationStatus; use App\Enums\VerificationStatus;
use App\Filament\Pages\AdminVerification\Actions\ApproveAction; use App\Filament\Pages\AdminVerification\Concerns\HasVerificationActions;
use App\Filament\Pages\AdminVerification\Actions\RejectAction; use App\Filament\Pages\AdminVerification\Concerns\HasVerificationFilters;
use App\Filament\Pages\AdminVerification\Actions\RevisionAction; use App\Filament\Pages\AdminVerification\Concerns\HasVerificationNotifications;
use App\Filament\Resources\Manage\Partners\Pages\ViewPartner; use App\Filament\Resources\Manage\Partners\Pages\ViewPartner;
use App\Filament\Support\CheerfulNotification; use App\Filament\Support\CheerfulNotification;
use App\Models\VerificationRequest; use App\Models\VerificationRequest;
use BackedEnum; use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield; use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions; use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions; use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Concerns\InteractsWithForms; use Filament\Forms\Concerns\InteractsWithForms;
@ -24,11 +23,7 @@
class Index extends Page implements HasActions, HasForms class Index extends Page implements HasActions, HasForms
{ {
use HasPageShield, InteractsWithActions, InteractsWithForms; use HasPageShield, HasVerificationActions, HasVerificationFilters, HasVerificationNotifications, InteractsWithActions, InteractsWithForms;
public string $filterStatus = '1';
public int $rowsToDisplay = 12;
protected static string|UnitEnum|null $navigationGroup = 'Kelola'; protected static string|UnitEnum|null $navigationGroup = 'Kelola';
@ -44,7 +39,7 @@ class Index extends Page implements HasActions, HasForms
protected string $view = 'filament.pages.admin-verification'; protected string $view = 'filament.pages.admin-verification';
public static function canAccess(array $parameters = []): bool public static function canAccess(): bool
{ {
return auth()->user()->can('View:AdminVerification'); return auth()->user()->can('View:AdminVerification');
} }
@ -157,6 +152,7 @@ public function verifications(): LengthAwarePaginator
'createdAt' => $review->created_at->translatedFormat('l, d M Y H:i'), 'createdAt' => $review->created_at->translatedFormat('l, d M Y H:i'),
]), ]),
], ],
'messages' => [ 'messages' => [
'detail' => CheerfulNotification::getByKey('verification.admin.messages.detail'), 'detail' => CheerfulNotification::getByKey('verification.admin.messages.detail'),
'revision' => CheerfulNotification::getByKey('verification.admin.messages.revision'), 'revision' => CheerfulNotification::getByKey('verification.admin.messages.revision'),
@ -166,91 +162,4 @@ public function verifications(): LengthAwarePaginator
]; ];
}); });
} }
public function loadMore(): void
{
$this->rowsToDisplay += 12;
}
public function setFilter(string $status): void
{
$this->filterStatus = $status;
$this->rowsToDisplay = 12;
}
#[Computed]
public function getFilters(): array
{
return [
[
'id' => 'all',
'label' => 'Semua',
'icon' => 'heroicon-s-squares-2x2',
'color' => 'primary',
'count' => VerificationRequest::count(),
],
[
'id' => (string) VerificationStatus::PENDING->value,
'label' => 'Menunggu',
'icon' => 'heroicon-s-clock',
'color' => 'warning',
'count' => VerificationRequest::pending()->count(),
],
[
'id' => (string) VerificationStatus::NEED_REVISION->value,
'label' => 'Revisi',
'icon' => 'heroicon-s-pencil-square',
'color' => 'info',
'count' => VerificationRequest::needRevision()->count(),
],
[
'id' => (string) VerificationStatus::APPROVED->value,
'label' => 'Disetujui',
'icon' => 'heroicon-s-check-circle',
'color' => 'success',
'count' => VerificationRequest::approved()->count(),
],
[
'id' => (string) VerificationStatus::REJECTED->value,
'label' => 'Ditolak',
'icon' => 'heroicon-s-x-circle',
'color' => 'danger',
'count' => VerificationRequest::rejected()->count(),
],
];
}
/**
* Approve verification action
*/
public function approveAction(): Action
{
return ApproveAction::make();
}
/**
* Revision verification action
*/
public function revisionAction(): Action
{
return RevisionAction::make();
}
/**
* Reject verification action
*/
public function rejectAction(): Action
{
return RejectAction::make();
}
public function getEmptyStateHeading(): string
{
return CheerfulNotification::getByKey('verification.admin.empty_state.heading');
}
public function getEmptyStateDescription(): string
{
return CheerfulNotification::getByKey('verification.admin.empty_state.description');
}
} }

View File

@ -21,6 +21,9 @@ public function run(): void
"View:Analytic", "View:Analytic",
"View:AdminVerification", "View:AdminVerification",
"Approve:AdminVerification",
"Reject:AdminVerification",
"Revision:AdminVerification",
"ViewAny:Announcement", "ViewAny:Announcement",
"View:Announcement", "View:Announcement",
@ -281,6 +284,9 @@ public function run(): void
"View:Analytic", "View:Analytic",
"View:AdminVerification", "View:AdminVerification",
"Approve:AdminVerification",
"Reject:AdminVerification",
"Revision:AdminVerification",
"ViewAny:Announcement", "ViewAny:Announcement",
"View:Announcement", "View:Announcement",

View File

@ -265,16 +265,6 @@
], ],
'admin' => [ 'admin' => [
'process' => [ 'process' => [
'not_found' => [
'title' => [
'cheerful' => 'Belum ketemu nih 🙌',
'formal' => 'Data Tidak Ditemukan',
],
'body' => [
'cheerful' => 'Data verifikasi belum kami temukan. Coba sebentar lagi, ya.',
'formal' => 'Permintaan verifikasi tidak ditemukan dalam sistem kami.',
],
],
'success' => [ 'success' => [
'title' => [ 'title' => [
'cheerful' => 'Berhasil Diproses! ✨', 'cheerful' => 'Berhasil Diproses! ✨',

View File

@ -1,24 +1,31 @@
<x-filament-panels::page> <x-filament-panels::page>
<div class="flex flex-wrap items-center gap-3"> <div class="flex flex-wrap items-center gap-3">
@foreach ($this->getFilters as $filter) @foreach ($this->getFilters as $filter)
<button wire:click="setFilter('{{ $filter['id'] }}')" <x-filament::button wire:click="setFilter('{{ $filter['id'] }}')" @class([
@class([ 'group inline-flex items-center gap-2.5 px-5 py-2.5 text-sm font-bold custom-rounded transition-all duration-300 active:scale-95 shadow-sm hover:shadow-md',
'group inline-flex items-center gap-2.5 px-5 py-2.5 text-sm font-bold custom-rounded transition-all duration-300 active:scale-95 shadow-sm hover:shadow-md', // Active Styles
// Active Styles $filterStatus === $filter['id']
$filterStatus === $filter['id'] ? match($filter['color']) { ? match ($filter['color']) {
'primary' => 'bg-primary-500 text-white shadow-primary-500/30 ring-2 ring-primary-500/20', 'primary'
'warning' => 'bg-warning-500 text-white shadow-warning-500/30 ring-2 ring-warning-500/20', => 'bg-primary-500 text-white shadow-primary-500/30 ring-2 ring-primary-500/20',
'success' => 'bg-success-500 text-white shadow-success-500/30 ring-2 ring-success-500/20', 'warning'
'danger' => 'bg-danger-500 text-white shadow-danger-500/30 ring-2 ring-danger-500/20', => 'bg-warning-500 text-white shadow-warning-500/30 ring-2 ring-warning-500/20',
'info' => 'bg-info-500 text-white shadow-info-500/30 ring-2 ring-info-500/20', 'success'
=> 'bg-success-500 text-white shadow-success-500/30 ring-2 ring-success-500/20',
'danger'
=> 'bg-danger-500 text-white shadow-danger-500/30 ring-2 ring-danger-500/20',
'info'
=> 'bg-info-500 text-white shadow-info-500/30 ring-2 ring-info-500/20',
default => 'bg-gray-500 text-white shadow-gray-500/25', default => 'bg-gray-500 text-white shadow-gray-500/25',
} : 'bg-white dark:bg-gray-900 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-800 hover:border-gray-300 dark:hover:border-gray-700', }
])> : 'bg-white dark:bg-gray-900 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-800 hover:border-gray-300 dark:hover:border-gray-700',
])>
@if ($filter['icon']) @if ($filter['icon'])
<x-dynamic-component :component="$filter['icon']" <x-dynamic-component :component="$filter['icon']" @class([
@class([ 'w-5 h-5 transition-transform duration-300 group-hover:scale-110',
'w-5 h-5 transition-transform duration-300 group-hover:scale-110', $filterStatus === $filter['id']
$filterStatus === $filter['id'] ? 'text-white' : match($filter['color']) { ? 'text-white'
: match ($filter['color']) {
'primary' => 'text-primary-500', 'primary' => 'text-primary-500',
'warning' => 'text-warning-500', 'warning' => 'text-warning-500',
'success' => 'text-success-500', 'success' => 'text-success-500',
@ -26,257 +33,278 @@
'info' => 'text-info-500', 'info' => 'text-info-500',
default => 'text-gray-500', default => 'text-gray-500',
}, },
]) ]) />
/>
@endif @endif
<span>{{ $filter['label'] }}</span> <span>{{ $filter['label'] }}</span>
<span @class([ <span @class([
'px-2.5 py-0.5 text-[10px] font-extrabold rounded-full transition-all duration-300', 'px-2.5 py-0.5 text-[10px] font-extrabold rounded-full transition-all duration-300',
$filterStatus === $filter['id'] ? 'bg-white/20 text-white shadow-inner' : match($filter['color']) { $filterStatus === $filter['id']
'primary' => 'bg-primary-100 text-primary-700 dark:bg-primary-500/20 dark:text-primary-400', ? 'bg-white/20 text-white shadow-inner'
'warning' => 'bg-warning-100 text-warning-700 dark:bg-warning-500/20 dark:text-warning-300', : match ($filter['color']) {
'success' => 'bg-success-100 text-success-700 dark:bg-success-500/20 dark:text-success-400', 'primary'
'danger' => 'bg-danger-100 text-danger-700 dark:bg-danger-500/20 dark:text-danger-400', => 'bg-primary-100 text-primary-700 dark:bg-primary-500/20 dark:text-primary-400',
'info' => 'bg-info-100 text-info-700 dark:bg-info-500/20 dark:text-info-400', 'warning'
default => 'bg-gray-100 text-gray-700 dark:bg-gray-500/20 dark:text-gray-400', => 'bg-warning-100 text-warning-700 dark:bg-warning-500/20 dark:text-warning-300',
}, 'success'
=> 'bg-success-100 text-success-700 dark:bg-success-500/20 dark:text-success-400',
'danger'
=> 'bg-danger-100 text-danger-700 dark:bg-danger-500/20 dark:text-danger-400',
'info'
=> 'bg-info-100 text-info-700 dark:bg-info-500/20 dark:text-info-400',
default
=> 'bg-gray-100 text-gray-700 dark:bg-gray-500/20 dark:text-gray-400',
},
])> ])>
{{ $filter['count'] ?? 0 }} {{ $filter['count'] ?? 0 }}
</span> </span>
</button> </x-filament::button>
@endforeach @endforeach
</div> </div>
{{-- Verification Cards Grid --}} <x-filament::section>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> {{-- Verification Cards Grid --}}
@forelse ($this->verifications as $verification) <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<x-filament::section class="h-full"> @forelse ($this->verifications as $verification)
<x-slot name="heading"> <x-filament::section class="h-full">
<div class="flex items-start justify-between gap-3"> <x-slot name="heading">
<div class="min-w-0 flex-1"> <div class="flex items-start justify-between gap-3">
<h3 class="font-bold text-lg text-gray-900 dark:text-white truncate" <div class="min-w-0 flex-1">
title="{{ $verification['company']['name'] }}"> <h3 class="font-bold text-lg text-gray-900 dark:text-white truncate"
{{ $verification['company']['name'] }} title="{{ $verification['company']['name'] }}">
</h3> {{ $verification['company']['name'] }}
</div> </h3>
{{-- Status Badge --}}
<x-filament::badge :color="$verification['status']['color']" size="lg">
{{ $verification['status']['label'] }}
</x-filament::badge>
</div>
</x-slot>
<div class="space-y-4">
{{-- Submitter Info --}}
<div class="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 custom-rounded">
<div class="shrink-0">
<div
class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-500/20 flex items-center justify-center">
<x-heroicon-s-user class="w-5 h-5 text-primary-500" />
</div> </div>
</div>
<div class="min-w-0 flex-1">
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Diajukan oleh
</p>
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
{{ $verification['submittedBy'] }}
</p>
</div>
</div>
{{-- Data Summary --}} {{-- Status Badge --}}
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 p-3 bg-gray-50 dark:bg-gray-800 custom-rounded"> <x-filament::badge :color="$verification['status']['color']" size="lg">
{{-- Company Summary --}} {{ $verification['status']['label'] }}
<div> </x-filament::badge>
<div class="flex items-center gap-2 mb-1"> </div>
<x-heroicon-o-building-office-2 class="w-4 h-4 text-primary-500" /> </x-slot>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Perusahaan
<div class="space-y-4">
{{-- Submitter Info --}}
<div class="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 custom-rounded">
<div class="shrink-0">
<div
class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-500/20 flex items-center justify-center">
<x-heroicon-s-user class="w-5 h-5 text-primary-500" />
</div>
</div>
<div class="min-w-0 flex-1">
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Diajukan
oleh
</p>
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
{{ $verification['submittedBy'] }}
</p> </p>
</div> </div>
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
{{ $verification['summary']['company']['name'] }}
</p>
<x-filament::badge :color="$verification['summary']['company']['exists'] ? 'success' : 'warning'" size="xs" class="mt-1">
{{ $verification['summary']['company']['exists'] ? '✓ Lengkap' : '⚠ Belum diisi' }}
</x-filament::badge>
</div> </div>
{{-- Media Summary --}} {{-- Data Summary --}}
<div> <div
<div class="flex items-center gap-2 mb-1"> class="grid grid-cols-1 md:grid-cols-3 gap-3 p-3 bg-gray-50 dark:bg-gray-800 custom-rounded">
<x-heroicon-o-newspaper class="w-4 h-4 text-info-500" /> {{-- Company Summary --}}
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Media</p> <div>
</div> <div class="flex items-center gap-2 mb-1">
<p class="text-sm font-medium text-gray-900 dark:text-white truncate"> <x-heroicon-o-building-office-2 class="w-4 h-4 text-primary-500" />
{{ $verification['summary']['media']['name'] }} <p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">
</p> Perusahaan
<x-filament::badge :color="$verification['summary']['media']['exists'] ? 'success' : 'warning'" size="xs" class="mt-1"> </p>
{{ $verification['summary']['media']['exists'] ? '✓ Lengkap' : '⚠ Belum diisi' }}
</x-filament::badge>
</div>
{{-- Journalists Summary --}}
<div>
<div class="flex items-center gap-2 mb-1">
<x-heroicon-o-identification class="w-4 h-4 text-info-500" />
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Jurnalis</p>
</div>
<p class="text-sm font-medium text-gray-900 dark:text-white">
{{ $verification['summary']['journalists']['count'] }} jurnalis
</p>
<x-filament::badge :color="$verification['summary']['journalists']['exists'] ? 'success' : 'warning'" size="xs" class="mt-1">
{{ $verification['summary']['journalists']['exists'] ? '✓ Lengkap' : '⚠ Belum diisi' }}
</x-filament::badge>
</div>
</div>
{{-- Timeline Info --}}
<div class="grid grid-cols-2 gap-3 text-sm">
<div>
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Diajukan</p>
<p class="text-gray-700 dark:text-gray-300">
{{ $verification['timestamps']['submittedAt'] }}
</p>
<p class="text-xs text-gray-500">
{{ $verification['timestamps']['submittedAtHuman'] }}
</p>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Pembaruan
Terakhir
</p>
<p class="text-gray-700 dark:text-gray-300">
{{ $verification['timestamps']['updatedAt'] }}
</p>
<p class="text-xs text-gray-500">
{{ $verification['timestamps']['updatedAtHuman'] }}
</p>
</div>
</div>
{{-- Latest Review Note --}}
@if ($verification['reviews']['latest']['note'])
<div class="p-3 custom-rounded border {{ $verification['reviews']['latest']['classes'] }}">
<p
class="text-xs font-semibold uppercase mb-1 {{ $verification['reviews']['latest']['titleClasses'] }}">
Catatan Terakhir
</p>
<p class="text-sm text-gray-700 dark:text-gray-300 line-clamp-2">
{{ $verification['reviews']['latest']['note'] }}
</p>
<p class="text-xs text-gray-500 mt-1">
oleh {{ $verification['reviews']['latest']['reviewer'] }}
{{ $verification['reviews']['latest']['createdAtHuman'] }}
</p>
</div>
@endif
{{-- Review History Accordion --}}
@if ($verification['reviews']['count'] > 0)
<div x-data="{ open: false }"
class="border border-gray-200 dark:border-gray-700 custom-rounded overflow-hidden">
<button @click="open = !open"
class="w-full flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<div
class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300">
<x-heroicon-o-clock class="w-4 h-4" />
Riwayat Review ({{ $verification['reviews']['count'] }})
</div> </div>
<x-heroicon-o-chevron-down <p class="text-sm font-medium text-gray-900 dark:text-white truncate">
class="w-4 h-4 text-gray-500 transition-transform duration-200" {{ $verification['summary']['company']['name'] }}
x-bind:class="{ 'rotate-180': open }" /> </p>
</button> <x-filament::badge :color="$verification['summary']['company']['exists'] ? 'success' : 'warning'" size="xs" class="mt-1">
{{ $verification['summary']['company']['exists'] ? '✓ Lengkap' : '⚠ Belum diisi' }}
</x-filament::badge>
</div>
<div x-show="open" x-collapse class="border-t border-gray-200 dark:border-gray-700"> {{-- Media Summary --}}
<div class="divide-y divide-gray-100 dark:divide-gray-700 max-h-60 overflow-y-auto"> <div>
@foreach ($verification['reviews']['history'] as $review) <div class="flex items-center gap-2 mb-1">
<div class="p-3"> <x-heroicon-o-newspaper class="w-4 h-4 text-info-500" />
<div class="flex items-start justify-between gap-3"> <p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Media
<div class="flex-1 min-w-0"> </p>
<div class="flex items-center gap-2 mb-1"> </div>
<x-filament::badge :color="$review['decisionColor']" size="sm"> <p class="text-sm font-medium text-gray-900 dark:text-white truncate">
{{ $review['decisionLabel'] }} {{ $verification['summary']['media']['name'] }}
</x-filament::badge> </p>
<span class="text-xs text-gray-500 dark:text-gray-400"> <x-filament::badge :color="$verification['summary']['media']['exists'] ? 'success' : 'warning'" size="xs" class="mt-1">
oleh {{ $review['reviewerName'] }} {{ $verification['summary']['media']['exists'] ? '✓ Lengkap' : '⚠ Belum diisi' }}
</span> </x-filament::badge>
</div>
{{-- Journalists Summary --}}
<div>
<div class="flex items-center gap-2 mb-1">
<x-heroicon-o-identification class="w-4 h-4 text-info-500" />
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Jurnalis
</p>
</div>
<p class="text-sm font-medium text-gray-900 dark:text-white">
{{ $verification['summary']['journalists']['count'] }} jurnalis
</p>
<x-filament::badge :color="$verification['summary']['journalists']['exists'] ? 'success' : 'warning'" size="xs" class="mt-1">
{{ $verification['summary']['journalists']['exists'] ? '✓ Lengkap' : '⚠ Belum diisi' }}
</x-filament::badge>
</div>
</div>
{{-- Timeline Info --}}
<div class="grid grid-cols-2 gap-3 text-sm">
<div>
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Diajukan</p>
<p class="text-gray-700 dark:text-gray-300">
{{ $verification['timestamps']['submittedAt'] }}
</p>
<p class="text-xs text-gray-500">
{{ $verification['timestamps']['submittedAtHuman'] }}
</p>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Pembaruan
Terakhir
</p>
<p class="text-gray-700 dark:text-gray-300">
{{ $verification['timestamps']['updatedAt'] }}
</p>
<p class="text-xs text-gray-500">
{{ $verification['timestamps']['updatedAtHuman'] }}
</p>
</div>
</div>
{{-- Latest Review Note --}}
@if ($verification['reviews']['latest']['note'])
<div class="p-3 custom-rounded border {{ $verification['reviews']['latest']['classes'] }}">
<p
class="text-xs font-semibold uppercase mb-1 {{ $verification['reviews']['latest']['titleClasses'] }}">
Catatan Terakhir
</p>
<p class="text-sm text-gray-700 dark:text-gray-300 line-clamp-2">
{{ $verification['reviews']['latest']['note'] }}
</p>
<p class="text-xs text-gray-500 mt-1">
oleh {{ $verification['reviews']['latest']['reviewer'] }}
{{ $verification['reviews']['latest']['createdAtHuman'] }}
</p>
</div>
@endif
{{-- Review History Accordion --}}
@if ($verification['reviews']['count'] > 0)
<div x-data="{ open: false }"
class="border border-gray-200 dark:border-gray-700 custom-rounded overflow-hidden">
<button @click="open = !open"
class="w-full flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<div
class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300">
<x-heroicon-o-clock class="w-4 h-4" />
Riwayat Review ({{ $verification['reviews']['count'] }})
</div>
<x-heroicon-o-chevron-down
class="w-4 h-4 text-gray-500 transition-transform duration-200"
x-bind:class="{ 'rotate-180': open }" />
</button>
<div x-show="open" x-collapse class="border-t border-gray-200 dark:border-gray-700">
<div class="divide-y divide-gray-100 dark:divide-gray-700 max-h-60 overflow-y-auto">
@foreach ($verification['reviews']['history'] as $review)
<div class="p-3">
<div class="flex items-start justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<x-filament::badge :color="$review['decisionColor']" size="sm">
{{ $review['decisionLabel'] }}
</x-filament::badge>
<span class="text-xs text-gray-500 dark:text-gray-400">
oleh {{ $review['reviewerName'] }}
</span>
</div>
@if ($review['note'])
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
{{ $review['note'] }}
</p>
@endif
</div>
<div class="shrink-0 text-xs text-gray-400">
{{ $review['createdAt'] }}
</div> </div>
@if ($review['note'])
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
{{ $review['note'] }}
</p>
@endif
</div>
<div class="shrink-0 text-xs text-gray-400">
{{ $review['createdAt'] }}
</div> </div>
</div> </div>
</div> @endforeach
@endforeach </div>
</div> </div>
</div> </div>
</div>
@endif
</div>
<x-slot name="footer">
<div class="flex flex-wrap items-center gap-2">
{{-- View Detail Button --}}
@if ($verification['company']['detailUrl'])
<x-filament::button color="gray" size="sm" icon="heroicon-o-eye" tag="a"
:href="$verification['company']['detailUrl']">
{{ $verification['messages']['detail'] }}
</x-filament::button>
@endif
{{-- Action Buttons based on status --}}
@if ($verification['status']['isPending'])
{{ ($this->approveAction)(['id' => $verification['id']]) }}
{{ ($this->revisionAction)(['id' => $verification['id']]) }}
{{ ($this->rejectAction)(['id' => $verification['id']]) }}
@elseif($verification['status']['isRevision'])
<span
class="flex items-center gap-1 text-xs text-warning-600 dark:text-warning-400 font-medium italic">
<x-heroicon-s-clock class="w-3 h-3 animate-pulse" />
{{ $verification['messages']['revision'] }}
</span>
@elseif($verification['status']['isApproved'])
<span
class="flex items-center gap-1 text-xs text-success-600 dark:text-success-400 font-medium">
<x-heroicon-s-check-circle class="w-3 h-3" />
{{ $verification['messages']['approved'] }}
</span>
@elseif($verification['status']['isRejected'])
<span
class="flex items-center gap-1 text-xs text-danger-600 dark:text-danger-400 font-medium">
<x-heroicon-s-x-circle class="w-3 h-3" />
{{ $verification['messages']['rejected'] }}
</span>
@endif @endif
</div> </div>
</x-slot>
</x-filament::section>
@empty
<div class="col-span-full">
<x-filament::empty-state
icon="heroicon-o-inbox"
:heading="$this->getEmptyStateHeading()"
:description="$this->getEmptyStateDescription()"
icon-color="gray">
</x-filament::empty-state>
</div>
@endforelse
</div>
{{-- Infinite Scroll Sentinel --}} <x-slot name="footer">
@if ($this->verifications->hasMorePages()) <div class="flex flex-wrap items-center gap-2">
<div x-intersect="$wire.loadMore()" class="py-6 flex justify-center"> {{-- View Detail Button --}}
<x-filament::loading-indicator class="w-8 h-8 text-primary-500" /> @can('View:AdminVerification')
@if ($verification['company']['detailUrl'])
{{ ($this->viewAction)(['id' => $verification['id']]) }}
@endif
@endcan
{{-- Action Buttons based on status --}}
@if ($verification['status']['isPending'])
@canAny(['Approve:AdminVerification', 'Revision:AdminVerification',
'Reject:AdminVerification'])
@can('Approve:AdminVerification')
{{ ($this->approveAction)(['id' => $verification['id']]) }}
@endcan
@can('Revision:AdminVerification')
{{ ($this->revisionAction)(['id' => $verification['id']]) }}
@endcan
@can('Reject:AdminVerification')
{{ ($this->rejectAction)(['id' => $verification['id']]) }}
@endcan
@endcanAny
@elseif($verification['status']['isRevision'])
<span
class="flex items-center gap-1 text-xs text-warning-600 dark:text-warning-400 font-medium italic">
<x-heroicon-s-clock class="w-3 h-3 animate-pulse" />
{{ $verification['messages']['revision'] }}
</span>
@elseif($verification['status']['isApproved'])
<span
class="flex items-center gap-1 text-xs text-success-600 dark:text-success-400 font-medium">
<x-heroicon-s-check-circle class="w-3 h-3" />
{{ $verification['messages']['approved'] }}
</span>
@elseif($verification['status']['isRejected'])
<span
class="flex items-center gap-1 text-xs text-danger-600 dark:text-danger-400 font-medium">
<x-heroicon-s-x-circle class="w-3 h-3" />
{{ $verification['messages']['rejected'] }}
</span>
@endif
</div>
</x-slot>
</x-filament::section>
@empty
<div class="col-span-full">
<x-filament::empty-state icon="heroicon-o-inbox" :heading="$this->getEmptyStateHeading()" :description="$this->getEmptyStateDescription()"
icon-color="gray">
</x-filament::empty-state>
</div>
@endforelse
</div> </div>
@endif
<x-filament-actions::modals /> {{-- Infinite Scroll Sentinel --}}
@if ($this->verifications->hasMorePages())
<div x-intersect="$wire.loadMore()" class="py-6 flex justify-center">
<x-filament::loading-indicator class="w-8 h-8 text-primary-500" />
</div>
@endif
<x-filament-actions::modals />
</x-filament::section>
</x-filament-panels::page> </x-filament-panels::page>