diff --git a/app/Filament/Pages/Company/Index.php b/app/Filament/Pages/Company/Index.php index 318b60d..cc3b0f8 100644 --- a/app/Filament/Pages/Company/Index.php +++ b/app/Filament/Pages/Company/Index.php @@ -13,7 +13,6 @@ use BezhanSalleh\FilamentShield\Traits\HasPageShield; use Filament\Actions\Concerns\InteractsWithActions; use Filament\Actions\Contracts\HasActions; -use Filament\Facades\Filament; use Filament\Forms\Concerns\InteractsWithForms; use Filament\Forms\Contracts\HasForms; use Filament\Pages\Page; @@ -52,12 +51,12 @@ class Index extends Page implements HasActions, HasForms public static function canAccess(): bool { - return Filament::auth()->user()->can('View:Company'); + return auth()->user()->can('View:Company'); } public function mount(): void { - $company = Filament::auth()->user()?->company; + $company = auth()->user()?->company; if (! $company) { return; diff --git a/app/Filament/Pages/Media.php b/app/Filament/Pages/Media.php deleted file mode 100644 index 3819c7a..0000000 --- a/app/Filament/Pages/Media.php +++ /dev/null @@ -1,427 +0,0 @@ -user()->hasRole(RoleEnum::PERUSAHAAN->value) && ! auth()->user()->company) { - $this->restricted = true; - - return; - } - - $company = auth()->user()->company; - - if (! $company) { - $this->restricted = true; - - return; - } - - $this->company = $company; - - $this->isVerificationStage = in_array($company?->verificationRequest?->status, [ - VerificationStatus::PENDING, - VerificationStatus::REJECTED, - ]) || $company->verificationRequest()->pending()->exists(); - - $verificationRequest = $this->company->verificationRequest() - ->latest() - ->first(); - - $this->verificationRequest = $verificationRequest; - - $media = $company->partnerMedia; - - if (! $media) { - return; - } - - $this->media = $media; - - $this->form->fill($media->toArray()); - - $documents = [ - 'journalism_organization', - 'press_council_certificate', - ]; - - $mediaItems = $media->getMedia('partnerMedia'); - - foreach ($documents as $docType) { - $mediaItem = $mediaItems - ->where('custom_properties.doc_type', str($docType)->replace('_docs', '')->slug('-')) - ->sortByDesc('created_at') - ->first(); - - if ($mediaItem) { - $path = $mediaItem->getPathRelativeToRoot(); - $this->data["{$docType}_docs"] = [$path]; - $this->originalDocuments["{$docType}_docs"] = [$path]; - } - } - } - - public static function form(Schema $schema): Schema - { - $documents = [ - [ - 'title' => 'Organisasi Kewartawanan', - 'text_name' => 'journalism_organization', - 'placeholder' => '*****', - 'max' => 255, - 'max_size' => 1024 * 10, - 'file_name' => 'journalism_organization_docs', - 'accept' => ['application/pdf'], - 'folder' => 'media/journalism-organization/', - ], - [ - 'title' => 'Sertifikat Dewan Pers', - 'text_name' => 'press_council_certificate', - 'placeholder' => '*****', - 'max' => 255, - 'max_size' => 1024 * 10, - 'file_name' => 'press_council_certificate_docs', - 'accept' => ['application/pdf'], - 'folder' => 'media/press-council-certificate/', - ], - ]; - - return $schema - ->components([ - Section::make(fn () => CheerfulNotification::getByKey('media.section.title')) - ->description(fn () => CheerfulNotification::getByKey('media.section.description')) - ->icon(fn () => CheerfulNotification::getNotifStyle() === NotifStyle::CHEERFUL ? 'heroicon-o-newspaper' : null) - ->schema([ - TextInput::make('name') - ->label('Nama') - ->placeholder('PWK News') - ->autocomplete(false) - ->autofocus() - ->required() - ->maxLength(50), - - TextInput::make('link') - ->label('Tautan') - ->placeholder('https://www.pwknews.com') - ->autocomplete(false) - ->nullable() - ->maxLength(255) - ->url(), - - Textarea::make('address') - ->label('Alamat') - ->placeholder('Jl. Kebontoro, Jakarta Pusat') - ->autocomplete(false) - ->required() - ->columnSpanFull(), - - Radio::make('type') - ->label('Jenis') - ->options(MediaType::options()) - ->inline() - ->required() - ->in(implode(',', array_column(MediaType::cases(), 'value'))), - - Radio::make('classification') - ->label('Klasifikasi') - ->options(MediaClassification::options()) - ->inline() - ->required() - ->in(implode(',', array_column(MediaClassification::cases(), 'value'))), - ]) - ->columns(2) - ->columnSpan(2), - - Group::make() - ->schema( - collect($documents) - ->map(function (array $doc): Section { - return Section::make(fn () => CheerfulNotification::getByKey('general.empty_msg', ['doc__title' => $doc['title']])) - ->icon(fn () => CheerfulNotification::getNotifStyle() === NotifStyle::CHEERFUL ? 'heroicon-o-document-text' : null) - ->schema([ - TextInput::make($doc['text_name']) - ->hiddenLabel() - ->placeholder($doc['placeholder']) - ->autocomplete(false) - ->required() - ->maxLength($doc['max']), - - AdvancedFileUpload::make($doc['file_name']) - ->hiddenLabel() - ->disk(config('filesystems.default')) - ->acceptedFileTypes($doc['accept']) - ->maxSize($doc['max_size']) - ->directory($doc['folder'].now()->toDateString()) - ->helperText('Format yang diterima: '.implode(', ', array_map(fn ($t) => strtoupper(str_replace('application/', '', $t)), $doc['accept'])).'. Ukuran maksimum: '.($doc['max_size'] / 1024).' MB.'), - ]); - })->toArray() - ) - ->columns(2), - ]) - ->statePath('data'); - } - - public function save(): void - { - $this->mountAction('save'); - } - - public function saveAction(): Action - { - $needsReview = $this->media?->company?->verificationRequest?->status !== null; - - return Action::make('save') - ->label('Simpan') - ->modalHeading(fn () => CheerfulNotification::getByKey('media.save.title')) - ->modalDescription(fn () => CheerfulNotification::getByKey('media.save.description')) - ->modal($needsReview) - ->schema( - $needsReview - ? [ - Textarea::make('change_reason') - ->label('Alasan') - ->placeholder('Jelaskan alasan perubahan data ini...') - ->autocomplete(false) - ->autofocus() - ->required(), - ] - : [] - ) - ->action(function (array $data = []) use ($needsReview) { - $this->performSave($data, $needsReview); - }) - ->modalWidth(Width::Large); - } - - public function performSave(array $data = [], bool $needsReview = false): void - { - if ($needsReview) { - $existingRequest = DataChangeRequest::where('entity_type', PartnerMedia::class) - ->where('entity_id', $this->media->id) - ->where('status', DataChangeStatus::PENDING) - ->exists(); - - if ($existingRequest) { - CheerfulNotification::warning( - CheerfulNotification::getByKey('data_change.existing.title'), - CheerfulNotification::getByKey('data_change.existing.body') - ) - ->send(); - - return; - } - - $state = $this->form->getState(); - $data = array_merge($state, $data); - - $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( - CheerfulNotification::getByKey('data_change.no_change.title'), - CheerfulNotification::getByKey('data_change.no_change.body') - ) - ->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( - CheerfulNotification::getByKey('data_change.success.title'), - CheerfulNotification::getByKey('data_change.success.body') - ) - ->send(); - - User::superAdmin() - ->get() - ->each(function ($admin) use ($dataChangeRequest): void { - $admin->notify(new BroadcastNotification([ - 'title' => CheerfulNotification::getByKey('data_change.admin_notify.title'), - 'body' => CheerfulNotification::getByKey('data_change.admin_notify.body', ['name' => auth()->user()->name]), - '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; - } - - foreach ((array) $this->data[$field] as $filePath) { - $fullPath = Storage::disk(config('filesystems.default'))->path($filePath); - - 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/Pages/Media/Actions/SaveMediaAction.php b/app/Filament/Pages/Media/Actions/SaveMediaAction.php new file mode 100644 index 0000000..3bb9df7 --- /dev/null +++ b/app/Filament/Pages/Media/Actions/SaveMediaAction.php @@ -0,0 +1,40 @@ +label('Simpan') + ->modalHeading(fn () => CheerfulNotification::getByKey('media.save.title')) + ->modalDescription(fn () => CheerfulNotification::getByKey('media.save.description')) + ->modal(fn (): bool => (bool) ($this->getLivewire()?->needsMediaReview())) + ->schema(fn (): array => ($this->getLivewire()?->needsMediaReview()) + ? [ + Textarea::make('change_reason') + ->label('Alasan') + ->placeholder('Jelaskan alasan perubahan data ini...') + ->autocomplete(false) + ->autofocus() + ->required(), + ] + : []) + ->action(function (array $data = []): void { + $this->getLivewire()?->handleMediaSave($data); + }) + ->modalWidth(Width::Large); + } +} diff --git a/app/Filament/Pages/Media/Concerns/HasMediaActions.php b/app/Filament/Pages/Media/Concerns/HasMediaActions.php new file mode 100644 index 0000000..b393ff7 --- /dev/null +++ b/app/Filament/Pages/Media/Concerns/HasMediaActions.php @@ -0,0 +1,14 @@ +user()->can('View:Media'); + } + + public function mount(): void + { + if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) && ! auth()->user()->company) { + $this->restricted = true; + + return; + } + + $company = auth()->user()->company; + + if (! $company) { + $this->restricted = true; + + return; + } + + $this->company = $company; + + $this->isVerificationStage = in_array($company?->verificationRequest?->status, [ + VerificationStatus::PENDING, + VerificationStatus::REJECTED, + ]) || $company->verificationRequest()->pending()->exists(); + + $verificationRequest = $this->company->verificationRequest() + ->latest() + ->first(); + + $this->verificationRequest = $verificationRequest; + + $media = $company->partnerMedia; + + if (! $media) { + return; + } + + $this->media = $media; + + $this->form->fill($media->toArray()); + + $documents = [ + 'journalism_organization', + 'press_council_certificate', + ]; + + $mediaItems = $media->getMedia('partnerMedia'); + + foreach ($documents as $docType) { + $mediaItem = $mediaItems + ->where('custom_properties.doc_type', str($docType)->replace('_docs', '')->slug('-')) + ->sortByDesc('created_at') + ->first(); + + if ($mediaItem) { + $path = $mediaItem->getPathRelativeToRoot(); + $this->data["{$docType}_docs"] = [$path]; + $this->originalDocuments["{$docType}_docs"] = [$path]; + } + } + } + + public static function form(Schema $schema): Schema + { + return MediaForm::configure($schema); + } + + public function save(): void + { + $this->mountAction('save'); + } + + public function needsMediaReview(): bool + { + return $this->media?->company?->verificationRequest?->status !== null; + } + + public function handleMediaSave(array $data = []): void + { + try { + $media = MediaSaveService::handle( + existingMedia: $this->media, + companyId: $this->company->id, + formState: $this->form->getState(), + pageData: $this->data, + originalDocuments: $this->originalDocuments, + needsReview: $this->needsMediaReview(), + actionData: $data, + ); + + if ($this->needsMediaReview()) { + return; + } + + $this->media = $media; + + CheerfulNotification::create()->send(); + } catch (ValidationException $e) { + $this->unmountAction(); + + throw $e; + } + } +} diff --git a/app/Filament/Pages/Media/Schemas/MediaForm.php b/app/Filament/Pages/Media/Schemas/MediaForm.php new file mode 100644 index 0000000..62228f0 --- /dev/null +++ b/app/Filament/Pages/Media/Schemas/MediaForm.php @@ -0,0 +1,122 @@ +components([ + Section::make(fn () => CheerfulNotification::getByKey('media.section.title')) + ->description(fn () => CheerfulNotification::getByKey('media.section.description')) + ->icon(fn () => CheerfulNotification::getNotifStyle() === NotifStyle::CHEERFUL ? 'heroicon-o-newspaper' : null) + ->schema([ + TextInput::make('name') + ->label('Nama') + ->placeholder('PWK News') + ->autocomplete(false) + ->autofocus() + ->required() + ->maxLength(50), + + TextInput::make('link') + ->label('Tautan') + ->placeholder('https://www.pwknews.com') + ->autocomplete(false) + ->nullable() + ->maxLength(255) + ->url(), + + Textarea::make('address') + ->label('Alamat') + ->placeholder('Jl. Kebontoro, Jakarta Pusat') + ->autocomplete(false) + ->required() + ->columnSpanFull(), + + Radio::make('type') + ->label('Jenis') + ->options(MediaType::options()) + ->inline() + ->required() + ->in(implode(',', array_column(MediaType::cases(), 'value'))), + + Radio::make('classification') + ->label('Klasifikasi') + ->options(MediaClassification::options()) + ->inline() + ->required() + ->in(implode(',', array_column(MediaClassification::cases(), 'value'))), + ]) + ->columns(2) + ->columnSpan(2), + + Group::make() + ->schema( + collect($documents) + ->map(function (array $doc): Section { + return Section::make(fn () => $doc['title']) + ->schema([ + TextInput::make($doc['text_name']) + ->hiddenLabel() + ->placeholder($doc['placeholder']) + ->autocomplete(false) + ->required() + ->maxLength($doc['max']), + + AdvancedFileUpload::make($doc['file_name']) + ->hiddenLabel() + ->disk(config('filesystems.default')) + ->acceptedFileTypes($doc['accept']) + ->maxSize($doc['max_size']) + ->directory($doc['folder'].now()->toDateString()) + ->helperText('Format yang diterima: '.implode(', ', array_map(fn ($t) => strtoupper(str_replace('application/', '', $t)), $doc['accept'])).'. Ukuran maksimum: '.($doc['max_size'] / 1024).' MB.'), + ]); + })->toArray() + ) + ->columns(2), + ]) + ->statePath('data'); + } + + public static function getDocuments(): array + { + return [ + [ + 'title' => 'Organisasi Kewartawanan', + 'text_name' => 'journalism_organization', + 'placeholder' => '*****', + 'max' => 255, + 'max_size' => 1024 * 10, + 'file_name' => 'journalism_organization_docs', + 'accept' => ['application/pdf'], + 'folder' => 'media/journalism-organization/', + ], + [ + 'title' => 'Sertifikat Dewan Pers', + 'text_name' => 'press_council_certificate', + 'placeholder' => '*****', + 'max' => 255, + 'max_size' => 1024 * 10, + 'file_name' => 'press_council_certificate_docs', + 'accept' => ['application/pdf'], + 'folder' => 'media/press-council-certificate/', + ], + ]; + } +} diff --git a/app/Filament/Pages/Media/Services/MediaSaveService.php b/app/Filament/Pages/Media/Services/MediaSaveService.php new file mode 100644 index 0000000..7257359 --- /dev/null +++ b/app/Filament/Pages/Media/Services/MediaSaveService.php @@ -0,0 +1,214 @@ +where('entity_id', $media->id) + ->where('status', DataChangeStatus::PENDING) + ->exists(); + + if ($existingRequest) { + CheerfulNotification::warning( + CheerfulNotification::getByKey('data_change.existing.title'), + CheerfulNotification::getByKey('data_change.existing.body') + )->send(); + + return; + } + + $data = array_merge($formState, $actionData); + + $fieldLabels = PartnerMedia::dataChangeEntryLabels(); + + $editableFields = [ + 'name', + 'link', + 'address', + 'type', + 'classification', + 'journalism_organization', + 'press_council_certificate', + ]; + + $changedFields = []; + $oldDataArr = []; + + $casts = $media->getCasts(); + foreach ($editableFields as $field) { + $val = $data[$field] ?? null; + $oldVal = $media->{$field}; + + if (isset($casts[$field]) && ! ($val instanceof BackedEnum) && enum_exists($casts[$field])) { + /** @var class-string $enumClass */ + $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; + } + } + + $documents = [ + 'journalism_organization_docs', + 'press_council_certificate_docs', + ]; + + foreach ($documents as $field) { + $newValue = $pageData[$field] ?? []; + $oldValue = $originalDocuments[$field] ?? []; + + if (json_encode($newValue) !== json_encode($oldValue)) { + $label = $fieldLabels[$field] ?? $field; + $changedFields[$label] = $newValue; + $oldDataArr[$label] = $oldValue; + } + } + + if (empty($changedFields)) { + CheerfulNotification::info( + CheerfulNotification::getByKey('data_change.no_change.title'), + CheerfulNotification::getByKey('data_change.no_change.body') + )->send(); + + return; + } + + $dataChangeRequest = DataChangeRequest::create([ + 'user_id' => auth()->id(), + 'entity_type' => PartnerMedia::class, + 'entity_id' => $media->id, + 'old_data' => $oldDataArr, + 'new_data' => $changedFields, + 'change_reason' => $data['change_reason'] ?? null, + ]); + + CheerfulNotification::success( + CheerfulNotification::getByKey('data_change.success.title'), + CheerfulNotification::getByKey('data_change.success.body') + )->send(); + + User::superAdmin() + ->get() + ->each(function ($admin) use ($dataChangeRequest): void { + $admin->notify(new BroadcastNotification([ + 'title' => CheerfulNotification::getByKey('data_change.admin_notify.title'), + 'body' => CheerfulNotification::getByKey('data_change.admin_notify.body', ['name' => auth()->user()->name]), + 'action' => [ + Action::make('view') + ->label('Lihat') + ->url(DataChangesResource::getUrl('view', ['record' => $dataChangeRequest->id])), + ], + ])); + }); + } + + private static function saveMediaDirectly( + ?PartnerMedia $existingMedia, + int $companyId, + array $formState, + array $pageData, + ): PartnerMedia { + $media = PartnerMedia::updateOrCreate([ + 'id' => $existingMedia?->id, + ], [ + 'company_id' => $companyId, + 'name' => $formState['name'], + 'link' => $formState['link'], + 'address' => $formState['address'], + 'type' => $formState['type'], + 'classification' => $formState['classification'], + 'journalism_organization' => $formState['journalism_organization'], + 'press_council_certificate' => $formState['press_council_certificate'], + ]); + + $documents = [ + 'journalism_organization_docs', + 'press_council_certificate_docs', + ]; + + foreach ($documents as $field) { + if (empty($pageData[$field])) { + continue; + } + + foreach ((array) $pageData[$field] as $filePath) { + $fullPath = Storage::disk(config('filesystems.default'))->path($filePath); + + 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'); + } + } + + return $media; + } +}