From c7a72a169e24645ed10fa037b049b0c62719e1e2 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Tue, 16 Dec 2025 12:37:53 +0700 Subject: [PATCH] feat: implement comprehensive cooperation management module with models, migrations, and Filament resources --- COOPERATION_MODULE.md | 320 +++++++++++++++ app/Enums/ApprovalStatus.php | 42 ++ app/Enums/CooperationStatus.php | 48 +++ app/Filament/Pages/CustomLogin.php | 2 +- .../Cooperations/CooperationResource.php | 157 +++++++ .../Cooperations/Pages/CreateCooperation.php | 30 ++ .../Cooperations/Pages/EditCooperation.php | 28 ++ .../Cooperations/Pages/ListCooperations.php | 27 ++ .../Cooperations/Pages/ViewCooperation.php | 321 +++++++++++++++ .../CooperationMediaRelationManager.php | 43 ++ .../CooperationProposalRelationManager.php | 136 +++++++ .../ReportRelationManager.php | 147 +++++++ .../TaskAssignmentRelationManager.php | 126 ++++++ .../Cooperations/Schemas/CooperationForm.php | 99 +++++ .../Cooperations/Tables/CooperationsTable.php | 383 ++++++++++++++++++ .../Manage/Partners/PartnerResource.php | 2 +- app/Models/Cooperation.php | 59 +++ app/Models/CooperationMedia.php | 40 ++ app/Models/CooperationProposal.php | 40 ++ app/Models/MediaTaskAssignment.php | 40 ++ app/Models/PartnerMedia.php | 29 ++ app/Models/RejectionReason.php | 19 + app/Models/Report.php | 37 ++ app/Models/TaskAssignment.php | 52 +++ ...12_12_111212_create_cooperations_table.php | 39 ++ ..._111307_create_cooperation_media_table.php | 34 ++ ...245_create_cooperation_proposals_table.php | 38 ++ ...2_132453_create_task_assignments_table.php | 34 ++ ...823_create_media_task_assignment_table.php | 34 ++ ...2025_12_12_135847_create_reports_table.php | 37 ++ ..._135855_create_rejection_reasons_table.php | 30 ++ 31 files changed, 2471 insertions(+), 2 deletions(-) create mode 100644 COOPERATION_MODULE.md create mode 100644 app/Enums/ApprovalStatus.php create mode 100644 app/Enums/CooperationStatus.php create mode 100644 app/Filament/Resources/Manage/Cooperations/CooperationResource.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Pages/CreateCooperation.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Pages/EditCooperation.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Pages/ListCooperations.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Pages/ViewCooperation.php create mode 100644 app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php create mode 100644 app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationProposalRelationManager.php create mode 100644 app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php create mode 100644 app/Filament/Resources/Manage/Cooperations/RelationManagers/TaskAssignmentRelationManager.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Schemas/CooperationForm.php create mode 100644 app/Filament/Resources/Manage/Cooperations/Tables/CooperationsTable.php create mode 100644 app/Models/Cooperation.php create mode 100644 app/Models/CooperationMedia.php create mode 100644 app/Models/CooperationProposal.php create mode 100644 app/Models/MediaTaskAssignment.php create mode 100644 app/Models/RejectionReason.php create mode 100644 app/Models/Report.php create mode 100644 app/Models/TaskAssignment.php create mode 100644 database/migrations/2025_12_12_111212_create_cooperations_table.php create mode 100644 database/migrations/2025_12_12_111307_create_cooperation_media_table.php create mode 100644 database/migrations/2025_12_12_132245_create_cooperation_proposals_table.php create mode 100644 database/migrations/2025_12_12_132453_create_task_assignments_table.php create mode 100644 database/migrations/2025_12_12_133823_create_media_task_assignment_table.php create mode 100644 database/migrations/2025_12_12_135847_create_reports_table.php create mode 100644 database/migrations/2025_12_12_135855_create_rejection_reasons_table.php diff --git a/COOPERATION_MODULE.md b/COOPERATION_MODULE.md new file mode 100644 index 0000000..01d5b4c --- /dev/null +++ b/COOPERATION_MODULE.md @@ -0,0 +1,320 @@ +# Modul Kerja Sama - Dokumentasi + +## Overview +Modul Kerja Sama adalah sistem manajemen kerjasama antara Admin dan Media Partner. Modul ini mengelola seluruh flow dari pengajuan kerjasama hingga pelaporan. + +## Flow Kerja Sama + +### 1. Admin Mengajukan Kerja Sama +- Admin membuat data Cooperation baru dengan informasi: + - Judul Kerja Sama + - Tanggal Pengajuan Awal (initial_submission_date) + - Tanggal Pengajuan Akhir (final_submission_date) + - Deadline Penugasan (assignment_deadline) + - Deadline Verifikasi (verification_deadline) + - Deskripsi +- Admin mengirim pengajuan ke Media Partner (CooperationMedia) +- Status awal: **MENUNGGU** + +### 2. Media Menerima/Menolak Pengajuan +- Media mereview pengajuan kerjasama dalam periode initial_submission_date sampai final_submission_date +- Media dapat: + - **MENERIMA**: Status berubah menjadi ACCEPTED, lanjut ke step 3 + - **MENOLAK**: Status berubah menjadi REJECTED, flow selesai (dengan alasan penolakan/rejection reason) + +### 3. Media Mengajukan Proposal +- Jika Media menerima, Media mengajukan proposal (CooperationProposal) +- Proposal berisi: + - Deskripsi + - Tanggal mulai dan akhir proposed + - E-Catalog + - File dokumen (via Spatie Media Library) + - Jurnalis yang terlibat +- Status proposal: **MENUNGGU** + +### 4. Admin Meninjau Proposal +- Admin mereview proposal yang diajukan Media +- Admin dapat: + - **MENERIMA**: Status proposal menjadi ACCEPTED, lanjut ke step 5 + - **MENOLAK**: Status proposal menjadi REJECTED, Media harus ajukan ulang (dengan alasan penolakan) + +### 5. Admin Kirim Penugasan +- Jika Admin menerima proposal, Admin membuat Task Assignment +- Task Assignment berisi: + - Start Date + - End Date + - Task Description + - Report Amount (jumlah report yang harus dibuat) +- Admin assign task ke Media Partner (TaskAssignmentMedia) +- Status: **MENUNGGU** + +### 6. Media Mengirim Laporan +- Media membuat Report berdasarkan Task Assignment +- Report berisi: + - Title + - Publication Date + - Link + - Description + - Image (via Spatie Media Library) +- Status report: **MENUNGGU** (untuk verifikasi admin) +- Media dapat membuat multiple reports sesuai report_amount yang diminta + +### 7. Admin Menerima/Menolak Laporan +- Admin mereview laporan yang dikirim Media +- Admin dapat: + - **MENERIMA**: Status report menjadi ACCEPTED + - **MENOLAK**: Status report menjadi REJECTED, Media harus kirim ulang (dengan alasan penolakan) + +## Status Otomatis Berdasarkan Deadline + +### Status Cooperation (otomatis berubah berdasarkan waktu): +- **MENUNGGU**: Status awal ketika admin kirim kerja sama +- **PENUGASAN**: Ketika tanggal final_submission_date habis (media sudah tidak bisa ajukan proposal) +- **VERIFIKASI**: Ketika assignment_deadline habis (media sudah selesai kirim laporan) +- **PEMBAYARAN**: Ketika verification_deadline habis (admin sudah selesai verifikasi laporan) +- **COMPLETED**: Ketika pembayaran sudah selesai (payment_date diisi) + +### Status Individual Components: +- **ACCEPTED**: Diterima (untuk proposal, report, dll) +- **REJECTED**: Ditolak (untuk proposal, report, dll) - selalu dengan alasan penolakan + +## Database Schema + +### Table: cooperations +- id +- title (varchar 200) +- initial_submission_date (date) - Tanggal mulai pengajuan proposal +- final_submission_date (date) - Tanggal akhir pengajuan proposal +- assignment_deadline (date, nullable) - Deadline penyelesaian penugasan +- verification_deadline (date, nullable) - Deadline verifikasi laporan +- description (text) +- payment_amount (unsigned integer, nullable) - Jumlah pembayaran +- payment_date (date, nullable) - Tanggal pembayaran +- status (enum: MENUNGGU, PENUGASAN, VERIFIKASI, PEMBAYARAN, COMPLETED, ACCEPTED, REJECTED) +- created_at, updated_at, deleted_at + +### Table: cooperation_media (Pivot) +- id +- cooperation_id (FK) +- partner_media_id (FK) +- status (enum: PENDING, ACCEPTED, REJECTED) +- created_at, updated_at + +### Table: cooperation_proposals +- id +- cooperation_id (FK) +- partner_media_id (FK) +- description (text, nullable) +- e_catalog (varchar 50, nullable) +- status (enum: PENDING, ACCEPTED, REJECTED) +- created_at, updated_at + +### Table: task_assignments +- id +- cooperation_id (FK) +- start_date (date) +- end_date (date) +- task_description (text) +- report_amount (unsigned integer) +- status (enum: PENDING, ACCEPTED, REJECTED) +- created_at, updated_at + +### Table: task_assignment_media (Pivot) +- id +- task_assignment_id (FK) +- partner_media_id (FK) +- status (enum: PENDING, ACCEPTED, REJECTED) +- created_at, updated_at + +### Table: reports +- id +- task_assignment_id (FK) +- title (varchar 200) +- publication_date (date) +- link (varchar 50, nullable) +- description (text, nullable) +- status (enum: MENUNGGU, ACCEPTED, REJECTED) +- created_at, updated_at, deleted_at +- Media files (handled by Spatie Media Library) + +### Table: rejection_reasons (Polymorphic) +- id +- rejectable_type (morphs) +- rejectable_id (morphs) +- reason (text) +- created_at, updated_at + +## Models + +### Cooperation +**Location**: `app/Models/Cooperation.php` + +**Relationships**: +- `partnerMedia()`: BelongsToMany - Media yang terlibat dalam kerjasama +- `cooperationMedia()`: HasMany - Pivot records dengan status +- `cooperationProposals()`: HasMany - Proposal yang diupload media +- `taskAssignments()`: HasMany - Task yang diberikan ke media + +### CooperationMedia (Pivot) +**Location**: `app/Models/CooperationMedia.php` + +**Relationships**: +- `cooperation()`: BelongsTo +- `partnerMedia()`: BelongsTo +- `rejectionReasons()`: MorphMany + +### CooperationProposal +**Location**: `app/Models/CooperationProposal.php` + +**Relationships**: +- `cooperation()`: BelongsTo +- `partnerMedia()`: BelongsTo +- `rejectionReasons()`: MorphMany + +### TaskAssignment +**Location**: `app/Models/TaskAssignment.php` + +**Relationships**: +- `cooperation()`: BelongsTo +- `partnerMedia()`: BelongsToMany (via task_assignment_media) +- `taskAssignmentMedia()`: HasMany +- `rejectionReasons()`: MorphMany +- `reports()`: HasMany (commented, needs to be uncommented) + +### Report +**Location**: `app/Models/Report.php` + +**Relationships**: +- `taskAssignment()`: BelongsTo +- `rejectionReasons()`: MorphMany + +**Implements**: HasMedia (Spatie Media Library) untuk handle image dan file attachments + +**Implements**: HasMedia (Spatie Media Library) + +### RejectionReason +**Location**: `app/Models/RejectionReason.php` + +**Relationships**: +- `rejectable()`: MorphTo - Dapat digunakan untuk CooperationMedia, CooperationProposal, TaskAssignment, Report + +## Filament Resources + +### CooperationResource +**Location**: `app/Filament/Resources/Manage/Cooperations/CooperationResource.php` + +**Navigation**: +- Group: Manage +- Label: Kerja Sama +- Icon: HandRaised +- Sort: 20 +- Slug: manage/cooperations + +**Pages**: +- List: `/manage/cooperations` +- Create: `/manage/cooperations/create` +- Edit: `/manage/cooperations/{record}/edit` + +**Form Fields** (CooperationForm): +- Title (required, max 200) +- Initial Submission Date (required, date) +- Final Submission Date (required, date, after initial_submission_date) +- Description (required, textarea) + +**Table Columns** (CooperationsTable): +- Title +- Initial Submission Date +- Final Submission Date +- Description +- Created At +- Updated At + +## Status Enum + +### CooperationStatus +**Location**: `app/Enums/CooperationStatus.php` + +**Values**: +- `MENUNGGU` (1): Menunggu - Status awal kerja sama +- `PENUGASAN` (2): Penugasan - Ketika deadline proposal habis +- `VERIFIKASI` (3): Verifikasi - Ketika deadline penugasan habis +- `PEMBAYARAN` (4): Pembayaran - Ketika verifikasi selesai +- `COMPLETED` (5): Selesai - Ketika pembayaran selesai +- `ACCEPTED` (6): Diterima - Untuk proposal/report yang diterima +- `REJECTED` (7): Ditolak - Untuk proposal/report yang ditolak + +**Colors**: +- MENUNGGU: warning (yellow) +- PENUGASAN: info (blue) +- VERIFIKASI: primary (blue) +- PEMBAYARAN: secondary (gray) +- COMPLETED: success (green) +- ACCEPTED: success (green) +- REJECTED: danger (red) + +## Next Steps / TODO + +1. **Uncomment Report Relationship** di TaskAssignment model +2. **Fix Status Colors** di CooperationStatus enum (ACCEPTED should be success, REJECTED should be danger) +3. **Create Relation Managers** untuk: + - CooperationMedia (di Cooperation Resource) + - CooperationProposal (di Cooperation Resource) + - TaskAssignment (di Cooperation Resource) + - Report (di TaskAssignment Resource) +4. **Create Custom Actions** untuk: + - Send to Media (dari Cooperation) + - Accept/Reject Cooperation (dari Media side) + - Upload Proposal (dari Media side) + - Accept/Reject Proposal (dari Admin side) + - Create Task Assignment (dari Admin side) + - Create Report (dari Media side) +5. **Add Permissions** untuk role Admin dan Media +6. **Create Notifications** untuk setiap perubahan status +7. **Add Validation** untuk memastikan flow berjalan dengan benar +8. **Create Dashboard Widgets** untuk monitoring kerjasama + +## File Structure + +``` +app/ +├── Enums/ +│ └── CooperationStatus.php +├── Filament/ +│ └── Resources/ +│ └── Manage/ +│ └── Cooperations/ +│ ├── CooperationResource.php +│ ├── Pages/ +│ │ ├── ListCooperations.php +│ │ ├── CreateCooperation.php +│ │ └── EditCooperation.php +│ ├── Schemas/ +│ │ └── CooperationForm.php +│ └── Tables/ +│ └── CooperationsTable.php +└── Models/ + ├── Cooperation.php + ├── CooperationMedia.php + ├── CooperationProposal.php + ├── TaskAssignment.php + ├── Report.php + └── RejectionReason.php + +database/ +└── migrations/ + ├── 2025_12_12_111212_create_cooperations_table.php + ├── 2025_12_12_111307_create_cooperation_media_table.php + ├── 2025_12_12_132245_create_cooperation_proposals_table.php + ├── 2025_12_12_132453_create_task_assignments_table.php + ├── 2025_12_12_133823_create_media_task_assignment_table.php + ├── 2025_12_12_135847_create_reports_table.php + └── 2025_12_12_135855_create_rejection_reasons_table.php +``` + +## Notes + +- Semua tabel sudah di-migrate +- Model Cooperation sudah dilengkapi dengan relationships +- Resource Cooperation sudah dibuat dan siap digunakan +- Typo di migration task_assignments sudah diperbaiki (task_description dari date ke text) +- Icon Handshake diganti dengan HandRaised karena tidak tersedia di Heroicon diff --git a/app/Enums/ApprovalStatus.php b/app/Enums/ApprovalStatus.php new file mode 100644 index 0000000..24e2023 --- /dev/null +++ b/app/Enums/ApprovalStatus.php @@ -0,0 +1,42 @@ + 'Menunggu', + self::ACCEPTED => 'Menerima', + self::REJECTED => 'Menolak', + }; + } + + public function getColor(): string|array|null + { + return match ($this) { + self::PENDING => 'warning', + self::ACCEPTED => 'success', + self::REJECTED => 'danger', + }; + } + + public static function options(): array + { + return collect(self::cases()) + ->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()]) + ->toArray(); + } +} diff --git a/app/Enums/CooperationStatus.php b/app/Enums/CooperationStatus.php new file mode 100644 index 0000000..3e7e379 --- /dev/null +++ b/app/Enums/CooperationStatus.php @@ -0,0 +1,48 @@ + 'Menunggu', + self::ASSIGNMENT => 'Penugasan', + self::VERIFICATION => 'Verifikasi', + self::PAYMENT => 'Pembayaran', + self::COMPLETED => 'Selesai', + }; + } + + public function getColor(): string|array|null + { + return match ($this) { + self::PENDING => 'warning', + self::ASSIGNMENT => 'info', + self::VERIFICATION => 'primary', + self::PAYMENT => 'secondary', + self::COMPLETED => 'success', + }; + } + + public static function options(): array + { + return collect(self::cases()) + ->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()]) + ->toArray(); + } +} diff --git a/app/Filament/Pages/CustomLogin.php b/app/Filament/Pages/CustomLogin.php index 2aa3934..95478cb 100644 --- a/app/Filament/Pages/CustomLogin.php +++ b/app/Filament/Pages/CustomLogin.php @@ -32,7 +32,7 @@ public function form(Schema $schema): Schema ->components([ TextInput::make('login') ->label('Nama Pengguna atau Alamat Surel') - ->placeholder('johndoe@puskesmas.com') + ->placeholder('johndoe@gmail.com') ->required() ->autocomplete(false) ->autofocus(), diff --git a/app/Filament/Resources/Manage/Cooperations/CooperationResource.php b/app/Filament/Resources/Manage/Cooperations/CooperationResource.php new file mode 100644 index 0000000..7e8d2a3 --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/CooperationResource.php @@ -0,0 +1,157 @@ +modifyQueryUsing(function (Builder $query) { + if (auth()->user()->hasRole('Perusahaan')) { + $query->whereHas('cooperationMedia', function ($subQuery) { + $subQuery->whereHas('partnerMedia', function ($mediaQuery) { + $mediaQuery->whereHas('company', function ($companyQuery) { + $companyQuery->where('user_id', auth()->id()); + }); + }); + }); + } + + return $query; + }); + } + + public static function infolist(Schema $schema): Schema + { + return $schema + ->components([ + Section::make('Informasi Kerja Sama') + ->headerActions([ + Action::make('status') + ->label(fn ($record) => $record->status->getLabel()) + ->badge() + ->color(fn ($record) => $record->status->getColor()) + ->disabled(), + ]) + ->schema([ + TextEntry::make('title') + ->label('Judul'), + + Grid::make(4) + ->schema([ + TextEntry::make('initial_submission_date') + ->label('Tanggal Pengajuan Awal') + ->date('l, d F Y'), + + TextEntry::make('final_submission_date') + ->label('Tanggal Pengajuan Akhir') + ->date('l, d F Y'), + + TextEntry::make('assignment_deadline') + ->label('Deadline Penugasan') + ->date('l, d F Y') + ->placeholder('Belum ditentukan'), + + TextEntry::make('verification_deadline') + ->label('Deadline Verifikasi') + ->date('l, d F Y') + ->placeholder('Belum ditentukan'), + ]), + + Section::make('Informasi Pembayaran') + ->schema([ + Grid::make(2) + ->schema([ + TextEntry::make('payment_amount') + ->label('Jumlah Pembayaran') + ->money('IDR') + ->placeholder('Belum ditentukan'), + + TextEntry::make('payment_date') + ->label('Tanggal Pembayaran') + ->date('l, d F Y') + ->placeholder('Belum dibayar'), + ]), + ]) + ->visible(fn ($record) => $record->payment_amount || $record->payment_date), + + TextEntry::make('description') + ->label('Deskripsi') + ->markdown(), + ]), + ]) + ->columns(1); + } + + public static function getRelations(): array + { + return [ + CooperationMediaRelationManager::class, + CooperationProposalRelationManager::class, + TaskAssignmentRelationManager::class, + ReportRelationManager::class, + ]; + } + + public static function getPages(): array + { + return [ + 'index' => ListCooperations::route('/'), + 'create' => CreateCooperation::route('/create'), + 'view' => ViewCooperation::route('/{record}'), + 'edit' => EditCooperation::route('/{record}/edit'), + ]; + } + + public static function getRecordRouteBindingEloquentQuery(): Builder + { + return parent::getRecordRouteBindingEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Pages/CreateCooperation.php b/app/Filament/Resources/Manage/Cooperations/Pages/CreateCooperation.php new file mode 100644 index 0000000..2c023c2 --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Pages/CreateCooperation.php @@ -0,0 +1,30 @@ +getResource()::getUrl('index'); + } + + protected function getHeaderActions(): array + { + return [ + Action::make('back') + ->label('Kembali') + ->url(fn (): string => route('filament.dashboard.resources.manage.cooperations.index')) + ->outlined() + ->color('secondary'), + ]; + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Pages/EditCooperation.php b/app/Filament/Resources/Manage/Cooperations/Pages/EditCooperation.php new file mode 100644 index 0000000..af25baa --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Pages/EditCooperation.php @@ -0,0 +1,28 @@ +getResource()::getUrl('index'); + } + + protected function getHeaderActions(): array + { + return [ + Action::make('back') + ->label('Kembali') + ->url(fn (): string => route('filament.dashboard.resources.manage.cooperations.index')) + ->outlined() + ->color('secondary'), + ]; + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Pages/ListCooperations.php b/app/Filament/Resources/Manage/Cooperations/Pages/ListCooperations.php new file mode 100644 index 0000000..1240d26 --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Pages/ListCooperations.php @@ -0,0 +1,27 @@ +hasRole('Perusahaan')) { + $actions[] = CreateAction::make() + ->label('Tambah'); + } + + return $actions; + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Pages/ViewCooperation.php b/app/Filament/Resources/Manage/Cooperations/Pages/ViewCooperation.php new file mode 100644 index 0000000..46e0ab6 --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Pages/ViewCooperation.php @@ -0,0 +1,321 @@ +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 ($state) => "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(CooperationStatus::ACCEPTED)) { + return 'completed'; + } + + if ($mediaStatuses->contains(CooperationStatus::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->isNotEmpty()) { + 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 ($proposals->where('status', CooperationStatus::ACCEPTED)->isNotEmpty()) { + return 'completed'; + } + + if ($proposals->where('status', CooperationStatus::REJECTED)->isNotEmpty()) { + 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->taskAssignments->isNotEmpty()) { + return 'completed'; + } + + return 'pending'; + } + + protected function getTaskDate($record) + { + $latestTask = $record->taskAssignments->sortByDesc('created_at')->first(); + + return $latestTask?->created_at?->format('d/m/Y H:i'); + } + + protected function getReportStatus($record): string + { + $tasks = $record->taskAssignments; + + if ($tasks->isEmpty()) { + return 'pending'; + } + + $totalReports = $tasks->sum('report_amount'); + $completedReports = $tasks->flatMap->reports->count(); + + if ($completedReports >= $totalReports && $totalReports > 0) { + return 'completed'; + } + + if ($completedReports > 0) { + return 'in_progress'; + } + + return 'pending'; + } + + protected function getReportDate($record) + { + $latestReport = $record->taskAssignments + ->flatMap->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 + { + $isPerusahaan = Auth::user()->hasRole('Perusahaan'); + $record = $this->getRecord(); + + $actions = []; + + if (! $isPerusahaan) { + $actions[] = Action::make('mark_completed') + ->label('Tandai Selesai') + ->icon('heroicon-o-check-circle') + ->color('success') + ->action(function () { + // Logic to mark cooperation as completed + Notification::make() + ->title('Kerja Sama Ditandai Selesai') + ->success() + ->send(); + }) + ->visible(fn () => $this->canMarkAsCompleted()); + } else { + // For Perusahaan users, add accept/reject actions if they have a cooperation media record + $cooperationMedia = $record->cooperationMedia() + ->whereHas('partnerMedia', function ($query) { + $query->whereHas('company', function ($subQuery) { + $subQuery->where('user_id', Auth::id()); + }); + }) + ->first(); + + if ($cooperationMedia && $cooperationMedia->status === CooperationStatus::PENDING) { + $actions[] = Action::make('accept') + ->label('Terima Kerja Sama') + ->icon('heroicon-o-check-circle') + ->color('success') + ->action(function () use ($cooperationMedia) { + $cooperationMedia->update(['status' => CooperationStatus::ACCEPTED]); + Notification::make() + ->title('Kerja Sama Diterima') + ->success() + ->send(); + }); + + $actions[] = Action::make('reject') + ->label('Tolak Kerja Sama') + ->icon('heroicon-o-x-circle') + ->color('danger') + ->action(function () use ($cooperationMedia) { + $cooperationMedia->update(['status' => CooperationStatus::REJECTED]); + Notification::make() + ->title('Kerja Sama Ditolak') + ->success() + ->send(); + }); + } + } + + $actions[] = Action::make('back') + ->label('Kembali') + ->url(fn (): string => static::getResource()::getUrl('index')) + ->outlined() + ->color('secondary'); + + return $actions; + } + + protected function canMarkAsCompleted(): bool + { + $record = $this->getRecord(); + + // Check if all tasks have required number of reports + foreach ($record->taskAssignments as $task) { + $reportCount = $task->reports()->count(); + if ($reportCount < $task->report_amount) { + return false; + } + } + + return $record->taskAssignments->isNotEmpty(); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php new file mode 100644 index 0000000..8ddf452 --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationMediaRelationManager.php @@ -0,0 +1,43 @@ +user()->hasRole('Perusahaan'); + } + + public function table(Table $table): Table + { + return $table + ->recordTitleAttribute('partnerMedia.name') + ->columns([ + TextColumn::make('partnerMedia.name') + ->label('Nama') + ->searchable(), + + TextColumn::make('status') + ->label('Status') + ->badge(), + ]) + ->filters([]) + ->headerActions([]) + ->recordActions([ + ViewAction::make(), + ]) + ->toolbarActions([]) + ->recordAction(null); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationProposalRelationManager.php b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationProposalRelationManager.php new file mode 100644 index 0000000..f85282c --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/RelationManagers/CooperationProposalRelationManager.php @@ -0,0 +1,136 @@ +proposals()->count(); + } + + public function table(Table $table): Table + { + return $table + ->recordTitleAttribute('description') + ->columns([ + TextColumn::make('partnerMedia.name') + ->label('Media') + ->searchable() + ->visible(! auth()->user()->hasRole('Perusahaan')), + + TextColumn::make('e_catalog') + ->label('E-Catalog') + ->url(fn ($record) => $record->e_catalog) + ->openUrlInNewTab() + ->color('primary'), + + TextColumn::make('description') + ->label('Deskripsi'), + + TextColumn::make('status') + ->label('Status') + ->badge(), + + TextColumn::make('submitted_at') + ->label('Diajukan') + ->dateTime('l, d F Y H:i:s') + ->sortable(), + + TextColumn::make('responded_at') + ->label('Ditanggapi') + ->dateTime('l, d F Y H:i:s') + ->sortable(), + + TextColumn::make('rejectionReasons.reason') + ->label('Alasan Penolakan') + ->searchable(), + ]) + ->filters([]) + ->headerActions([]) + ->recordActions([ + ViewAction::make(), + Action::make('accept') + ->label('Terima') + ->icon(Heroicon::OutlinedCheck) + ->color('success') + ->action(function (CooperationProposal $record) { + $record->update([ + 'status' => ApprovalStatus::ACCEPTED, + 'responded_at' => now(), + ]); + + Notification::make() + ->title('Proposal Diterima') + ->body("Proposal dari {$record->partnerMedia->name} telah diterima.") + ->success() + ->send(); + }) + ->visible(fn (CooperationProposal $record) => ! auth()->user()->hasRole('Perusahaan') && $record->status === ApprovalStatus::PENDING), + Action::make('reject') + ->label('Tolak') + ->icon(Heroicon::XMark) + ->color('danger') + ->schema([ + Textarea::make('reason') + ->label('Alasan Penolakan') + ->placeholder('...') + ->required(), + ]) + ->action(function (CooperationProposal $record, array $data) { + $record->update([ + 'status' => ApprovalStatus::REJECTED, + 'responded_at' => now(), + ]); + + $record->rejectionReasons()->create(['reason' => $data['reason']]); + + Notification::make() + ->title('Proposal Ditolak') + ->body("Proposal dari {$record->partnerMedia->name} telah ditolak.") + ->warning() + ->send(); + }) + ->visible(fn (CooperationProposal $record) => ! auth()->user()->hasRole('Perusahaan') && $record->status === ApprovalStatus::PENDING) + ->modalWidth('lg'), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]) + ->recordAction(null); + } + + // public function infolist(Infolist $infolist): Infolist + // { + // return $infolist + // ->schema([ + // TextEntry::make('e_catalog') + // ->label('File Proposal') + // ->url(fn($record) => $record->e_catalog) + // ->openUrlInNewTab() + // ->columnSpanFull(), + // ]); + // } +} diff --git a/app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php b/app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php new file mode 100644 index 0000000..6dd0ce9 --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/RelationManagers/ReportRelationManager.php @@ -0,0 +1,147 @@ +components([ + TextInput::make('title') + ->placeholder('Dirgahayu Purwakarta') + ->required() + ->maxLength(200), + DatePicker::make('publication_date') + ->label('Tanggal Publikasi') + ->placeholder(fn () => now()->format('l, d F Y')) + ->native(false) + ->displayFormat('l, d F Y') + ->required(), + TextInput::make('link') + ->label('Tautan') + ->placeholder('https://example.com') + ->maxLength(50) + ->url() + ->required(), + Textarea::make('description') + ->label('Deskripsi') + ->placeholder('....') + ->required(), + SpatieMediaLibraryFileUpload::make('image') + ->label('Gambar') + ->disk(config('filesystems.default')) + ->acceptedFileTypes(['images/*']) + ->maxSize(1024 * 3) + ->collection('reports') + ->image() + ->required(), + ]) + ->columns(1); + } + + public function table(Table $table): Table + { + return $table + ->recordTitleAttribute('title') + ->columns([ + TextColumn::make('title') + ->label('Judul') + ->searchable(), + TextColumn::make('publication_date') + ->label('Tanggal Publikasi') + ->date('l, d F Y'), + TextColumn::make('link') + ->label('Link') + ->limit(30) + ->url(fn ($record) => $record->link) + ->openUrlInNewTab(), + TextColumn::make('status') + ->label('Status') + ->badge(), + TextColumn::make('rejectionReasons.reason') + ->label('Alasan Penolakan') + ->searchable(), + ]) + ->filters([]) + ->recordActions([ + Action::make('accept') + ->label('Terima') + ->icon(Heroicon::OutlinedCheck) + ->color('success') + ->action(function (Report $record) { + $record->update([ + 'status' => ApprovalStatus::ACCEPTED, + ]); + + Notification::make() + ->title('Laporan Diterima') + ->body("Laporan '{$record->title}' telah diterima.") + ->success() + ->send(); + }) + ->visible(fn (Report $record) => ! auth()->user()->hasRole('Perusahaan') && $record->status === ApprovalStatus::PENDING), + Action::make('reject') + ->label('Tolak') + ->icon(Heroicon::XMark) + ->color('danger') + ->schema([ + Textarea::make('reason') + ->label('Alasan Penolakan') + ->placeholder('...') + ->required(), + ]) + ->action(function (Report $record, array $data) { + $record->update([ + 'status' => ApprovalStatus::REJECTED, + ]); + + $record->rejectionReasons()->create(['reason' => $data['reason']]); + + Notification::make() + ->title('Laporan Ditolak') + ->body("Laporan '{$record->title}' telah ditolak.") + ->warning() + ->send(); + }) + ->visible(fn (Report $record) => ! auth()->user()->hasRole('Perusahaan') && $record->status === ApprovalStatus::PENDING) + ->modalWidth('lg'), + ]) + ->headerActions([ + CreateAction::make() + ->label('Buat Laporan') + ->modalWidth('lg') + ->modalHeading('Buat Laporan') + ->mutateDataUsing(function (array $data): array { + $data['task_assignment_id'] = $this->getOwnerRecord()->taskAssignments()->first()?->id; + + return $data; + }), + ]) + ->toolbarActions([]); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/RelationManagers/TaskAssignmentRelationManager.php b/app/Filament/Resources/Manage/Cooperations/RelationManagers/TaskAssignmentRelationManager.php new file mode 100644 index 0000000..448465d --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/RelationManagers/TaskAssignmentRelationManager.php @@ -0,0 +1,126 @@ +components([ + DatePicker::make('start_date') + ->label('Tanggal Mulai') + ->required() + ->native(false), + + DatePicker::make('end_date') + ->label('Tanggal Selesai') + ->required() + ->native(false) + ->after('start_date'), + + Textarea::make('task_description') + ->label('Deskripsi Tugas') + ->required() + ->rows(4) + ->maxLength(65535), + + TextInput::make('report_amount') + ->label('Jumlah Laporan') + ->numeric() + ->required() + ->minValue(1) + ->default(1), + + Select::make('partner_media_ids') + ->label('Media yang Ditugaskan') + ->relationship('partnerMedia', 'name') + ->multiple() + ->preload() + ->required(), + + Select::make('status') + ->options(ApprovalStatus::options()) + ->required() + ->default(ApprovalStatus::PENDING), + + Textarea::make('rejection_reason') + ->label('Alasan Penolakan') + ->rows(3) + ->columnSpanFull(), + ]); + } + + public function table(Table $table): Table + { + return $table + ->recordTitleAttribute('task_description') + ->columns([ + TextColumn::make('start_date') + ->label('Mulai') + ->date('l, d F Y'), + + TextColumn::make('end_date') + ->label('Selesai') + ->date('l, d F Y'), + + TextColumn::make('task_description') + ->label('Deskripsi Tugas') + ->limit(50), + + TextColumn::make('report_amount') + ->label('Jumlah Laporan'), + + TextColumn::make('report_count') + ->label('Laporan Diterima') + ->getStateUsing(function (TaskAssignment $record) { + return $record->reports()->count().' / '.($record->report_amount * $record->partnerMedia()->count()); + }), + ]) + ->filters([ + // + ]) + ->headerActions([ + CreateAction::make() + ->label('Buat Penugasan') + ->visible(fn () => $this->canCreateTaskAssignment()), + ]) + ->recordActions([ + EditAction::make(), + DeleteAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } + + protected function canCreateTaskAssignment(): bool + { + $cooperation = $this->getOwnerRecord(); + + // Can create task assignment only if there are accepted proposals + return $cooperation->proposals()->where('status', ApprovalStatus::ACCEPTED)->exists(); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Schemas/CooperationForm.php b/app/Filament/Resources/Manage/Cooperations/Schemas/CooperationForm.php new file mode 100644 index 0000000..b3e1c5c --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Schemas/CooperationForm.php @@ -0,0 +1,99 @@ +components([ + Section::make([ + TextInput::make('title') + ->label('Judul') + ->placeholder('Publikasi Pembangunan Daerah') + ->autocomplete(false) + ->autofocus() + ->required() + ->maxLength(200), + + Grid::make(2) + ->schema([ + DatePicker::make('initial_submission_date') + ->label('Tanggal Pengajuan Awal') + ->placeholder(fn () => now()->format('Y-m-d')) + ->native(false) + ->displayFormat('l, d F Y') + ->required(), + + DatePicker::make('final_submission_date') + ->label('Tanggal Pengajuan Akhir') + ->placeholder(fn () => now()->addDays(30)->format('Y-m-d')) + ->native(false) + ->displayFormat('l, d F Y') + ->required() + ->after('initial_submission_date'), + ]), + + Select::make('partner_media_ids') + ->label('Media') + ->relationship('partnerMedia', 'name') + ->multiple() + ->preload() + ->searchable() + ->native(false) + ->required() + ->selectablePlaceholder(false) + ->helperText('Pilih media yang akan diajak kerja sama.') + ->suffixAction( + Action::make('selectAll') + ->label('Select All') + ->icon(Heroicon::OutlinedCheckCircle) + ->action(function ($set, $livewire) { + $allMediaIds = PartnerMedia::pluck('id')->toArray(); + $set('partner_media_ids', $allMediaIds); + }) + ), + + Textarea::make('description') + ->label('Deskripsi') + ->placeholder('...') + ->required() + ->rows(5) + ->maxLength(65535), + ]) + ->columnSpan(2), + + Section::make([ + SpatieMediaLibraryFileUpload::make('banner') + ->label('Banner') + ->disk(config('filesystems.default')) + ->acceptedFileTypes(['images/*']) + ->maxSize(1024 * 3) + ->collection('cooperation') + ->image(), + + SpatieMediaLibraryFileUpload::make('proposal_template') + ->label('Template Pengajuan') + ->disk(config('filesystems.default')) + ->acceptedFileTypes(['application/pdf']) + ->maxSize(1024 * 10) + ->collection('cooperation') + ->required(), + ]), + ]) + ->columns(3); + } +} diff --git a/app/Filament/Resources/Manage/Cooperations/Tables/CooperationsTable.php b/app/Filament/Resources/Manage/Cooperations/Tables/CooperationsTable.php new file mode 100644 index 0000000..3f06a5e --- /dev/null +++ b/app/Filament/Resources/Manage/Cooperations/Tables/CooperationsTable.php @@ -0,0 +1,383 @@ +columns([ + TextColumn::make('title') + ->label('Judul') + ->searchable() + ->sortable() + ->wrap(), + + TextColumn::make('partnerMedia.name') + ->label('Media') + ->listWithLineBreaks() + ->limitList(3) + ->expandableLimitedList() + ->badge() + ->visible(! auth()->user()->hasRole('Perusahaan')), + + TextColumn::make('initial_submission_date') + ->label('Tgl Pengajuan Awal') + ->searchable() + ->sortable() + ->date('l, d F Y'), + + TextColumn::make('final_submission_date') + ->label('Tgl Pengajuan Akhir') + ->searchable() + ->sortable() + ->date('l, d F Y') + ->description( + fn (Cooperation $record): string => now()->greaterThan($record->final_submission_date) + ? 'Waktu Pengajuan Habis' + : 'Sisa waktu pengajuan: '.now()->diffForHumans( + $record->final_submission_date, + ['syntax' => CarbonInterface::DIFF_ABSOLUTE] + ) + ), + + 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() + ->badge(), + + TextColumn::make('media_count') + ->label('Tanggapan Media') + ->html() + ->getStateUsing(function (Cooperation $record) { + $record->loadCount([ + 'partnerMedia as approved_count' => fn ($query) => $query->where('status', ApprovalStatus::ACCEPTED), + 'partnerMedia as rejected_count' => fn ($query) => $query->where('status', ApprovalStatus::REJECTED), + 'partnerMedia as pending_count' => fn ($query) => $query->where('status', ApprovalStatus::PENDING), + ]); + + $totalCount = $record->partnerMedia()->count(); + + return "Menerima: {$record->approved_count}
+ Menolak: {$record->rejected_count}
+ Belum Menanggapi: {$record->pending_count}
+ Total: {$totalCount}"; + }) + ->visible(! auth()->user()->hasRole('Perusahaan')), + + TextColumn::make('status_tanggapan') + ->label('Tanggapan') + ->getStateUsing(function ($record) { + if (auth()->user()->hasRole('Perusahaan')) { + $cooperationMedia = $record->cooperationMedia() + ->whereHas('partnerMedia', function ($query) { + $query->whereHas('company', function ($subQuery) { + $subQuery->where('user_id', auth()->id()); + }); + }) + ->first(); + + return $cooperationMedia?->status; + } + + return null; + }) + ->badge() + ->visible(auth()->user()->hasRole('Perusahaan')), + + TextColumn::make('payment_amount') + ->label('Jumlah Pembayaran') + ->money('IDR') + ->sortable() + ->placeholder('Belum ditentukan') + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('payment_date') + ->label('Tanggal Pembayaran') + ->date('l, d F Y') + ->sortable() + ->placeholder('Belum dibayar') + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('description') + ->label('Deskripsi') + ->searchable() + ->limit(50) + ->wrap() + ->toggleable(isToggledHiddenByDefault: true), + + ...TimestampColumns::make(), + ]) + ->filters([ + TrashedFilter::make()->native(false), + ]) + ->recordActions([ + ViewAction::make(), + Action::make('accept') + ->label('Terima') + ->icon(Heroicon::OutlinedCheck) + ->color('success') + ->action(function ($record) { + $cooperationMedia = $record->cooperationMedia() + ->whereHas('partnerMedia', function ($query) { + $query->whereHas('company', function ($subQuery) { + $subQuery->where('user_id', auth()->id()); + }); + }) + ->first(); + + if ($cooperationMedia) { + $cooperationMedia->update(['status' => ApprovalStatus::ACCEPTED]); + Notification::make() + ->title('Kerja sama diterima') + ->success() + ->send(); + } + }) + ->visible(function ($record) { + if (! auth()->user()->hasRole('Perusahaan')) { + return false; + } + + $cooperationMedia = $record->cooperationMedia() + ->whereHas('partnerMedia', function ($query) { + $query->whereHas('company', function ($subQuery) { + $subQuery->where('user_id', auth()->id()); + }); + }) + ->first(); + + return $cooperationMedia && $cooperationMedia->status === ApprovalStatus::PENDING; + }), + Action::make('reject') + ->label('Tolak') + ->icon(Heroicon::XMark) + ->color('danger') + ->action(function ($record) { + $cooperationMedia = $record->cooperationMedia() + ->whereHas('partnerMedia', function ($query) { + $query->whereHas('company', function ($subQuery) { + $subQuery->where('user_id', auth()->id()); + }); + }) + ->first(); + + if ($cooperationMedia) { + $cooperationMedia->update(['status' => ApprovalStatus::REJECTED]); + Notification::make() + ->title('Kerja sama ditolak') + ->success() + ->send(); + } + }) + ->visible(function ($record) { + if (! auth()->user()->hasRole('Perusahaan')) { + return false; + } + + $cooperationMedia = $record->cooperationMedia() + ->whereHas('partnerMedia', function ($query) { + $query->whereHas('company', function ($subQuery) { + $subQuery->where('user_id', auth()->id()); + }); + }) + ->first(); + + return $cooperationMedia && $cooperationMedia->status === ApprovalStatus::PENDING; + }), + Action::make('send_proposal') + ->label('Ajukan Proposal') + ->icon(Heroicon::DocumentText) + ->color('info') + ->schema([ + TextInput::make('e_catalog') + ->label('E-Catalog') + ->placeholder('https://katalog.inaproc.id/purwakarta-kita-media-karya') + ->autocomplete(false) + ->autofocus() + ->nullable() + ->url(), + + Textarea::make('description') + ->label('Deskripsi Proposal') + ->autocomplete(false) + ->required() + ->rows(4) + ->placeholder('...'), + + AdvancedFileUpload::make('proposal_attachment') + ->label('Lampiran Proposal') + ->disk(config('filesystems.default')) + ->acceptedFileTypes(['application/pdf']) + ->maxSize(1024 * 10) + ->directory('cooperation/proposal/'.now()->toDateString()) + ->required(), + ]) + ->action(function ($record, array $data) { + $cooperationMedia = $record->cooperationMedia() + ->whereHas('partnerMedia', function ($query) { + $query->whereHas('company', function ($subQuery) { + $subQuery->where('user_id', auth()->id()); + }); + }) + ->first(); + + if ($cooperationMedia) { + $record->proposals()->create([ + 'partner_media_id' => $cooperationMedia->partner_media_id, + 'description' => $data['description'], + 'e_catalog' => $data['e_catalog'], + 'status' => ApprovalStatus::PENDING->value, + ]); + + Notification::make() + ->title('Proposal kerja sama berhasil dikirim') + ->success() + ->send(); + } + }) + ->modalHeading('Proposal Kerja Sama') + ->modalSubmitActionLabel('Kirim Proposal') + ->visible(function ($record) { + if (! auth()->user()->hasRole('Perusahaan')) { + return false; + } + + $cooperationMedia = $record->cooperationMedia() + ->whereHas('partnerMedia', function ($query) { + $query->whereHas('company', function ($subQuery) { + $subQuery->where('user_id', auth()->id()); + }); + }) + ->first(); + + return $cooperationMedia + && $cooperationMedia->status === ApprovalStatus::ACCEPTED + && ! $record->proposals() + ->where('partner_media_id', $cooperationMedia->partner_media_id) + ->whereIn('status', [ + ApprovalStatus::ACCEPTED, + ApprovalStatus::PENDING, + ]) + ->exists(); + }) + ->modalWidth('lg'), + Action::make('create_task_assignment') + ->label('Buat Penugasan') + ->icon(Heroicon::Plus) + ->color('primary') + ->schema([ + DatePicker::make('start_date') + ->label('Tanggal Mulai') + ->placeholder(fn () => now()->translatedFormat('l, d F Y')) + ->native(false) + ->displayFormat('l, d F Y') + ->locale('id') + ->required(), + + DatePicker::make('end_date') + ->label('Tanggal Seleisa') + ->placeholder(fn () => now()->addDays(30)->translatedFormat('l, d F Y')) + ->native(false) + ->displayFormat('l, d F Y') + ->required() + ->after('start_date'), + + TextInput::make('report_amount') + ->label('Jumlah Laporan') + ->placeholder(10) + ->numeric() + ->minValue(1) + ->required(), + Textarea::make('task_description') + ->label('Deskripsi Tugas') + ->required() + ->rows(4) + ->placeholder('...'), + ]) + ->action(function (array $data, Cooperation $record) { + $taskAssignment = $record->taskAssignments()->create([ + 'start_date' => $data['start_date'], + 'end_date' => $data['end_date'], + 'task_description' => $data['task_description'], + 'report_amount' => $data['report_amount'], + ]); + + $partnerMedias = $record->partnerMedia() + ->wherePivot('status', ApprovalStatus::ACCEPTED) + ->get(); + + foreach ($partnerMedias as $partnerMedia) { + $taskAssignment->mediaTaskAssignments()->create([ + 'partner_media_id' => $partnerMedia->id, + 'status' => ApprovalStatus::PENDING, + ]); + } + + Notification::make() + ->title('Penugasan berhasil dibuat') + ->success() + ->send(); + }) + ->modalHeading('Buat Penugasan') + ->modalSubmitActionLabel('Simpan') + ->visible( + fn (Cooperation $record): bool => ! auth()->user()->hasRole('Perusahaan') + && ! $record->taskAssignments()->exists() + && $record->status === CooperationStatus::ASSIGNMENT + ) + ->modalWidth('lg'), + EditAction::make() + ->visible(fn () => ! auth()->user()->hasRole('Perusahaan')), + DeleteAction::make() + ->visible(fn () => ! auth()->user()->hasRole('Perusahaan')), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + ...DefaultBulkActions::make('Kerja Sama'), + ]), + ]) + ->emptyStateIcon(Heroicon::HandRaised) + ->emptyStateDescription('Setelah Anda membuat data pertama, maka akan muncul disini.') + ->defaultSort('created_at', 'desc') + ->deferFilters(false); + } +} diff --git a/app/Filament/Resources/Manage/Partners/PartnerResource.php b/app/Filament/Resources/Manage/Partners/PartnerResource.php index 0a51316..7083df6 100644 --- a/app/Filament/Resources/Manage/Partners/PartnerResource.php +++ b/app/Filament/Resources/Manage/Partners/PartnerResource.php @@ -69,7 +69,7 @@ public static function table(Table $table): Table ->label('Jurnalis') ->searchable() ->sortable() - ->wrap(), + ->badge(), TextColumn::make('created_at') ->label('Tgl Daftar') diff --git a/app/Models/Cooperation.php b/app/Models/Cooperation.php new file mode 100644 index 0000000..b708f7a --- /dev/null +++ b/app/Models/Cooperation.php @@ -0,0 +1,59 @@ + 'date', + 'final_submission_date' => 'date', + 'assignment_deadline' => 'date', + 'verification_deadline' => 'date', + 'payment_amount' => 'integer', + 'payment_date' => 'date', + 'status' => CooperationStatus::class, + ]; + } + + public function proposals(): HasMany + { + return $this->hasMany(CooperationProposal::class); + } + + public function taskAssignments(): HasMany + { + return $this->hasMany(TaskAssignment::class); + } + + public function cooperationMedia(): HasMany + { + return $this->hasMany(CooperationMedia::class); + } + + public function partnerMedia(): BelongsToMany + { + return $this->belongsToMany(PartnerMedia::class, 'cooperation_media') + ->withPivot(['status']) + ->withTimestamps(); + } + + public function reports(): \Illuminate\Database\Eloquent\Relations\HasManyThrough + { + return $this->hasManyThrough(Report::class, TaskAssignment::class); + } +} diff --git a/app/Models/CooperationMedia.php b/app/Models/CooperationMedia.php new file mode 100644 index 0000000..6207c66 --- /dev/null +++ b/app/Models/CooperationMedia.php @@ -0,0 +1,40 @@ + ApprovalStatus::class, + ]; + } + + public function cooperation(): BelongsTo + { + return $this->belongsTo(Cooperation::class); + } + + public function partnerMedia(): BelongsTo + { + return $this->belongsTo(PartnerMedia::class); + } + + public function rejectionReasons(): MorphMany + { + return $this->morphMany(RejectionReason::class, 'rejectable'); + } +} diff --git a/app/Models/CooperationProposal.php b/app/Models/CooperationProposal.php new file mode 100644 index 0000000..5f77e0a --- /dev/null +++ b/app/Models/CooperationProposal.php @@ -0,0 +1,40 @@ + ApprovalStatus::class, + 'proposed_start_date' => 'date', + 'proposed_end_date' => 'date', + ]; + } + + public function cooperation(): BelongsTo + { + return $this->belongsTo(Cooperation::class); + } + + public function partnerMedia(): BelongsTo + { + return $this->belongsTo(PartnerMedia::class); + } + + public function rejectionReasons(): MorphMany + { + return $this->morphMany(RejectionReason::class, 'rejectable'); + } +} diff --git a/app/Models/MediaTaskAssignment.php b/app/Models/MediaTaskAssignment.php new file mode 100644 index 0000000..3c4ad6a --- /dev/null +++ b/app/Models/MediaTaskAssignment.php @@ -0,0 +1,40 @@ + ApprovalStatus::class, + ]; + } + + public function taskAssignment(): BelongsTo + { + return $this->belongsTo(TaskAssignment::class); + } + + public function partnerMedia(): BelongsTo + { + return $this->belongsTo(PartnerMedia::class); + } + + public function rejectionReasons(): MorphMany + { + return $this->morphMany(RejectionReason::class, 'rejectable'); + } +} diff --git a/app/Models/PartnerMedia.php b/app/Models/PartnerMedia.php index 6c3b215..54a83f7 100644 --- a/app/Models/PartnerMedia.php +++ b/app/Models/PartnerMedia.php @@ -41,4 +41,33 @@ public function announcements(): BelongsToMany { return $this->belongsToMany(Announcement::class, 'announcement_media'); } + + public function cooperationProposals(): HasMany + { + return $this->hasMany(CooperationProposal::class); + } + + public function taskAssignments(): BelongsToMany + { + return $this->belongsToMany(TaskAssignment::class, 'task_assignment_media') + ->withPivot(['status']) + ->withTimestamps(); + } + + public function taskAssignmentMedias(): HasMany + { + return $this->hasMany(TaskAssignmentMedia::class); + } + + public function cooperations(): BelongsToMany + { + return $this->belongsToMany(Cooperation::class, 'cooperation_media') + ->withPivot(['status']) + ->withTimestamps(); + } + + public function cooperationMedias(): HasMany + { + return $this->hasMany(CooperationMedia::class); + } } diff --git a/app/Models/RejectionReason.php b/app/Models/RejectionReason.php new file mode 100644 index 0000000..899f228 --- /dev/null +++ b/app/Models/RejectionReason.php @@ -0,0 +1,19 @@ +morphTo(); + } +} diff --git a/app/Models/Report.php b/app/Models/Report.php new file mode 100644 index 0000000..44a1769 --- /dev/null +++ b/app/Models/Report.php @@ -0,0 +1,37 @@ + 'date', + 'status' => ApprovalStatus::class, + ]; + } + + public function taskAssignment(): BelongsTo + { + return $this->belongsTo(TaskAssignment::class); + } + + public function rejectionReasons(): MorphMany + { + return $this->morphMany(RejectionReason::class, 'rejectable'); + } +} diff --git a/app/Models/TaskAssignment.php b/app/Models/TaskAssignment.php new file mode 100644 index 0000000..6e3f7eb --- /dev/null +++ b/app/Models/TaskAssignment.php @@ -0,0 +1,52 @@ + 'date', + 'end_date' => 'date', + ]; + } + + public function cooperation(): BelongsTo + { + return $this->belongsTo(Cooperation::class); + } + + public function reports(): HasMany + { + return $this->hasMany(Report::class); + } + + public function rejectionReasons(): MorphMany + { + return $this->morphMany(RejectionReason::class, 'rejectable'); + } + + public function mediaTaskAssignments(): HasMany + { + return $this->hasMany(MediaTaskAssignment::class); + } + + public function partnerMedia(): BelongsToMany + { + return $this->belongsToMany(PartnerMedia::class, 'media_task_assignment') + ->withPivot(['status']) + ->withTimestamps(); + } +} diff --git a/database/migrations/2025_12_12_111212_create_cooperations_table.php b/database/migrations/2025_12_12_111212_create_cooperations_table.php new file mode 100644 index 0000000..4b0f59a --- /dev/null +++ b/database/migrations/2025_12_12_111212_create_cooperations_table.php @@ -0,0 +1,39 @@ +id(); + $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(); + $table->enum('status', [CooperationStatus::values()])->default(CooperationStatus::PENDING)->comment(CooperationStatus::comment()); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cooperations'); + } +}; diff --git a/database/migrations/2025_12_12_111307_create_cooperation_media_table.php b/database/migrations/2025_12_12_111307_create_cooperation_media_table.php new file mode 100644 index 0000000..86ec253 --- /dev/null +++ b/database/migrations/2025_12_12_111307_create_cooperation_media_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignIdFor(Cooperation::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(PartnerMedia::class)->constrained()->cascadeOnDelete(); + $table->enum('status', [ApprovalStatus::values()])->default(ApprovalStatus::PENDING)->comment(ApprovalStatus::comment()); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cooperation_media'); + } +}; diff --git a/database/migrations/2025_12_12_132245_create_cooperation_proposals_table.php b/database/migrations/2025_12_12_132245_create_cooperation_proposals_table.php new file mode 100644 index 0000000..ca3019d --- /dev/null +++ b/database/migrations/2025_12_12_132245_create_cooperation_proposals_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignIdFor(Cooperation::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(PartnerMedia::class)->constrained()->cascadeOnDelete(); + $table->text('description')->nullable(); + $table->string('e_catalog', 50)->nullable(); + $table->enum('status', [ApprovalStatus::values()])->default(ApprovalStatus::PENDING)->comment(ApprovalStatus::comment()); + $table->timestamp('submitted_at')->useCurrent(); + $table->timestamp('responded_at')->nullable(); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cooperation_proposals'); + } +}; diff --git a/database/migrations/2025_12_12_132453_create_task_assignments_table.php b/database/migrations/2025_12_12_132453_create_task_assignments_table.php new file mode 100644 index 0000000..c150d67 --- /dev/null +++ b/database/migrations/2025_12_12_132453_create_task_assignments_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignIdFor(Cooperation::class)->constrained()->cascadeOnDelete(); + $table->date('start_date'); + $table->date('end_date'); + $table->text('task_description'); + $table->unsignedInteger('report_amount'); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('task_assignments'); + } +}; diff --git a/database/migrations/2025_12_12_133823_create_media_task_assignment_table.php b/database/migrations/2025_12_12_133823_create_media_task_assignment_table.php new file mode 100644 index 0000000..b067bc6 --- /dev/null +++ b/database/migrations/2025_12_12_133823_create_media_task_assignment_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignIdFor(TaskAssignment::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(PartnerMedia::class)->constrained()->cascadeOnDelete(); + $table->enum('status', [ApprovalStatus::values()])->default(ApprovalStatus::PENDING)->comment(ApprovalStatus::comment()); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('media_task_assignment'); + } +}; diff --git a/database/migrations/2025_12_12_135847_create_reports_table.php b/database/migrations/2025_12_12_135847_create_reports_table.php new file mode 100644 index 0000000..c527c73 --- /dev/null +++ b/database/migrations/2025_12_12_135847_create_reports_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignIdFor(TaskAssignment::class)->constrained()->cascadeOnDelete(); + $table->string('title', 200); + $table->date('publication_date'); + $table->string('link', 50); + $table->text('description'); + $table->enum('status', [ApprovalStatus::values()])->default(ApprovalStatus::PENDING)->comment(ApprovalStatus::comment()); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('reports'); + } +}; diff --git a/database/migrations/2025_12_12_135855_create_rejection_reasons_table.php b/database/migrations/2025_12_12_135855_create_rejection_reasons_table.php new file mode 100644 index 0000000..27e74a4 --- /dev/null +++ b/database/migrations/2025_12_12_135855_create_rejection_reasons_table.php @@ -0,0 +1,30 @@ +id(); + $table->morphs('rejectable'); // Creates rejectable_type and rejectable_id + $table->text('reason'); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('rejection_reasons'); + } +};