diff --git a/app/Enums/DataChangeDecision.php b/app/Enums/DataChangeDecision.php
new file mode 100644
index 0000000..b61cbec
--- /dev/null
+++ b/app/Enums/DataChangeDecision.php
@@ -0,0 +1,39 @@
+ 'Disetujui',
+ self::REJECTED => 'Ditolak',
+ };
+ }
+
+ public function getColor(): string|array|null
+ {
+ return match ($this) {
+ self::APPROVED => 'success',
+ self::REJECTED => 'danger',
+ };
+ }
+
+ public static function options(): array
+ {
+ return collect(self::cases())
+ ->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
+ ->toArray();
+ }
+}
diff --git a/app/Enums/DataChangeStatus.php b/app/Enums/DataChangeStatus.php
new file mode 100644
index 0000000..5b9e4d0
--- /dev/null
+++ b/app/Enums/DataChangeStatus.php
@@ -0,0 +1,42 @@
+ 'Menunggu Verifikasi',
+ self::APPROVED => 'Disetujui',
+ self::REJECTED => 'Ditolak',
+ };
+ }
+
+ public function getColor(): string|array|null
+ {
+ return match ($this) {
+ self::PENDING => 'warning',
+ self::APPROVED => 'success',
+ self::REJECTED => 'danger',
+ };
+ }
+
+ public static function options(): array
+ {
+ return collect(self::cases())
+ ->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
+ ->toArray();
+ }
+}
diff --git a/app/Filament/Pages/AdminVerification.php b/app/Filament/Pages/AdminVerification.php
index 0eddcc3..e2dbc14 100644
--- a/app/Filament/Pages/AdminVerification.php
+++ b/app/Filament/Pages/AdminVerification.php
@@ -250,7 +250,7 @@ protected function processReview(int $requestId, DecisionAdmin $decision, ?strin
CheerfulNotification::success(
'Berhasil Diproses! ✨',
- "Mantap! Verifikasi telah {$actionLabel} dan notifikasi sudah dikirim ke user. Kerja bagus, Admin! 💪"
+ "Mantap! Verifikasi telah {$actionLabel} dan notifikasi sudah dikirim ke pengguna. Kerja bagus, Admin! 💪"
)
->send();
diff --git a/app/Filament/Pages/Company.php b/app/Filament/Pages/Company.php
index fdfaacf..4b739c8 100644
--- a/app/Filament/Pages/Company.php
+++ b/app/Filament/Pages/Company.php
@@ -2,23 +2,33 @@
namespace App\Filament\Pages;
+use App\Enums\DataChangeStatus;
+use App\Enums\VerificationStatus;
+use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\Company as CompanyModel;
+use App\Models\DataChangeRequest;
+use App\Models\User;
use App\Models\VerificationRequest;
+use App\Notifications\BroadcastNotification;
use Asmit\FilamentUpload\Forms\Components\AdvancedFileUpload;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
+use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
+use Filament\Forms\Contracts\HasForms;
use Filament\Pages\Page;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
+use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\Storage;
+use Illuminate\Validation\ValidationException;
use UnitEnum;
-class Company extends Page
+class Company extends Page implements HasForms
{
use HasPageShield;
@@ -26,6 +36,8 @@ class Company extends Page
public array $data = [];
+ public array $originalDocuments = [];
+
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::BuildingOffice2;
@@ -80,7 +92,9 @@ public function mount(): void
->first();
if ($media) {
- $this->data["{$docType}_docs"] = [$media->getPathRelativeToRoot()];
+ $path = $media->getPathRelativeToRoot();
+ $this->data["{$docType}_docs"] = [$path];
+ $this->originalDocuments["{$docType}_docs"] = [$path];
}
}
}
@@ -250,67 +264,218 @@ public static function form(Schema $schema): Schema
->statePath('data');
}
- public function save()
+ public function save(): void
{
- $data = $this->form->getState();
+ $this->mountAction('save');
+ }
- $company = CompanyModel::updateOrCreate([
- 'id' => $this->company?->id,
- ], [
- 'user_id' => auth()->id(),
- 'name' => $data['name'],
- 'email' => $data['email'],
- 'address' => $data['address'],
- 'director_name' => $data['director_name'],
- 'director_nik' => $data['director_nik'],
- 'deed_incorporation' => $data['deed_incorporation'],
- 'trade_license' => $data['trade_license'],
- 'tax_id_number' => $data['tax_id_number'],
- 'taxable_enterprise' => $data['taxable_enterprise'],
- 'annual_tax_return' => $data['annual_tax_return'],
- 'domicile_certificate' => $data['domicile_certificate'],
- 'profile' => $data['profile'],
- ]);
+ public function saveAction(): Action
+ {
+ $needsReview = $this->company?->status === VerificationStatus::APPROVED;
- $this->company = $company;
+ return Action::make('save')
+ ->label('Simpan')
+ ->modalHeading('Ajukan Perubahan')
+ ->modal($needsReview)
+ ->schema(
+ $needsReview
+ ? [
+ Textarea::make('change_reason')
+ ->label('Alasan')
+ ->placeholder('Jelaskan alasan perubahan data ini...')
+ ->autocomplete(false)
+ ->autofocus(),
+ ]
+ : []
+ )
+ ->action(function (array $data = []) use ($needsReview) {
+ try {
+ $this->performSave($data, $needsReview);
+ } catch (ValidationException $e) {
+ $this->unmountAction();
- $documents = [
- 'director_nik_docs',
- 'deed_incorporation_docs',
- 'trade_license_docs',
- 'tax_id_number_docs',
- 'taxable_enterprise_docs',
- 'annual_tax_return_docs',
- 'domicile_certificate_docs',
- 'profile_docs',
- ];
+ throw $e;
+ }
+ })
+ ->modalWidth(Width::Large);
+ }
- foreach ($documents as $field) {
- if (empty($this->data[$field])) {
- continue;
+ public function performSave(array $data, bool $needsReview = false): void
+ {
+ if ($needsReview) {
+ $existingRequest = DataChangeRequest::where('entity_type', CompanyModel::class)
+ ->where('entity_id', $this->company->id)
+ ->where('status', DataChangeStatus::PENDING)
+ ->exists();
+
+ if ($existingRequest) {
+ CheerfulNotification::warning(
+ 'Pengajuan Sudah Ada ⏳',
+ 'Masih ada pengajuan perubahan data yang sedang menunggu verifikasi. Silakan tunggu hingga selesai diproses.'
+ )
+ ->send();
+
+ return;
}
- foreach ((array) $this->data[$field] as $filePath) {
- $fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
+ $state = $this->form->getState();
- if (! file_exists($fullPath)) {
+ $data = array_merge($state, $data);
+
+ $fieldLabels = CompanyModel::dataChangeEntryLabels();
+
+ $editableFields = [
+ 'name',
+ 'email',
+ 'address',
+ 'director_name',
+ 'director_nik',
+ 'deed_incorporation',
+ 'trade_license',
+ 'tax_id_number',
+ 'taxable_enterprise',
+ 'annual_tax_return',
+ 'domicile_certificate',
+ 'profile',
+ ];
+
+ $changedFields = [];
+ $oldData = [];
+
+ foreach ($editableFields as $field) {
+ if ($this->company->{$field} !== ($data[$field] ?? null)) {
+ $label = $fieldLabels[$field] ?? $field;
+ $changedFields[$label] = $data[$field] ?? null;
+ $oldData[$label] = $this->company->{$field};
+ }
+ }
+
+ $documents = [
+ 'director_nik_docs',
+ 'deed_incorporation_docs',
+ 'trade_license_docs',
+ 'tax_id_number_docs',
+ 'taxable_enterprise_docs',
+ 'annual_tax_return_docs',
+ 'domicile_certificate_docs',
+ 'profile_docs',
+ ];
+
+ foreach ($documents as $field) {
+ $newValue = $this->data[$field] ?? [];
+ $oldValue = $this->originalDocuments[$field] ?? [];
+
+ // Compare as arrays since file uploads are often arrays
+ if (json_encode($newValue) !== json_encode($oldValue)) {
+ $label = $fieldLabels[$field] ?? $field;
+ $changedFields[$label] = $newValue;
+ $oldData[$label] = $oldValue;
+ }
+ }
+
+ if (empty($changedFields)) {
+ CheerfulNotification::info(
+ 'Belum ada yang berubah ✨',
+ 'Ubah data terlebih dulu lalu simpan kembali 💪'
+ )
+ ->send();
+
+ return;
+ }
+
+ $dataChangeRequest = DataChangeRequest::create([
+ 'user_id' => auth()->id(),
+ 'entity_type' => CompanyModel::class,
+ 'entity_id' => $this->company->id,
+ 'old_data' => $oldData,
+ 'new_data' => $changedFields,
+ 'change_reason' => $data['change_reason'],
+ ]);
+
+ CheerfulNotification::success(
+ 'Permohonan Berhasil 🎉',
+ 'Data sudah dikirim dan sedang menunggu verifikasi admin ⏳'
+ )
+ ->send();
+
+ User::superAdmin()
+ ->get()
+ ->each(function ($admin) use ($dataChangeRequest): void {
+ $admin->notify(new BroadcastNotification([
+ 'title' => 'Ada Pengajuan Perubahan Data ✨',
+ 'body' => 'Halooo Admin! 👋 '.auth()->user()->name.' baru saja mengajukan perubahan data. Yuk, cek detailnya dan lakukan verifikasi ya! 🚀',
+ 'action' => [
+ Action::make('view')
+ ->label('Lihat')
+ ->url(DataChangesResource::getUrl('view', ['record' => $dataChangeRequest->id])),
+ ],
+ ]));
+ });
+ } else {
+ $data = $this->form->getState();
+
+ $company = CompanyModel::updateOrCreate([
+ 'id' => $this->company?->id,
+ ], [
+ 'user_id' => auth()->id(),
+ 'name' => $data['name'],
+ 'email' => $data['email'],
+ 'address' => $data['address'],
+ 'director_name' => $data['director_name'],
+ 'director_nik' => $data['director_nik'],
+ 'deed_incorporation' => $data['deed_incorporation'],
+ 'trade_license' => $data['trade_license'],
+ 'tax_id_number' => $data['tax_id_number'],
+ 'taxable_enterprise' => $data['taxable_enterprise'],
+ 'annual_tax_return' => $data['annual_tax_return'],
+ 'domicile_certificate' => $data['domicile_certificate'],
+ 'profile' => $data['profile'],
+ ]);
+
+ $this->company = $company;
+
+ $documents = [
+ 'director_nik_docs',
+ 'deed_incorporation_docs',
+ 'trade_license_docs',
+ 'tax_id_number_docs',
+ 'taxable_enterprise_docs',
+ 'annual_tax_return_docs',
+ 'domicile_certificate_docs',
+ 'profile_docs',
+ ];
+
+ foreach ($documents as $field) {
+ if (empty($this->data[$field])) {
continue;
}
- $company
- ->addMediaFromDisk($filePath, config('filesystems.default'))
- ->preservingOriginal()
- ->withCustomProperties([
- 'feature' => 'companies',
- 'date' => now()->toDateString(),
- 'doc_type' => str($field)
- ->replace('_docs', '')
- ->slug('-'),
- ])
- ->toMediaCollection('companies');
+ foreach ((array) $this->data[$field] as $filePath) {
+ $fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
+
+ if (! file_exists($fullPath)) {
+ continue;
+ }
+
+ $company
+ ->addMediaFromDisk($filePath, config('filesystems.default'))
+ ->preservingOriginal()
+ ->withCustomProperties([
+ 'feature' => 'companies',
+ 'date' => now()->toDateString(),
+ 'doc_type' => str($field)
+ ->replace('_docs', '')
+ ->slug('-'),
+ ])
+ ->toMediaCollection('companies');
+ }
+ }
+
+ if ($this->company) {
+ CheerfulNotification::update()->send();
+ } else {
+ CheerfulNotification::create()->send();
}
}
-
- CheerfulNotification::update()->send();
}
}
diff --git a/app/Filament/Pages/Media.php b/app/Filament/Pages/Media.php
index 938c90e..d184083 100644
--- a/app/Filament/Pages/Media.php
+++ b/app/Filament/Pages/Media.php
@@ -2,28 +2,38 @@
namespace App\Filament\Pages;
+use App\Enums\DataChangeStatus;
use App\Enums\MediaClassification;
use App\Enums\MediaType;
use App\Enums\RoleEnum;
+use App\Enums\VerificationStatus;
+use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\Company;
+use App\Models\DataChangeRequest;
use App\Models\PartnerMedia;
+use App\Models\User;
use App\Models\VerificationRequest;
+use App\Notifications\BroadcastNotification;
use Asmit\FilamentUpload\Forms\Components\AdvancedFileUpload;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
+use Filament\Actions\Action;
use Filament\Forms\Components\Radio;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
+use Filament\Forms\Contracts\HasForms;
use Filament\Pages\Page;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
+use Filament\Support\Contracts\HasLabel;
+use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\Storage;
use UnitEnum;
-class Media extends Page
+class Media extends Page implements HasForms
{
use HasPageShield;
@@ -31,6 +41,8 @@ class Media extends Page
public array $data = [];
+ public array $originalDocuments = [];
+
public ?PartnerMedia $media = null;
protected static ?string $model = PartnerMedia::class;
@@ -63,18 +75,28 @@ public function mount(): void
return;
}
- $this->company = auth()->user()->company;
+ $company = auth()->user()->company;
- $media = $this->company?->partnerMedia;
+ if (! $company) {
+ $this->restricted = true;
+
+ return;
+ }
+
+ $this->company = $company;
+
+ $verificationRequest = $this->company->verificationRequest()
+ ->latest()
+ ->first();
+
+ $this->verificationRequest = $verificationRequest;
+
+ $media = $company->partnerMedia;
if (! $media) {
return;
}
- $this->verificationRequest = $this->company->verificationRequest()
- ->latest()
- ->first();
-
$this->media = $media;
$this->form->fill($media->toArray());
@@ -87,13 +109,15 @@ public function mount(): void
$mediaItems = $media->getMedia('partnerMedia');
foreach ($documents as $docType) {
- $media = $mediaItems
+ $mediaItem = $mediaItems
->where('custom_properties.doc_type', str($docType)->replace('_docs', '')->slug('-'))
->sortByDesc('created_at')
->first();
- if ($media) {
- $this->data["{$docType}_docs"] = [$media->getPathRelativeToRoot()];
+ if ($mediaItem) {
+ $path = $mediaItem->getPathRelativeToRoot();
+ $this->data["{$docType}_docs"] = [$path];
+ $this->originalDocuments["{$docType}_docs"] = [$path];
}
}
}
@@ -185,8 +209,7 @@ public static function form(Schema $schema): Schema
->disk(config('filesystems.default'))
->acceptedFileTypes($doc['accept'])
->maxSize($doc['max_size'])
- ->directory($doc['folder'].now()->toDateString())
- ->required(),
+ ->directory($doc['folder'].now()->toDateString()),
]);
})->toArray()
)
@@ -195,56 +218,199 @@ public static function form(Schema $schema): Schema
->statePath('data');
}
- public function save()
+ public function save(): void
{
- $data = $this->form->getState();
+ $this->mountAction('save');
+ }
- $media = PartnerMedia::updateOrCreate([
- 'id' => $this->media?->id,
- ], [
- 'company_id' => $this->company->id,
- 'name' => $data['name'],
- 'link' => $data['link'],
- 'address' => $data['address'],
- 'type' => $data['type'],
- 'classification' => $data['classification'],
- 'journalism_organization' => $data['journalism_organization'],
- 'press_council_certificate' => $data['press_council_certificate'],
- ]);
+ public function saveAction(): Action
+ {
+ $needsReview = $this->media?->status === VerificationStatus::APPROVED;
- $this->media = $media;
+ return Action::make('save')
+ ->label('Simpan')
+ ->modalHeading('Ajukan Perubahan')
+ ->modal($needsReview)
+ ->schema(
+ $needsReview
+ ? [
+ Textarea::make('change_reason')
+ ->label('Alasan')
+ ->required()
+ ->placeholder('Jelaskan alasan perubahan data ini...')
+ ->autocomplete(false)
+ ->autofocus(),
+ ]
+ : []
+ )
+ ->action(function (array $data = []) use ($needsReview) {
+ $this->performSave($data, $needsReview);
+ })
+ ->modalWidth(Width::Large);
+ }
- $documents = [
- 'journalism_organization_docs',
- 'press_council_certificate_docs',
- ];
+ public function performSave(array $data = [], bool $needsReview = false): void
+ {
+ if ($needsReview && $this->media !== null) {
+ $existingRequest = DataChangeRequest::where('entity_type', PartnerMedia::class)
+ ->where('entity_id', $this->media->id)
+ ->where('status', DataChangeStatus::PENDING)
+ ->exists();
- foreach ($documents as $field) {
- if (empty($this->data[$field])) {
- continue;
+ if ($existingRequest) {
+ CheerfulNotification::warning(
+ 'Pengajuan Sudah Ada ⏳',
+ 'Masih ada pengajuan perubahan data yang sedang menunggu verifikasi. Silakan tunggu hingga selesai diproses.'
+ )
+ ->send();
+
+ return;
}
- foreach ((array) $this->data[$field] as $filePath) {
- $fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
+ $state = $this->form->getState();
+ $data = array_merge($state, $data);
- if (! file_exists($fullPath)) {
+ $fieldLabels = PartnerMedia::dataChangeEntryLabels();
+
+ $editableFields = [
+ 'name',
+ 'link',
+ 'address',
+ 'type',
+ 'classification',
+ 'journalism_organization',
+ 'press_council_certificate',
+ ];
+
+ $changedFields = [];
+ $oldDataArr = [];
+
+ $casts = $this->media->getCasts();
+ foreach ($editableFields as $field) {
+ $val = $data[$field];
+ $oldVal = $this->media->{$field};
+
+ // If value is a raw scalar but should be an enum, resolve it for display
+ if (isset($casts[$field]) && ! ($val instanceof BackedEnum) && enum_exists($casts[$field])) {
+ $enumClass = $casts[$field];
+ $val = $enumClass::tryFrom($val) ?? $val;
+ }
+
+ // Use labels for Enums if they implement HasLabel
+ $displayVal = ($val instanceof HasLabel) ? $val->getLabel() : ($val instanceof BackedEnum ? $val->value : $val);
+ $displayOldVal = ($oldVal instanceof HasLabel) ? $oldVal->getLabel() : ($oldVal instanceof BackedEnum ? $oldVal->value : $oldVal);
+
+ if ($displayOldVal !== $displayVal) {
+ $label = $fieldLabels[$field] ?? $field;
+ $changedFields[$label] = $displayVal;
+ $oldDataArr[$label] = $displayOldVal;
+ }
+ }
+
+ $documents = [
+ 'journalism_organization_docs',
+ 'press_council_certificate_docs',
+ ];
+
+ foreach ($documents as $field) {
+ $newValue = $this->data[$field] ?? [];
+ $oldValue = $this->originalDocuments[$field] ?? [];
+
+ if (json_encode($newValue) !== json_encode($oldValue)) {
+ $label = $fieldLabels[$field] ?? $field;
+ $changedFields[$label] = $newValue;
+ $oldDataArr[$label] = $oldValue;
+ }
+ }
+
+ if (empty($changedFields)) {
+ CheerfulNotification::info(
+ 'Belum ada yang berubah ✨',
+ 'Ubah data terlebih dulu lalu simpan kembali 💪'
+ )
+ ->send();
+
+ return;
+ }
+
+ $dataChangeRequest = DataChangeRequest::create([
+ 'user_id' => auth()->id(),
+ 'entity_type' => PartnerMedia::class,
+ 'entity_id' => $this->media->id,
+ 'old_data' => $oldDataArr,
+ 'new_data' => $changedFields,
+ 'change_reason' => $data['change_reason'],
+ ]);
+
+ CheerfulNotification::success(
+ 'Permohonan Berhasil 🎉',
+ 'Data sudah dikirim dan sedang menunggu verifikasi admin ⏳'
+ )
+ ->send();
+
+ User::superAdmin()
+ ->get()
+ ->each(function ($admin) use ($dataChangeRequest): void {
+ $admin->notify(new BroadcastNotification([
+ 'title' => 'Ada Pengajuan Perubahan Data Media ✨',
+ 'body' => 'Halooo Admin! 👋 '.auth()->user()->name.' baru saja mengajukan perubahan data media. Yuk, cek detailnya! 🚀',
+ 'action' => [
+ Action::make('view')
+ ->label('Lihat')
+ ->url(DataChangesResource::getUrl('view', ['record' => $dataChangeRequest->id])),
+ ],
+ ]));
+ });
+ } else {
+ $state = $this->form->getState();
+
+ $media = PartnerMedia::updateOrCreate([
+ 'id' => $this->media?->id,
+ ], [
+ 'company_id' => $this->company->id,
+ 'name' => $state['name'],
+ 'link' => $state['link'],
+ 'address' => $state['address'],
+ 'type' => $state['type'],
+ 'classification' => $state['classification'],
+ 'journalism_organization' => $state['journalism_organization'],
+ 'press_council_certificate' => $state['press_council_certificate'],
+ ]);
+
+ $this->media = $media;
+
+ $documents = [
+ 'journalism_organization_docs',
+ 'press_council_certificate_docs',
+ ];
+
+ foreach ($documents as $field) {
+ if (empty($this->data[$field])) {
continue;
}
- $media
- ->addMediaFromDisk($filePath, config('filesystems.default'))
- ->preservingOriginal()
- ->withCustomProperties([
- 'feature' => 'media',
- 'date' => now()->toDateString(),
- 'doc_type' => str($field)
- ->replace('_docs', '')
- ->slug('-'),
- ])
- ->toMediaCollection('partnerMedia');
- }
- }
+ foreach ((array) $this->data[$field] as $filePath) {
+ $fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
- CheerfulNotification::update()->send();
+ if (! file_exists($fullPath)) {
+ continue;
+ }
+
+ $media
+ ->addMediaFromDisk($filePath, config('filesystems.default'))
+ ->preservingOriginal()
+ ->withCustomProperties([
+ 'feature' => 'media',
+ 'date' => now()->toDateString(),
+ 'doc_type' => str($field)
+ ->replace('_docs', '')
+ ->slug('-'),
+ ])
+ ->toMediaCollection('partnerMedia');
+ }
+ }
+
+ CheerfulNotification::create()->send();
+ }
}
}
diff --git a/app/Filament/Resources/Manage/DataChanges/Actions/DataChangeRequest/AcceptAction.php b/app/Filament/Resources/Manage/DataChanges/Actions/DataChangeRequest/AcceptAction.php
new file mode 100644
index 0000000..2223512
--- /dev/null
+++ b/app/Filament/Resources/Manage/DataChanges/Actions/DataChangeRequest/AcceptAction.php
@@ -0,0 +1,162 @@
+label('Setujui')
+ ->icon(Heroicon::Check)
+ ->color('success')
+ ->requiresConfirmation()
+ ->modalHeading('Setujui Perubahan Data')
+ ->modalDescription('Apakah Anda yakin ingin menyetujui dan menerapkan perubahan data ini?')
+ ->action(function (DataChangeRequest $record): void {
+ $entity = $record->entity;
+ $newData = $record->new_data;
+
+ if (isset($newData['__delete_request__']) && $newData['__delete_request__'] === true) {
+ $entity->delete();
+
+ $record->update(['status' => DataChangeStatus::APPROVED]);
+
+ DataChangeReview::create([
+ 'data_change_request_id' => $record->id,
+ 'reviewer_id' => auth()->id(),
+ 'decision' => DataChangeDecision::APPROVED,
+ ]);
+
+ CheerfulNotification::success(
+ 'Penghapusan Disetujui! ✅',
+ 'Data telah berhasil dihapus sesuai dengan permohonan.'
+ )->send();
+
+ // Notify User
+ $record->user?->notify(new BroadcastNotification([
+ 'title' => 'Permohonan Penghapusan Disetujui ✨',
+ 'body' => "Halo! Permohonan penghapusan data Anda untuk {$record->change_reason} telah disetujui oleh admin. 🚀",
+ ]));
+
+ return;
+ }
+
+ if (! $entity) {
+ CheerfulNotification::danger(
+ 'Entitas Tidak Ditemukan ❌',
+ 'Data asli untuk permohonan ini tidak dapat ditemukan.'
+ )->send();
+
+ return;
+ }
+
+ $fields = [];
+ $docs = [];
+
+ $reverseLabels = [];
+ if (method_exists($record->entity_type, 'dataChangeEntryLabels')) {
+ $reverseLabels = array_flip(($record->entity_type)::dataChangeEntryLabels());
+ }
+
+ $casts = $entity->getCasts();
+ foreach ($newData as $key => $value) {
+ $actualKey = $reverseLabels[$key] ?? $key;
+
+ if (str_ends_with($actualKey, '_docs')) {
+ $docs[$actualKey] = $value;
+ } else {
+ // Handle Enum labels reverse mapping
+ if (isset($casts[$actualKey])) {
+ $castType = $casts[$actualKey];
+ if (is_string($castType) && enum_exists($castType)) {
+ if (is_subclass_of($castType, HasLabel::class)) {
+ foreach ($castType::cases() as $case) {
+ if ($case->getLabel() === $value) {
+ $value = $case->value;
+ break;
+ }
+ }
+ }
+
+ // If it's an int-backed enum but we have a string (e.g. from older data or failed mapping)
+ if (is_string($value) && is_numeric($value)) {
+ $reflection = new \ReflectionEnum($castType);
+ if ($reflection->isBacked() && $reflection->getBackingType()?->getName() === 'int') {
+ $value = (int) $value;
+ }
+ }
+ }
+ }
+ $fields[$actualKey] = $value;
+ }
+ }
+
+ // Update model fields
+ $entity->update($fields);
+
+ // Update media/documents
+ foreach ($docs as $field => $filePaths) {
+ if (empty($filePaths)) {
+ continue;
+ }
+
+ $docType = str($field)->replace('_docs', '')->slug('-');
+ $collection = match ($record->entity_type) {
+ 'App\Models\PartnerMedia' => 'partnerMedia',
+ default => str($record->entity_type)->afterLast('\\')->plural()->lower()->toString(),
+ };
+
+ foreach ((array) $filePaths as $filePath) {
+ $fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
+
+ if (! file_exists($fullPath)) {
+ continue;
+ }
+
+ $entity->addMediaFromDisk($filePath, config('filesystems.default'))
+ ->preservingOriginal()
+ ->withCustomProperties([
+ 'feature' => $collection,
+ 'date' => now()->toDateString(),
+ 'doc_type' => $docType,
+ ])
+ ->toMediaCollection($collection);
+ }
+ }
+
+ $record->update(['status' => DataChangeStatus::APPROVED]);
+
+ DataChangeReview::create([
+ 'data_change_request_id' => $record->id,
+ 'reviewer_id' => auth()->id(),
+ 'decision' => DataChangeDecision::APPROVED,
+ ]);
+
+ CheerfulNotification::success(
+ 'Perubahan Disetujui! ✅',
+ 'Data telah berhasil diperbarui sesuai dengan permohonan.'
+ )->send();
+
+ // Notify User
+ $record->user?->notify(new BroadcastNotification([
+ 'title' => 'Permohonan Perubahan Disetujui ✨',
+ 'body' => "Halo! Permohonan perubahan data Anda untuk {$record->change_reason} telah disetujui oleh admin. Cek sekarang ya! 🚀",
+ ]));
+ })
+ ->visible(fn (DataChangeRequest $record): bool => $record->status === DataChangeStatus::PENDING && ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value));
+ }
+}
diff --git a/app/Filament/Resources/Manage/DataChanges/Actions/DataChangeRequest/RejectAction.php b/app/Filament/Resources/Manage/DataChanges/Actions/DataChangeRequest/RejectAction.php
new file mode 100644
index 0000000..3ba5ba9
--- /dev/null
+++ b/app/Filament/Resources/Manage/DataChanges/Actions/DataChangeRequest/RejectAction.php
@@ -0,0 +1,65 @@
+label('Tolak')
+ ->icon(Heroicon::XMark)
+ ->color('danger')
+ ->schema([
+ Textarea::make('rejection_reason')
+ ->label('Alasan Penolakan')
+ ->placeholder('Berikan alasan mengapa permohonan ini ditolak...')
+ ->required(),
+ ])
+ ->modalHeading('Tolak Perubahan Data')
+ ->action(function (DataChangeRequest $record, array $data): void {
+ $record->update([
+ 'status' => DataChangeStatus::REJECTED,
+ ]);
+
+ DataChangeReview::create([
+ 'data_change_request_id' => $record->id,
+ 'reviewer_id' => auth()->id(),
+ 'decision' => DataChangeDecision::REJECTED,
+ 'note' => $data['rejection_reason'],
+ ]);
+
+ // If it was a new addition (empty old_data), delete the entity
+ $entity = $record->entity;
+ if (empty($record->old_data) && $entity) {
+ $entity->forceDelete();
+ }
+
+ CheerfulNotification::info(
+ 'Permohonan Ditolak 🛑',
+ 'Permohonan perubahan data telah ditolak. 😔'
+ )->send();
+
+ // Notify User
+ $record->user?->notify(new BroadcastNotification([
+ 'title' => 'Permohonan Perubahan Ditolak 🛑',
+ 'body' => "Halo! Mohon maaf, permohonan perubahan data Anda ({$record->change_reason}) ditolak oleh admin. Alasan: {$data['rejection_reason']} 😔",
+ ]));
+ })
+ ->visible(fn (DataChangeRequest $record): bool => $record->status === DataChangeStatus::PENDING && ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value))
+ ->modalWidth(Width::Large);
+ }
+}
diff --git a/app/Filament/Resources/Manage/DataChanges/DataChangesResource.php b/app/Filament/Resources/Manage/DataChanges/DataChangesResource.php
new file mode 100644
index 0000000..ccf7191
--- /dev/null
+++ b/app/Filament/Resources/Manage/DataChanges/DataChangesResource.php
@@ -0,0 +1,282 @@
+when(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value), function (Builder $query): void {
+ $query->where('user_id', auth()->id());
+ })
+ ->count();
+ }
+
+ public static function infolist(Schema $schema): Schema
+ {
+ return $schema
+ ->schema([
+ Section::make('Perbandingan Data')
+ ->schema([
+ TextEntry::make('comparison')
+ ->hiddenLabel()
+ ->html()
+ ->state(function (DataChangeRequest $record): string {
+ $old = $record->old_data ?? [];
+ $new = $record->new_data ?? [];
+
+ if (! is_array($old) || ! is_array($new)) {
+ return 'Format data tidak valid.';
+ }
+
+ $renderValue = function ($value) {
+ if (empty($value)) {
+ return 'Kosong';
+ }
+
+ $paths = is_array($value) ? $value : [$value];
+ $isProbablyPath = false;
+
+ foreach ($paths as $path) {
+ if (is_string($path) && (str_contains($path, '/') || str_contains($path, '\\'))) {
+ $isProbablyPath = true;
+ break;
+ }
+ }
+
+ if ($isProbablyPath) {
+ return collect($paths)
+ ->map(function ($path) {
+ if (! is_string($path)) {
+ return e(json_encode($path));
+ }
+
+ $url = Storage::disk(config('filesystems.default'))->url($path);
+ $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
+
+ if (in_array($extension, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'])) {
+ return "";
+ }
+
+ if ($extension === 'pdf') {
+ return "
+
User mengajukan penghapusan permanen untuk data ini. Semua informasi terkait akan dihapus setelah disetujui.
+Berikut adalah data baru yang akan dibuat.
+