refactor: Remove AdminVerification page and restructure verification actions into separate classes for improved organization and maintainability.

This commit is contained in:
Yoga Pangestu 2026-03-17 14:04:10 +07:00
parent 938680e2cc
commit c600ee131b
7 changed files with 535 additions and 501 deletions

View File

@ -1,340 +0,0 @@
<?php
namespace App\Filament\Pages;
use App\Enums\DecisionAdmin;
use App\Enums\VerificationStatus;
use App\Filament\Resources\Manage\Partners\Pages\ViewPartner;
use App\Filament\Support\CheerfulNotification;
use App\Models\User;
use App\Models\VerificationRequest;
use App\Models\VerificationReview;
use App\Notifications\BroadcastNotification;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Pages\Page;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use UnitEnum;
class AdminVerification extends Page implements HasActions, HasForms
{
use HasPageShield, InteractsWithActions, InteractsWithForms;
public string $filterStatus = 'all';
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedUserGroup;
protected static ?string $navigationLabel = 'Verifikasi Rekanan';
protected static ?int $navigationSort = 6;
protected static ?string $slug = 'manage/admin-verification';
protected static ?string $title = 'Verifikasi Rekanan';
protected string $view = 'filament.pages.admin-verification';
public static function getNavigationBadge(): ?string
{
return VerificationRequest::query()->pending()->count();
}
public function getVerifications(): LengthAwarePaginator
{
$query = VerificationRequest::with([
'company.user',
'company.partnerMedia',
'company.partnerMedia.journalists',
'submittedBy',
'reviews.reviewer',
])
->latest('updated_at');
// Apply status filter
if ($this->filterStatus !== 'all') {
$query->where('status', $this->filterStatus);
}
return $query->paginate(12);
}
public function setFilter(string $status): void
{
$this->filterStatus = $status;
}
public function getStatusCounts(): array
{
return [
'all' => VerificationRequest::count(),
'pending' => VerificationRequest::pending()->count(),
'revision' => VerificationRequest::needRevision()->count(),
'approved' => VerificationRequest::approved()->count(),
'rejected' => VerificationRequest::rejected()->count(),
];
}
/**
* Get company name from verification request
*/
public function getCompanyName(VerificationRequest $request): string
{
return $request->company?->name ?? 'Data tidak ditemukan';
}
/**
* Get detail URL for the company
*/
public function getDetailUrl(VerificationRequest $request): ?string
{
if (! $request->company || ! $request->company->user_id) {
return null;
}
return ViewPartner::getUrl(['record' => $request->company->user_id]);
}
/**
* Get verification summary data
*/
public function getVerificationSummary(VerificationRequest $request): array
{
$company = $request->company;
$media = $company?->partnerMedia;
$journalists = $media?->journalists ?? collect();
return [
'company' => [
'name' => $company?->name ?? 'Belum diisi',
'email' => $company?->email ?? '-',
'exists' => $company !== null,
],
'media' => [
'name' => $media?->name ?? 'Belum diisi',
'type' => $media?->type?->getLabel() ?? '-',
'exists' => $media !== null,
],
'journalists' => [
'count' => $journalists->count(),
'names' => $journalists->pluck('name')->take(3)->toArray(),
'exists' => $journalists->count() > 0,
],
];
}
/**
* Get card data for verification request
*/
public function getCardData(VerificationRequest $verification): array
{
$latestReview = $verification->reviews->first();
$summary = $this->getVerificationSummary($verification);
$status = $verification->status;
return [
'companyName' => $this->getCompanyName($verification),
'detailUrl' => $this->getDetailUrl($verification),
'status' => $status,
'statusLabel' => $status->getLabel(),
'statusColor' => $status->getColor(),
'summary' => $summary,
'latestReview' => $latestReview,
'latestReviewNote' => $latestReview?->note,
'latestReviewDecision' => $latestReview?->decision,
'latestReviewDecisionLabel' => $latestReview?->decision?->getLabel(),
'latestReviewDecisionColor' => $latestReview?->decision?->getColor(),
'latestReviewReviewer' => $latestReview?->reviewer?->name ?? 'Admin',
'latestReviewCreatedAt' => $latestReview?->created_at,
'submittedBy' => $verification->submittedBy?->name ?? 'Unknown',
'reviewsCount' => $verification->reviews->count(),
'canApprove' => $status === VerificationStatus::PENDING,
'isPending' => $status === VerificationStatus::PENDING,
'isRevision' => $status === VerificationStatus::NEED_REVISION,
'isApproved' => $status === VerificationStatus::APPROVED,
'isRejected' => $status === VerificationStatus::REJECTED,
];
}
/**
* Approve verification action
*/
public function approveAction(): Action
{
return Action::make('approve')
->label('Setujui')
->color('success')
->icon('heroicon-o-check')
->size('sm')
->requiresConfirmation()
->modalHeading('Setujui Verifikasi')
->modalDescription('Apakah Anda yakin ingin menyetujui verifikasi ini? Pastikan data sudah benar dan lengkap.')
->modalSubmitActionLabel('Ya, Setujui')
->action(function (array $arguments, array $data): void {
$this->processReview($arguments['id'], DecisionAdmin::APPROVED, $data['note'] ?? null);
});
}
/**
* Revision verification action
*/
public function revisionAction(): Action
{
return Action::make('revision')
->label('Minta Revisi')
->color('warning')
->icon('heroicon-o-pencil-square')
->size('sm')
->modalHeading('Minta Revisi')
->modalDescription('Pengguna akan diminta untuk memperbaiki data sesuai catatan yang diberikan.')
->schema([
Textarea::make('note')
->label('Catatan Revisi')
->required()
->placeholder('Jelaskan bagian yang perlu diperbaiki...')
->autocomplete(false)
->autofocus(),
])
->action(function (array $arguments, array $data): void {
$this->processReview($arguments['id'], DecisionAdmin::NEED_REVISION, $data['note']);
})
->modalWidth(Width::Large);
}
/**
* Reject verification action
*/
public function rejectAction(): Action
{
return Action::make('reject')
->label('Tolak')
->color('danger')
->icon('heroicon-o-x-mark')
->size('sm')
->requiresConfirmation()
->modalHeading('Tolak Verifikasi')
->modalDescription('Pengajuan akan ditolak secara permanen. Pengguna tidak dapat mengajukan ulang untuk data ini.')
->modalSubmitActionLabel('Ya, Tolak')
->schema([
Textarea::make('note')
->label('Alasan Penolakan')
->required()
->placeholder('Jelaskan alasan pengajuan ditolak...')
->autocomplete(false)
->autofocus(),
])
->action(function (array $arguments, array $data): void {
$this->processReview($arguments['id'], DecisionAdmin::REJECTED, $data['note']);
});
}
/**
* Process review decision
*/
protected function processReview(int $requestId, DecisionAdmin $decision, ?string $note): void
{
$verificationRequest = VerificationRequest::with(['company.partnerMedia.journalists'])->find($requestId);
if (! $verificationRequest) {
CheerfulNotification::danger(
'Belum ketemu nih 🙌',
'Data verifikasi belum kami temukan. Coba sebentar lagi, ya.',
)
->send();
return;
}
try {
DB::beginTransaction();
// Create review record
VerificationReview::create([
'verification_request_id' => $verificationRequest->id,
'reviewer_id' => auth()->id(),
'decision' => $decision,
'note' => $note,
]);
// Update verification request status
$newStatus = match ($decision) {
DecisionAdmin::APPROVED => VerificationStatus::APPROVED,
DecisionAdmin::NEED_REVISION => VerificationStatus::NEED_REVISION,
DecisionAdmin::REJECTED => VerificationStatus::REJECTED,
};
$verificationRequest->update([
'status' => $newStatus,
]);
DB::commit();
$actionLabel = match ($decision) {
DecisionAdmin::APPROVED => 'disetujui',
DecisionAdmin::NEED_REVISION => 'diminta revisi',
DecisionAdmin::REJECTED => 'ditolak',
};
CheerfulNotification::success(
'Berhasil Diproses! ✨',
"Mantap! Verifikasi telah {$actionLabel} dan notifikasi sudah dikirim ke pengguna. Kerja bagus, Admin! 💪"
)
->send();
// Notify User
$partnerUser = $verificationRequest->company?->user;
if ($partnerUser) {
$statusTitle = match ($decision) {
DecisionAdmin::APPROVED => 'Horee! Verifikasi Disetujui 🎉',
DecisionAdmin::NEED_REVISION => 'Yah, Ada Yang Perlu Direvisi nih 📝',
DecisionAdmin::REJECTED => 'Maaf, Verifikasi Ditolak 😔',
};
$body = match ($decision) {
DecisionAdmin::APPROVED => 'Yeay! Pengajuan verifikasi perusahaan Anda telah disetujui. Sekarang Anda sudah resmi terverifikasi dan bekerja sama dengan kami! 🎊',
DecisionAdmin::NEED_REVISION => 'Ayo sedikit lagi! Admin meminta beberapa perbaikan pada pengajuan Anda agar bisa segera disetujui. 💪',
DecisionAdmin::REJECTED => 'Yah, maaf banget... pengajuan verifikasi perusahaan Anda belum bisa kami terima saat ini. 💔',
};
if ($note) {
$body .= "\n\nCatatan Admin: {$note}";
}
$partnerUser->notify(new BroadcastNotification([
'title' => $statusTitle,
'body' => $body,
'action' => [
Action::make('view')
->label('Lihat')
->url(Verification::getUrl()),
],
]));
}
} catch (\Throwable $e) {
DB::rollBack();
Log::error('Verification processing failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'request_id' => $requestId,
'decision' => $decision->value,
]);
CheerfulNotification::danger(
'Terjadi Kesalahan ⚠️',
'Proses verifikasi gagal karena kesalahan sistem. Tim kami sudah mencatat masalah ini. Silakan coba lagi nanti.',
)
->send();
}
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Filament\Pages\AdminVerification\Actions;
use App\Enums\DecisionAdmin;
use App\Filament\Pages\AdminVerification\Actions\Concerns\CanProcessReview;
use Filament\Actions\Action;
class ApproveAction extends Action
{
use CanProcessReview;
public static function getDefaultName(): ?string
{
return 'approve';
}
protected function setUp(): void
{
parent::setUp();
$this->label('Setujui')
->color('success')
->icon('heroicon-o-check')
->size('sm')
->requiresConfirmation()
->modalHeading('Setujui Verifikasi')
->modalDescription('Apakah Anda yakin ingin menyetujui verifikasi ini? Pastikan data sudah benar dan lengkap.')
->modalSubmitActionLabel('Ya, Setujui')
->action(function (array $arguments): void {
$this->processReview($arguments['id'], DecisionAdmin::APPROVED, null);
});
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Filament\Pages\AdminVerification\Actions\Concerns;
use App\Enums\DecisionAdmin;
use App\Enums\VerificationStatus;
use App\Filament\Pages\Verification;
use App\Filament\Support\CheerfulNotification;
use App\Models\VerificationRequest;
use App\Models\VerificationReview;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
trait CanProcessReview
{
protected function processReview(int $requestId, DecisionAdmin $decision, ?string $note): void
{
$verificationRequest = VerificationRequest::with(['company.partnerMedia.journalists'])->find($requestId);
if (! $verificationRequest) {
CheerfulNotification::danger(
'Belum ketemu nih 🙌',
'Data verifikasi belum kami temukan. Coba sebentar lagi, ya.',
)
->send();
return;
}
try {
DB::beginTransaction();
// Create review record
VerificationReview::create([
'verification_request_id' => $verificationRequest->id,
'reviewer_id' => auth()->id(),
'decision' => $decision,
'note' => $note,
]);
// Update verification request status
$newStatus = match ($decision) {
DecisionAdmin::APPROVED => VerificationStatus::APPROVED,
DecisionAdmin::NEED_REVISION => VerificationStatus::NEED_REVISION,
DecisionAdmin::REJECTED => VerificationStatus::REJECTED,
};
$verificationRequest->update([
'status' => $newStatus,
]);
DB::commit();
$actionLabel = match ($decision) {
DecisionAdmin::APPROVED => 'disetujui',
DecisionAdmin::NEED_REVISION => 'diminta revisi',
DecisionAdmin::REJECTED => 'ditolak',
};
CheerfulNotification::success(
'Berhasil Diproses! ✨',
"Mantap! Verifikasi telah {$actionLabel} dan notifikasi sudah dikirim ke pengguna. Kerja bagus, Admin! 💪"
)
->send();
// Notify User
$partnerUser = $verificationRequest->company?->user;
if ($partnerUser) {
$statusTitle = match ($decision) {
DecisionAdmin::APPROVED => 'Horee! Verifikasi Disetujui 🎉',
DecisionAdmin::NEED_REVISION => 'Yah, Ada Yang Perlu Direvisi nih 📝',
DecisionAdmin::REJECTED => 'Maaf, Verifikasi Ditolak 😔',
};
$body = match ($decision) {
DecisionAdmin::APPROVED => 'Yeay! Pengajuan verifikasi perusahaan Anda telah disetujui. Sekarang Anda sudah resmi terverifikasi dan bekerja sama dengan kami! 🎊',
DecisionAdmin::NEED_REVISION => 'Ayo sedikit lagi! Admin meminta beberapa perbaikan pada pengajuan Anda agar bisa segera disetujui. 💪',
DecisionAdmin::REJECTED => 'Yah, maaf banget... pengajuan verifikasi perusahaan Anda belum bisa kami terima saat ini. 💔',
};
if ($note) {
$body .= "\n\nCatatan Admin: {$note}";
}
$partnerUser->notify(new BroadcastNotification([
'title' => $statusTitle,
'body' => $body,
'action' => [
Action::make('view')
->label('Lihat')
->url(Verification::getUrl()),
],
]));
}
} catch (\Throwable $e) {
DB::rollBack();
Log::error('Verification processing failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'request_id' => $requestId,
'decision' => $decision->value,
]);
CheerfulNotification::danger(
'Terjadi Kesalahan ⚠️',
'Proses verifikasi gagal karena kesalahan sistem. Tim kami sudah mencatat masalah ini. Silakan coba lagi nanti.',
)
->send();
}
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Filament\Pages\AdminVerification\Actions;
use App\Enums\DecisionAdmin;
use App\Filament\Pages\AdminVerification\Actions\Concerns\CanProcessReview;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
class RejectAction extends Action
{
use CanProcessReview;
public static function getDefaultName(): ?string
{
return 'reject';
}
protected function setUp(): void
{
parent::setUp();
$this->label('Tolak')
->color('danger')
->icon('heroicon-o-x-mark')
->size('sm')
->requiresConfirmation()
->modalHeading('Tolak Verifikasi')
->modalDescription('Pengajuan akan ditolak secara permanen. Pengguna tidak dapat mengajukan ulang untuk data ini.')
->modalSubmitActionLabel('Ya, Tolak')
->schema([
Textarea::make('note')
->label('Alasan Penolakan')
->required()
->placeholder('Jelaskan alasan pengajuan ditolak...')
->autocomplete(false)
->autofocus(),
])
->action(function (array $arguments, array $data): void {
$this->processReview($arguments['id'], DecisionAdmin::REJECTED, $data['note']);
});
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Filament\Pages\AdminVerification\Actions;
use App\Enums\DecisionAdmin;
use App\Filament\Pages\AdminVerification\Actions\Concerns\CanProcessReview;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Support\Enums\Width;
class RevisionAction extends Action
{
use CanProcessReview;
public static function getDefaultName(): ?string
{
return 'revision';
}
protected function setUp(): void
{
parent::setUp();
$this->label('Minta Revisi')
->color('warning')
->icon('heroicon-o-pencil-square')
->size('sm')
->modalHeading('Minta Revisi')
->modalDescription('Pengguna akan diminta untuk memperbaiki data sesuai catatan yang diberikan.')
->schema([
Textarea::make('note')
->label('Catatan Revisi')
->required()
->placeholder('Jelaskan bagian yang perlu diperbaiki...')
->autocomplete(false)
->autofocus(),
])
->action(function (array $arguments, array $data): void {
$this->processReview($arguments['id'], DecisionAdmin::NEED_REVISION, $data['note']);
})
->modalWidth(Width::Large);
}
}

View File

@ -0,0 +1,239 @@
<?php
namespace App\Filament\Pages\AdminVerification;
use App\Enums\VerificationStatus;
use App\Filament\Pages\AdminVerification\Actions\ApproveAction;
use App\Filament\Pages\AdminVerification\Actions\RejectAction;
use App\Filament\Pages\AdminVerification\Actions\RevisionAction;
use App\Filament\Resources\Manage\Partners\Pages\ViewPartner;
use App\Models\VerificationRequest;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Pages\Page;
use Filament\Support\Icons\Heroicon;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Livewire\Attributes\Computed;
use UnitEnum;
class Index extends Page implements HasActions, HasForms
{
use HasPageShield, InteractsWithActions, InteractsWithForms;
public string $filterStatus = '1';
public int $rowsToDisplay = 12;
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedUserGroup;
protected static ?string $navigationLabel = 'Verifikasi Rekanan';
protected static ?int $navigationSort = 6;
protected static ?string $slug = 'manage/admin-verification';
protected static ?string $title = 'Verifikasi Rekanan';
protected string $view = 'filament.pages.admin-verification';
public static function canAccess(array $parameters = []): bool
{
return auth()->user()->can('View:AdminVerification');
}
public static function getNavigationBadge(): ?string
{
return VerificationRequest::query()->pending()->count();
}
#[Computed]
public function verifications(): LengthAwarePaginator
{
$query = VerificationRequest::with([
'company.user',
'company.partnerMedia',
'company.partnerMedia.journalists',
'submittedBy',
'reviews.reviewer',
])
->latest('updated_at');
// Apply status filter
if ($this->filterStatus !== 'all') {
$query->where('status', $this->filterStatus);
}
return $query->paginate($this->rowsToDisplay)
->through(function (VerificationRequest $verification) {
$latestReview = $verification->reviews->first();
$status = $verification->status;
$company = $verification->company;
$media = $company?->partnerMedia;
$journalists = $media?->journalists ?? collect();
// Prepare Review classes
$reviewColor = $latestReview?->decision?->getColor() ?? 'gray';
$reviewClasses = match ($reviewColor) {
'success' => 'bg-success-50 dark:bg-success-500/10 border-success-200 dark:border-success-500/20',
'warning' => 'bg-warning-50 dark:bg-warning-500/10 border-warning-200 dark:border-warning-500/20',
'danger' => 'bg-danger-50 dark:bg-danger-500/10 border-danger-200 dark:border-danger-500/20',
'info' => 'bg-info-50 dark:bg-info-500/10 border-info-200 dark:border-info-500/20',
default => 'bg-gray-50 dark:bg-gray-500/10 border-gray-200 dark:border-gray-500/20',
};
$reviewTitleClasses = match ($reviewColor) {
'success' => 'text-success-700 dark:text-success-400',
'warning' => 'text-warning-700 dark:text-warning-400',
'danger' => 'text-danger-700 dark:text-danger-400',
'info' => 'text-info-700 dark:text-info-400',
default => 'text-gray-700 dark:text-gray-400',
};
return [
'id' => $verification->id,
'company' => [
'name' => $company?->name ?? 'Data tidak ditemukan',
'detailUrl' => ($company && $company->user_id) ? ViewPartner::getUrl(['record' => $company->user_id]) : null,
],
'submittedBy' => $verification->submittedBy?->name ?? 'Unknown',
'status' => [
'value' => $status,
'label' => $status->getLabel(),
'color' => $status->getColor(),
'isPending' => $status === VerificationStatus::PENDING,
'isRevision' => $status === VerificationStatus::NEED_REVISION,
'isApproved' => $status === VerificationStatus::APPROVED,
'isRejected' => $status === VerificationStatus::REJECTED,
],
'timestamps' => [
'submittedAt' => $verification->created_at->translatedFormat('l, d M Y H:i'),
'submittedAtHuman' => $verification->created_at->diffForHumans(),
'updatedAt' => $verification->updated_at->translatedFormat('l, d M Y H:i'),
'updatedAtHuman' => $verification->updated_at->diffForHumans(),
],
'summary' => [
'company' => [
'name' => $company?->name ?? 'Belum diisi',
'email' => $company?->email ?? '-',
'exists' => $company !== null,
],
'media' => [
'name' => $media?->name ?? 'Belum diisi',
'type' => $media?->type?->getLabel() ?? '-',
'exists' => $media !== null,
],
'journalists' => [
'count' => $journalists->count(),
'names' => $journalists->pluck('name')->take(3)->toArray(),
'exists' => $journalists->count() > 0,
],
],
'reviews' => [
'count' => $verification->reviews->count(),
'latest' => [
'note' => $latestReview?->note,
'reviewer' => $latestReview?->reviewer?->name ?? 'Admin',
'createdAtHuman' => $latestReview?->created_at?->diffForHumans(),
'classes' => $reviewClasses,
'titleClasses' => $reviewTitleClasses,
],
'history' => $verification->reviews->map(fn ($review) => [
'decisionLabel' => $review->decision->getLabel(),
'decisionColor' => $review->decision->getColor(),
'reviewerName' => $review->reviewer?->name ?? 'Admin',
'note' => $review->note,
'createdAt' => $review->created_at->translatedFormat('l, d M Y H:i'),
]),
],
];
});
}
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' => null,
'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();
}
}

View File

@ -1,123 +1,43 @@
<x-filament-panels::page>
@php
$counts = $this->getStatusCounts();
@endphp
<div class="mb-6">
<div class="flex flex-wrap gap-2">
<button wire:click="setFilter('all')" @class([
<div class="flex flex-wrap gap-2">
@foreach ($this->getFilters as $filter)
<button wire:click="setFilter('{{ $filter['id'] }}')" @class([
'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200',
'bg-primary-500 text-white shadow-md shadow-primary-500/20' =>
$filterStatus === 'all',
"bg-{$filter['color']}-500 text-white shadow-md shadow-{$filter['color']}-500/20" =>
$filterStatus === $filter['id'],
'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700' =>
$filterStatus !== 'all',
$filterStatus !== $filter['id'],
])>
<span>Semua</span>
@if ($filter['icon'])
<x-dynamic-component :component="$filter['icon']" class="w-4 h-4" />
@endif
<span>{{ $filter['label'] }}</span>
<span @class([
'px-2 py-0.5 text-xs rounded-full',
'bg-white/20' => $filterStatus === 'all',
'bg-gray-200 dark:bg-gray-700' => $filterStatus !== 'all',
])>{{ $counts['all'] }}</span>
'bg-white/20' => $filterStatus === $filter['id'],
"bg-{$filter['color']}-100 text-{$filter['color']}-700 dark:bg-{$filter['color']}-500/20 dark:text-{$filter['color']}-400" =>
$filterStatus !== $filter['id'],
])>{{ $filter['count'] ?? 0 }}</span>
</button>
<button wire:click="setFilter('{{ \App\Enums\VerificationStatus::PENDING->value }}')"
@class([
'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200',
'bg-warning-500 text-white shadow-md shadow-warning-500/20' =>
$filterStatus == \App\Enums\VerificationStatus::PENDING->value,
'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700' =>
$filterStatus != \App\Enums\VerificationStatus::PENDING->value,
])>
<x-heroicon-s-clock class="w-4 h-4" />
<span>Menunggu</span>
<span @class([
'px-2 py-0.5 text-xs rounded-full',
'bg-white/20' =>
$filterStatus == \App\Enums\VerificationStatus::PENDING->value,
'bg-warning-100 text-warning-700 dark:bg-warning-500/20 dark:text-warning-400' =>
$filterStatus != \App\Enums\VerificationStatus::PENDING->value,
])>{{ $counts['pending'] }}</span>
</button>
<button wire:click="setFilter('{{ \App\Enums\VerificationStatus::NEED_REVISION->value }}')"
@class([
'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200',
'bg-info-500 text-white shadow-md shadow-info-500/20' =>
$filterStatus == \App\Enums\VerificationStatus::NEED_REVISION->value,
'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700' =>
$filterStatus != \App\Enums\VerificationStatus::NEED_REVISION->value,
])>
<x-heroicon-s-pencil-square class="w-4 h-4" />
<span>Revisi</span>
<span @class([
'px-2 py-0.5 text-xs rounded-full',
'bg-white/20' =>
$filterStatus == \App\Enums\VerificationStatus::NEED_REVISION->value,
'bg-info-100 text-info-700 dark:bg-info-500/20 dark:text-info-400' =>
$filterStatus != \App\Enums\VerificationStatus::NEED_REVISION->value,
])>{{ $counts['revision'] }}</span>
</button>
<button wire:click="setFilter('{{ \App\Enums\VerificationStatus::APPROVED->value }}')"
@class([
'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200',
'bg-success-500 text-white shadow-md shadow-success-500/20' =>
$filterStatus == \App\Enums\VerificationStatus::APPROVED->value,
'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700' =>
$filterStatus != \App\Enums\VerificationStatus::APPROVED->value,
])>
<x-heroicon-s-check-circle class="w-4 h-4" />
<span>Disetujui</span>
<span @class([
'px-2 py-0.5 text-xs rounded-full',
'bg-white/20' =>
$filterStatus == \App\Enums\VerificationStatus::APPROVED->value,
'bg-success-100 text-success-700 dark:bg-success-500/20 dark:text-success-400' =>
$filterStatus != \App\Enums\VerificationStatus::APPROVED->value,
])>{{ $counts['approved'] }}</span>
</button>
<button wire:click="setFilter('{{ \App\Enums\VerificationStatus::REJECTED->value }}')"
@class([
'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200',
'bg-danger-500 text-white shadow-md shadow-danger-500/20' =>
$filterStatus == \App\Enums\VerificationStatus::REJECTED->value,
'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700' =>
$filterStatus != \App\Enums\VerificationStatus::REJECTED->value,
])>
<x-heroicon-s-x-circle class="w-4 h-4" />
<span>Ditolak</span>
<span @class([
'px-2 py-0.5 text-xs rounded-full',
'bg-white/20' =>
$filterStatus == \App\Enums\VerificationStatus::REJECTED->value,
'bg-danger-100 text-danger-700 dark:bg-danger-500/20 dark:text-danger-400' =>
$filterStatus != \App\Enums\VerificationStatus::REJECTED->value,
])>{{ $counts['rejected'] }}</span>
</button>
</div>
@endforeach
</div>
{{-- Verification Cards Grid --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
@forelse ($this->getVerifications() as $verification)
@php
$card = $this->getCardData($verification);
@endphp
@forelse ($this->verifications as $verification)
<x-filament::section class="h-full">
<x-slot name="heading">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<h3 class="font-bold text-lg text-gray-900 dark:text-white truncate"
title="{{ $card['companyName'] }}">
{{ $card['companyName'] }}
title="{{ $verification['company']['name'] }}">
{{ $verification['company']['name'] }}
</h3>
</div>
{{-- Status Badge --}}
<x-filament::badge :color="$card['statusColor']" size="lg">
{{ $card['statusLabel'] }}
<x-filament::badge :color="$verification['status']['color']" size="lg">
{{ $verification['status']['label'] }}
</x-filament::badge>
</div>
</x-slot>
@ -135,7 +55,7 @@ class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-500/20 flex items-c
<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">
{{ $card['submittedBy'] }}
{{ $verification['submittedBy'] }}
</p>
</div>
</div>
@ -150,10 +70,10 @@ class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-500/20 flex items-c
</p>
</div>
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
{{ $card['summary']['company']['name'] }}
{{ $verification['summary']['company']['name'] }}
</p>
<x-filament::badge :color="$card['summary']['company']['exists'] ? 'success' : 'warning'" size="xs" class="mt-1">
{{ $card['summary']['company']['exists'] ? '✓ Lengkap' : '⚠ Belum diisi' }}
<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>
@ -164,10 +84,10 @@ class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-500/20 flex items-c
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Media</p>
</div>
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
{{ $card['summary']['media']['name'] }}
{{ $verification['summary']['media']['name'] }}
</p>
<x-filament::badge :color="$card['summary']['media']['exists'] ? 'success' : 'warning'" size="xs" class="mt-1">
{{ $card['summary']['media']['exists'] ? '✓ Lengkap' : '⚠ Belum diisi' }}
<x-filament::badge :color="$verification['summary']['media']['exists'] ? 'success' : 'warning'" size="xs" class="mt-1">
{{ $verification['summary']['media']['exists'] ? '✓ Lengkap' : '⚠ Belum diisi' }}
</x-filament::badge>
</div>
@ -178,10 +98,10 @@ class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-500/20 flex items-c
<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">
{{ $card['summary']['journalists']['count'] }} jurnalis
{{ $verification['summary']['journalists']['count'] }} jurnalis
</p>
<x-filament::badge :color="$card['summary']['journalists']['exists'] ? 'success' : 'warning'" size="xs" class="mt-1">
{{ $card['summary']['journalists']['exists'] ? '✓ Lengkap' : '⚠ Belum diisi' }}
<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>
@ -191,10 +111,10 @@ class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-500/20 flex items-c
<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->created_at->translatedFormat('l, d M Y H:i') }}
{{ $verification['timestamps']['submittedAt'] }}
</p>
<p class="text-xs text-gray-500">
{{ $verification->created_at->diffForHumans() }}
{{ $verification['timestamps']['submittedAtHuman'] }}
</p>
</div>
<div>
@ -202,51 +122,33 @@ class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-500/20 flex items-c
Terakhir
</p>
<p class="text-gray-700 dark:text-gray-300">
{{ $verification->updated_at->translatedFormat('l, d M Y H:i') }}
{{ $verification['timestamps']['updatedAt'] }}
</p>
<p class="text-xs text-gray-500">
{{ $verification->updated_at->diffForHumans() }}
{{ $verification['timestamps']['updatedAtHuman'] }}
</p>
</div>
</div>
{{-- Latest Review Note --}}
@if ($card['latestReviewNote'])
@php
$reviewClasses = match ($card['latestReviewDecisionColor']) {
'success'
=> 'bg-success-50 dark:bg-success-500/10 border-success-200 dark:border-success-500/20',
'warning'
=> 'bg-warning-50 dark:bg-warning-500/10 border-warning-200 dark:border-warning-500/20',
'danger'
=> 'bg-danger-50 dark:bg-danger-500/10 border-danger-200 dark:border-danger-500/20',
'info' => 'bg-info-50 dark:bg-info-500/10 border-info-200 dark:border-info-500/20',
default => 'bg-gray-50 dark:bg-gray-500/10 border-gray-200 dark:border-gray-500/20',
};
$reviewTitleClasses = match ($card['latestReviewDecisionColor']) {
'success' => 'text-success-700 dark:text-success-400',
'warning' => 'text-warning-700 dark:text-warning-400',
'danger' => 'text-danger-700 dark:text-danger-400',
'info' => 'text-info-700 dark:text-info-400',
default => 'text-gray-700 dark:text-gray-400',
};
@endphp
<div class="p-3 rounded-lg border {{ $reviewClasses }}">
<p class="text-xs font-semibold uppercase mb-1 {{ $reviewTitleClasses }}">
@if ($verification['reviews']['latest']['note'])
<div class="p-3 rounded-lg 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">
{{ $card['latestReviewNote'] }}
{{ $verification['reviews']['latest']['note'] }}
</p>
<p class="text-xs text-gray-500 mt-1">
oleh {{ $card['latestReviewReviewer'] }}
{{ $card['latestReviewCreatedAt']->diffForHumans() }}
oleh {{ $verification['reviews']['latest']['reviewer'] }}
{{ $verification['reviews']['latest']['createdAtHuman'] }}
</p>
</div>
@endif
{{-- Review History Accordion --}}
@if ($card['reviewsCount'] > 0)
@if ($verification['reviews']['count'] > 0)
<div x-data="{ open: false }"
class="border border-gray-200 dark:border-gray-700 rounded-xl overflow-hidden">
<button @click="open = !open"
@ -254,7 +156,7 @@ class="w-full flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800
<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 ({{ $card['reviewsCount'] }})
Riwayat Review ({{ $verification['reviews']['count'] }})
</div>
<x-heroicon-o-chevron-down
class="w-4 h-4 text-gray-500 transition-transform duration-200"
@ -263,26 +165,26 @@ class="w-4 h-4 text-gray-500 transition-transform duration-200"
<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 as $review)
@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->decision->getColor()" size="sm">
{{ $review->decision->getLabel() }}
<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->reviewer?->name ?? 'Admin' }}
oleh {{ $review['reviewerName'] }}
</span>
</div>
@if ($review->note)
@if ($review['note'])
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
{{ $review->note }}
{{ $review['note'] }}
</p>
@endif
</div>
<div class="shrink-0 text-xs text-gray-400">
{{ $review->created_at->translatedFormat('l, d M Y H:i') }}
{{ $review['createdAt'] }}
</div>
</div>
</div>
@ -296,31 +198,31 @@ class="w-4 h-4 text-gray-500 transition-transform duration-200"
<x-slot name="footer">
<div class="flex flex-wrap items-center gap-2">
{{-- View Detail Button --}}
@if ($card['detailUrl'])
@if ($verification['company']['detailUrl'])
<x-filament::button color="gray" size="sm" icon="heroicon-m-eye" tag="a"
:href="$card['detailUrl']">
:href="$verification['company']['detailUrl']">
Detail
</x-filament::button>
@endif
{{-- Action Buttons based on status --}}
@if ($card['isPending'])
{{ ($this->approveAction)(['id' => $verification->id]) }}
{{ ($this->revisionAction)(['id' => $verification->id]) }}
{{ ($this->rejectAction)(['id' => $verification->id]) }}
@elseif($card['isRevision'])
@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" />
Menunggu revisi dari pengguna...
</span>
@elseif($card['isApproved'])
@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" />
Verifikasi selesai
</span>
@elseif($card['isRejected'])
@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" />
@ -339,10 +241,10 @@ class="flex items-center gap-1 text-xs text-danger-600 dark:text-danger-400 font
@endforelse
</div>
{{-- Pagination --}}
@if ($this->getVerifications()->hasPages())
<div class="mt-6">
{{ $this->getVerifications()->links() }}
{{-- 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