From ec7a9bdf3048155b09ab94e4f0fb84ff140828ec Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 10 Apr 2026 16:17:34 +0700 Subject: [PATCH] feat: implement cooperation payment workflow with invoice submission, approval, and rejection actions --- .../Cooperation/CompleteCooperationAction.php | 42 ++++- .../Cooperations/Actions/Media/PayAction.php | 91 ----------- .../Actions/Payment/AcceptAction.php | 59 +++++++ .../Actions/Payment/RejectAction.php | 71 +++++++++ .../Actions/Payment/SubmitInvoiceAction.php | 137 ++++++++++++++++ .../Actions/Report/SubmitReportAction.php | 146 ++++++++++++++++++ .../CooperationMediaRelationManager.php | 46 ------ .../CooperationPaymentRelationManager.php | 129 +++++++++++++--- .../ReportRelationManager.php | 133 +--------------- app/Models/CooperationMedia.php | 6 +- app/Models/CooperationPayment.php | 28 ++++ app/Models/Report.php | 6 +- ...0256_create_cooperation_payments_table.php | 4 +- lang/id/notif.php | 52 +++++++ 14 files changed, 648 insertions(+), 302 deletions(-) delete mode 100644 app/Filament/Resources/Manage/Cooperations/Actions/Media/PayAction.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Actions/Payment/AcceptAction.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Actions/Payment/RejectAction.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Actions/Payment/SubmitInvoiceAction.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Actions/Report/SubmitReportAction.php diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CompleteCooperationAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CompleteCooperationAction.php index 381fb54..ed50224 100644 --- a/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CompleteCooperationAction.php +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CompleteCooperationAction.php @@ -2,12 +2,15 @@ namespace App\Filament\Resources\Manage\Cooperations\Actions\Cooperation; +use App\Enums\ApprovalStatus; use App\Enums\CooperationStatus; use App\Enums\RoleEnum; use App\Filament\Support\CheerfulNotification; use App\Models\Cooperation; +use App\Models\User; use Filament\Actions\Action; use Filament\Support\Icons\Heroicon; +use Illuminate\Support\Facades\Auth; class CompleteCooperationAction extends Action { @@ -27,7 +30,23 @@ protected function setUp(): void ->modalHeading(fn () => CheerfulNotification::getByKey('cooperation.complete_title')) ->modalDescription(fn () => CheerfulNotification::getByKey('cooperation.complete_desc')) ->action(function (Cooperation $record): void { - // Verify all accepted media are paid + // Check for any pending payments first (globally for this cooperation) + $hasPendingPayments = $record->payments() + ->where('cooperation_payments.status', ApprovalStatus::PENDING) + ->exists(); + + if ($hasPendingPayments) { + CheerfulNotification::danger( + CheerfulNotification::getByKey('cooperation.payment_pending'), + CheerfulNotification::getByKey('cooperation.payment_pending_desc') + )->send(); + + $this->halt(); + + return; + } + + // 1. Media whose proposal was accepted $acceptedMedia = $record->cooperationMedia()->accepted()->get(); if ($acceptedMedia->isEmpty()) { @@ -39,8 +58,17 @@ protected function setUp(): void return; } - $unpaidMedia = $acceptedMedia->filter(function ($media) { - return ! $media->payment()->exists(); + // 2. Filter media that have at least one accepted report + $mediaWithObligation = $acceptedMedia->filter(function ($media) use ($record) { + return $record->reports() + ->whereHas('mediaTaskAssignment.partnerMedia', fn ($q) => $q->where('partner_media.id', $media->partner_media_id)) + ->accepted() + ->exists(); + }); + + // 3. Of those media, verify they all have an ACCEPTED payment + $unpaidMedia = $mediaWithObligation->filter(function ($media) { + return ! $media->payment()->accepted()->exists(); }); if ($unpaidMedia->isNotEmpty()) { @@ -54,8 +82,9 @@ protected function setUp(): void return; } - // Calculate total payment from media payments - $totalPayment = $acceptedMedia->sum(fn ($media) => $media->payment->amount); + // Calculate total payment from all accepted media payments + // This will count all accepted payments, even if they didn't have reports (unlikely but possible) + $totalPayment = $record->payments()->accepted()->sum('amount'); $record->update([ 'payment_amount' => $totalPayment, @@ -69,7 +98,8 @@ protected function setUp(): void )->send(); }) ->visible(function (Cooperation $record): bool { - $user = auth()->user(); + /** @var User $user */ + $user = Auth::user(); return $user && ! $user->hasRole(RoleEnum::PERUSAHAAN->value) && $record->status === CooperationStatus::PAYMENT; diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Media/PayAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Media/PayAction.php deleted file mode 100644 index f69a671..0000000 --- a/app/Filament/Resources/Manage/Cooperations/Actions/Media/PayAction.php +++ /dev/null @@ -1,91 +0,0 @@ -label('Bayar') - ->icon(Heroicon::OutlinedCreditCard) - ->color('success') - ->visible(fn (CooperationMedia $record): bool => $record->cooperation->status === CooperationStatus::PAYMENT && ! $record->payment()->exists() && $record->status === ApprovalStatus::ACCEPTED) - ->schema([ - TextInput::make('amount') - ->label('Nominal Pembayaran') - ->placeholder('1,000,000') - ->autocomplete(false) - ->autofocus() - ->required() - ->mask(RawJs::make('$money($input)')) - ->prefix('Rp'), - - SpatieMediaLibraryFileUpload::make('payment_proof') - ->label('Bukti Pembayaran') - ->disk(config('filesystems.default')) - ->acceptedFileTypes(['image/*']) - ->maxSize(1024 * 3) - ->collection('cooperations') - ->customProperties(fn (): array => [ - 'feature' => 'cooperations', - 'date' => now()->toDateString(), - 'doc_type' => 'payment-proof', - ]) - ->image() - ->helperText('Format yang diterima: JPEG, PNG, GIF, WEBP. Ukuran maksimum: 3 MB.') - ->required(), - - Textarea::make('description') - ->label('Keterangan') - ->placeholder('...') - ->autocomplete(false), - ]) - ->action(function (CooperationMedia $record, array $data): void { - $record->payment()->create([ - 'amount' => str_replace(['Rp ', ','], '', $data['amount']), - 'description' => $data['description'], - 'payment_date' => now(), - ]); - - CheerfulNotification::success( - CheerfulNotification::getByKey('cooperation.payment_recorded_success'), - CheerfulNotification::getByKey('notification.payment_processed', ['record__partnerMedia__name' => $record->partnerMedia->name]) - )->send(); - - $partnerUser = $record->partnerMedia->company?->user; - if ($partnerUser) { - $partnerUser->notify(new BroadcastNotification([ - 'title' => CheerfulNotification::getByKey('cooperation.payment_sent'), - 'body' => CheerfulNotification::getByKey('notification.payment_sent_desc', ['record__cooperation__title' => $record->cooperation->title]), - 'action' => [ - Action::make('view') - ->label('Lihat') - ->url(CooperationResource::getUrl('view', ['record' => $record->cooperation_id, 'relation' => 0])), - ], - ])); - } - }) - ->modalWidth(Width::Large); - } -} diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Payment/AcceptAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Payment/AcceptAction.php new file mode 100644 index 0000000..0324ee8 --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Payment/AcceptAction.php @@ -0,0 +1,59 @@ +label('Terima') + ->icon(Heroicon::OutlinedCheck) + ->color('success') + ->action(function (CooperationPayment $record): void { + $record->update([ + 'status' => ApprovalStatus::ACCEPTED, + 'payment_date' => now(), + ]); + + CheerfulNotification::success( + CheerfulNotification::getByKey('cooperation.payment_approved'), + CheerfulNotification::getByKey('notification.payment_approved_notice', ['record__partnerMedia__name' => $record->cooperationMedia->partnerMedia->name]) + )->send(); + + $partnerUser = $record->cooperationMedia->partnerMedia->company?->user; + if ($partnerUser) { + $partnerUser->notify(new BroadcastNotification([ + 'title' => CheerfulNotification::getByKey('cooperation.payment_approved'), + 'body' => CheerfulNotification::getByKey('notification.payment_approved_notice', ['record__partnerMedia__name' => $record->cooperationMedia->partnerMedia->name]), + 'action' => [ + Action::make('view') + ->label('Lihat') + ->url(CooperationResource::getUrl('view', ['record' => $record->cooperationMedia->cooperation_id, 'relation' => 4])), + ], + ])); + } + }) + ->visible( + fn (CooperationPayment $record): bool => $record->cooperationMedia->cooperation->status === CooperationStatus::PAYMENT && + ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) && + $record->status === ApprovalStatus::PENDING + ); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Payment/RejectAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Payment/RejectAction.php new file mode 100644 index 0000000..d552388 --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Payment/RejectAction.php @@ -0,0 +1,71 @@ +label('Tolak') + ->icon(Heroicon::OutlinedXMark) + ->color('danger') + ->schema([ + Textarea::make('reason') + ->label('Alasan Penolakan') + ->placeholder('Jelaskan alasan pengajuan ditolak...') + ->required(), + ]) + ->modalHeading(fn () => CheerfulNotification::getByKey('cooperation.reject_payment_title')) + ->modalDescription(fn () => CheerfulNotification::getByKey('cooperation.reject_payment_desc')) + ->action(function (CooperationPayment $record, array $data): void { + $record->update([ + 'status' => ApprovalStatus::REJECTED, + ]); + + $record->rejectionReasons()->create(['reason' => $data['reason']]); + + CheerfulNotification::warning( + CheerfulNotification::getByKey('cooperation.payment_rejected'), + CheerfulNotification::getByKey('notification.payment_rejected_reason', ['record__partnerMedia__name' => $record->cooperationMedia->partnerMedia->name, 'data__reason' => $data['reason']]) + )->send(); + + $partnerUser = $record->cooperationMedia->partnerMedia->company?->user; + if ($partnerUser) { + $partnerUser->notify(new BroadcastNotification([ + 'title' => CheerfulNotification::getByKey('cooperation.payment_rejected'), + 'body' => CheerfulNotification::getByKey('notification.payment_rejected_reason', ['record__partnerMedia__name' => $record->cooperationMedia->partnerMedia->name, 'data__reason' => $data['reason']]), + 'action' => [ + Action::make('view') + ->label('Lihat') + ->url(CooperationResource::getUrl('view', ['record' => $record->cooperationMedia->cooperation_id, 'relation' => 4])), + ], + ])); + } + }) + ->visible( + fn (CooperationPayment $record): bool => $record->cooperationMedia->cooperation->status === CooperationStatus::PAYMENT && + ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) && + $record->status === ApprovalStatus::PENDING + ) + ->modalWidth(Width::Large); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Payment/SubmitInvoiceAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Payment/SubmitInvoiceAction.php new file mode 100644 index 0000000..dbfb9ca --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Payment/SubmitInvoiceAction.php @@ -0,0 +1,137 @@ +label('Ajukan Pembayaran') + ->modalWidth(Width::Large) + ->modalHeading(fn () => CheerfulNotification::getByKey('cooperation.create_payment_title')) + ->modalDescription(fn () => CheerfulNotification::getByKey('cooperation.create_payment_desc')) + ->schema([ + TextInput::make('amount') + ->label('Nominal Pembayaran') + ->placeholder('1,000,000') + ->autocomplete(false) + ->autofocus() + ->required() + ->mask(RawJs::make('$money($input)')) + ->prefix('Rp'), + + Textarea::make('description') + ->label('Keterangan') + ->placeholder('Silakan isi keterangan atau rujukan pembayaran...'), + + AdvancedFileUpload::make('payment_attachment') + ->label('Lampiran Penagihan') + ->disk(config('filesystems.default')) + ->acceptedFileTypes(['application/pdf']) + ->maxSize(1024 * 10) + ->directory(fn (): string => 'cooperations/payment-attachment/'.now()->toDateString()) + ->helperText('Format yang diterima: PDF. Ukuran maksimum: 10 MB.') + ->required(), + ]) + ->mutateDataUsing(function (array $data): array { + $cooperation = $this->getLivewire()->getOwnerRecord(); + + if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) { + $cooperationMedia = $cooperation->cooperationMedia() + ->whereHas('partnerMedia.company', function ($query) { + $query->where('user_id', auth()->id()); + }) + ->first(); + + $data['cooperation_media_id'] = $cooperationMedia?->id; + $data['amount'] = str_replace(['Rp ', ' ', ','], '', $data['amount']); + } + + return $data; + }) + ->successNotification(null) + ->after(function (CooperationPayment $record, array $data): void { + if (! empty($data['payment_attachment'])) { + $filePath = collect($data['payment_attachment'])->first(); + $fullPath = Storage::disk(config('filesystems.default'))->path($filePath); + + if (file_exists($fullPath)) { + $record->addMediaFromDisk($filePath, config('filesystems.default')) + ->preservingOriginal() + ->withCustomProperties([ + 'feature' => 'cooperations', + 'date' => now()->toDateString(), + 'doc_type' => 'payment-attachment', + ]) + ->toMediaCollection('cooperations'); + } + } + + CheerfulNotification::success( + CheerfulNotification::getByKey('cooperation.payment_requested_success'), + CheerfulNotification::getByKey('cooperation.payment_requested_desc') + )->send(); + + // Notify Admins + User::query() + ->superAdmin() + ->get() + ->each(function ($admin) use ($record): void { + $user = auth()->user(); + + $admin->notify(new BroadcastNotification([ + 'title' => CheerfulNotification::getByKey('cooperation.create_payment_title'), + 'body' => CheerfulNotification::getByKey('notification.payment_requested', ['user__name' => $user->name, 'record__partnerMedia__name' => $record->cooperationMedia->partnerMedia->name, 'record__cooperation__title' => $this->getLivewire()->getOwnerRecord()->title]), + 'action' => [ + Action::make('view') + ->label('Lihat') + ->url(CooperationResource::getUrl('view', ['record' => $this->getLivewire()->getOwnerRecord()->id, 'relation' => 4])), + ], + ])); + }); + }) + ->visible(function (): bool { + $user = auth()->user(); + + if (! $user || ! $user->hasRole(RoleEnum::PERUSAHAAN->value)) { + return false; + } + + $cooperation = $this->getLivewire()->getOwnerRecord(); + + return $cooperation->status === CooperationStatus::PAYMENT + && $cooperation->proposal() + ->whereHas('partnerMedia.company', fn ($q) => $q->where('user_id', auth()->id())) + ->accepted() + ->exists() + && ! $cooperation->payments() + ->whereHas('cooperationMedia.partnerMedia.company', fn ($q) => $q->where('user_id', auth()->id())) + ->where('cooperation_payments.status', '!=', ApprovalStatus::REJECTED) + ->exists(); + }); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Report/SubmitReportAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Report/SubmitReportAction.php new file mode 100644 index 0000000..e41cfe5 --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Report/SubmitReportAction.php @@ -0,0 +1,146 @@ +label('Buat Bukti Tayang') + ->modalWidth(Width::Large) + ->modalHeading(fn () => CheerfulNotification::getByKey('cooperation.create_report_title')) + ->modalDescription(fn () => CheerfulNotification::getByKey('cooperation.create_report_desc')) + ->schema([ + TextInput::make('title') + ->label('Judul') + ->placeholder('Dirgahayu Purwakarta') + ->required() + ->autocomplete(false) + ->autofocus(), + + DatePicker::make('publication_date') + ->label('Tanggal Publikasi') + ->placeholder(fn (): string => now()->translatedFormat('l, d F Y')) + ->native(false) + ->displayFormat('l, d F Y') + ->required(), + + TextInput::make('link') + ->label('Tautan') + ->placeholder('https://example.com') + ->maxLength(255) + ->url() + ->required() + ->autocomplete(false), + + Textarea::make('description') + ->label('Deskripsi') + ->placeholder('....') + ->required() + ->autocomplete(false), + + SpatieMediaLibraryFileUpload::make('image') + ->label('Gambar') + ->disk(config('filesystems.default')) + ->acceptedFileTypes(['image/*']) + ->maxSize(1024 * 3) + ->collection('reports') + ->image() + ->helperText('Format yang diterima: JPEG, PNG, GIF, WEBP. Ukuran maksimum: 3 MB.') + ->required(), + ]) + ->using(function (array $data, string $model): Model { + return $model::create($data); + }) + ->mutateDataUsing(function (array $data): array { + $taskAssignment = $this->getLivewire()->getOwnerRecord()->taskAssignment; + + if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) { + $mediaTaskAssignment = $taskAssignment?->mediaTaskAssignments() + ->whereHas('partnerMedia.company', function ($query) { + $query->where('user_id', auth()->id()); + }) + ->first(); + + $data['media_task_assignment_id'] = $mediaTaskAssignment?->id; + } + + return $data; + }) + ->successNotification(null) + ->after(function (Report $record): void { + CheerfulNotification::success( + CheerfulNotification::getByKey('cooperation.report_sent_success'), + CheerfulNotification::getByKey('cooperation.report_sent_desc') + )->send(); + + // Notify Admins + User::query() + ->superAdmin() + ->get() + ->each(function ($admin) use ($record): void { + $user = auth()->user(); + + $admin->notify(new BroadcastNotification([ + 'title' => CheerfulNotification::getByKey('cooperation.new_report'), + 'body' => CheerfulNotification::getByKey('notification.user_submitted_report', ['user__name' => $user->name, 'record__title' => $record->title, 'this__getOwnerRecord____title' => $this->getLivewire()->getOwnerRecord()->title]), + 'action' => [ + Action::make('view') + ->label('Lihat') + ->url(CooperationResource::getUrl('view', ['record' => $this->getLivewire()->getOwnerRecord()->id, 'relation' => 3])), + ], + ])); + }); + }) + ->visible(function (): bool { + $user = auth()->user(); + + if (! $user || ! $user->hasRole(RoleEnum::PERUSAHAAN->value)) { + return false; + } + + $cooperation = $this->getLivewire()->getOwnerRecord(); + $taskAssignment = $cooperation->taskAssignment; + + if (! $taskAssignment) { + return false; + } + + return $cooperation->status === CooperationStatus::ASSIGNMENT + && $taskAssignment->start_date->toDateString() <= now()->toDateString() + && $taskAssignment->end_date->toDateString() >= now()->toDateString() + && $cooperation->proposal() + ->whereHas('partnerMedia.company', fn ($q) => $q->where('user_id', auth()->id())) + ->accepted() + ->exists() + && $taskAssignment->reports() + ->whereHas('mediaTaskAssignment.partnerMedia.company', fn ($q) => $q->where('user_id', auth()->id())) + ->where('reports.status', '!=', ApprovalStatus::REJECTED) + ->count() < $taskAssignment->report_amount; + }); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php index 129bbd1..866f359 100644 --- a/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php +++ b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php @@ -2,18 +2,10 @@ namespace App\Filament\Resources\Manage\Cooperations\RelationManagers; -use App\Enums\ApprovalStatus; use App\Enums\RoleEnum; use App\Filament\Columns\RowIndexColumn; -use App\Filament\Resources\Manage\Cooperations\Actions\Media\PayAction; use App\Filament\Support\CheerfulNotification; -use App\Models\CooperationMedia; -use Filament\Actions\ViewAction; -use Filament\Infolists\Components\ImageEntry; -use Filament\Infolists\Components\TextEntry; use Filament\Resources\RelationManagers\RelationManager; -use Filament\Schemas\Components\Grid; -use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; @@ -45,51 +37,13 @@ public function table(Table $table): Table ->label('Nama') ->searchable(), - TextColumn::make('payment.amount') - ->label('Jumlah Pembayaran') - ->money('IDR', decimalPlaces: 0) - ->placeholder('Belum ditentukan'), - TextColumn::make('status') ->label('Status') ->badge(), ]) - ->recordActions([ - PayAction::make(), - - ViewAction::make() - ->visible(fn (CooperationMedia $record): bool => $record->status === ApprovalStatus::ACCEPTED && $record->payment()->exists()), - ]) ->emptyStateIcon(Heroicon::OutlinedNewspaper) ->emptyStateHeading(fn () => CheerfulNotification::getByKey('cooperation.media_data_empty_heading')) ->emptyStateDescription(fn () => CheerfulNotification::getByKey('cooperation.media_data_empty')) ->defaultSort('created_at', 'desc'); } - - public function infolist(Schema $infolist): Schema - { - return $infolist - ->components([ - Grid::make(2) - ->schema([ - TextEntry::make('payment.amount') - ->label('Nominal') - ->money('IDR', decimalPlaces: 0), - - TextEntry::make('payment.payment_date') - ->label('Tanggal Bayar') - ->dateTime('l, d F Y'), - ]), - - TextEntry::make('payment.description') - ->label('Keterangan') - ->visible(fn (CooperationMedia $record): bool => $record->description !== null), - - ImageEntry::make('payment_proof') - ->label('Bukti Pembayaran') - ->getStateUsing(fn (CooperationMedia $record): ?string => optional($record->getMedia('cooperations')->where('custom_properties.doc_type', 'payment-proof')->sortByDesc('created_at')->first())->getPathRelativeToRoot()) - ->disk(config('filesystems.default')), - ]) - ->columns(1); - } } diff --git a/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationPaymentRelationManager.php b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationPaymentRelationManager.php index 16b9242..bcec9ef 100644 --- a/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationPaymentRelationManager.php +++ b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationPaymentRelationManager.php @@ -2,17 +2,24 @@ namespace App\Filament\Resources\Manage\Cooperations\RelationManagers; +use App\Enums\ApprovalStatus; use App\Enums\RoleEnum; -use App\Filament\Columns\RowIndexColumn; +use App\Filament\Resources\Manage\Cooperations\Actions\Payment\AcceptAction; +use App\Filament\Resources\Manage\Cooperations\Actions\Payment\RejectAction; +use App\Filament\Resources\Manage\Cooperations\Actions\Payment\SubmitInvoiceAction; use App\Filament\Support\CheerfulNotification; use App\Models\CooperationPayment; +use Filament\Actions\ViewAction; +use Filament\Infolists\Components\TextEntry; use Filament\Resources\RelationManagers\RelationManager; +use Filament\Schemas\Components\Grid; +use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; -use Filament\Tables\Columns\ImageColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Joaopaulolndev\FilamentPdfViewer\Infolists\Components\PdfViewerEntry; class CooperationPaymentRelationManager extends RelationManager { @@ -20,53 +27,133 @@ class CooperationPaymentRelationManager extends RelationManager protected static ?string $title = 'Pembayaran'; + public function isReadOnly(): bool + { + return false; + } + public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool { - if (! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) { - return false; + if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) { + return $ownerRecord->cooperationMedia() + ->whereHas('partnerMedia.company', function ($query) { + $query->where('user_id', auth()->id()); + }) + ->accepted() + ->exists(); } - return $ownerRecord->cooperationMedia() - ->whereHas('partnerMedia.company', function ($query) { - $query->where('user_id', auth()->id()); - }) - ->accepted() - ->exists(); + return true; + } + + public static function getBadge(Model $ownerRecord, string $pageClass): ?string + { + $query = $ownerRecord->payments(); + + if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) { + $query->whereHas('cooperationMedia.partnerMedia.company', function ($q) { + $q->where('user_id', auth()->id()); + }); + } + + return (string) $query->count(); } public function table(Table $table): Table { return $table ->columns([ - ...RowIndexColumn::make(), + TextColumn::make('cooperationMedia.partnerMedia.name') + ->label('Media') + ->searchable() + ->visible(! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)), TextColumn::make('amount') ->label('Nominal') - ->money('IDR', decimalPlaces: 0), + ->money('IDR', decimalPlaces: 0) + ->placeholder('Menunggu Verifikasi'), TextColumn::make('payment_date') ->label('Tanggal Bayar') - ->date('l, d F Y'), + ->date('l, d F Y') + ->placeholder('-'), TextColumn::make('description') ->label('Keterangan') ->limit(50), - ImageColumn::make('payment_proof') - ->label('Bukti Pembayaran') - ->getStateUsing(fn (CooperationPayment $record): ?string => optional($record->cooperationMedia->getMedia('cooperations')->where('custom_properties.doc_type', 'payment-proof')->sortByDesc('created_at')->first())->getPathRelativeToRoot()) - ->disk(config('filesystems.default')), + TextColumn::make('status') + ->label('Status') + ->badge(), + + TextColumn::make('rejectionReasons.reason') + ->label('Alasan Penolakan') + ->searchable(), + ]) + ->recordActions([ + ViewAction::make(), + + AcceptAction::make(), + + RejectAction::make(), + ]) + ->headerActions([ + SubmitInvoiceAction::make(), ]) ->emptyStateIcon(Heroicon::OutlinedCreditCard) ->emptyStateHeading(fn () => CheerfulNotification::getByKey('cooperation.payment_empty_heading')) ->emptyStateDescription(fn () => CheerfulNotification::getByKey('cooperation.payment_empty')) ->defaultSort('created_at', 'desc') ->modifyQueryUsing(function (Builder $query): void { - $query->when(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value), function (Builder $subQuery): void { - $subQuery->whereHas('partnerMedia.company', function (Builder $q): void { - $q->where('user_id', auth()->id()); + $query->with(['cooperationMedia.media']) + ->when(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value), function (Builder $subQuery): void { + $subQuery->whereHas('cooperationMedia.partnerMedia.company', function (Builder $q): void { + $q->where('user_id', auth()->id()); + }); }); - }); }); } + + public function infolist(Schema $infolist): Schema + { + return $infolist + ->components([ + Grid::make(2) + ->schema([ + TextEntry::make('cooperationMedia.partnerMedia.name') + ->label('Media') + ->visible(! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)), + + TextEntry::make('status') + ->label('Status') + ->badge(), + + TextEntry::make('amount') + ->label('Nominal Penagihan') + ->money('IDR', decimalPlaces: 0), + + TextEntry::make('created_at') + ->label('Diajukan') + ->dateTime('l, d F Y H:i:s'), + ]), + + TextEntry::make('description') + ->label('Keterangan') + ->columnSpanFull() + ->placeholder('-'), + + PdfViewerEntry::make('payment_attachment') + ->label('Lampiran Penagihan') + ->disk(config('filesystems.default')) + ->getStateUsing(fn (CooperationPayment $record): ?string => optional($record->getMedia('cooperations')->where('custom_properties.doc_type', 'payment-attachment')->sortByDesc('created_at')->first())->getPathRelativeToRoot()) + ->columnSpanFull(), + + TextEntry::make('rejectionReasons.reason') + ->label('Alasan Penolakan') + ->listWithLineBreaks() + ->visible(fn (CooperationPayment $record): bool => $record->status === ApprovalStatus::REJECTED && $record->rejectionReasons->isNotEmpty()) + ->columnSpanFull(), + ]) + ->columns(1); + } } diff --git a/app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php b/app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php index bf34515..daf57fd 100644 --- a/app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php +++ b/app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php @@ -2,26 +2,14 @@ namespace App\Filament\Resources\Manage\Cooperations\RelationManagers; -use App\Enums\ApprovalStatus; -use App\Enums\CooperationStatus; use App\Enums\RoleEnum; use App\Filament\Columns\RowIndexColumn; use App\Filament\Resources\Manage\Cooperations\Actions\Report\AcceptAction; use App\Filament\Resources\Manage\Cooperations\Actions\Report\RejectAction; -use App\Filament\Resources\Manage\Cooperations\CooperationResource; +use App\Filament\Resources\Manage\Cooperations\Actions\Report\SubmitReportAction; use App\Filament\Support\CheerfulNotification; use App\Models\Report; -use App\Models\User; -use App\Notifications\BroadcastNotification; -use Filament\Actions\Action; -use Filament\Actions\CreateAction; -use Filament\Forms\Components\DatePicker; -use Filament\Forms\Components\SpatieMediaLibraryFileUpload; -use Filament\Forms\Components\Textarea; -use Filament\Forms\Components\TextInput; use Filament\Resources\RelationManagers\RelationManager; -use Filament\Schemas\Schema; -use Filament\Support\Enums\Width; use Filament\Support\Icons\Heroicon; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; @@ -66,51 +54,6 @@ public static function getBadge(Model $ownerRecord, string $pageClass): ?string return (string) $query->count(); } - public function form(Schema $schema): Schema - { - return $schema - ->components([ - TextInput::make('title') - ->label('Judul') - ->placeholder('Dirgahayu Purwakarta') - ->required() - ->autocomplete(false) - ->autofocus(), - - DatePicker::make('publication_date') - ->label('Tanggal Publikasi') - ->placeholder(fn (): string => now()->translatedFormat('l, d F Y')) - ->native(false) - ->displayFormat('l, d F Y') - ->required(), - - TextInput::make('link') - ->label('Tautan') - ->placeholder('https://example.com') - ->maxLength(255) - ->url() - ->required() - ->autocomplete(false), - - Textarea::make('description') - ->label('Deskripsi') - ->placeholder('....') - ->required() - ->autocomplete(false), - - SpatieMediaLibraryFileUpload::make('image') - ->label('Gambar') - ->disk(config('filesystems.default')) - ->acceptedFileTypes(['image/*']) - ->maxSize(1024 * 3) - ->collection('reports') - ->image() - ->helperText('Format yang diterima: JPEG, PNG, GIF, WEBP. Ukuran maksimum: 3 MB.') - ->required(), - ]) - ->columns(1); - } - public function table(Table $table): Table { return $table @@ -150,79 +93,7 @@ public function table(Table $table): Table RejectAction::make(), ]) ->headerActions([ - CreateAction::make() - ->label('Buat Bukti Tayang') - ->modalWidth(Width::Large) - ->modalHeading(fn () => CheerfulNotification::getByKey('cooperation.create_report_title')) - ->modalDescription(fn () => CheerfulNotification::getByKey('cooperation.create_report_desc')) - ->using(function (array $data, string $model): Model { - return $model::create($data); - }) - ->mutateDataUsing(function (array $data): array { - $taskAssignment = $this->getOwnerRecord()->taskAssignment; - - if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) { - $mediaTaskAssignment = $taskAssignment?->mediaTaskAssignments() - ->whereHas('partnerMedia.company', function ($query) { - $query->where('user_id', auth()->id()); - }) - ->first(); - - $data['media_task_assignment_id'] = $mediaTaskAssignment?->id; - } - - return $data; - }) - ->successNotification(null) - ->after(function (Report $record): void { - CheerfulNotification::success( - CheerfulNotification::getByKey('cooperation.report_sent_success'), - CheerfulNotification::getByKey('cooperation.report_sent_desc') - )->send(); - - // Notify Admins - User::query() - ->superAdmin() - ->get() - ->each(function ($admin) use ($record): void { - $user = auth()->user(); - - $admin->notify(new BroadcastNotification([ - 'title' => CheerfulNotification::getByKey('cooperation.new_report'), - 'body' => CheerfulNotification::getByKey('notification.user_submitted_report', ['user__name' => $user->name, 'record__title' => $record->title, 'this__getOwnerRecord____title' => $this->getOwnerRecord()->title]), - 'action' => [ - Action::make('view') - ->label('Lihat') - ->url(CooperationResource::getUrl('view', ['record' => $this->getOwnerRecord()->id, 'relation' => 3])), - ], - ])); - }); - }) - ->visible(function (): bool { - $user = auth()->user(); - - if (! $user || ! $user->hasRole(RoleEnum::PERUSAHAAN->value)) { - return false; - } - - $taskAssignment = $this->getOwnerRecord()->taskAssignment; - - if (! $taskAssignment) { - return false; - } - - return $this->getOwnerRecord()->status === CooperationStatus::ASSIGNMENT - && $taskAssignment->start_date->toDateString() <= now()->toDateString() - && $taskAssignment->end_date->toDateString() >= now()->toDateString() - && $this->getOwnerRecord()->proposal() - ->whereHas('partnerMedia.company', fn ($q) => $q->where('user_id', auth()->id())) - ->accepted() - ->exists() - && $taskAssignment->reports() - ->whereHas('mediaTaskAssignment.partnerMedia.company', fn ($q) => $q->where('user_id', auth()->id())) - ->where('reports.status', '!=', ApprovalStatus::REJECTED) - ->count() < $taskAssignment->report_amount; - }), + SubmitReportAction::make(), ]) ->toolbarActions([]) ->emptyStateIcon(Heroicon::OutlinedInboxArrowDown) diff --git a/app/Models/CooperationMedia.php b/app/Models/CooperationMedia.php index f4b589b..d530df3 100644 --- a/app/Models/CooperationMedia.php +++ b/app/Models/CooperationMedia.php @@ -34,19 +34,19 @@ protected function casts(): array #[Scope] protected function pending(Builder $query): void { - $query->where('status', ApprovalStatus::PENDING); + $query->where($this->getTable().'.status', ApprovalStatus::PENDING); } #[Scope] protected function accepted(Builder $query): void { - $query->where('status', ApprovalStatus::ACCEPTED); + $query->where($this->getTable().'.status', ApprovalStatus::ACCEPTED); } #[Scope] protected function rejected(Builder $query): void { - $query->where('status', ApprovalStatus::REJECTED); + $query->where($this->getTable().'.status', ApprovalStatus::REJECTED); } public function cooperation(): BelongsTo diff --git a/app/Models/CooperationPayment.php b/app/Models/CooperationPayment.php index 467abef..1e253ed 100644 --- a/app/Models/CooperationPayment.php +++ b/app/Models/CooperationPayment.php @@ -2,9 +2,13 @@ namespace App\Models; +use App\Enums\ApprovalStatus; +use Illuminate\Database\Eloquent\Attributes\Scope; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphMany; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; use Swindon\FilamentHashids\Traits\HasHashid; @@ -20,11 +24,35 @@ protected function casts(): array return [ 'amount' => 'integer', 'payment_date' => 'date', + 'status' => ApprovalStatus::class, ]; } + #[Scope] + protected function accepted(Builder $query): void + { + $query->where($this->getTable().'.status', ApprovalStatus::ACCEPTED); + } + + #[Scope] + protected function pending(Builder $query): void + { + $query->where($this->getTable().'.status', ApprovalStatus::PENDING); + } + + #[Scope] + protected function rejected(Builder $query): void + { + $query->where($this->getTable().'.status', ApprovalStatus::REJECTED); + } + public function cooperationMedia(): BelongsTo { return $this->belongsTo(CooperationMedia::class); } + + public function rejectionReasons(): MorphMany + { + return $this->morphMany(RejectionReason::class, 'rejectable'); + } } diff --git a/app/Models/Report.php b/app/Models/Report.php index f5a935e..5f9b284 100644 --- a/app/Models/Report.php +++ b/app/Models/Report.php @@ -31,19 +31,19 @@ protected function casts(): array #[Scope] protected function accepted(Builder $query): void { - $query->where('status', ApprovalStatus::ACCEPTED); + $query->where($this->getTable().'.status', ApprovalStatus::ACCEPTED); } #[Scope] protected function pending(Builder $query): void { - $query->where('status', ApprovalStatus::PENDING); + $query->where($this->getTable().'.status', ApprovalStatus::PENDING); } #[Scope] protected function rejected(Builder $query): void { - $query->where('status', ApprovalStatus::REJECTED); + $query->where($this->getTable().'.status', ApprovalStatus::REJECTED); } public function mediaTaskAssignment(): BelongsTo diff --git a/database/migrations/2026_01_14_140256_create_cooperation_payments_table.php b/database/migrations/2026_01_14_140256_create_cooperation_payments_table.php index 73f16b3..5a000a3 100644 --- a/database/migrations/2026_01_14_140256_create_cooperation_payments_table.php +++ b/database/migrations/2026_01_14_140256_create_cooperation_payments_table.php @@ -1,5 +1,6 @@ foreignId('cooperation_media_id')->constrained('cooperation_media')->cascadeOnDelete(); $table->unsignedInteger('amount')->index(); $table->text('description')->nullable(); - $table->date('payment_date')->useCurrent()->index(); + $table->date('payment_date')->nullable()->index(); + $table->enum('status', ApprovalStatus::values())->default(ApprovalStatus::PENDING->value)->comment(ApprovalStatus::comment())->index(); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); }); diff --git a/lang/id/notif.php b/lang/id/notif.php index 10d3f07..d130890 100644 --- a/lang/id/notif.php +++ b/lang/id/notif.php @@ -1105,6 +1105,14 @@ 'cheerful' => 'Masih ada media yang belum dibayar. Harap selesaikan pembayaran untuk semua media yang menerima kerja sama.', 'formal' => 'Terdapat kewajiban pembayaran yang belum diselesaikan untuk media terkait.', ], + 'payment_pending' => [ + 'cheerful' => 'Pembayaran Masih Pending! ⏳', + 'formal' => 'Pembayaran Masih Menunggu Verifikasi', + ], + 'payment_pending_desc' => [ + 'cheerful' => 'Ada media yang sudah mengajukan pembayaran tapi belum diverifikasi. Cek dulu yuk! 🧐', + 'formal' => 'Terdapat pengajuan pembayaran yang masih menunggu proses verifikasi oleh pihak Admin.', + ], 'completed_success' => [ 'cheerful' => 'Kerja Sama Selesai! πŸŽ‰βœ¨', 'formal' => 'Kerja Sama Berhasil Diselesaikan', @@ -1217,6 +1225,38 @@ 'cheerful' => 'Pembayaran Telah Dikirim! πŸ’Έβœ¨', 'formal' => 'Pembayaran Kerja Sama Telah Dikirim', ], + 'create_payment_title' => [ + 'cheerful' => 'Ajukan Pembayaran Baru πŸ“πŸ’Έ', + 'formal' => 'Ajukan Pembayaran Baru', + ], + 'create_payment_desc' => [ + 'cheerful' => 'Sudah waktunya gajian! Unggah dokumen penagihanmu (invoice/kwitansi) agar admin bisa segera memproses pembayarannya ya! πŸš€πŸ’°', + 'formal' => 'Silakan unggah dokumen penagihan (invoice/kwitansi) Anda untuk diproses oleh admin.', + ], + 'payment_requested_success' => [ + 'cheerful' => 'Pengajuan Pembayaran Berhasil! πŸš€βœ¨', + 'formal' => 'Pengajuan Pembayaran Berhasil', + ], + 'payment_requested_desc' => [ + 'cheerful' => 'Sip! Pengajuan pembayaran Anda sudah masuk ke sistem. Admin akan segera memprosesnya. Mohon ditunggu ya! πŸ˜ŠπŸ’°', + 'formal' => 'Pengajuan pembayaran Anda telah berhasil dikirimkan dan sedang menunggu verifikasi admin.', + ], + 'payment_approved' => [ + 'cheerful' => 'Pembayaran Disetujui! βœ…πŸ’°', + 'formal' => 'Pembayaran Disetujui', + ], + 'payment_rejected' => [ + 'cheerful' => 'Pembayaran Ditolak πŸ›‘', + 'formal' => 'Pembayaran Ditolak', + ], + 'reject_payment_title' => [ + 'cheerful' => 'Tolak Pengajuan Pembayaran? πŸ›‘πŸ€”', + 'formal' => 'Tolak Pengajuan Pembayaran', + ], + 'reject_payment_desc' => [ + 'cheerful' => 'Apakah Anda yakin ingin menolak pengajuan pembayaran ini? Berikan alasan agar media bisa memperbaikinya! πŸ“', + 'formal' => 'Apakah Anda yakin ingin menolak pengajuan pembayaran ini? Silakan berikan alasan penolakan.', + ], 'new_offer' => [ 'cheerful' => 'Ada Penawaran Kerja Sama Baru! 🀝✨', 'formal' => 'Penawaran Kerja Sama Baru', @@ -1571,6 +1611,18 @@ 'cheerful' => 'Halo Admin! :user__name baru saja mengirimkan bukti tayang ":record__title" untuk kerja sama terkait. Yuk, dicek! 😊', 'formal' => 'Pengguna :user__name telah mengirimkan bukti tayang baru ":record__title" untuk pengajuan kerja sama.', ], + 'payment_requested' => [ + 'cheerful' => 'Halo Admin! πŸ‘‹ :user__name baru saja mengajukan pembayaran untuk media :record__partnerMedia__name pada kerja sama :record__cooperation__title. Yuk, segera diproses! πŸ’ΈπŸš€', + 'formal' => 'Pengguna :user__name telah mengajukan pembayaran untuk media :record__partnerMedia__name pada kerja sama :record__cooperation__title.', + ], + 'payment_approved_notice' => [ + 'cheerful' => 'Mantap! Pengajuan pembayaran untuk :record__partnerMedia__name telah disetujui. πŸ’Έβœ¨', + 'formal' => 'Pengajuan pembayaran untuk :record__partnerMedia__name telah berhasil disetujui.', + ], + 'payment_rejected_reason' => [ + 'cheerful' => 'Duh, pengajuan pembayaran untuk :record__partnerMedia__name terpaksa ditolak. Alasan: :data__reason πŸ›‘', + 'formal' => 'Pengajuan pembayaran untuk :record__partnerMedia__name telah ditolak dengan alasan: :data__reason', + ], 'data_change_rejected' => [ 'cheerful' => 'Halo! Mohon maaf, permohonan perubahan data Anda untuk :record__change_reason ditolak oleh admin. Alasan: :data__rejection_reason πŸ˜”', 'formal' => 'Permohonan perubahan data Anda untuk :record__change_reason telah ditolak oleh Admin. Alasan: :data__rejection_reason',