Refactor verification process and UI updates

- Updated JournalistResource to conditionally display actions based on user roles and verification status.
- Modified ManageJournalists page to hide header actions if verification request status is not NEED_REVISION.
- Changed CategoryResource navigation label from 'Kategori Berita' to 'Kategori'.
- Adjusted ManageCategories page to reflect the new category label and modal headings.
- Refactored Company model to change verificationRequest relationship from MorphOne to HasOne.
- Removed verificationRequest method from Journalist and PartnerMedia models.
- Updated VerificationRequest model to change verifiable relationship from MorphTo to BelongsTo.
- Introduced VerificationService to handle verification logic, including data completeness checks and submission processes.
- Created a new migration to adjust the verification_requests table structure.
- Enhanced admin verification view to display detailed verification progress and review history.
- Updated company and media forms to conditionally show the save button based on verification request status.
This commit is contained in:
Yoga Pangestu 2026-01-08 13:55:37 +07:00
parent bd122f77b9
commit 69c86e5d3c
18 changed files with 907 additions and 519 deletions

View File

@ -4,10 +4,9 @@
use App\Enums\DecisionAdmin;
use App\Enums\VerificationStatus;
use App\Filament\Resources\Manage\Journalists\Pages\ManageJournalists;
use App\Filament\Resources\Manage\Partners\Pages\ViewPartner;
use App\Models\VerificationRequest;
use App\Models\VerificationReview;
use App\Services\VerificationService;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Actions\Action;
@ -19,7 +18,6 @@
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Auth;
use UnitEnum;
class AdminVerification extends Page implements HasActions, HasForms
@ -40,6 +38,13 @@ class AdminVerification extends Page implements HasActions, HasForms
public string $filterStatus = 'all';
protected VerificationService $verificationService;
public function boot(): void
{
$this->verificationService = app(VerificationService::class);
}
public static function getNavigationBadge(): ?string
{
return VerificationRequest::query()->pending()->count();
@ -48,7 +53,9 @@ public static function getNavigationBadge(): ?string
public function getVerifications(): LengthAwarePaginator
{
$query = VerificationRequest::with([
'verifiable',
'company.user',
'company.partnerMedia',
'company.partnerMedia.journalists',
'submittedBy',
'reviews.reviewer',
])
@ -79,52 +86,64 @@ public function getStatusCounts(): array
}
/**
* Get the entity type label
* Get company name from verification request
*/
public function getEntityTypeLabel(VerificationRequest $request): string
public function getCompanyName(VerificationRequest $request): string
{
$type = class_basename($request->verifiable_type);
return match ($type) {
'Company' => 'Perusahaan',
'PartnerMedia' => 'Media',
'Journalist' => 'Jurnalis',
default => $type,
};
return $this->verificationService->getCompanyName($request);
}
/**
* Get the entity name
*/
public function getEntityName(VerificationRequest $request): string
{
$entity = $request->verifiable;
if (! $entity) {
return 'Data tidak ditemukan';
}
return $entity->name ?? 'Tanpa Nama';
}
/**
* Get detail URL based on entity type
* Get detail URL for the company
*/
public function getDetailUrl(VerificationRequest $request): ?string
{
$entity = $request->verifiable;
$type = class_basename($request->verifiable_type);
if (! $entity) {
if (! $request->company || ! $request->company->user_id) {
return null;
}
return match ($type) {
'Company' => ViewPartner::getUrl(['record' => $entity->user_id]),
'PartnerMedia' => ViewPartner::getUrl(['record' => $entity->company?->user_id]),
'Journalist' => ManageJournalists::getUrl(),
default => null,
};
return ViewPartner::getUrl(['record' => $request->company->user_id]);
}
/**
* Get verification summary data
*/
public function getVerificationSummary(VerificationRequest $request): array
{
return $this->verificationService->getVerificationSummary($request);
}
/**
* 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,
];
}
/**
@ -138,14 +157,9 @@ public function approveAction(): Action
->icon('heroicon-o-check')
->size('sm')
->requiresConfirmation()
->modalHeading('Setujui Verifikasi')
->modalDescription('Apakah Anda yakin ingin menyetujui verifikasi ini?')
->modalHeading('Setujui Verifikasi Lengkap')
->modalDescription('Apakah Anda yakin ingin menyetujui verifikasi ini? Status akan diterapkan pada Perusahaan, Media, dan semua Jurnalis sekaligus.')
->modalSubmitActionLabel('Ya, Setujui')
->schema([
Textarea::make('note')
->label('Catatan (Opsional)')
->placeholder('Tambahkan catatan jika diperlukan...'),
])
->action(function (array $arguments, array $data) {
$this->processReview($arguments['id'], DecisionAdmin::APPROVED, $data['note'] ?? null);
});
@ -163,11 +177,11 @@ public function revisionAction(): Action
->size('sm')
->modalHeading('Minta Revisi')
->modalDescription('User akan diminta untuk memperbaiki data sesuai catatan yang diberikan.')
->form([
->schema([
Textarea::make('note')
->label('Catatan Revisi')
->required()
->placeholder('Jelaskan bagian yang perlu diperbaiki...')
->placeholder('Jelaskan bagian yang perlu diperbaiki (Perusahaan, Media, atau Jurnalis)...')
->helperText('Catatan ini akan ditampilkan kepada user.'),
])
->action(function (array $arguments, array $data) {
@ -186,10 +200,10 @@ public function rejectAction(): Action
->icon('heroicon-o-x-mark')
->size('sm')
->requiresConfirmation()
->modalHeading('Tolak Verifikasi')
->modalDescription('Pengajuan akan ditolak secara permanen. User tidak dapat mengajukan ulang untuk data ini.')
->modalHeading('Tolak Verifikasi Lengkap')
->modalDescription('Pengajuan akan ditolak secara permanen. Status REJECTED akan diterapkan pada Perusahaan, Media, dan semua Jurnalis. User tidak dapat mengajukan ulang untuk data ini.')
->modalSubmitActionLabel('Ya, Tolak')
->form([
->schema([
Textarea::make('note')
->label('Alasan Penolakan')
->required()
@ -206,7 +220,7 @@ public function rejectAction(): Action
*/
protected function processReview(int $requestId, DecisionAdmin $decision, ?string $note): void
{
$verificationRequest = VerificationRequest::find($requestId);
$verificationRequest = VerificationRequest::with(['company.partnerMedia.journalists'])->find($requestId);
if (! $verificationRequest) {
Notification::make()
@ -218,47 +232,32 @@ protected function processReview(int $requestId, DecisionAdmin $decision, ?strin
return;
}
// Create review record
VerificationReview::create([
'verification_request_id' => $verificationRequest->id,
'reviewer_id' => Auth::id(),
'decision' => $decision,
'note' => $note,
]);
try {
$this->verificationService->processReview($verificationRequest, $decision, $note);
// Update verification request status
$newStatus = match ($decision) {
DecisionAdmin::APPROVED => VerificationStatus::APPROVED,
DecisionAdmin::NEED_REVISION => VerificationStatus::NEED_REVISION,
DecisionAdmin::REJECTED => VerificationStatus::REJECTED,
};
$actionLabel = match ($decision) {
DecisionAdmin::APPROVED => 'disetujui',
DecisionAdmin::NEED_REVISION => 'diminta revisi',
DecisionAdmin::REJECTED => 'ditolak',
};
$verificationRequest->update([
'status' => $newStatus,
]);
$color = match ($decision) {
DecisionAdmin::APPROVED => 'success',
DecisionAdmin::NEED_REVISION => 'info',
DecisionAdmin::REJECTED => 'danger',
};
// Update the related entity status if needed
$entity = $verificationRequest->verifiable;
if ($entity && method_exists($entity, 'update')) {
$entity->update(['status' => $newStatus]);
Notification::make()
->title('Berhasil')
->body("Verifikasi lengkap telah {$actionLabel}. Status telah diterapkan pada Perusahaan, Media, dan semua Jurnalis.")
->color($color)
->send();
} catch (\Exception $e) {
Notification::make()
->title('Error')
->body('Terjadi kesalahan saat memproses verifikasi: '.$e->getMessage())
->danger()
->send();
}
$actionLabel = match ($decision) {
DecisionAdmin::APPROVED => 'disetujui',
DecisionAdmin::NEED_REVISION => 'diminta revisi',
DecisionAdmin::REJECTED => 'ditolak',
};
$color = match ($decision) {
DecisionAdmin::APPROVED => 'success',
DecisionAdmin::NEED_REVISION => 'info',
DecisionAdmin::REJECTED => 'danger',
};
Notification::make()
->title('Berhasil')
->body("Verifikasi telah {$actionLabel}.")
->color($color)
->send();
}
}

View File

@ -3,6 +3,7 @@
namespace App\Filament\Pages;
use App\Models\Company;
use App\Models\VerificationRequest;
use Asmit\FilamentUpload\Forms\Components\AdvancedFileUpload;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
@ -37,6 +38,8 @@ class CustomCompany extends Page
public ?Company $company = null;
public ?VerificationRequest $verificationRequest = null;
public array $data = [];
public function mount(): void
@ -47,6 +50,10 @@ public function mount(): void
return;
}
$this->verificationRequest = $company->verificationRequest()
->latest()
->first();
$this->company = $company;
$this->form->fill($company->toArray());

View File

@ -6,6 +6,7 @@
use App\Enums\MediaType;
use App\Models\Company;
use App\Models\PartnerMedia;
use App\Models\VerificationRequest;
use Asmit\FilamentUpload\Forms\Components\AdvancedFileUpload;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
@ -45,6 +46,8 @@ class CustomMedia extends Page
public ?PartnerMedia $media = null;
public ?VerificationRequest $verificationRequest = null;
public bool $restricted = false;
public array $data = [];
@ -65,6 +68,10 @@ public function mount(): void
return;
}
$this->verificationRequest = $this->company->verificationRequest()
->latest()
->first();
$this->media = $media;
$this->form->fill($media->toArray());

View File

@ -2,13 +2,10 @@
namespace App\Filament\Pages;
use App\Enums\DecisionAdmin;
use App\Enums\VerificationStatus;
use App\Filament\Resources\Manage\Journalists\Pages\ManageJournalists;
use App\Models\Company;
use App\Models\Journalist;
use App\Models\PartnerMedia;
use App\Models\VerificationRequest;
use App\Services\VerificationService;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Actions\Action;
@ -18,7 +15,6 @@
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Collection;
use UnitEnum;
class CustomVerification extends Page implements HasActions, HasForms
@ -39,39 +35,22 @@ class CustomVerification extends Page implements HasActions, HasForms
public ?Company $company = null;
public ?PartnerMedia $partnerMedia = null;
public ?VerificationRequest $verificationRequest = null;
public ?Journalist $journalist = null;
protected VerificationService $verificationService;
public ?VerificationRequest $companyVerification = null;
public ?VerificationRequest $mediaVerification = null;
public ?VerificationRequest $journalistVerification = null;
public function boot(): void
{
$this->verificationService = app(VerificationService::class);
}
public function mount(): void
{
$this->company = auth()->user()->company;
$this->partnerMedia = auth()->user()->company?->partnerMedia;
$this->journalist = auth()->user()->company?->partnerMedia?->journalists?->first();
// Load verification requests with reviews
// Load unified verification request
if ($this->company) {
$this->companyVerification = $this->company->verificationRequest()
->with(['reviews.reviewer'])
->latest()
->first();
}
if ($this->partnerMedia) {
$this->mediaVerification = $this->partnerMedia->verificationRequest()
->with(['reviews.reviewer'])
->latest()
->first();
}
if ($this->journalist) {
$this->journalistVerification = $this->journalist->verificationRequest()
$this->verificationRequest = $this->company->verificationRequest()
->with(['reviews.reviewer'])
->latest()
->first();
@ -79,345 +58,79 @@ public function mount(): void
}
/**
* Determine the current step based on status
* Get unified verification progress
*/
protected function resolveStep(?VerificationStatus $status, bool $hasData = false): int
public function getVerificationProgress(): array
{
if (! $hasData) {
return 1; // No data yet
}
$progress = $this->verificationService->getVerificationProgress(
$this->company,
$this->verificationRequest
);
return match ($status) {
VerificationStatus::PENDING,
VerificationStatus::NEED_REVISION => 2, // In review or needs revision
// Add additional data for view
$progress['missingDataButtons'] = $this->verificationService->getMissingDataButtons($this->company);
$progress['alert'] = $this->verificationService->getAlertData($this->verificationRequest);
$progress['lastSubmittedAt'] = $this->verificationRequest?->updated_at?->format('d M Y, H:i');
VerificationStatus::APPROVED,
VerificationStatus::REJECTED => 3, // Final decision
default => 1, // Draft / not submitted yet
};
return $progress;
}
/**
* Check if user can submit verification for entity
* Submit unified verification action
*/
protected function canSubmit($entity, ?VerificationRequest $verification): bool
public function submitVerificationAction(): Action
{
if (! $entity) {
return false;
}
$progress = $this->getVerificationProgress();
// No verification yet - can submit
if (! $verification) {
return true;
}
// Can resubmit only if status is NEED_REVISION
return $verification->status === VerificationStatus::NEED_REVISION;
}
/**
* Check if entity is approved
*/
protected function isApproved(?VerificationRequest $verification): bool
{
return $verification?->status === VerificationStatus::APPROVED;
}
/**
* Check if entity is rejected
*/
protected function isRejected(?VerificationRequest $verification): bool
{
return $verification?->status === VerificationStatus::REJECTED;
}
/**
* Check if entity is pending review
*/
protected function isPending(?VerificationRequest $verification): bool
{
return $verification?->status === VerificationStatus::PENDING;
}
/**
* Check if entity needs revision
*/
protected function needsRevision(?VerificationRequest $verification): bool
{
return $verification?->status === VerificationStatus::NEED_REVISION;
}
/**
* Get the latest review notes for an entity
*/
protected function getLatestReviewNotes(?VerificationRequest $verification): ?string
{
if (! $verification) {
return null;
}
$latestReview = $verification->reviews()
->whereIn('decision', [DecisionAdmin::NEED_REVISION, DecisionAdmin::REJECTED])
->latest()
->first();
return $latestReview?->note;
}
/**
* Get all reviews history for an entity
*/
protected function getReviewHistory(?VerificationRequest $verification): Collection
{
if (! $verification) {
return collect();
}
return $verification->reviews()
->with('reviewer')
->latest()
->get();
}
public function companyProgress(): array
{
$hasData = $this->company !== null;
$currentStep = $this->resolveStep($this->companyVerification?->status, $hasData);
return [
'title' => 'Perusahaan',
'description' => 'Progress verifikasi perusahaan',
'icon' => 'heroicon-o-building-office-2',
'currentStep' => $currentStep,
'entity' => $this->company,
'verification' => $this->companyVerification,
'canSubmit' => $this->canSubmit($this->company, $this->companyVerification),
'isApproved' => $this->isApproved($this->companyVerification),
'isRejected' => $this->isRejected($this->companyVerification),
'isPending' => $this->isPending($this->companyVerification),
'needsRevision' => $this->needsRevision($this->companyVerification),
'latestNotes' => $this->getLatestReviewNotes($this->companyVerification),
'reviewHistory' => $this->getReviewHistory($this->companyVerification),
'editUrl' => CustomCompany::getUrl(),
'steps' => [
[
'title' => 'Pengajuan',
'description' => $hasData ? 'Data perusahaan sudah diisi' : 'Lengkapi data perusahaan',
],
[
'title' => 'Verifikasi',
'description' => match ($this->companyVerification?->status) {
VerificationStatus::NEED_REVISION => 'Perlu perbaikan data',
VerificationStatus::PENDING => 'Sedang diverifikasi admin',
default => 'Menunggu pengajuan',
},
],
[
'title' => 'Selesai',
'description' => match ($this->companyVerification?->status) {
VerificationStatus::APPROVED => 'Perusahaan disetujui',
VerificationStatus::REJECTED => 'Perusahaan ditolak',
default => 'Menunggu keputusan',
},
],
],
];
}
public function mediaProgress(): array
{
$hasData = $this->partnerMedia !== null;
$currentStep = $this->resolveStep($this->mediaVerification?->status, $hasData);
return [
'title' => 'Media',
'description' => 'Progress verifikasi media',
'currentStep' => $currentStep,
'entity' => $this->partnerMedia,
'verification' => $this->mediaVerification,
'canSubmit' => $this->canSubmit($this->partnerMedia, $this->mediaVerification),
'isApproved' => $this->isApproved($this->mediaVerification),
'isRejected' => $this->isRejected($this->mediaVerification),
'isPending' => $this->isPending($this->mediaVerification),
'needsRevision' => $this->needsRevision($this->mediaVerification),
'latestNotes' => $this->getLatestReviewNotes($this->mediaVerification),
'reviewHistory' => $this->getReviewHistory($this->mediaVerification),
'editUrl' => CustomMedia::getUrl(),
'steps' => [
[
'title' => 'Pengajuan',
'description' => $hasData ? 'Data media sudah diisi' : 'Lengkapi data media',
],
[
'title' => 'Verifikasi',
'description' => match ($this->mediaVerification?->status) {
VerificationStatus::NEED_REVISION => 'Perlu perbaikan data',
VerificationStatus::PENDING => 'Sedang diverifikasi admin',
default => 'Menunggu pengajuan',
},
],
[
'title' => 'Selesai',
'description' => match ($this->mediaVerification?->status) {
VerificationStatus::APPROVED => 'Media disetujui',
VerificationStatus::REJECTED => 'Media ditolak',
default => 'Menunggu keputusan',
},
],
],
];
}
public function journalistProgress(): array
{
$hasData = $this->journalist !== null;
$currentStep = $this->resolveStep($this->journalistVerification?->status, $hasData);
return [
'title' => 'Jurnalis',
'description' => 'Progress verifikasi jurnalis',
'currentStep' => $currentStep,
'entity' => $this->journalist,
'verification' => $this->journalistVerification,
'canSubmit' => $this->canSubmit($this->journalist, $this->journalistVerification),
'isApproved' => $this->isApproved($this->journalistVerification),
'isRejected' => $this->isRejected($this->journalistVerification),
'isPending' => $this->isPending($this->journalistVerification),
'needsRevision' => $this->needsRevision($this->journalistVerification),
'latestNotes' => $this->getLatestReviewNotes($this->journalistVerification),
'reviewHistory' => $this->getReviewHistory($this->journalistVerification),
'editUrl' => ManageJournalists::getUrl(),
'steps' => [
[
'title' => 'Pengajuan',
'description' => $hasData ? 'Data jurnalis sudah diisi' : 'Lengkapi data jurnalis',
],
[
'title' => 'Verifikasi',
'description' => match ($this->journalistVerification?->status) {
VerificationStatus::NEED_REVISION => 'Perlu perbaikan data',
VerificationStatus::PENDING => 'Sedang diverifikasi admin',
default => 'Menunggu pengajuan',
},
],
[
'title' => 'Selesai',
'description' => match ($this->journalistVerification?->status) {
VerificationStatus::APPROVED => 'Jurnalis disetujui',
VerificationStatus::REJECTED => 'Jurnalis ditolak',
default => 'Menunggu keputusan',
},
],
],
];
}
/**
* Submit company verification action
*/
public function submitCompanyAction(): Action
{
return Action::make('submitCompany')
->label(fn () => $this->needsRevision($this->companyVerification) ? 'Kirim Ulang' : 'Ajukan Verifikasi')
return Action::make('submitVerification')
->label(fn () => $this->verificationRequest && $this->verificationRequest->status === VerificationStatus::NEED_REVISION
? 'Kirim Ulang'
: 'Ajukan Verifikasi')
->icon('heroicon-o-paper-airplane')
->color('primary')
->requiresConfirmation()
->modalHeading('Ajukan Verifikasi Perusahaan')
->modalDescription('Apakah Anda yakin ingin mengajukan data perusahaan untuk diverifikasi? Pastikan semua data sudah benar.')
->modalHeading('Ajukan Verifikasi Lengkap')
->modalDescription(function () use ($progress) {
if (! $progress['isDataComplete']) {
return 'Data belum lengkap. Silakan lengkapi terlebih dahulu: '.implode(', ', $progress['missingData']);
}
return 'Apakah Anda yakin ingin mengajukan verifikasi untuk Perusahaan, Media, dan Jurnalis? Pastikan semua data sudah benar dan lengkap.';
})
->modalSubmitActionLabel('Ya, Ajukan')
->visible(fn () => $this->canSubmit($this->company, $this->companyVerification))
->visible(fn () => $progress['canSubmit'])
->disabled(fn () => ! $progress['isDataComplete'])
->action(function () {
$this->submitVerification('company', $this->company, $this->companyVerification);
$this->submitVerification();
});
}
/**
* Submit media verification action
* Handle unified verification submission
*/
public function submitMediaAction(): Action
protected function submitVerification(): void
{
return Action::make('submitMedia')
->label(fn () => $this->needsRevision($this->mediaVerification) ? 'Kirim Ulang' : 'Ajukan Verifikasi')
->icon('heroicon-o-paper-airplane')
->color('primary')
->requiresConfirmation()
->modalHeading('Ajukan Verifikasi Media')
->modalDescription('Apakah Anda yakin ingin mengajukan data media untuk diverifikasi? Pastikan semua data sudah benar.')
->modalSubmitActionLabel('Ya, Ajukan')
->visible(fn () => $this->canSubmit($this->partnerMedia, $this->mediaVerification))
->action(function () {
$this->submitVerification('media', $this->partnerMedia, $this->mediaVerification);
});
}
$progress = $this->getVerificationProgress();
/**
* Submit journalist verification action
*/
public function submitJournalistAction(): Action
{
return Action::make('submitJournalist')
->label(fn () => $this->needsRevision($this->journalistVerification) ? 'Kirim Ulang' : 'Ajukan Verifikasi')
->icon('heroicon-o-paper-airplane')
->color('primary')
->requiresConfirmation()
->modalHeading('Ajukan Verifikasi Jurnalis')
->modalDescription('Apakah Anda yakin ingin mengajukan data jurnalis untuk diverifikasi? Pastikan semua data sudah benar.')
->modalSubmitActionLabel('Ya, Ajukan')
->visible(fn () => $this->canSubmit($this->journalist, $this->journalistVerification))
->action(function () {
$this->submitVerification('journalist', $this->journalist, $this->journalistVerification);
});
}
/**
* Handle verification submission
*/
protected function submitVerification(string $type, $entity, ?VerificationRequest $existingRequest): void
{
if (! $entity) {
// Validate data completeness
if (! $progress['isDataComplete']) {
Notification::make()
->title('Error')
->body('Data tidak ditemukan. Silakan lengkapi data terlebih dahulu.')
->title('Data Belum Lengkap')
->body('Silakan lengkapi data terlebih dahulu: '.implode(', ', $progress['missingData']))
->danger()
->send();
return;
}
if ($existingRequest && $existingRequest->status === VerificationStatus::NEED_REVISION) {
// Resubmit - update existing request
$existingRequest->update([
'status' => VerificationStatus::PENDING,
]);
$verification = $existingRequest;
} else {
// New submission
$verification = VerificationRequest::create([
'verifiable_type' => get_class($entity),
'verifiable_id' => $entity->id,
'submitted_by' => auth()->id(),
'status' => VerificationStatus::PENDING,
]);
}
// Update the local property
match ($type) {
'company' => $this->companyVerification = $verification->fresh(['reviews.reviewer']),
'media' => $this->mediaVerification = $verification->fresh(['reviews.reviewer']),
'journalist' => $this->journalistVerification = $verification->fresh(['reviews.reviewer']),
};
$entityName = match ($type) {
'company' => 'Perusahaan',
'media' => 'Media',
'journalist' => 'Jurnalis',
};
$this->verificationRequest = $this->verificationService->submitVerification(
$this->company,
$this->verificationRequest
);
Notification::make()
->title('Berhasil!')
->body("Data {$entityName} berhasil diajukan untuk diverifikasi.")
->body('Verifikasi lengkap (Perusahaan, Media, dan Jurnalis) berhasil diajukan untuk diverifikasi.')
->success()
->send();
}
@ -425,9 +138,7 @@ protected function submitVerification(string $type, $entity, ?VerificationReques
public function getViewData(): array
{
return [
'companyProgress' => $this->companyProgress(),
'mediaProgress' => $this->mediaProgress(),
'journalistProgress' => $this->journalistProgress(),
'verificationProgress' => $this->getVerificationProgress(),
];
}
}

View File

@ -238,9 +238,53 @@ public static function table(Table $table): Table
}
return $record;
})
->visible(function () {
$user = auth()->user();
if (! $user->hasRole('Perusahaan')) {
return true;
}
if (! $user->company?->partnerMedia) {
return false;
}
$verificationRequest = $user->company
->verificationRequest()
->latest()
->first();
if ($verificationRequest && $verificationRequest->status !== \App\Enums\VerificationStatus::NEED_REVISION) {
return false;
}
return true;
}),
DeleteAction::make(),
DeleteAction::make()
->visible(function () {
$user = auth()->user();
if (! $user->hasRole('Perusahaan')) {
return true;
}
if (! $user->company?->partnerMedia) {
return false;
}
$verificationRequest = $user->company
->verificationRequest()
->latest()
->first();
if ($verificationRequest && $verificationRequest->status !== \App\Enums\VerificationStatus::NEED_REVISION) {
return false;
}
return true;
}),
ForceDeleteAction::make(),

