From 1d52463286436d04ec97d2f63a0702b846683399 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 14 Jan 2026 15:41:23 +0700 Subject: [PATCH] feat(cooperation): refactor workflow and implement payment system - Refactored cooperation workflow to include manual status transitions. - Implemented payment tracking with CooperationPayment model and PayAction. - Updated UpdateCooperationStatusCommand for automatic assignment and verification transitions. - Removed deprecated date columns (assignment_deadline, verification_deadline). - Enhanced Filament UI with new actions (Complete, Proceed to Payment, Pay). - Added CooperationSeeder for testing purposes. --- .../UpdateCooperationStatusCommand.php | 40 +-- .../Cooperation/CompleteCooperationAction.php | 72 ++++++ .../CreateTaskAssignmentAction.php | 7 +- .../Cooperation/ProceedToPaymentAction.php | 49 ++++ .../Actions/Cooperation/RejectAction.php | 1 - .../Cooperation/SendProposalAction.php | 17 +- .../Cooperations/Actions/Media/PayAction.php | 70 +++++ .../Actions/Report/AcceptAction.php | 8 +- .../Actions/Report/RejectAction.php | 8 +- .../Cooperations/CooperationResource.php | 24 +- .../Cooperations/Pages/ViewCooperation.php | 243 +----------------- .../CooperationMediaRelationManager.php | 55 +++- .../CooperationProposalRelationManager.php | 25 +- .../ReportRelationManager.php | 31 ++- .../TaskAssignmentRelationManager.php | 74 ++---- .../Cooperations/Tables/CooperationsTable.php | 22 +- app/Models/Cooperation.php | 6 +- app/Models/CooperationMedia.php | 12 +- app/Models/CooperationPayment.php | 29 +++ ...12_12_111212_create_cooperations_table.php | 2 - ...0256_create_cooperation_payments_table.php | 33 +++ database/seeders/CooperationSeeder.php | 36 +++ 22 files changed, 463 insertions(+), 401 deletions(-) create mode 100644 app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CompleteCooperationAction.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/ProceedToPaymentAction.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Actions/Media/PayAction.php create mode 100644 app/Models/CooperationPayment.php create mode 100644 database/migrations/2026_01_14_140256_create_cooperation_payments_table.php create mode 100644 database/seeders/CooperationSeeder.php diff --git a/app/Console/Commands/UpdateCooperationStatusCommand.php b/app/Console/Commands/UpdateCooperationStatusCommand.php index 6d530ac..da89fd7 100644 --- a/app/Console/Commands/UpdateCooperationStatusCommand.php +++ b/app/Console/Commands/UpdateCooperationStatusCommand.php @@ -40,7 +40,7 @@ public function handle() // 1. PENDING -> ASSIGNMENT (final_submission_date) // Automatically reject media that haven't responded $rejectedPendingMedia = CooperationMedia::whereHas('cooperation', function ($query) use ($today) { - $query->pending()->whereDate('final_submission_date', '<=', $today); + $query->pending()->whereDate('final_submission_date', '<', $today); }) ->pending() ->update(['status' => ApprovalStatus::REJECTED]); @@ -50,53 +50,37 @@ public function handle() } $updatedPending = Cooperation::pending() - ->whereDate('final_submission_date', '<=', $today) + ->whereDate('final_submission_date', '<', $today) ->update(['status' => CooperationStatus::ASSIGNMENT]); if ($updatedPending > 0) { Log::info("Updated {$updatedPending} cooperations from PENDING to ASSIGNMENT."); } - // 2. ASSIGNMENT -> VERIFICATION (assignment_deadline) - // Automatically reject media that haven't responded + // 2. ASSIGNMENT -> VERIFICATION (taskAssignment->end_date) + // Automatically reject media that haven't responded to the invitation $rejectedMedia = CooperationMedia::whereHas('cooperation', function ($query) use ($today) { - $query->assignment()->whereDate('assignment_deadline', '<=', $today); + $query->assignment()->whereHas('taskAssignment', function ($subQuery) use ($today) { + $subQuery->whereDate('end_date', '<', $today); + }); }) ->pending() ->update(['status' => ApprovalStatus::REJECTED]); if ($rejectedMedia > 0) { - Log::info("Updated {$rejectedMedia} media assignments to REJECTED because they didn't respond before the deadline."); + Log::info("Updated {$rejectedMedia} media assignments to REJECTED because they didn't respond before the task deadline."); } - $updatedAssignment = Cooperation::assignment() - ->whereDate('assignment_deadline', '<=', $today) + $updatedAssignment = Cooperation::where('status', CooperationStatus::ASSIGNMENT) + ->whereHas('taskAssignment', function ($query) use ($today) { + $query->whereDate('end_date', '<', $today); + }) ->update(['status' => CooperationStatus::VERIFICATION]); if ($updatedAssignment > 0) { Log::info("Updated {$updatedAssignment} cooperations from ASSIGNMENT to VERIFICATION."); } - // 3. VERIFICATION -> PAYMENT (verification_deadline) - // Automatically reject media that haven't responded - $rejectedVerificationMedia = CooperationMedia::whereHas('cooperation', function ($query) use ($today) { - $query->verification()->whereDate('verification_deadline', '<=', $today); - }) - ->pending() - ->update(['status' => ApprovalStatus::REJECTED]); - - if ($rejectedVerificationMedia > 0) { - Log::info("Updated {$rejectedVerificationMedia} media in VERIFICATION cooperations to REJECTED because they didn't respond before verification_deadline."); - } - - $updatedVerification = Cooperation::verification() - ->whereDate('verification_deadline', '<=', $today) - ->update(['status' => CooperationStatus::PAYMENT]); - - if ($updatedVerification > 0) { - Log::info("Updated {$updatedVerification} cooperations from VERIFICATION to PAYMENT."); - } - $this->info('Cooperation statuses updated successfully.'); return Command::SUCCESS; diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CompleteCooperationAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CompleteCooperationAction.php new file mode 100644 index 0000000..7abe191 --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CompleteCooperationAction.php @@ -0,0 +1,72 @@ +label('Selesaikan Kerjasama') + ->icon(Heroicon::CheckBadge) + ->color('success') + ->requiresConfirmation() + ->modalHeading('Selesaikan Kerja Sama') + ->modalDescription('Apakah Anda yakin ingin menyelesaikan kerja sama ini? Pastikan semua pembayaran kepada media yang bekerja sama telah lunas.') + ->action(function (Cooperation $record): void { + // Verify all accepted media are paid + $acceptedMedia = $record->cooperationMedia()->accepted()->get(); + + if ($acceptedMedia->isEmpty()) { + CheerfulNotification::danger( + 'Tidak Ada Media! ❌', + 'Tidak ada media yang menerima kerja sama ini. Tidak dapat diselesaikan.' + )->send(); + + return; + } + + $unpaidMedia = $acceptedMedia->filter(function ($media) { + return ! $media->payment()->exists(); + }); + + if ($unpaidMedia->isNotEmpty()) { + CheerfulNotification::danger( + 'Pembayaran Belum Lunas! ⚠️', + 'Masih ada media yang belum dibayar. Harap selesaikan pembayaran untuk semua media yang menerima kerja sama.' + )->send(); + + $this->halt(); + + return; + } + + // Calculate total payment from media payments + $totalPayment = $acceptedMedia->sum(fn ($media) => $media->payment->amount); + + $record->update([ + 'payment_amount' => $totalPayment, + 'payment_date' => now(), + 'status' => CooperationStatus::COMPLETED, + ]); + + CheerfulNotification::success( + 'Kerja Sama Selesai! πŸŽ‰βœ¨', + 'Alhamdulillah! Kerja sama ini telah resmi diselesaikan. Terima kasih atas kerja samanya! πŸ™ŒπŸ˜Š' + )->send(); + }) + ->visible(function (Cooperation $record): bool { + $user = auth()->user(); + + return $user && ! $user->hasRole('Perusahaan') + && $record->status === CooperationStatus::PAYMENT; + }); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CreateTaskAssignmentAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CreateTaskAssignmentAction.php index aa40f27..25d2110 100644 --- a/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CreateTaskAssignmentAction.php +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/CreateTaskAssignmentAction.php @@ -14,7 +14,6 @@ use Filament\Forms\Components\TextInput; use Filament\Support\Enums\Width; use Filament\Support\Icons\Heroicon; -use Illuminate\Support\Facades\Auth; class CreateTaskAssignmentAction extends Action { @@ -34,8 +33,7 @@ protected function setUp(): void ->displayFormat('l, d F Y') ->locale('id') ->required() - ->minDate($record->final_submission_date) - ->autofocus(), + ->minDate($record->final_submission_date), DatePicker::make('end_date') ->label('Tanggal Selesai') @@ -103,8 +101,7 @@ protected function setUp(): void ->modalHeading('Buat Penugasan') ->modalSubmitActionLabel('Simpan') ->visible(function (Cooperation $record): bool { - - $user = Auth::user(); + $user = auth()->user(); return $user && ! $user->hasRole('Perusahaan') && ! $record->taskAssignment()->exists() diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/ProceedToPaymentAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/ProceedToPaymentAction.php new file mode 100644 index 0000000..9297bcb --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/ProceedToPaymentAction.php @@ -0,0 +1,49 @@ +label('Lanjutkan ke Pembayaran') + ->icon(Heroicon::Banknotes) + ->color('success') + ->requiresConfirmation() + ->modalHeading('Lanjutkan ke Pembayaran?') + ->modalDescription('Apakah Anda yakin ingin melanjutkan kerja sama ini ke tahap pembayaran? Pastikan semua laporan telah diverifikasi.') + ->action(function (Cooperation $record): void { + $record->update(['status' => CooperationStatus::PAYMENT]); + + CheerfulNotification::success( + 'Tahap Pembayaran Dimulai! πŸ’Έβœ¨', + 'Kerja sama telah dipindahkan ke tahap pembayaran. Silakan proses pembayarannya ya! 😊' + )->send(); + }) + ->visible(function (Cooperation $record): bool { + $user = auth()->user(); + + if (! $user || $user->hasRole('Perusahaan') || $record->status !== CooperationStatus::VERIFICATION) { + return false; + } + + // Check if all reports are verified (either Accepted or Rejected, not Pending) + $reports = $record->reports; + + if ($reports->isEmpty()) { + return true; + } + + return $reports->every(fn ($report) => $report->status !== ApprovalStatus::PENDING); + }); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/RejectAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/RejectAction.php index 56c4e0a..32a10c6 100644 --- a/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/RejectAction.php +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/RejectAction.php @@ -48,7 +48,6 @@ protected function setUp(): void } }) ->visible(function (Cooperation $record): bool { - $user = auth()->user(); if (! $user || ! $user->hasRole('Perusahaan')) { diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/SendProposalAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/SendProposalAction.php index a01a7a7..bddc304 100644 --- a/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/SendProposalAction.php +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Cooperation/SendProposalAction.php @@ -15,7 +15,6 @@ use Filament\Support\Enums\Width; use Filament\Support\Icons\Heroicon; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage; class SendProposalAction extends Action @@ -55,19 +54,19 @@ protected function setUp(): void $cooperationMedia = $record->cooperationMedia() ->whereHas('partnerMedia', function (Builder $query): void { $query->whereHas('company', function (Builder $subQuery): void { - $subQuery->where('user_id', Auth::id()); + $subQuery->where('user_id', auth()->id()); }); }) ->first(); if ($cooperationMedia) { - $existingProposal = $record->proposals() + $existingProposal = $record->proposal() ->where('partner_media_id', $cooperationMedia->partner_media_id) ->first(); $isRevision = (bool) $existingProposal; - $proposal = $record->proposals()->create([ + $proposal = $record->proposal()->create([ 'partner_media_id' => $cooperationMedia->partner_media_id, 'description' => $data['description'], 'e_catalog' => $data['e_catalog'], @@ -99,8 +98,8 @@ protected function setUp(): void User::superAdmin() ->get() ->each(function ($admin) use ($isRevision, $record, $cooperationMedia): void { + $user = auth()->user(); - $user = Auth::user(); $admin->notify(new BroadcastNotification([ 'title' => $isRevision ? 'Pembaruan Proposal Kerja Sama ✨' : 'Ada Proposal Kerja Sama Baru! πŸš€', 'body' => $isRevision @@ -118,8 +117,7 @@ protected function setUp(): void ->modalHeading('Proposal Kerja Sama') ->modalSubmitActionLabel('Kirim Proposal') ->visible(function (Cooperation $record): bool { - - $user = Auth::user(); + $user = auth()->user(); if (! $user->hasRole('Perusahaan')) { return false; @@ -128,15 +126,14 @@ protected function setUp(): void $cooperationMedia = $record->cooperationMedia() ->whereHas('partnerMedia', function (Builder $query): void { $query->whereHas('company', function (Builder $subQuery): void { - $subQuery->where('user_id', Auth::id()); + $subQuery->where('user_id', auth()->id()); }); }) ->first(); return $cooperationMedia && $cooperationMedia->status === ApprovalStatus::ACCEPTED - && $record->initial_submission_date->toDateString() === now()->toDateString() - && ! $record->proposals() + && ! $record->proposal() ->where('partner_media_id', $cooperationMedia->partner_media_id) ->where(fn (Builder $q): Builder => $q->accepted()->orWhere(fn (Builder $q): Builder => $q->pending())) ->exists(); diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Media/PayAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Media/PayAction.php new file mode 100644 index 0000000..58d1771 --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Media/PayAction.php @@ -0,0 +1,70 @@ +label('Bayar') + ->icon(Heroicon::CreditCard) + ->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() + ->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( + 'Pembayaran Berhasil! πŸ’Έβœ¨', + "Pembayaran untuk {$record->partnerMedia->name} telah berhasil dicatat." + )->send(); + }) + ->modalWidth(Width::Large); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Report/AcceptAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Report/AcceptAction.php index 5f2c068..60b2a63 100644 --- a/app/Filament/Resources/Manage/Cooperations/Actions/Report/AcceptAction.php +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Report/AcceptAction.php @@ -3,6 +3,8 @@ namespace App\Filament\Resources\Manage\Cooperations\Actions\Report; use App\Enums\ApprovalStatus; +use App\Enums\CooperationStatus; +use App\Enums\RoleEnum; use App\Filament\Resources\Manage\Cooperations\CooperationResource; use App\Models\Report; use App\Notifications\BroadcastNotification; @@ -50,6 +52,10 @@ protected function setUp(): void } } }) - ->visible(fn (Report $record): bool => ! auth()->user()->hasRole('Perusahaan') && $record->status === ApprovalStatus::PENDING); + ->visible( + fn (Report $record): bool => $record->taskAssignment?->cooperation?->status === CooperationStatus::VERIFICATION && + ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) && + $record->status === ApprovalStatus::PENDING + ); } } diff --git a/app/Filament/Resources/Manage/Cooperations/Actions/Report/RejectAction.php b/app/Filament/Resources/Manage/Cooperations/Actions/Report/RejectAction.php index 0fc8334..12ceefc 100644 --- a/app/Filament/Resources/Manage/Cooperations/Actions/Report/RejectAction.php +++ b/app/Filament/Resources/Manage/Cooperations/Actions/Report/RejectAction.php @@ -3,6 +3,8 @@ namespace App\Filament\Resources\Manage\Cooperations\Actions\Report; use App\Enums\ApprovalStatus; +use App\Enums\CooperationStatus; +use App\Enums\RoleEnum; use App\Filament\Resources\Manage\Cooperations\CooperationResource; use App\Models\Report; use App\Notifications\BroadcastNotification; @@ -60,7 +62,11 @@ protected function setUp(): void } } }) - ->visible(fn (Report $record): bool => ! auth()->user()->hasRole('Perusahaan') && $record->status === ApprovalStatus::PENDING) + ->visible( + fn (Report $record): bool => $record->taskAssignment?->cooperation?->status === CooperationStatus::VERIFICATION && + ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) && + $record->status === ApprovalStatus::PENDING + ) ->modalWidth(Width::Large); } } diff --git a/app/Filament/Resources/Manage/Cooperations/CooperationResource.php b/app/Filament/Resources/Manage/Cooperations/CooperationResource.php index 8615bce..36abd9e 100644 --- a/app/Filament/Resources/Manage/Cooperations/CooperationResource.php +++ b/app/Filament/Resources/Manage/Cooperations/CooperationResource.php @@ -106,21 +106,16 @@ public static function infolist(Schema $schema): Schema ->label('Tanggal Pengajuan Akhir') ->date('l, d F Y'), - TextEntry::make('assignment_deadline') - ->label('Deadline Penugasan') + TextEntry::make('taskAssignment.start_date') + ->label('Tanggal Mulai Penugasan') ->date('l, d F Y') ->placeholder('Belum ditentukan'), - TextEntry::make('verification_deadline') - ->label('Deadline Verifikasi') + TextEntry::make('taskAssignment.end_date') + ->label('Tanggal Selesai Penugasan') ->date('l, d F Y') ->placeholder('Belum ditentukan'), - TextEntry::make('payment_amount') - ->label('Jumlah Pembayaran') - ->money('IDR') - ->placeholder('Belum ditentukan'), - TextEntry::make('payment_date') ->label('Tanggal Pembayaran') ->date('l, d F Y') @@ -133,18 +128,19 @@ public static function infolist(Schema $schema): Schema ->label('Template Proposal') ->getStateUsing(fn (Cooperation $record): ?string => optional($record->getMedia('cooperations')->where('custom_properties.doc_type', 'proposal-template')->sortByDesc('created_at')->first())->getPathRelativeToRoot()) ->disk(config('filesystems.default')) - ->columnSpan(2), + ->columnSpan(2) + ->visible(fn (Cooperation $record): bool => $record->getMedia('cooperations')->where('custom_properties.doc_type', 'proposal-template')->isNotEmpty()), ImageEntry::make('banner') ->label('Banner') ->getStateUsing(fn (Cooperation $record): ?string => optional($record->getMedia('cooperations')->where('custom_properties.doc_type', 'banner')->sortByDesc('created_at')->first())->getPathRelativeToRoot()) ->disk(config('filesystems.default')) - ->visible(fn (Cooperation $record): bool => $record->getMedia('cooperations')->isNotEmpty()), - ]), + ->visible(fn (Cooperation $record): bool => $record->getMedia('cooperations')->where('custom_properties.doc_type', 'banner')->isNotEmpty()), + ]) + ->visible(fn (Cooperation $record): bool => $record->getMedia('cooperations')->where('custom_properties.doc_type', 'banner')->isNotEmpty() && $record->getMedia('cooperations')->where('custom_properties.doc_type', 'proposal-template')->isNotEmpty()), TextEntry::make('description') - ->label('Deskripsi') - ->markdown(), + ->label('Deskripsi'), ]), ]) ->columns(1); diff --git a/app/Filament/Resources/Manage/Cooperations/Pages/ViewCooperation.php b/app/Filament/Resources/Manage/Cooperations/Pages/ViewCooperation.php index 4d6847a..8b935a2 100644 --- a/app/Filament/Resources/Manage/Cooperations/Pages/ViewCooperation.php +++ b/app/Filament/Resources/Manage/Cooperations/Pages/ViewCooperation.php @@ -2,263 +2,22 @@ namespace App\Filament\Resources\Manage\Cooperations\Pages; -use App\Enums\ApprovalStatus; use App\Filament\Resources\Manage\Cooperations\CooperationResource; use Filament\Actions\Action; -use Filament\Infolists\Components\RepeatableEntry; -use Filament\Infolists\Components\TextEntry; use Filament\Resources\Pages\ViewRecord; -use Filament\Support\Enums\FontWeight; class ViewCooperation extends ViewRecord { protected static string $resource = CooperationResource::class; - protected function getWorkflowSteps() - { - $record = $this->getRecord(); - - $steps = [ - [ - 'step' => 1, - 'title' => 'Pengajuan Kerja Sama', - 'description' => 'Admin mengajukan kerja sama ke media', - 'status' => 'completed', // Always completed since we're viewing it - 'date' => $record->created_at?->format('d/m/Y H:i'), - ], - [ - 'step' => 2, - 'title' => 'Respon Media', - 'description' => 'Media menerima atau menolak pengajuan', - 'status' => $this->getMediaResponseStatus($record), - 'date' => $this->getMediaResponseDate($record), - ], - [ - 'step' => 3, - 'title' => 'Upload Proposal', - 'description' => 'Media mengupload dokumen proposal', - 'status' => $this->getProposalStatus($record), - 'date' => $this->getProposalDate($record), - ], - [ - 'step' => 4, - 'title' => 'Review Proposal', - 'description' => 'Admin mereview dan menyetujui proposal', - 'status' => $this->getProposalReviewStatus($record), - 'date' => $this->getProposalReviewDate($record), - ], - [ - 'step' => 5, - 'title' => 'Penugasan', - 'description' => 'Admin membuat task assignment', - 'status' => $this->getTaskStatus($record), - 'date' => $this->getTaskDate($record), - ], - [ - 'step' => 6, - 'title' => 'Upload Laporan', - 'description' => 'Media mengupload laporan', - 'status' => $this->getReportStatus($record), - 'date' => $this->getReportDate($record), - ], - [ - 'step' => 7, - 'title' => 'Selesai', - 'description' => 'Kerja sama telah selesai', - 'status' => $this->getCompletionStatus($record), - 'date' => $this->getCompletionDate($record), - ], - ]; - - return RepeatableEntry::make('workflow_steps') - ->label('') - ->schema([ - TextEntry::make('step') - ->label('Langkah') - ->formatStateUsing(fn (int $state): string => "Langkah {$state}") - ->weight(FontWeight::Bold), - - TextEntry::make('title') - ->label('Judul'), - - TextEntry::make('description') - ->label('Deskripsi'), - - TextEntry::make('status') - ->label('Status') - ->badge() - ->color(fn (string $state): string => match ($state) { - 'completed' => 'success', - 'in_progress' => 'warning', - 'pending' => 'gray', - 'rejected' => 'danger', - default => 'gray', - }) - ->formatStateUsing(fn (string $state): string => match ($state) { - 'completed' => 'Selesai', - 'in_progress' => 'Sedang Berjalan', - 'pending' => 'Menunggu', - 'rejected' => 'Ditolak', - default => $state, - }), - - TextEntry::make('date') - ->label('Tanggal') - ->placeholder('Belum ada'), - ]) - ->columns(5) - ->state($steps); - } - - protected function getMediaResponseStatus($record): string - { - $mediaStatuses = $record->cooperationMedia->pluck('status')->unique(); - - if ($mediaStatuses->contains(ApprovalStatus::ACCEPTED)) { - return 'completed'; - } - - if ($mediaStatuses->contains(ApprovalStatus::REJECTED)) { - return 'rejected'; - } - - return 'pending'; - } - - protected function getMediaResponseDate($record) - { - $latestResponse = $record->cooperationMedia - ->whereNotNull('updated_at') - ->sortByDesc('updated_at') - ->first(); - - return $latestResponse?->updated_at?->format('d/m/Y H:i'); - } - - protected function getProposalStatus($record): string - { - if ($record->proposals()->exists()) { - return 'completed'; - } - - return 'pending'; - } - - protected function getProposalDate($record) - { - $latestProposal = $record->proposals->sortByDesc('created_at')->first(); - - return $latestProposal?->created_at?->format('d/m/Y H:i'); - } - - protected function getProposalReviewStatus($record): string - { - $proposals = $record->proposals; - - if ($record->proposals()->accepted()->exists()) { - return 'completed'; - } - - if ($record->proposals()->rejected()->exists()) { - return 'rejected'; - } - - if ($proposals->isNotEmpty()) { - return 'in_progress'; - } - - return 'pending'; - } - - protected function getProposalReviewDate($record) - { - $latestProposal = $record->proposals->sortByDesc('updated_at')->first(); - - return $latestProposal?->updated_at?->format('d/m/Y H:i'); - } - - protected function getTaskStatus($record): string - { - if ($record->taskAssignment()->exists()) { - return 'completed'; - } - - return 'pending'; - } - - protected function getTaskDate($record) - { - return $record->taskAssignment?->created_at?->format('d/m/Y H:i'); - } - - protected function getReportStatus($record): string - { - $task = $record->taskAssignment; - - if (! $task) { - return 'pending'; - } - - $totalReports = $task->report_amount; - $completedReports = $task->reports->count(); - - if ($completedReports >= $totalReports && $totalReports > 0) { - return 'completed'; - } - - if ($completedReports > 0) { - return 'in_progress'; - } - - return 'pending'; - } - - protected function getReportDate($record) - { - $latestReport = $record->taskAssignment?->reports - ->sortByDesc('created_at') - ->first(); - - return $latestReport?->created_at?->format('d/m/Y H:i'); - } - - protected function getCompletionStatus($record): string - { - // Logic for completion - could be based on all reports accepted, etc. - return 'pending'; - } - - protected function getCompletionDate($record) - { - return null; - } - protected function getHeaderActions(): array { return [ Action::make('back') ->label('Kembali') - ->url(fn (): string => static::getResource()::getUrl('index')) + ->url(ListCooperations::getUrl()) ->outlined() ->color('secondary'), ]; } - - protected function canMarkAsCompleted(): bool - { - $record = $this->getRecord(); - - $task = $record->taskAssignment; - - if (! $task) { - return false; - } - - $reportCount = $task->reports()->count(); - if ($reportCount < $task->report_amount) { - return false; - } - - return true; - } } diff --git a/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php index 2ec48c1..a93dcfa 100644 --- a/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php +++ b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php @@ -2,11 +2,15 @@ namespace App\Filament\Resources\Manage\Cooperations\RelationManagers; -use App\Filament\Resources\Manage\Partners\PartnerResource; +use App\Enums\ApprovalStatus; +use App\Filament\Resources\Manage\Cooperations\Actions\Media\PayAction; use App\Models\CooperationMedia; -use Filament\Actions\Action; +use Filament\Actions\ViewAction; +use Filament\Infolists\Components\ImageEntry; +use Filament\Infolists\Components\TextEntry; use Filament\Resources\RelationManagers\RelationManager; -use Filament\Support\Icons\Heroicon; +use Filament\Schemas\Components\Grid; +use Filament\Schemas\Schema; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Model; @@ -30,12 +34,16 @@ public static function getBadge(Model $ownerRecord, string $pageClass): ?string public function table(Table $table): Table { return $table - ->recordTitleAttribute('partnerMedia.name') ->columns([ TextColumn::make('partnerMedia.name') ->label('Nama') ->searchable(), + TextColumn::make('payment.amount') + ->label('Jumlah Pembayaran') + ->money('IDR', decimalPlaces: 0) + ->placeholder('Belum ditentukan'), + TextColumn::make('status') ->label('Status') ->badge(), @@ -43,15 +51,38 @@ public function table(Table $table): Table ->filters([]) ->headerActions([]) ->recordActions([ - Action::make('view') - ->label('Lihat') - ->icon(Heroicon::Eye) - ->url(function (CooperationMedia $record): string { - return PartnerResource::getUrl('view', [ - 'record' => $record->partnerMedia->company->user_id, - ]); - }), + PayAction::make('pay'), + + ViewAction::make() + ->visible(fn (CooperationMedia $record): bool => $record->status === ApprovalStatus::ACCEPTED && $record->payment()->exists()), ]) ->recordAction(null); } + + public function infolist(Schema $infolist): Schema + { + return $infolist + ->components([ + Grid::make(2) + ->schema([ + TextEntry::make('payment.amount') + ->label('Nominal') + ->money('IDR'), + + TextEntry::make('payment.payment_date') + ->label('Tanggal Bayar') + ->dateTime(), + ]), + + 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/CooperationProposalRelationManager.php b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationProposalRelationManager.php index 963d241..5c41a7c 100644 --- a/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationProposalRelationManager.php +++ b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationProposalRelationManager.php @@ -3,6 +3,7 @@ namespace App\Filament\Resources\Manage\Cooperations\RelationManagers; use App\Enums\ApprovalStatus; +use App\Enums\RoleEnum; use App\Filament\Resources\Manage\Cooperations\Actions\Proposal\AcceptAction; use App\Filament\Resources\Manage\Cooperations\Actions\Proposal\RejectAction; use App\Models\CooperationProposal; @@ -16,14 +17,36 @@ use Filament\Schemas\Schema; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Model; use Joaopaulolndev\FilamentPdfViewer\Infolists\Components\PdfViewerEntry; class CooperationProposalRelationManager extends RelationManager { - protected static string $relationship = 'proposals'; + protected static string $relationship = 'proposal'; protected static ?string $title = 'Proposal'; + public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool + { + if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) { + return $ownerRecord->cooperationMedia() + ->whereHas('partnerMedia', function ($query) { + $query->whereHas('company', function ($query) { + $query->where('user_id', auth()->id()); + }); + }) + ->where('status', ApprovalStatus::ACCEPTED) + ->exists(); + } + + return true; + } + + public static function getBadge(Model $ownerRecord, string $pageClass): ?string + { + return $ownerRecord->proposal()->count(); + } + public function table(Table $table): Table { return $table diff --git a/app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php b/app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php index 7404f20..c76099c 100644 --- a/app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php +++ b/app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php @@ -3,6 +3,8 @@ namespace App\Filament\Resources\Manage\Cooperations\RelationManagers; use App\Enums\ApprovalStatus; +use App\Enums\CooperationStatus; +use App\Enums\RoleEnum; 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; @@ -22,7 +24,6 @@ use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\Auth; class ReportRelationManager extends RelationManager { @@ -30,6 +31,22 @@ class ReportRelationManager extends RelationManager protected static ?string $title = 'Laporan'; + public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool + { + if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) { + return $ownerRecord->cooperationMedia() + ->whereHas('partnerMedia', function ($query) { + $query->whereHas('company', function ($query) { + $query->where('user_id', auth()->id()); + }); + }) + ->where('status', ApprovalStatus::ACCEPTED) + ->exists(); + } + + return true; + } + public function isReadOnly(): bool { return false; @@ -138,8 +155,8 @@ public function table(Table $table): Table User::superAdmin() ->get() ->each(function ($admin) use ($record): void { + $user = auth()->user(); - $user = Auth::user(); $admin->notify(new BroadcastNotification([ 'title' => 'Ada Laporan Baru! πŸ“βœ¨', 'body' => "Halo Admin! {$user->name} baru saja mengirimkan laporan \"{$record->title}\" untuk kerja sama \"{$this->getOwnerRecord()->title}\". Yuk, dicek! 😊", @@ -152,8 +169,7 @@ public function table(Table $table): Table }); }) ->visible(function (): bool { - - $user = Auth::user(); + $user = auth()->user(); if (! $user || ! $user->hasRole('Perusahaan')) { return false; @@ -165,9 +181,10 @@ public function table(Table $table): Table return false; } - return $taskAssignment->reports() - ->where('status', '!=', ApprovalStatus::REJECTED) - ->count() < $taskAssignment->report_amount; + return $this->getOwnerRecord()->status === CooperationStatus::ASSIGNMENT + && $taskAssignment->reports() + ->where('status', '!=', ApprovalStatus::REJECTED) + ->count() < $taskAssignment->report_amount; }), ]) ->toolbarActions([]); diff --git a/app/Filament/Resources/Manage/Cooperations/RelationManagers/TaskAssignmentRelationManager.php b/app/Filament/Resources/Manage/Cooperations/RelationManagers/TaskAssignmentRelationManager.php index da8ff23..99623a5 100644 --- a/app/Filament/Resources/Manage/Cooperations/RelationManagers/TaskAssignmentRelationManager.php +++ b/app/Filament/Resources/Manage/Cooperations/RelationManagers/TaskAssignmentRelationManager.php @@ -3,20 +3,17 @@ namespace App\Filament\Resources\Manage\Cooperations\RelationManagers; use App\Enums\ApprovalStatus; +use App\Enums\RoleEnum; use App\Filament\Actions\Cheerful\CreateAction; use App\Filament\Actions\Cheerful\DeleteAction; use App\Filament\Actions\Cheerful\EditAction; use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; -use Filament\Forms\Components\DatePicker; -use Filament\Forms\Components\Select; -use Filament\Forms\Components\Textarea; -use Filament\Forms\Components\TextInput; use Filament\Notifications\Notification; use Filament\Resources\RelationManagers\RelationManager; -use Filament\Schemas\Schema; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Model; class TaskAssignmentRelationManager extends RelationManager { @@ -24,58 +21,25 @@ class TaskAssignmentRelationManager extends RelationManager protected static ?string $title = 'Penugasan'; - public function form(Schema $schema): Schema + public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool { - return $schema - ->components([ - DatePicker::make('start_date') - ->label('Tanggal Mulai') - ->required() - ->native(false) - ->autocomplete(false) - ->autofocus(), + if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) { + return $ownerRecord->cooperationMedia() + ->whereHas('partnerMedia', function ($query) { + $query->whereHas('company', function ($query) { + $query->where('user_id', auth()->id()); + }); + }) + ->where('status', ApprovalStatus::ACCEPTED) + ->exists(); + } - DatePicker::make('end_date') - ->label('Tanggal Selesai') - ->required() - ->native(false) - ->after('start_date') - ->autocomplete(false), + return true; + } - Textarea::make('task_description') - ->label('Deskripsi Tugas') - ->required() - ->rows(4) - ->maxLength(65535) - ->autocomplete(false), - - TextInput::make('report_amount') - ->label('Jumlah Laporan') - ->numeric() - ->required() - ->minValue(1) - ->default(1) - ->autocomplete(false), - - Select::make('partner_media_ids') - ->label('Media yang Ditugaskan') - ->relationship('partnerMedia', 'name') - ->multiple() - ->preload() - ->required() - ->autocomplete(false), - - Select::make('status') - ->options(ApprovalStatus::options()) - ->required() - ->default(ApprovalStatus::PENDING), - - Textarea::make('rejection_reason') - ->label('Alasan Penolakan') - ->rows(3) - ->columnSpanFull() - ->autocomplete(false), - ]); + public static function getBadge(Model $ownerRecord, string $pageClass): ?string + { + return $ownerRecord->taskAssignment()->count(); } public function table(Table $table): Table @@ -107,7 +71,7 @@ public function table(Table $table): Table ->visible(function (): bool { $cooperation = $this->getOwnerRecord(); - return $cooperation->proposals()->accepted()->exists() + return $cooperation->proposal()->accepted()->exists() && ! $cooperation->taskAssignment()->exists(); }) ->successNotification( diff --git a/app/Filament/Resources/Manage/Cooperations/Tables/CooperationsTable.php b/app/Filament/Resources/Manage/Cooperations/Tables/CooperationsTable.php index c56a207..6309fe0 100644 --- a/app/Filament/Resources/Manage/Cooperations/Tables/CooperationsTable.php +++ b/app/Filament/Resources/Manage/Cooperations/Tables/CooperationsTable.php @@ -8,7 +8,9 @@ use App\Filament\Actions\DefaultBulkActions; use App\Filament\Columns\TimestampColumns; use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\AcceptAction; +use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\CompleteCooperationAction; use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\CreateTaskAssignmentAction; +use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\ProceedToPaymentAction; use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\RejectAction; use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\SendProposalAction; use App\Models\Cooperation; @@ -61,22 +63,6 @@ public static function configure(Table $table): Table ) ), - TextColumn::make('assignment_deadline') - ->label('Deadline Penugasan') - ->searchable() - ->sortable() - ->date('l, d F Y') - ->placeholder('Belum ditentukan') - ->toggleable(isToggledHiddenByDefault: true), - - TextColumn::make('verification_deadline') - ->label('Deadline Verifikasi') - ->searchable() - ->sortable() - ->date('l, d F Y') - ->placeholder('Belum ditentukan') - ->toggleable(isToggledHiddenByDefault: true), - TextColumn::make('status') ->searchable() ->sortable() @@ -158,6 +144,10 @@ public static function configure(Table $table): Table CreateTaskAssignmentAction::make('createTaskAssignment'), + ProceedToPaymentAction::make('proceedToPayment'), + + CompleteCooperationAction::make('completeCooperation'), + EditAction::make() ->visible(fn (): bool => ! auth()->user()->hasRole('Perusahaan')), diff --git a/app/Models/Cooperation.php b/app/Models/Cooperation.php index 33fed35..3089dcb 100644 --- a/app/Models/Cooperation.php +++ b/app/Models/Cooperation.php @@ -26,8 +26,6 @@ protected function casts(): array return [ 'initial_submission_date' => 'date', 'final_submission_date' => 'date', - 'assignment_deadline' => 'date', - 'verification_deadline' => 'date', 'payment_amount' => 'integer', 'payment_date' => 'date', 'status' => CooperationStatus::class, @@ -64,9 +62,9 @@ protected function completed(Builder $query): void $query->where('status', CooperationStatus::COMPLETED); } - public function proposals(): HasMany + public function proposal(): HasOne { - return $this->hasMany(CooperationProposal::class); + return $this->hasOne(CooperationProposal::class); } public function taskAssignment(): HasOne diff --git a/app/Models/CooperationMedia.php b/app/Models/CooperationMedia.php index 6978a4c..731e53a 100644 --- a/app/Models/CooperationMedia.php +++ b/app/Models/CooperationMedia.php @@ -7,12 +7,15 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\Pivot; +use Spatie\MediaLibrary\HasMedia; +use Spatie\MediaLibrary\InteractsWithMedia; -class CooperationMedia extends Pivot +class CooperationMedia extends Pivot implements HasMedia { - use HasFactory; + use HasFactory, InteractsWithMedia; protected $table = 'cooperation_media'; @@ -57,4 +60,9 @@ public function rejectionReasons(): MorphMany { return $this->morphMany(RejectionReason::class, 'rejectable'); } + + public function payment(): HasOne + { + return $this->hasOne(CooperationPayment::class, 'cooperation_media_id'); + } } diff --git a/app/Models/CooperationPayment.php b/app/Models/CooperationPayment.php new file mode 100644 index 0000000..9db1a0e --- /dev/null +++ b/app/Models/CooperationPayment.php @@ -0,0 +1,29 @@ + 'integer', + 'payment_date' => 'datetime', + ]; + } + + public function cooperationMedia(): BelongsTo + { + return $this->belongsTo(CooperationMedia::class); + } +} diff --git a/database/migrations/2025_12_12_111212_create_cooperations_table.php b/database/migrations/2025_12_12_111212_create_cooperations_table.php index 4b0f59a..7cb214a 100644 --- a/database/migrations/2025_12_12_111212_create_cooperations_table.php +++ b/database/migrations/2025_12_12_111212_create_cooperations_table.php @@ -17,8 +17,6 @@ public function up(): void $table->string('title', 200); $table->date('initial_submission_date'); $table->date('final_submission_date'); - $table->date('assignment_deadline')->nullable(); - $table->date('verification_deadline')->nullable(); $table->text('description'); $table->unsignedInteger('payment_amount')->nullable(); $table->date('payment_date')->nullable(); 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 new file mode 100644 index 0000000..b42bec3 --- /dev/null +++ b/database/migrations/2026_01_14_140256_create_cooperation_payments_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignIdFor(CooperationMedia::class)->constrained()->cascadeOnDelete(); + $table->unsignedInteger('amount'); + $table->text('description')->nullable(); + $table->date('payment_date')->useCurrent(); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cooperation_payments'); + } +}; diff --git a/database/seeders/CooperationSeeder.php b/database/seeders/CooperationSeeder.php new file mode 100644 index 0000000..f69aed6 --- /dev/null +++ b/database/seeders/CooperationSeeder.php @@ -0,0 +1,36 @@ + 'Kerja Sama Media '.now()->format('Y'), + 'description' => 'Kerja sama media untuk penyebarluasan informasi publik.', + 'initial_submission_date' => now()->subDay(), + 'final_submission_date' => now(), + 'status' => CooperationStatus::PENDING, + ]); + + $verifiedMedia = PartnerMedia::verified()->get(); + + foreach ($verifiedMedia as $media) { + $cooperation->partnerMedia()->attach($media->id, [ + 'status' => ApprovalStatus::PENDING, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } +}