feat: introduce a comprehensive data change request and review system with new Filament resources, models, migrations, and actions for managing data changes.
This commit is contained in:
parent
25e6713a65
commit
2626d890a4
39
app/Enums/DataChangeDecision.php
Normal file
39
app/Enums/DataChangeDecision.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Traits\Enum\WithComment;
|
||||
use App\Traits\Enum\WithValue;
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
|
||||
enum DataChangeDecision: int implements HasColor, HasLabel
|
||||
{
|
||||
use WithComment, WithValue;
|
||||
|
||||
case APPROVED = 1;
|
||||
case REJECTED = 2;
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return match ($this) {
|
||||
self::APPROVED => '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();
|
||||
}
|
||||
}
|
||||
42
app/Enums/DataChangeStatus.php
Normal file
42
app/Enums/DataChangeStatus.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Traits\Enum\WithComment;
|
||||
use App\Traits\Enum\WithValue;
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
|
||||
enum DataChangeStatus: int implements HasColor, HasLabel
|
||||
{
|
||||
use WithComment, WithValue;
|
||||
|
||||
case PENDING = 1;
|
||||
case APPROVED = 2;
|
||||
case REJECTED = 3;
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return match ($this) {
|
||||
self::PENDING => '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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Manage\DataChanges\Actions\DataChangeRequest;
|
||||
|
||||
use App\Enums\DataChangeDecision;
|
||||
use App\Enums\DataChangeStatus;
|
||||
use App\Enums\RoleEnum;
|
||||
use App\Filament\Support\CheerfulNotification;
|
||||
use App\Models\DataChangeRequest;
|
||||
use App\Models\DataChangeReview;
|
||||
use App\Notifications\BroadcastNotification;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class AcceptAction extends Action
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->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));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Manage\DataChanges\Actions\DataChangeRequest;
|
||||
|
||||
use App\Enums\DataChangeDecision;
|
||||
use App\Enums\DataChangeStatus;
|
||||
use App\Enums\RoleEnum;
|
||||
use App\Filament\Support\CheerfulNotification;
|
||||
use App\Models\DataChangeRequest;
|
||||
use App\Models\DataChangeReview;
|
||||
use App\Notifications\BroadcastNotification;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
|
||||
class RejectAction extends Action
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Manage\DataChanges;
|
||||
|
||||
use App\Enums\DataChangeStatus;
|
||||
use App\Enums\RoleEnum;
|
||||
use App\Filament\Actions\DefaultBulkActions;
|
||||
use App\Filament\Resources\Manage\DataChanges\Actions\DataChangeRequest\AcceptAction;
|
||||
use App\Filament\Resources\Manage\DataChanges\Actions\DataChangeRequest\RejectAction;
|
||||
use App\Filament\Resources\Manage\DataChanges\Pages\ManageDataChanges;
|
||||
use App\Filament\Resources\Manage\DataChanges\Pages\ViewDataChange;
|
||||
use App\Models\Company;
|
||||
use App\Models\DataChangeRequest;
|
||||
use App\Models\Journalist;
|
||||
use App\Models\PartnerMedia;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use UnitEnum;
|
||||
|
||||
class DataChangesResource extends Resource
|
||||
{
|
||||
protected static ?string $model = DataChangeRequest::class;
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::DocumentText;
|
||||
|
||||
protected static ?string $navigationLabel = 'Permohonan Perubahan Data';
|
||||
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'change_reason';
|
||||
|
||||
protected static ?string $slug = 'manage/data-changes';
|
||||
|
||||
public static function getNavigationBadge(): string
|
||||
{
|
||||
return static::getModel()::pending()
|
||||
->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 '<em>Format data tidak valid.</em>';
|
||||
}
|
||||
|
||||
$renderValue = function ($value) {
|
||||
if (empty($value)) {
|
||||
return '<em>Kosong</em>';
|
||||
}
|
||||
|
||||
$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 "<a href='{$url}' target='_blank'><img src='{$url}' style='max-height:100px; border-radius:8px; margin-top:4px; border:1px solid #e5e7eb;'></a>";
|
||||
}
|
||||
|
||||
if ($extension === 'pdf') {
|
||||
return "
|
||||
<div style='margin-top:4px;'>
|
||||
<iframe src='{$url}' style='width:100%; height:300px; border:1px solid #e5e7eb; border-radius:8px;'></iframe>
|
||||
<a href='{$url}' target='_blank' style='display:block; margin-top:4px; color:#3b82f6; text-decoration:underline; font-size:0.75rem;'>↗️ Buka PDF di Tab Baru</a>
|
||||
</div>
|
||||
";
|
||||
}
|
||||
|
||||
return "<a href='{$url}' target='_blank' style='color:#3b82f6; text-decoration:underline; font-size:0.875rem;'>📎 Buka Dokumen (".strtoupper($extension).')</a>';
|
||||
})
|
||||
->implode('<br>');
|
||||
}
|
||||
|
||||
return e(is_scalar($value) ? $value : json_encode($value));
|
||||
};
|
||||
|
||||
// 1. Deletion Request
|
||||
if (isset($new['__delete_request__']) && $new['__delete_request__'] === true) {
|
||||
return "
|
||||
<div style='background-color:#fee2e2; border:1px solid #f87171; color:#991b1b; padding:16px; border-radius:8px; margin-bottom:16px;'>
|
||||
<div style='font-weight:700; font-size:1.125rem; margin-bottom:4px;'>🚮 Permohonan Penghapusan Data</div>
|
||||
<p style='font-size:0.875rem;'>User mengajukan penghapusan permanen untuk data ini. Semua informasi terkait akan dihapus setelah disetujui.</p>
|
||||
</div>
|
||||
";
|
||||
}
|
||||
|
||||
// 2. New Addition Request
|
||||
if (empty($old)) {
|
||||
$rows = collect($new)
|
||||
->map(function ($value, $key) use ($renderValue) {
|
||||
return sprintf(
|
||||
"<div style='margin-bottom:12px; border-bottom:1px solid #f3f4f6; padding-bottom:8px'>
|
||||
<div style='font-weight:600; color:#374151; margin-bottom:4px'>%s</div>
|
||||
<div style='color:#16a34a; font-weight:500;'>[BARU]</div>
|
||||
<div style='margin-top:4px'>%s</div>
|
||||
</div>",
|
||||
e($key),
|
||||
$renderValue($value)
|
||||
);
|
||||
})
|
||||
->implode('');
|
||||
|
||||
return "
|
||||
<div style='background-color:#f0fdf4; border:1px solid #bbf7d0; color:#166534; padding:16px; border-radius:8px; margin-bottom:16px;'>
|
||||
<div style='font-weight:700; font-size:1.125rem; margin-bottom:4px;'>✨ Permohonan Penambahan Data</div>
|
||||
<p style='font-size:0.875rem;'>Berikut adalah data baru yang akan dibuat.</p>
|
||||
</div>
|
||||
{$rows}
|
||||
";
|
||||
}
|
||||
|
||||
// 3. Standard Comparison (Update)
|
||||
return collect($new)
|
||||
->map(function ($newValue, $key) use ($old, $renderValue) {
|
||||
$oldValue = $old[$key] ?? null;
|
||||
|
||||
if ($oldValue === $newValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
"<div style='margin-bottom:16px; border-bottom:1px solid #f3f4f6; padding-bottom:12px'>
|
||||
<div style='font-weight:600; color:#374151; margin-bottom:4px'>%s</div>
|
||||
<div style='display:grid; grid-template-columns: 1fr 1fr; gap:16px'>
|
||||
<div>
|
||||
<span style='font-size:0.75rem; font-weight:700; color:#dc2626; text-transform:uppercase'>LAMA</span>
|
||||
<div style='margin-top:2px'>%s</div>
|
||||
</div>
|
||||
<div>
|
||||
<span style='font-size:0.75rem; font-weight:700; color:#16a34a; text-transform:uppercase'>BARU</span>
|
||||
<div style='margin-top:2px'>%s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>",
|
||||
e($key),
|
||||
$renderValue($oldValue),
|
||||
$renderValue($newValue)
|
||||
);
|
||||
})
|
||||
->filter()
|
||||
->implode('');
|
||||
})
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
])
|
||||
->columns(1);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('user.name')
|
||||
->label('Pemohon')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->description(fn (DataChangeRequest $record): ?string => $record?->entity?->name)
|
||||
->visible(! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)),
|
||||
|
||||
TextColumn::make('entity_type')
|
||||
->label('Jenis Data')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->description(fn (DataChangeRequest $record): ?string => match (true) {
|
||||
isset($record->new_data['__delete_request__']) => '🗑️ Penghapusan',
|
||||
empty($record->old_data) => '✨ Penambahan',
|
||||
default => '📝 Perubahan',
|
||||
})
|
||||
->formatStateUsing(fn (DataChangeRequest $record): ?string => match ($record->entity_type) {
|
||||
Company::class => 'Perusahaan',
|
||||
PartnerMedia::class => 'Media',
|
||||
Journalist::class => 'Jurnalis',
|
||||
default => 'Lainnya',
|
||||
})
|
||||
->color(fn (DataChangeRequest $record): ?string => match ($record->entity_type) {
|
||||
Company::class => 'success',
|
||||
PartnerMedia::class => 'info',
|
||||
Journalist::class => 'warning',
|
||||
default => 'secondary',
|
||||
})
|
||||
->badge(),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->badge()
|
||||
->formatStateUsing(fn (DataChangeStatus $state): string => $state->getLabel())
|
||||
->color(fn (DataChangeStatus $state): string => $state->getColor())
|
||||
->description(fn (DataChangeRequest $record): ?string => $record?->review ? 'Oleh: '.$record->review?->reviewer?->name : ''),
|
||||
|
||||
TextColumn::make('change_reason')
|
||||
->label('Alasan Perubahan')
|
||||
->limit(50),
|
||||
|
||||
TextColumn::make('review.note')
|
||||
->label('Alasan Penolakan')
|
||||
->limit(50),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label('Tgl Permohonan')
|
||||
->date()
|
||||
->sortable()
|
||||
->dateTime('l, d F Y H:i:s')
|
||||
->wrap(),
|
||||
])
|
||||
->filters([
|
||||
TrashedFilter::make()
|
||||
->native(false)
|
||||
->visible(fn () => auth()->user()?->hasRole('Developer')),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
AcceptAction::make('accept'),
|
||||
RejectAction::make('reject'),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
...DefaultBulkActions::make('permohonan perubahan data'),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon(Heroicon::DocumentText)
|
||||
->emptyStateDescription('Belum ada permohonan perubahan data.')
|
||||
->defaultSort('created_at', 'desc')
|
||||
->deferFilters(false)
|
||||
->modifyQueryUsing(function (Builder $query): void {
|
||||
$query->when(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value), function (Builder $q): Builder {
|
||||
return $q->where('user_id', auth()->id());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ManageDataChanges::route('/'),
|
||||
'view' => ViewDataChange::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Manage\DataChanges\Pages;
|
||||
|
||||
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ManageDataChanges extends ListRecords
|
||||
{
|
||||
protected static string $resource = DataChangesResource::class;
|
||||
|
||||
protected static ?string $title = 'Permohonan Perubahan Data';
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Manage\DataChanges\Pages;
|
||||
|
||||
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewDataChange extends ViewRecord
|
||||
{
|
||||
protected static string $resource = DataChangesResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('back')
|
||||
->label('Kembali')
|
||||
->url(DataChangesResource::getUrl())
|
||||
->outlined()
|
||||
->color('secondary'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Manage\Journalists\Actions;
|
||||
|
||||
use App\Enums\VerificationStatus;
|
||||
use App\Filament\Actions\Cheerful\CreateAction;
|
||||
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
|
||||
use App\Filament\Support\CheerfulNotification;
|
||||
use App\Models\DataChangeRequest;
|
||||
use App\Models\Journalist;
|
||||
use App\Models\User;
|
||||
use App\Notifications\BroadcastNotification;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CreateJournalistAction extends CreateAction
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label('Tambah')
|
||||
->modalHeading('Tambah Jurnalis')
|
||||
->modalSubmitActionLabel('Simpan')
|
||||
->modalCancelActionLabel('Batal')
|
||||
->successNotification(null)
|
||||
->extraModalFooterActions(fn (CreateAction $action): array => [
|
||||
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
|
||||
->label('Simpan dan Tambah Lagi'),
|
||||
])
|
||||
->modalWidth(Width::Large)
|
||||
->using(function (array $data, string $model): Model {
|
||||
$data['partner_media_id'] = auth()->user()->company?->partnerMedia?->id;
|
||||
|
||||
$journalist = $model::create($data);
|
||||
|
||||
$documents = [
|
||||
'press_card_docs',
|
||||
'ukw_certificate_docs',
|
||||
];
|
||||
|
||||
foreach ($documents as $field) {
|
||||
if (empty($data[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ((array) $data[$field] as $filePath) {
|
||||
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
|
||||
|
||||
if (! file_exists($fullPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$journalist
|
||||
->addMediaFromDisk($filePath, config('filesystems.default'))
|
||||
->preservingOriginal()
|
||||
->withCustomProperties([
|
||||
'feature' => 'journalists',
|
||||
'date' => now()->toDateString(),
|
||||
'doc_type' => str($field)
|
||||
->replace('_docs', '')
|
||||
->slug('-'),
|
||||
])
|
||||
->toMediaCollection('journalists');
|
||||
}
|
||||
}
|
||||
|
||||
// If company is already approved, addition of new journalist needs permohonan/review
|
||||
if (auth()->user()->company?->status === VerificationStatus::APPROVED) {
|
||||
$fieldLabels = Journalist::dataChangeEntryLabels();
|
||||
$newFields = [];
|
||||
|
||||
foreach (['name', 'email', 'phone_number', 'press_card', 'ukw_certificate'] as $f) {
|
||||
$label = $fieldLabels[$f] ?? $f;
|
||||
$newFields[$label] = $data[$f] ?? '-';
|
||||
}
|
||||
|
||||
foreach (['press_card_docs', 'ukw_certificate_docs'] as $df) {
|
||||
$label = $fieldLabels[$df] ?? $df;
|
||||
$newFields[$label] = $data[$df] ?? [];
|
||||
}
|
||||
|
||||
$dataChangeRequest = DataChangeRequest::create([
|
||||
'user_id' => auth()->id(),
|
||||
'entity_type' => Journalist::class,
|
||||
'entity_id' => $journalist->id,
|
||||
'old_data' => [],
|
||||
'new_data' => $newFields,
|
||||
'change_reason' => 'Penambahan jurnalis baru',
|
||||
]);
|
||||
|
||||
CheerfulNotification::success(
|
||||
'Permohonan Berhasil 🎉',
|
||||
'Data jurnalis baru telah dikirim dan sedang menunggu verifikasi admin ⏳'
|
||||
)->send();
|
||||
|
||||
User::superAdmin()
|
||||
->get()
|
||||
->each(function ($admin) use ($dataChangeRequest): void {
|
||||
$admin->notify(new BroadcastNotification([
|
||||
'title' => 'Ada Penambahan Jurnalis Baru ✨',
|
||||
'body' => 'Halooo Admin! 👋 '.auth()->user()->name.' baru saja menambahkan jurnalis baru. Yuk, cek detailnya! 🚀',
|
||||
'action' => [
|
||||
Action::make('view')
|
||||
->label('Lihat')
|
||||
->url(DataChangesResource::getUrl('view', ['record' => $dataChangeRequest->id])),
|
||||
],
|
||||
]));
|
||||
});
|
||||
} else {
|
||||
CheerfulNotification::create()->send();
|
||||
}
|
||||
|
||||
return $journalist;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,18 @@
|
||||
|
||||
namespace App\Filament\Resources\Manage\Journalists\Actions;
|
||||
|
||||
use App\Enums\DataChangeStatus;
|
||||
use App\Enums\RoleEnum;
|
||||
use App\Enums\VerificationStatus;
|
||||
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
|
||||
use App\Filament\Support\CheerfulNotification;
|
||||
use App\Models\DataChangeRequest;
|
||||
use App\Models\Journalist;
|
||||
use App\Models\User;
|
||||
use App\Notifications\BroadcastNotification;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
|
||||
class DeleteJournalistAction extends DeleteAction
|
||||
{
|
||||
@ -13,12 +21,74 @@ protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->successNotification(
|
||||
CheerfulNotification::delete()
|
||||
);
|
||||
$this->successNotification(null);
|
||||
|
||||
$this->modalHeading(fn () => auth()->user()->company?->status === VerificationStatus::APPROVED && auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) ? 'Ajukan Penghapusan Jurnalis' : 'Hapus Jurnalis');
|
||||
|
||||
$this->schema(fn () => auth()->user()->company?->status === VerificationStatus::APPROVED && auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) ? [
|
||||
Textarea::make('deletion_reason')
|
||||
->label('Alasan Penghapusan')
|
||||
->placeholder('Jelaskan alasan mengapa jurnalis ini dihapus...')
|
||||
->required(),
|
||||
] : []);
|
||||
|
||||
$this->action(function (Journalist $record, array $data): void {
|
||||
$user = auth()->user();
|
||||
$needsReview = $user->company?->status === VerificationStatus::APPROVED &&
|
||||
$user->hasRole(RoleEnum::PERUSAHAAN->value);
|
||||
|
||||
if ($needsReview) {
|
||||
$existingRequest = DataChangeRequest::where('entity_type', Journalist::class)
|
||||
->where('entity_id', $record->id)
|
||||
->where('status', DataChangeStatus::PENDING)
|
||||
->exists();
|
||||
|
||||
if ($existingRequest) {
|
||||
CheerfulNotification::warning(
|
||||
'Pengajuan Sudah Ada ⏳',
|
||||
'Masih ada pengajuan penghapusan jurnalis yang sedang menunggu verifikasi.'
|
||||
)->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$dataChangeRequest = DataChangeRequest::create([
|
||||
'user_id' => $user->id,
|
||||
'entity_type' => Journalist::class,
|
||||
'entity_id' => $record->id,
|
||||
'old_data' => $record->toArray(),
|
||||
'new_data' => ['__delete_request__' => true],
|
||||
'change_reason' => $data['deletion_reason'] ?? 'Penghapusan jurnalis',
|
||||
]);
|
||||
|
||||
CheerfulNotification::success(
|
||||
'Permohonan Berhasil 🎉',
|
||||
'Permohonan penghapusan jurnalis telah dikirim dan sedang menunggu verifikasi admin ⏳'
|
||||
)->send();
|
||||
|
||||
User::superAdmin()
|
||||
->get()
|
||||
->each(function ($admin) use ($user, $record, $dataChangeRequest): void {
|
||||
$admin->notify(new BroadcastNotification([
|
||||
'title' => 'Ada Pengajuan Penghapusan Jurnalis ✨',
|
||||
'body' => 'Halooo Admin! 👋 '.$user->name.' baru saja mengajukan penghapusan jurnalis '.$record->name.'. Yuk, cek detailnya! 🚀',
|
||||
'action' => [
|
||||
Action::make('view')
|
||||
->label('Lihat')
|
||||
->url(DataChangesResource::getUrl('view', ['record' => $dataChangeRequest->id])),
|
||||
],
|
||||
]));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$record->delete();
|
||||
|
||||
CheerfulNotification::delete()->send();
|
||||
});
|
||||
|
||||
$this->visible(function (): bool {
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user->hasRole(RoleEnum::PERUSAHAAN->value)) {
|
||||
@ -29,15 +99,6 @@ protected function setUp(): void
|
||||
return false;
|
||||
}
|
||||
|
||||
$verificationRequest = $user->company
|
||||
->verificationRequest()
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($verificationRequest && $verificationRequest->status !== VerificationStatus::NEED_REVISION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,13 +2,19 @@
|
||||
|
||||
namespace App\Filament\Resources\Manage\Journalists\Actions;
|
||||
|
||||
use App\Enums\DataChangeStatus;
|
||||
use App\Enums\RoleEnum;
|
||||
use App\Enums\VerificationStatus;
|
||||
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
|
||||
use App\Filament\Support\CheerfulNotification;
|
||||
use App\Models\DataChangeRequest;
|
||||
use App\Models\Journalist;
|
||||
use App\Models\User;
|
||||
use App\Notifications\BroadcastNotification;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class EditJournalistAction extends EditAction
|
||||
@ -17,83 +23,151 @@ protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->successNotification(
|
||||
CheerfulNotification::update()
|
||||
);
|
||||
|
||||
$this->modalWidth(Width::Large)
|
||||
->successNotification(null)
|
||||
->fillForm(function (Journalist $journalist): array {
|
||||
$data = [
|
||||
'name' => $journalist->name,
|
||||
'email' => $journalist->email,
|
||||
'phone_number' => $journalist->phone_number,
|
||||
'press_card' => $journalist->press_card,
|
||||
'ukw_certificate' => $journalist->ukw_certificate,
|
||||
];
|
||||
|
||||
$documents = [
|
||||
'press_card',
|
||||
'ukw_certificate',
|
||||
];
|
||||
|
||||
$mediaItems = $journalist->getMedia('journalists');
|
||||
|
||||
foreach ($documents as $docType) {
|
||||
$media = $mediaItems
|
||||
->where('custom_properties.doc_type', str($docType)->replace('_docs', '')->slug('-'))
|
||||
->sortByDesc('created_at')
|
||||
->first();
|
||||
|
||||
if ($media) {
|
||||
$data["{$docType}_docs"] = [$media->getPathRelativeToRoot()];
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
})
|
||||
->using(function (Model $record, array $data): Model {
|
||||
$record->update([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
'press_card' => $data['press_card'],
|
||||
'ukw_certificate' => $data['ukw_certificate'],
|
||||
]);
|
||||
$data = $journalist->attributesToArray();
|
||||
|
||||
$documents = [
|
||||
'press_card_docs',
|
||||
'ukw_certificate_docs',
|
||||
];
|
||||
|
||||
foreach ($documents as $field) {
|
||||
if (empty($data[$field])) {
|
||||
continue;
|
||||
}
|
||||
$mediaItems = $journalist->getMedia('journalists');
|
||||
|
||||
foreach ((array) $data[$field] as $filePath) {
|
||||
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
|
||||
foreach ($documents as $docField) {
|
||||
$docType = str($docField)->replace('_docs', '')->slug('-');
|
||||
$media = $mediaItems
|
||||
->where('custom_properties.doc_type', $docType)
|
||||
->sortByDesc('created_at')
|
||||
->first();
|
||||
|
||||
if (! file_exists($fullPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$record
|
||||
->addMediaFromDisk($filePath, config('filesystems.default'))
|
||||
->preservingOriginal()
|
||||
->withCustomProperties([
|
||||
'feature' => 'journalists',
|
||||
'date' => now()->toDateString(),
|
||||
'doc_type' => str($field)
|
||||
->replace('_docs', '')
|
||||
->slug('-'),
|
||||
])
|
||||
->toMediaCollection('journalists');
|
||||
if ($media) {
|
||||
$data[$docField] = [$media->getPathRelativeToRoot()];
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
})
|
||||
->using(function (Journalist $record, array $data): Journalist {
|
||||
$needsReview = auth()->user()->company?->status === VerificationStatus::APPROVED &&
|
||||
auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value);
|
||||
|
||||
if ($needsReview) {
|
||||
$existingRequest = DataChangeRequest::where('entity_type', Journalist::class)
|
||||
->where('entity_id', $record->id)
|
||||
->where('status', DataChangeStatus::PENDING)
|
||||
->exists();
|
||||
|
||||
if ($existingRequest) {
|
||||
CheerfulNotification::warning(
|
||||
'Pengajuan Sudah Ada ⏳',
|
||||
'Masih ada pengajuan perubahan data yang sedang menunggu verifikasi.'
|
||||
)->send();
|
||||
|
||||
return $record;
|
||||
}
|
||||
}
|
||||
|
||||
$fieldLabels = Journalist::dataChangeEntryLabels();
|
||||
$editableFields = ['name', 'email', 'phone_number', 'press_card', 'ukw_certificate'];
|
||||
|
||||
$changedFields = [];
|
||||
$oldDataArr = [];
|
||||
|
||||
$casts = $record->getCasts();
|
||||
foreach ($editableFields as $field) {
|
||||
$val = $data[$field] ?? null;
|
||||
$oldVal = $record->{$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;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
$documentFields = ['press_card_docs', 'ukw_certificate_docs'];
|
||||
$mediaItems = $record->getMedia('journalists');
|
||||
|
||||
foreach ($documentFields as $field) {
|
||||
$newDocs = array_values((array) ($data[$field] ?? []));
|
||||
sort($newDocs);
|
||||
|
||||
$docType = str($field)->replace('_docs', '')->slug('-');
|
||||
$currentMedia = $mediaItems
|
||||
->where('custom_properties.doc_type', $docType)
|
||||
->sortByDesc('created_at')
|
||||
->first();
|
||||
|
||||
$oldDocs = $currentMedia ? [$currentMedia->getPathRelativeToRoot()] : [];
|
||||
$oldDocs = array_values($oldDocs);
|
||||
sort($oldDocs);
|
||||
|
||||
if (json_encode($newDocs) !== json_encode($oldDocs)) {
|
||||
$label = $fieldLabels[$field] ?? $field;
|
||||
$changedFields[$label] = $newDocs;
|
||||
$oldDataArr[$label] = $oldDocs;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($changedFields)) {
|
||||
CheerfulNotification::info(
|
||||
'Belum ada yang berubah ✨',
|
||||
'Ubah data terlebih dulu lalu simpan kembali 💪'
|
||||
)->send();
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
if ($needsReview) {
|
||||
$dataChangeRequest = DataChangeRequest::create([
|
||||
'user_id' => auth()->id(),
|
||||
'entity_type' => Journalist::class,
|
||||
'entity_id' => $record->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 Jurnalis ✨',
|
||||
'body' => 'Halooo Admin! 👋 '.auth()->user()->name.' baru saja mengajukan perubahan data jurnalis. Yuk, cek detailnya! 🚀',
|
||||
'action' => [
|
||||
Action::make('view')
|
||||
->label('Lihat')
|
||||
->url(DataChangesResource::getUrl('view', ['record' => $dataChangeRequest->id])),
|
||||
],
|
||||
]));
|
||||
});
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
$this->performSave($record, $data);
|
||||
|
||||
CheerfulNotification::update()->send(); // Manual success notification
|
||||
|
||||
return $record;
|
||||
})
|
||||
->visible(function (): bool {
|
||||
->visible(function (?Journalist $record): bool {
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user->hasRole(RoleEnum::PERUSAHAAN->value)) {
|
||||
@ -104,16 +178,57 @@ protected function setUp(): void
|
||||
return false;
|
||||
}
|
||||
|
||||
$verificationRequest = $user->company
|
||||
->verificationRequest()
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($verificationRequest && $verificationRequest->status !== VerificationStatus::NEED_REVISION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private function performSave(Journalist $record, array $data): void
|
||||
{
|
||||
$record->update([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
'press_card' => $data['press_card'],
|
||||
'ukw_certificate' => $data['ukw_certificate'],
|
||||
]);
|
||||
|
||||
$documents = [
|
||||
'press_card_docs',
|
||||
'ukw_certificate_docs',
|
||||
];
|
||||
|
||||
$existingMedia = $record->getMedia('journalists');
|
||||
$existingPaths = $existingMedia->map(fn ($m) => $m->getPathRelativeToRoot())->toArray();
|
||||
|
||||
foreach ($documents as $field) {
|
||||
if (empty($data[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ((array) $data[$field] as $filePath) {
|
||||
// If this path is already from one of our media items, skip re-adding it
|
||||
if (in_array($filePath, $existingPaths)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
|
||||
|
||||
if (! file_exists($fullPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$record
|
||||
->addMediaFromDisk($filePath, config('filesystems.default'))
|
||||
->preservingOriginal()
|
||||
->withCustomProperties([
|
||||
'feature' => 'journalists',
|
||||
'date' => now()->toDateString(),
|
||||
'doc_type' => str($field)
|
||||
->replace('_docs', '')
|
||||
->slug('-'),
|
||||
])
|
||||
->toMediaCollection('journalists');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Resources\Manage\Journalists;
|
||||
|
||||
use App\Enums\RoleEnum;
|
||||
use App\Enums\VerificationStatus;
|
||||
use App\Filament\Actions\Cheerful\ForceDeleteAction;
|
||||
use App\Filament\Actions\Cheerful\RestoreAction;
|
||||
use App\Filament\Columns\TimestampColumns;
|
||||
@ -18,6 +19,7 @@
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Group;
|
||||
@ -47,6 +49,17 @@ class JournalistResource extends Resource
|
||||
|
||||
protected static ?string $slug = 'manage/journalists';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::pending()
|
||||
->when(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value), function (Builder $query): void {
|
||||
$query->whereHas('partnerMedia.company', function (Builder $q): void {
|
||||
$q->where('user_id', auth()->id());
|
||||
});
|
||||
})
|
||||
->count() ?: null;
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
$documents = [
|
||||
@ -123,6 +136,16 @@ public static function form(Schema $schema): Schema
|
||||
})
|
||||
->toArray()
|
||||
),
|
||||
|
||||
Section::make('Alasan Perubahan')
|
||||
->schema([
|
||||
Textarea::make('change_reason')
|
||||
->label('Alasan')
|
||||
->placeholder('Jelaskan alasan perubahan data ini...')
|
||||
->rows(3)
|
||||
->required(),
|
||||
])
|
||||
->visible(fn () => auth()->user()->company?->status === VerificationStatus::APPROVED && auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)),
|
||||
])
|
||||
->columns(1);
|
||||
}
|
||||
|
||||
@ -3,13 +3,9 @@
|
||||
namespace App\Filament\Resources\Manage\Journalists\Pages;
|
||||
|
||||
use App\Enums\RoleEnum;
|
||||
use App\Enums\VerificationStatus;
|
||||
use App\Filament\Actions\Cheerful\CreateAction;
|
||||
use App\Filament\Resources\Manage\Journalists\Actions\CreateJournalistAction;
|
||||
use App\Filament\Resources\Manage\Journalists\JournalistResource;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ManageJournalists extends ManageRecords
|
||||
{
|
||||
@ -23,63 +19,8 @@ protected function getHeaderActions(): array
|
||||
return [];
|
||||
}
|
||||
|
||||
$verificationRequest = auth()->user()->company?->verificationRequest()
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($verificationRequest && $verificationRequest->status !== VerificationStatus::NEED_REVISION) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('Tambah')
|
||||
->modalHeading('Tambah Jurnalis')
|
||||
->modalSubmitActionLabel('Simpan')
|
||||
->modalCancelActionLabel('Batal')
|
||||
->extraModalFooterActions(fn (CreateAction $action): array => [
|
||||
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
|
||||
->label('Simpan dan Tambah Lagi'),
|
||||
])
|
||||
->modalWidth(Width::Large)
|
||||
->using(function (array $data, string $model): Model {
|
||||
$data['partner_media_id'] = auth()->user()->company?->partnerMedia?->id;
|
||||
|
||||
$journalist = $model::create($data);
|
||||
|
||||
$documents = [
|
||||
'press_card_docs',
|
||||
'ukw_certificate_docs',
|
||||
];
|
||||
|
||||
foreach ($documents as $field) {
|
||||
if (empty($data[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ((array) $data[$field] as $filePath) {
|
||||
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
|
||||
|
||||
if (! file_exists($fullPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$journalist
|
||||
->addMediaFromDisk($filePath, config('filesystems.default'))
|
||||
->preservingOriginal()
|
||||
->withCustomProperties([
|
||||
'feature' => 'journalists',
|
||||
'date' => now()->toDateString(),
|
||||
'doc_type' => str($field)
|
||||
->replace('_docs', '')
|
||||
->slug('-'),
|
||||
])
|
||||
->toMediaCollection('journalists');
|
||||
}
|
||||
}
|
||||
|
||||
return $journalist;
|
||||
}),
|
||||
CreateJournalistAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,4 +45,30 @@ public function verificationRequest(): HasOne
|
||||
{
|
||||
return $this->hasOne(VerificationRequest::class);
|
||||
}
|
||||
|
||||
public static function dataChangeEntryLabels(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Nama',
|
||||
'email' => 'Alamat Surel',
|
||||
'address' => 'Alamat',
|
||||
'director_name' => 'Nama Direktur',
|
||||
'director_nik' => 'NIK Direktur',
|
||||
'director_nik_docs' => 'Dokumen NIK Direktur',
|
||||
'deed_incorporation' => 'No. Akta Pendirian',
|
||||
'deed_incorporation_docs' => 'Dokumen Akta Pendirian',
|
||||
'trade_license' => 'No. SIUP / NIB',
|
||||
'trade_license_docs' => 'Dokumen SIUP / NIB',
|
||||
'tax_id_number' => 'No. NPWP',
|
||||
'tax_id_number_docs' => 'Dokumen NPWP',
|
||||
'taxable_enterprise' => 'No. PKP',
|
||||
'taxable_enterprise_docs' => 'Dokumen PKP',
|
||||
'annual_tax_return' => 'No. SPT Tahunan',
|
||||
'annual_tax_return_docs' => 'Dokumen SPT Tahunan',
|
||||
'domicile_certificate' => 'No. Suket Domisili',
|
||||
'domicile_certificate_docs' => 'Dokumen Suket Domisili',
|
||||
'profile' => 'No. Profil Perusahaan',
|
||||
'profile_docs' => 'Dokumen Profil Perusahaan',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
62
app/Models/DataChangeRequest.php
Normal file
62
app/Models/DataChangeRequest.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\DataChangeStatus;
|
||||
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Swindon\FilamentHashids\Traits\HasHashid;
|
||||
|
||||
class DataChangeRequest extends Model
|
||||
{
|
||||
use HasHashid, SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => DataChangeStatus::class,
|
||||
'old_data' => 'array',
|
||||
'new_data' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
protected function pending(Builder $query): void
|
||||
{
|
||||
$query->where('status', DataChangeStatus::PENDING);
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
protected function approved(Builder $query): void
|
||||
{
|
||||
$query->where('status', DataChangeStatus::APPROVED);
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
protected function rejected(Builder $query): void
|
||||
{
|
||||
$query->where('status', DataChangeStatus::REJECTED);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function review(): HasOne
|
||||
{
|
||||
return $this->hasOne(DataChangeReview::class);
|
||||
}
|
||||
|
||||
public function entity(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
32
app/Models/DataChangeReview.php
Normal file
32
app/Models/DataChangeReview.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\DataChangeDecision;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class DataChangeReview extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'decision' => DataChangeDecision::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function request(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DataChangeRequest::class, 'data_change_request_id');
|
||||
}
|
||||
|
||||
public function reviewer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'reviewer_id');
|
||||
}
|
||||
}
|
||||
@ -28,4 +28,22 @@ public function partnerMedia(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PartnerMedia::class);
|
||||
}
|
||||
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('status', VerificationStatus::PENDING);
|
||||
}
|
||||
|
||||
public static function dataChangeEntryLabels(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Nama',
|
||||
'email' => 'Alamat Surel',
|
||||
'phone_number' => 'Nomor Telepon',
|
||||
'press_card' => 'No. Kartu Pers',
|
||||
'press_card_docs' => 'Dokumen Kartu Pers',
|
||||
'ukw_certificate' => 'No. Sertifikat UKW',
|
||||
'ukw_certificate_docs' => 'Dokumen Sertifikat UKW',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,4 +131,19 @@ public function taskAssignments(): BelongsToMany
|
||||
->withPivot(['status'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public static function dataChangeEntryLabels(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Nama',
|
||||
'link' => 'Tautan',
|
||||
'address' => 'Alamat',
|
||||
'type' => 'Jenis',
|
||||
'classification' => 'Klasifikasi',
|
||||
'journalism_organization' => 'No. Organisasi Kewartawanan',
|
||||
'journalism_organization_docs' => 'Dokumen Organisasi Kewartawanan',
|
||||
'press_council_certificate' => 'No. Sertifikat Dewan Pers',
|
||||
'press_council_certificate_docs' => 'Dokumen Sertifikat Dewan Pers',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,6 +110,8 @@ public function panel(Panel $panel): Panel
|
||||
'Pelindung',
|
||||
])
|
||||
->databaseNotifications()
|
||||
->databaseNotificationsPolling('30s');
|
||||
->databaseNotificationsPolling('30s')
|
||||
->brandLogo(fn () => Storage::url($generalSettings->site_logo))
|
||||
->favicon(fn () => Storage::url($generalSettings->site_icon));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\DataChangeStatus;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('data_change_requests', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(User::class);
|
||||
$table->morphs('entity');
|
||||
$table->enum('status', DataChangeStatus::cases())->default(DataChangeStatus::PENDING)->comment(DataChangeStatus::comment());
|
||||
$table->json('old_data')->nullable();
|
||||
$table->json('new_data');
|
||||
$table->text('change_reason')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('data_change_requests');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\DataChangeDecision;
|
||||
use App\Models\DataChangeRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('data_change_reviews', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(DataChangeRequest::class);
|
||||
$table->foreignIdFor(User::class, 'reviewer_id');
|
||||
$table->enum('decision', DataChangeDecision::cases())->comment(DataChangeDecision::comment());
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('data_change_reviews');
|
||||
}
|
||||
};
|
||||
@ -1,13 +1,9 @@
|
||||
<x-filament-panels::page>
|
||||
<form wire:submit="save">
|
||||
<form wire:submit.prevent="save">
|
||||
{{ $this->form }}
|
||||
|
||||
@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
|
||||
<div class="mt-6">
|
||||
{{ $this->saveAction() }}
|
||||
</div>
|
||||
</form>
|
||||
</x-filament-panels::page>
|
||||
|
||||
@ -29,16 +29,12 @@ class="dark:text-gray-400">
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<form wire:submit="save">
|
||||
<form wire:submit.prevent="save">
|
||||
{{ $this->form }}
|
||||
|
||||
@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
|
||||
<div class="mt-6">
|
||||
{{ $this->saveAction() }}
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</x-filament-panels::page>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user