View File

@ -21,6 +21,14 @@ protected function getHeaderActions(): array
return [];
}
$verificationRequest = auth()->user()->company?->verificationRequest()
->latest()
->first();
if ($verificationRequest && $verificationRequest->status !== \App\Enums\VerificationStatus::NEED_REVISION) {
return [];
}
return [
CreateAction::make()
->label('Tambah')

View File

@ -34,7 +34,7 @@ class CategoryResource extends Resource
protected static string|BackedEnum|null $navigationIcon = Heroicon::ListBullet;
protected static ?string $navigationLabel = 'Kategori Berita';
protected static ?string $navigationLabel = 'Kategori';
protected static ?string $recordTitleAttribute = 'name';
@ -92,7 +92,7 @@ public static function table(Table $table): Table
])
->toolbarActions([
BulkActionGroup::make([
...DefaultBulkActions::make('kategori berita'),
...DefaultBulkActions::make('kategori'),
]),
])
->emptyStateIcon(Heroicon::ListBullet)

View File

@ -9,7 +9,7 @@
class ManageCategories extends ManageRecords
{
protected static ?string $title = 'Kategori Berita';
protected static ?string $title = 'Kategori';
protected static string $resource = CategoryResource::class;
@ -18,11 +18,11 @@ protected function getHeaderActions(): array
return [
CreateAction::make()
->label('Tambah')
->modalHeading('Tambah Kategori Berita')
->modalHeading('Tambah Kategori')
->modalSubmitActionLabel('Simpan')
->modalCancelActionLabel('Batal')
->extraModalFooterActions(fn (CreateAction $action): array => [
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
$action->makeModalSubmitAction('createAknother', arguments: ['another' => true])
->label('Simpan dan Tambah Lagi'),
])
->modalWidth(Width::Large),

View File

@ -8,7 +8,6 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
@ -41,8 +40,8 @@ public function journalists(): HasManyThrough
return $this->hasManyThrough(Journalist::class, PartnerMedia::class);
}
public function verificationRequest(): MorphOne
public function verificationRequest(): HasOne
{
return $this->morphOne(VerificationRequest::class, 'verifiable');
return $this->hasOne(VerificationRequest::class);
}
}

