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