feat: implement cooperation payment workflow with invoice submission, approval, and rejection actions

This commit is contained in:
Yoga Pangestu 2026-04-10 16:17:34 +07:00
parent ba115a4502
commit ec7a9bdf30
14 changed files with 648 additions and 302 deletions

View File

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

View File

@ -1,91 +0,0 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Media;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\CooperationMedia;
use App\Notifications\BroadcastNotification;
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
{
public static function getDefaultName(): ?string
{
return 'pay';
}
protected function setUp(): void
{
parent::setUp();
$this->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);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Payment;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\CooperationPayment;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Support\Icons\Heroicon;
class AcceptAction extends Action
{
public static function getDefaultName(): ?string
{
return 'accept';
}
protected function setUp(): void
{
parent::setUp();
$this->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
);
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Payment;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\CooperationPayment;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
class RejectAction extends Action
{
public static function getDefaultName(): ?string
{
return 'reject';
}
protected function setUp(): void
{
parent::setUp();
$this->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);
}
}

View File

@ -0,0 +1,137 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Payment;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\CooperationPayment;
use App\Models\User;
use App\Notifications\BroadcastNotification;
use Asmit\FilamentUpload\Forms\Components\AdvancedFileUpload;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Support\Enums\Width;
use Filament\Support\RawJs;
use Illuminate\Support\Facades\Storage;
class SubmitInvoiceAction extends CreateAction
{
public static function getDefaultName(): ?string
{
return 'submit_invoice';
}
protected function setUp(): void
{
parent::setUp();
$this->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();
});
}
}

View File

@ -0,0 +1,146 @@
<?php
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\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\Support\Enums\Width;
use Illuminate\Database\Eloquent\Model;
class SubmitReportAction extends CreateAction
{
public static function getDefaultName(): ?string
{
return 'submit_report';
}
protected function setUp(): void
{
parent::setUp();
$this->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;
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
<?php
use App\Enums\ApprovalStatus;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
@ -16,7 +17,8 @@ public function up(): void
$table->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();
});

View File

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