View File

@ -6,7 +6,6 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
@ -28,9 +27,4 @@ public function partnerMedia(): BelongsTo
{
return $this->belongsTo(PartnerMedia::class);
}
public function verificationRequest(): MorphOne
{
return $this->morphOne(VerificationRequest::class, 'verifiable');
}
}

View File

@ -10,7 +10,6 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
@ -68,9 +67,4 @@ public function cooperationMedias(): HasMany
{
return $this->hasMany(CooperationMedia::class);
}
public function verificationRequest(): MorphOne
{
return $this->morphOne(VerificationRequest::class, 'verifiable');
}
}

View File

@ -8,7 +8,6 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class VerificationRequest extends Model
@ -30,9 +29,9 @@ protected function pending(Builder $query): void
$query->where('status', VerificationStatus::PENDING);
}
public function verifiable(): MorphTo
public function company(): BelongsTo
{
return $this->morphTo();
return $this->belongsTo(Company::class);
}
public function submittedBy(): BelongsTo

View File

@ -0,0 +1,391 @@
<?php
namespace App\Services;
use App\Enums\DecisionAdmin;
use App\Enums\VerificationStatus;
use App\Models\Company;
use App\Models\VerificationRequest;
use App\Models\VerificationReview;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class VerificationService
{
/**
* Check if all required data is complete for verification
*/
public function isDataComplete(?Company $company): bool
{
return $company !== null
&& $company->partnerMedia !== null
&& $company->partnerMedia?->journalists !== null
&& $company->partnerMedia->journalists->count() > 0;
}
/**
* Get missing data items for verification
*/
public function getMissingData(?Company $company): array
{
$missing = [];
if (! $company) {
$missing[] = 'Data Perusahaan';
}
if ($company && ! $company->partnerMedia) {
$missing[] = 'Data Media';
}
if ($company && $company->partnerMedia && ($company->partnerMedia?->journalists === null || $company->partnerMedia?->journalists?->count() === 0)) {
$missing[] = 'Data Jurnalis (minimal 1)';
}
return $missing;
}
/**
* Check if user can submit verification
*/
public function canSubmit(?Company $company, ?VerificationRequest $verificationRequest): bool
{
if (! $this->isDataComplete($company)) {
return false;
}
// No verification yet - can submit
if (! $verificationRequest) {
return true;
}
// Can resubmit only if status is NEED_REVISION
return $verificationRequest->status === VerificationStatus::NEED_REVISION;
}
/**
* Determine the current step based on status
*/
public function resolveStep(?Company $company, ?VerificationRequest $verificationRequest): int
{
if (! $this->isDataComplete($company)) {
return 1; // Data belum lengkap
}
if (! $verificationRequest) {
return 1; // Belum submit
}
return match ($verificationRequest->status) {
VerificationStatus::PENDING,
VerificationStatus::NEED_REVISION => 2, // In review or needs revision
VerificationStatus::APPROVED,
VerificationStatus::REJECTED => 3, // Final decision
default => 1, // Draft / not submitted yet
};
}
/**
* Get the latest review notes
*/
public function getLatestReviewNotes(?VerificationRequest $verificationRequest): ?string
{
if (! $verificationRequest) {
return null;
}
$latestReview = $verificationRequest->reviews()
->whereIn('decision', [DecisionAdmin::NEED_REVISION, DecisionAdmin::REJECTED])
->latest()
->first();
return $latestReview?->note;
}
/**
* Get all reviews history
*/
public function getReviewHistory(?VerificationRequest $verificationRequest): Collection
{
if (! $verificationRequest) {
return collect();
}
return $verificationRequest->reviews()
->with('reviewer')
->latest()
->get();
}
/**
* Get unified verification progress data
*/
public function getVerificationProgress(?Company $company, ?VerificationRequest $verificationRequest): array
{
$currentStep = $this->resolveStep($company, $verificationRequest);
$isComplete = $this->isDataComplete($company);
$missingData = $this->getMissingData($company);
$status = $verificationRequest?->status;
return [
'currentStep' => $currentStep,
'isDataComplete' => $isComplete,
'missingData' => $missingData,
'canSubmit' => $this->canSubmit($company, $verificationRequest),
'verification' => $verificationRequest,
'status' => [
'label' => $status?->getLabel() ?? 'Belum Mengajukan',
'color' => $status?->getColor() ?? 'gray',
],
'isApproved' => $status === VerificationStatus::APPROVED,
'isRejected' => $status === VerificationStatus::REJECTED,
'isPending' => $status === VerificationStatus::PENDING,
'needsRevision' => $status === VerificationStatus::NEED_REVISION,
'latestNotes' => $this->getLatestReviewNotes($verificationRequest),
'reviewHistory' => $this->getReviewHistory($verificationRequest),
'company' => $company,
'partnerMedia' => $company?->partnerMedia,
'journalists' => $company?->partnerMedia?->journalists ?? collect(),
'steps' => $this->getSteps($verificationRequest, $isComplete, $missingData),
];
}
/**
* Get steps array for stepper
*/
protected function getSteps(?VerificationRequest $verificationRequest, bool $isComplete, array $missingData): array
{
$status = $verificationRequest?->status;
return [
[
'title' => 'Pengajuan',
'description' => $isComplete
? 'Data sudah lengkap, silakan ajukan verifikasi'
: 'Silakan lengkapi data',
],
[
'title' => 'Verifikasi',
'description' => match ($status) {
VerificationStatus::NEED_REVISION => 'Perlu perbaikan data',
VerificationStatus::PENDING => 'Sedang diverifikasi admin',
default => 'Menunggu pengajuan',
},
],
[
'title' => 'Selesai',
'description' => match ($status) {
VerificationStatus::APPROVED => 'Verifikasi disetujui',
VerificationStatus::REJECTED => 'Verifikasi ditolak',
default => 'Menunggu keputusan',
},
],
];
}
/**
* Submit unified verification
*/
public function submitVerification(Company $company, ?VerificationRequest $existingRequest): VerificationRequest
{
if ($existingRequest && $existingRequest->status === VerificationStatus::NEED_REVISION) {
// Resubmit - update existing request
$existingRequest->update([
'status' => VerificationStatus::PENDING,
]);
return $existingRequest->fresh(['reviews.reviewer']);
}
// New submission
return VerificationRequest::create([
'company_id' => $company->id,
'submitted_by' => Auth::id(),
'status' => VerificationStatus::PENDING,
])->load(['reviews.reviewer']);
}
/**
* Process review decision - unified for Company, Media, and Journalists
*/
public function processReview(VerificationRequest $verificationRequest, DecisionAdmin $decision, ?string $note): void
{
$verificationRequest->load(['company.partnerMedia.journalists']);
DB::beginTransaction();
try {
// 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,
]);
// Update Company status
$company = $verificationRequest->company;
if ($company) {
$company->update(['status' => $newStatus]);
// Update Media status
$media = $company->partnerMedia;
if ($media) {
$media->update(['status' => $newStatus]);
// Update all Journalists status
$media->journalists()->update(['status' => $newStatus]);
}
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* Get company name from verification request
*/
public function getCompanyName(VerificationRequest $request): string
{
return $request->company?->name ?? 'Data tidak ditemukan';
}
/**
* Get verification summary data for admin
*/
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 alert data for verification status
*/
public function getAlertData(?VerificationRequest $verificationRequest): ?array
{
if (! $verificationRequest) {
return null;
}
$status = $verificationRequest->status;
$latestNotes = $this->getLatestReviewNotes($verificationRequest);
if ($status === VerificationStatus::NEED_REVISION && $latestNotes) {
return [
'type' => 'revision',
'icon' => 'exclamation-triangle',
'title' => 'Catatan Revisi dari Admin',
'message' => $latestNotes,
'color' => 'warning',
];
}
if ($status === VerificationStatus::REJECTED && $latestNotes) {
return [
'type' => 'rejected',
'icon' => 'x-circle',
'title' => 'Pengajuan Ditolak',
'message' => $latestNotes,
'color' => 'danger',
];
}
if ($status === VerificationStatus::APPROVED) {
return [
'type' => 'approved',
'icon' => 'check-circle',
'title' => 'Verifikasi Diterima',
'message' => 'Verifikasi lengkap Anda telah diterima dan disetujui oleh admin. Seluruh data (Perusahaan, Media, dan Jurnalis) telah terverifikasi.',
'color' => 'success',
];
}
if ($status === VerificationStatus::PENDING) {
return [
'type' => 'pending',
'icon' => 'clock',
'title' => 'Menunggu Verifikasi',
'message' => 'Data Anda sedang dalam proses verifikasi oleh admin. Mohon tunggu.',
'color' => 'info',
'animate' => true,
];
}
return null;
}
/**
* Get missing data buttons configuration
*/
public function getMissingDataButtons(?Company $company): array
{
$buttons = [];
if (! $company) {
$buttons[] = [
'label' => 'Lengkapi Data Perusahaan',
'url' => \App\Filament\Pages\CustomCompany::getUrl(),
'icon' => 'building-office-2',
'color' => 'warning',
];
}
if ($company && ! $company->partnerMedia) {
$buttons[] = [
'label' => 'Lengkapi Data Media',
'url' => \App\Filament\Pages\CustomMedia::getUrl(),
'icon' => 'newspaper',
'color' => 'warning',
];
}
if ($company && $company->partnerMedia && ($company->partnerMedia->journalists === null || $company->partnerMedia->journalists->count() === 0)) {
$buttons[] = [
'label' => 'Lengkapi Data Jurnalis',
'url' => \App\Filament\Resources\Manage\Journalists\Pages\ManageJournalists::getUrl(),
'icon' => 'user-circle',
'color' => 'warning',
];
}
return $buttons;
}
}

View File

@ -1,6 +1,7 @@
<?php
use App\Enums\VerificationStatus;
use App\Models\Company;
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@ -15,7 +16,7 @@ public function up(): void
{
Schema::create('verification_requests', function (Blueprint $table) {
$table->id();
$table->morphs('verifiable');
$table->foreignIdFor(Company::class);
$table->foreignIdFor(User::class, 'submitted_by');
$table->enum('status', VerificationStatus::cases())->default(VerificationStatus::PENDING)->comment(VerificationStatus::comment());
$table->timestamp('created_at')->useCurrent();

View File

@ -1,5 +1,4 @@
<x-filament-panels::page>
{{-- Status Filter Tabs --}}
@php
$counts = $this->getStatusCounts();
@endphp
@ -103,39 +102,25 @@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
@forelse ($this->getVerifications() as $verification)
@php
$entityType = $this->getEntityTypeLabel($verification);
$entityName = $this->getEntityName($verification);
$detailUrl = $this->getDetailUrl($verification);
$latestReview = $verification->reviews->first();
$card = $this->getCardData($verification);
@endphp
<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">
<div class="flex items-center gap-2 mb-1">
{{-- Entity Type Badge --}}
@php
$typeColor = match ($entityType) {
'Perusahaan' => 'primary',
'Media' => 'info',
'Jurnalis' => 'success',
default => 'gray',
};
@endphp
<x-filament::badge :color="$typeColor" size="sm">
{{ $entityType }}
</x-filament::badge>
</div>
<h3 class="font-bold text-lg text-gray-900 dark:text-white truncate"
title="{{ $entityName }}">
{{ $entityName }}
title="{{ $card['companyName'] }}">
{{ $card['companyName'] }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
Perusahaan, Media & Jurnalis
</p>
</div>
{{-- Status Badge --}}
<x-filament::badge :color="$verification->status->getColor()" size="lg">
{{ $verification->status->getLabel() }}
<x-filament::badge :color="$card['statusColor']" size="lg">
{{ $card['statusLabel'] }}
</x-filament::badge>
</div>
</x-slot>
@ -153,11 +138,57 @@ 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">
{{ $verification->submittedBy?->name ?? 'Unknown' }}
{{ $card['submittedBy'] }}
</p>
</div>
</div>
{{-- Data Summary --}}
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
{{-- Company Summary --}}
<div>
<div class="flex items-center gap-2 mb-1">
<x-heroicon-o-building-office-2 class="w-4 h-4 text-primary-500" />
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Perusahaan
</p>
</div>
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
{{ $card['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>
</div>
{{-- Media Summary --}}
<div>
<div class="flex items-center gap-2 mb-1">
<x-heroicon-o-newspaper class="w-4 h-4 text-info-500" />
<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'] }}
</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>
</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">
{{ $card['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>
</div>
</div>
{{-- Timeline Info --}}
<div class="grid grid-cols-2 gap-3 text-sm">
<div>
@ -182,38 +213,42 @@ class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-500/20 flex items-c
</div>
{{-- Latest Review Note --}}
@if ($latestReview && $latestReview->note)
<div
class="p-3 rounded-lg border
{{ match ($latestReview->decision) {
\App\Enums\DecisionAdmin::APPROVED
@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',
\App\Enums\DecisionAdmin::NEED_REVISION
'warning'
=> 'bg-warning-50 dark:bg-warning-500/10 border-warning-200 dark:border-warning-500/20',
\App\Enums\DecisionAdmin::REJECTED
'danger'
=> 'bg-danger-50 dark:bg-danger-500/10 border-danger-200 dark:border-danger-500/20',
} }}">
<p
class="text-xs font-semibold uppercase mb-1
{{ match ($latestReview->decision) {
\App\Enums\DecisionAdmin::APPROVED => 'text-success-700 dark:text-success-400',
\App\Enums\DecisionAdmin::NEED_REVISION => 'text-warning-700 dark:text-warning-400',
\App\Enums\DecisionAdmin::REJECTED => 'text-danger-700 dark:text-danger-400',
} }}">
'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 }}">
Catatan Terakhir
</p>
<p class="text-sm text-gray-700 dark:text-gray-300 line-clamp-2">
{{ $latestReview->note }}
{{ $card['latestReviewNote'] }}
</p>
<p class="text-xs text-gray-500 mt-1">
oleh {{ $latestReview->reviewer?->name ?? 'Admin' }}
{{ $latestReview->created_at->diffForHumans() }}
oleh {{ $card['latestReviewReviewer'] }}
{{ $card['latestReviewCreatedAt']->diffForHumans() }}
</p>
</div>
@endif
{{-- Review History Accordion --}}
@if ($verification->reviews->count() > 0)
@if ($card['reviewsCount'] > 0)
<div x-data="{ open: false }"
class="border border-gray-200 dark:border-gray-700 rounded-xl overflow-hidden">
<button @click="open = !open"
@ -221,7 +256,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 ({{ $verification->reviews->count() }})
Riwayat Review ({{ $card['reviewsCount'] }})
</div>
<x-heroicon-o-chevron-down
class="w-4 h-4 text-gray-500 transition-transform duration-200"
@ -263,31 +298,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 ($detailUrl)
@if ($card['detailUrl'])
<x-filament::button color="gray" size="sm" icon="heroicon-m-eye" tag="a"
:href="$detailUrl" target="_blank">
:href="$card['detailUrl']">
Detail
</x-filament::button>
@endif
{{-- Action Buttons based on status --}}
@if ($verification->status === \App\Enums\VerificationStatus::PENDING)
@if ($card['isPending'])
{{ ($this->approveAction)(['id' => $verification->id]) }}
{{ ($this->revisionAction)(['id' => $verification->id]) }}
{{ ($this->rejectAction)(['id' => $verification->id]) }}
@elseif($verification->status === \App\Enums\VerificationStatus::NEED_REVISION)
@elseif($card['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 user...
</span>
@elseif($verification->status === \App\Enums\VerificationStatus::APPROVED)
@elseif($card['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($verification->status === \App\Enums\VerificationStatus::REJECTED)
@elseif($card['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" />

View File

@ -2,8 +2,12 @@
<form wire:submit="save">
{{ $this->form }}
<div style="margin-top:20px;">
<x-filament::button type="submit">Simpan</x-filament::button>
</div>
@if (
!$verificationRequest ||
($verificationRequest && $verificationRequest->status === App\Enums\VerificationStatus::NEED_REVISION))
<div class="mt-6">
<x-filament::button type="submit">Simpan</x-filament::button>
</div>
@endif
</form>
</x-filament-panels::page>

View File

@ -32,11 +32,13 @@ class="dark:text-gray-400">
<form wire:submit="save">
{{ $this->form }}
<div class="mt-6">
<x-filament::button type="submit">
Simpan
</x-filament::button>
</div>
@if (
!$verificationRequest ||
($verificationRequest && $verificationRequest->status === App\Enums\VerificationStatus::NEED_REVISION))
<div class="mt-6">
<x-filament::button type="submit">Simpan</x-filament::button>
</div>
@endif
</form>
@endif
</x-filament-panels::page>

View File

@ -1,13 +1,206 @@
<x-filament-panels::page>
@php
$progress = $this->getVerificationProgress();
$alert = $progress['alert'];
$hasJournalists = $progress['journalists'] && $progress['journalists']->count() > 0;
@endphp
<div class="space-y-6">
{{-- Company Verification Section --}}
<x-verification-card :progress="$companyProgress" :submit-action="$this->submitCompanyAction" type="company" />
{{-- Unified Verification Card --}}
<x-filament::section>
<x-slot name="heading">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-primary-50 dark:bg-primary-500/10">
<x-heroicon-o-check-badge class="w-6 h-6 text-primary-500" />
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
Verifikasi Data
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
Lengkapi data perusahaan, media, dan jurnalis Anda untuk mengajukan verifikasi kepada
Administrator.
</p>
</div>
</div>
{{-- Media Verification Section --}}
<x-verification-card :progress="$mediaProgress" :submit-action="$this->submitMediaAction" type="media" />
{{-- Status Badge --}}
<x-filament::badge :color="$progress['status']['color']" size="lg">
{{ $progress['status']['label'] }}
</x-filament::badge>
</div>
</x-slot>
{{-- Journalist Verification Section --}}
<x-verification-card :progress="$journalistProgress" :submit-action="$this->submitJournalistAction" type="journalist" />
<div class="space-y-6">
{{-- Stepper Progress --}}
<div class="pt-4">
<x-stepper :current-step="$progress['currentStep']" :steps="$progress['steps']" />
</div>
{{-- Data Summary --}}
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{{-- Company Summary --}}
<div
class="p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<div class="flex items-center gap-2 mb-2">
<x-heroicon-o-building-office-2 class="w-5 h-5 text-primary-500" />
<h4 class="font-medium text-gray-900 dark:text-white">Perusahaan</h4>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ $progress['company']?->name }}</p>
<x-filament::badge :color="$progress['company'] ? 'success' : 'warning'" size="sm" class="mt-2">
{{ $progress['company'] ? '✓ Lengkap' : 'Belum Lengkap' }}
</x-filament::badge>
</div>
{{-- Media Summary --}}
<div
class="p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<div class="flex items-center gap-2 mb-2">
<x-heroicon-o-newspaper class="w-5 h-5 text-info-500" />
<h4 class="font-medium text-gray-900 dark:text-white">Media</h4>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ $progress['partnerMedia']?->name }}
</p>
<x-filament::badge :color="$progress['partnerMedia'] ? 'success' : 'warning'" size="sm" class="mt-2">
{{ $progress['partnerMedia'] ? '✓ Lengkap' : 'Belum Lengkap' }}
</x-filament::badge>
</div>
{{-- Journalists Summary --}}
<div
class="p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<div class="flex items-center gap-2 mb-2">
<x-heroicon-o-identification class="w-5 h-5 text-info-500" />
<h4 class="font-medium text-gray-900 dark:text-white">Jurnalis</h4>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $progress['journalists']?->count() }} jurnalis terdaftar
</p>
<x-filament::badge :color="$hasJournalists ? 'success' : 'warning'" size="sm" class="mt-2">
{{ $hasJournalists ? '✓ Lengkap' : 'Belum Lengkap' }}
</x-filament::badge>
</div>
</div>
{{-- Alert Messages --}}
@if ($alert)
@php
$alertClasses = match ($alert['color']) {
'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',
};
$alertIconClasses = match ($alert['color']) {
'success' => 'text-success-500',
'warning' => 'text-warning-500',
'danger' => 'text-danger-500',
'info' => 'text-info-500',
default => 'text-gray-500',
};
$alertTitleClasses = match ($alert['color']) {
'success' => 'text-success-800 dark:text-success-200',
'warning' => 'text-warning-800 dark:text-warning-200',
'danger' => 'text-danger-800 dark:text-danger-200',
'info' => 'text-info-800 dark:text-info-200',
default => 'text-gray-800 dark:text-gray-200',
};
$alertMessageClasses = match ($alert['color']) {
'success' => 'text-success-700 dark:text-success-300',
'warning' => 'text-warning-700 dark:text-warning-300',
'danger' => 'text-danger-700 dark:text-danger-300',
'info' => 'text-info-700 dark:text-info-300',
default => 'text-gray-700 dark:text-gray-300',
};
$iconName = 'heroicon-s-' . $alert['icon'];
$iconClasses =
$alertIconClasses . (isset($alert['animate']) && $alert['animate'] ? ' animate-pulse' : '');
@endphp
<div class="p-4 rounded-xl border {{ $alertClasses }}">
<div class="flex gap-3">
<div class="shrink-0">
<x-dynamic-component :component="$iconName" class="w-5 h-5 {{ $iconClasses }}" />
</div>
<div>
<h4 class="font-medium {{ $alertTitleClasses }}">
{{ $alert['title'] }}
</h4>
<p class="mt-1 text-sm {{ $alertMessageClasses }}">
{{ $alert['message'] }}
</p>
</div>
</div>
</div>
@endif
{{-- Review History Accordion --}}
@if ($progress['reviewHistory']->isNotEmpty())
<div x-data="{ open: false }"
class="border border-gray-200 dark:border-gray-700 rounded-xl overflow-hidden">
<button @click="open = !open"
class="w-full flex items-center justify-between p-4 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 ({{ $progress['reviewHistory']->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">
@foreach ($progress['reviewHistory'] as $review)
<div class="p-4">
<div class="flex items-start justify-between gap-4">
<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>
<span class="text-xs text-gray-500 dark:text-gray-400">
oleh {{ $review->reviewer?->name ?? 'Admin' }}
</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->created_at->format('d M Y, H:i') }}
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
@endif
</div>
{{-- Footer with Actions --}}
@if ($progress['canSubmit'])
<x-slot name="footer">
<div class="flex items-center justify-between">
<div class="text-sm text-gray-500 dark:text-gray-400">
@if ($progress['lastSubmittedAt'])
Terakhir diajukan: {{ $progress['lastSubmittedAt'] }}
@endif
</div>
<div class="flex items-center gap-2">
{{ $this->submitVerificationAction }}
</div>
</div>
</x-slot>
@endif
</x-filament::section>
</div>
<x-filament-actions::modals />