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.
This commit is contained in:
Yoga Pangestu 2026-01-14 15:41:23 +07:00
parent ad6335542e
commit 1d52463286
22 changed files with 463 additions and 401 deletions

View File

@ -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;

View File

@ -0,0 +1,72 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Cooperation;
use App\Enums\CooperationStatus;
use App\Filament\Support\CheerfulNotification;
use App\Models\Cooperation;
use Filament\Actions\Action;
use Filament\Support\Icons\Heroicon;
class CompleteCooperationAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->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;
});
}
}

View File

@ -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()

View File

@ -0,0 +1,49 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Cooperation;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Filament\Support\CheerfulNotification;
use App\Models\Cooperation;
use Filament\Actions\Action;
use Filament\Support\Icons\Heroicon;
class ProceedToPaymentAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->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);
});
}
}

View File

@ -48,7 +48,6 @@ protected function setUp(): void
}
})
->visible(function (Cooperation $record): bool {
$user = auth()->user();
if (! $user || ! $user->hasRole('Perusahaan')) {

View File

@ -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();

View File

@ -0,0 +1,70 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Media;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Filament\Support\CheerfulNotification;
use App\Models\CooperationMedia;
use Filament\Actions\Action;
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Filament\Support\RawJs;
class PayAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->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);
}
}

View File

@ -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
);
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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([]);

View File

@ -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(

View File

@ -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')),

View File

@ -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

View File

@ -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');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
class CooperationPayment extends Model implements HasMedia
{
use HasFactory, InteractsWithMedia;
protected $guarded = ['id'];
protected function casts(): array
{
return [
'amount' => 'integer',
'payment_date' => 'datetime',
];
}
public function cooperationMedia(): BelongsTo
{
return $this->belongsTo(CooperationMedia::class);
}
}

View File

@ -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();

View File

@ -0,0 +1,33 @@
<?php
use App\Models\CooperationMedia;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cooperation_payments', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,36 @@
<?php
namespace Database\Seeders;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Models\Cooperation;
use App\Models\PartnerMedia;
use Illuminate\Database\Seeder;
class CooperationSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$cooperation = Cooperation::create([
'title' => '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(),
]);
}
}
}