Fix merge master to main

This commit is contained in:
Alfin Afif 2026-01-30 11:28:17 +07:00
commit 179ebee693
490 changed files with 179100 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[compose.yaml]
indent_size = 4

65
.env.example Normal file
View File

@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
.gitattributes vendored Normal file
View File

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.fleet
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
Homestead.json
Homestead.yaml
Thumbs.db
.kiro/

320
COOPERATION_MODULE.md Normal file
View File

@ -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
- `taskAssignment()`: HasOne - 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

View File

@ -48,6 +48,15 @@ ### Premium Partners
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
=======
- **[Vehikl](https://vehikl.com)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**
## Contributing

91
README_CLEAN_CODE.md Normal file
View File

@ -0,0 +1,91 @@
# Clean Code Guidelines for Filament Resources
This document outlines the coding standards and structure for Filament Resources in this project.
## 1. Class Properties
Properties within the Resource class should be ordered alphabetically.
**Example:**
```php
protected static ?string $model = User::class;
protected static ?string $navigationGroup = 'Master';
protected static string|BackedEnum|null $navigationIcon = Heroicon::UserGroup;
protected static ?string $navigationLabel = 'Pengguna';
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $slug = 'master/users';
```
## 2. Form Structure
Form components should be structured with method calls in the following order:
1. **Basic**: `make`, `label`, `placeholder`
2. **HTML Attributes**: `autocomplete`, `autofocus`, etc.
3. **Validation**: `required`, `maxLength`, `email`, `unique`, `regex`, etc.
4. **Filament/Logic**: `relationship`, `live`, `afterStateUpdated`, etc.
**Formatting:**
- Use a blank line between each component definition.
**Example:**
```php
TextInput::make('name')
->label('Nama')
->placeholder('John Doe')
->autocomplete(false)
->autofocus()
->required()
->maxLength(100),
TextInput::make('email')
->label('Alamat Surel')
->placeholder('email@example.com')
->autocomplete(false)
->required()
->email(),
```
## 3. Table Structure
### Columns
Column definitions should be structured in the following order:
1. **Basic**: `make`, `label`
2. **Search & Sort**: `searchable`, `sortable` (These should be present for most fields)
3. **State & Attributes**: `getStateUsing`, `updateStateUsing`, `badge`, `icon`
4. **Formatting**: `dateTime`, `money`, etc.
**Formatting:**
- Use a blank line between each column definition.
**Example:**
```php
TextColumn::make('name')
->label('Nama')
->searchable()
->sortable(),
ToggleColumn::make('is_active')
->label('Status')
->sortable()
->updateStateUsing(...),
```
### Table Method Order
The methods chained to the `$table` object should follow this order:
1. `columns([\ ... ])`
2. `filters([\ ... ])`
3. `recordActions([\ ... ])` (or `actions`)
4. `toolbarActions([\ ... ])` (or `headerActions`)
5. `emptyStateIcon(...)`
6. `emptyStateDescription(...)`
7. `defaultSort(...)`
8. `deferFilters(...)`
9. Other configuration methods.
## 4. General Formatting
- Ensure consistent indentation.
- Always use a blank line to separate distinct logical blocks or component definitions to improve readability.

View File

@ -0,0 +1,61 @@
<?php
namespace App\Console\Commands;
use App\Models\Theme;
use App\Services\NewsCrawlerService;
use Illuminate\Console\Command;
class CrawlNewsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:crawl-news {keyword?} {--limit=10} {--all-themes}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Crawl news from Google News based on keywords or themes';
/**
* Execute the console command.
*/
public function handle(NewsCrawlerService $service)
{
$limit = (int) $this->option('limit');
$allThemes = $this->option('all-themes');
$keyword = $this->argument('keyword');
if ($allThemes) {
$themes = Theme::active()->get();
if ($themes->isEmpty()) {
$this->error('No active themes found.');
return;
}
foreach ($themes as $theme) {
$this->info("Crawling for theme: {$theme->name}");
$count = $service->crawlFromGoogleNews($theme->name, $limit, [$theme->id]);
$this->info("Saved {$count} news items for theme: {$theme->name}");
}
return;
}
if (! $keyword) {
$this->error('Please provide a keyword or use --all-themes');
return;
}
$this->info("Crawling for keyword: {$keyword}");
$count = $service->crawlFromGoogleNews($keyword, $limit);
$this->info("Saved {$count} news items for keyword: {$keyword}");
}
}

View File

@ -0,0 +1,130 @@
<?php
namespace App\Console\Commands;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Models\Cooperation;
use App\Models\CooperationMedia;
use Illuminate\Console\Command;
class SimulateCooperationTime extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cooperation:simulate-time {id? : The ID of the cooperation}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Simulate time passing for a specific Cooperation by moving dates to the past (Local only)';
/**
* Execute the console command.
*/
public function handle()
{
if (! app()->isLocal()) {
$this->error('This command can only be run in local environment.');
return Command::FAILURE;
}
$cooperationId = $this->argument('id');
// Interactive Selection if ID not provided
if (! $cooperationId) {
$cooperations = Cooperation::whereIn('status', [
CooperationStatus::PENDING,
CooperationStatus::ASSIGNMENT,
])->get();
if ($cooperations->isEmpty()) {
$this->warn('No active cooperations (PENDING or ASSIGNMENT) found to simulate.');
return Command::SUCCESS;
}
$choices = [];
foreach ($cooperations as $c) {
// Handle enum value safely
$status = $c->status instanceof \UnitEnum ? $c->status->value : $c->status;
$choices[$c->id] = "[ID: {$c->id}] {$c->title} ({$status})";
}
$choice = $this->choice(
'Which cooperation do you want to fast-forward?',
$choices
);
// Retrieve the ID associated with the choice
$cooperationId = array_search($choice, $choices);
}
$cooperation = Cooperation::find($cooperationId);
if (! $cooperation) {
$this->error("Cooperation with ID {$cooperationId} not found.");
return Command::FAILURE;
}
$this->info("Simulating time passage for: {$cooperation->title}");
if ($cooperation->status === CooperationStatus::PENDING) {
// 1. Move dates to past
$cooperation->update([
'initial_submission_date' => now()->subDays(8),
'final_submission_date' => now()->subDay(),
]);
$this->info('Moved dates to the past (Final Submission: Yesterday).');
// 2. Reject unresponsive media
$rejectedCount = CooperationMedia::where('cooperation_id', $cooperation->id)
->pending()
->update(['status' => ApprovalStatus::REJECTED]);
if ($rejectedCount > 0) {
$this->info("Rejected {$rejectedCount} media that didn't respond.");
}
// 3. Update Status
$cooperation->update(['status' => CooperationStatus::ASSIGNMENT]);
$this->info('Status transitioned to ASSIGNMENT.');
} elseif ($cooperation->status === CooperationStatus::ASSIGNMENT) {
if ($cooperation->taskAssignment) {
// 1. Move task dates to past
$cooperation->taskAssignment->update([
'start_date' => now()->subDays(8),
'end_date' => now()->subDay(),
]);
$this->info('Moved task dates to the past (Task End: Yesterday).');
// 2. Reject unresponsive media (assignments)
$rejectedCount = CooperationMedia::where('cooperation_id', $cooperation->id)
->pending()
->update(['status' => ApprovalStatus::REJECTED]);
if ($rejectedCount > 0) {
$this->info("Rejected {$rejectedCount} media that didn't respond to assignment.");
}
// 3. Update Status
$cooperation->update(['status' => CooperationStatus::VERIFICATION]);
$this->info('Status transitioned to VERIFICATION.');
} else {
$this->error('This ASSIGNMENT cooperation has no Task Assignment yet.');
}
} else {
$status = $cooperation->status instanceof \UnitEnum ? $cooperation->status->value : $cooperation->status;
$this->warn("Cooperation status '{$status}' is not supported for simulation. Only PENDING and ASSIGNMENT are supported.");
}
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace App\Console\Commands;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Models\Cooperation;
use App\Models\CooperationMedia;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class UpdateCooperationStatusCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cooperation:update-status';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Manually trigger the update of cooperation statuses based on deadlines';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Updating cooperation statuses...');
$today = Carbon::today();
Log::info('Running UpdateCooperationStatusJob at '.now());
// 1. PENDING -> ASSIGNMENT (final_submission_date)
// Automatically reject media that haven't responded
$rejectedPendingMedia = CooperationMedia::whereHas('cooperation', function ($query) use ($today) {
$query->pending()->whereDate('final_submission_date', '<', $today);
})
->pending()
->update(['status' => ApprovalStatus::REJECTED]);
if ($rejectedPendingMedia > 0) {
Log::info("Updated {$rejectedPendingMedia} media in PENDING cooperations to REJECTED because they didn't respond before final_submission_date.");
}
$updatedPending = Cooperation::pending()
->whereDate('final_submission_date', '<', $today)
->update(['status' => CooperationStatus::ASSIGNMENT]);
if ($updatedPending > 0) {
Log::info("Updated {$updatedPending} cooperations from PENDING to ASSIGNMENT.");
}
// 2. ASSIGNMENT -> VERIFICATION (taskAssignment->end_date)
// Automatically reject media that haven't responded to the invitation
$rejectedMedia = CooperationMedia::whereHas('cooperation', function ($query) use ($today) {
$query->assignment()->whereHas('taskAssignment', function ($subQuery) use ($today) {
$subQuery->whereDate('end_date', '<', $today);
});
})
->pending()
->update(['status' => ApprovalStatus::REJECTED]);
if ($rejectedMedia > 0) {
Log::info("Updated {$rejectedMedia} media assignments to REJECTED because they didn't respond before the task deadline.");
}
$updatedAssignment = Cooperation::where('status', CooperationStatus::ASSIGNMENT)
->whereHas('taskAssignment', function ($query) use ($today) {
$query->whereDate('end_date', '<', $today);
})
->update(['status' => CooperationStatus::VERIFICATION]);
if ($updatedAssignment > 0) {
Log::info("Updated {$updatedAssignment} cooperations from ASSIGNMENT to VERIFICATION.");
}
$this->info('Cooperation statuses updated successfully.');
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum AnnouncementType: int implements HasColor, HasLabel
{
use WithComment, WithValue;
case PUBLIC = 1;
case COMPANY = 2;
public function getLabel(): ?string
{
return match ($this) {
self::PUBLIC => 'Publik',
self::COMPANY => 'Perusahaan',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::PUBLIC => 'info',
self::COMPANY => 'warning',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum ApprovalStatus: int implements HasColor, HasLabel
{
use WithComment, WithValue;
case PENDING = 1;
case ACCEPTED = 2;
case REJECTED = 3;
public function getLabel(): ?string
{
return match ($this) {
self::PENDING => '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();
}
}

37
app/Enums/Channel.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use Filament\Support\Contracts\HasLabel;
enum Channel: string implements HasLabel
{
use WithComment;
case WEBSITE = 'Website';
case TIKTOK = 'Tiktok';
case YOUTUBE = 'Youtube';
case FACEBOOK = 'Facebook';
case INSTAGRAM = 'Instagram';
case OTHER = 'Other';
public function getLabel(): ?string
{
return match ($this) {
self::WEBSITE => 'Website',
self::TIKTOK => 'Tiktok',
self::YOUTUBE => 'Youtube',
self::FACEBOOK => 'Facebook',
self::INSTAGRAM => 'Instagram',
self::OTHER => 'Other',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

34
app/Enums/ContentType.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasLabel;
enum ContentType: string implements HasLabel
{
use WithComment, WithValue;
case FOTO = 'Foto';
case TEXT = 'Teks';
case GRAPHIC = 'Grafis';
case VIDEO = 'Video';
public function getLabel(): ?string
{
return match ($this) {
self::FOTO => 'Foto',
self::TEXT => 'Teks',
self::GRAPHIC => 'Grafis',
self::VIDEO => 'Video',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum CooperationStatus: int implements HasColor, HasLabel
{
use WithComment, WithValue;
case PENDING = 1;
case ASSIGNMENT = 2;
case VERIFICATION = 3;
case PAYMENT = 4;
case COMPLETED = 5;
public function getLabel(): ?string
{
return match ($this) {
self::PENDING => '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();
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum DataChangeDecision: int implements HasColor, HasLabel
{
use WithComment, WithValue;
case APPROVED = 1;
case REJECTED = 2;
public function getLabel(): ?string
{
return match ($this) {
self::APPROVED => 'Disetujui',
self::REJECTED => 'Ditolak',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::APPROVED => 'success',
self::REJECTED => 'danger',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum DataChangeStatus: int implements HasColor, HasLabel
{
use WithComment, WithValue;
case PENDING = 1;
case APPROVED = 2;
case REJECTED = 3;
public function getLabel(): ?string
{
return match ($this) {
self::PENDING => 'Menunggu Verifikasi',
self::APPROVED => 'Disetujui',
self::REJECTED => 'Ditolak',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::PENDING => 'warning',
self::APPROVED => 'success',
self::REJECTED => 'danger',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum DecisionAdmin: int implements HasColor, HasLabel
{
use WithComment, WithValue;
case APPROVED = 1;
case NEED_REVISION = 2;
case REJECTED = 3;
public function getLabel(): ?string
{
return match ($this) {
self::APPROVED => 'Disetujui',
self::NEED_REVISION => 'Perlu Revisi',
self::REJECTED => 'Ditolak',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::APPROVED => 'success',
self::NEED_REVISION => 'info',
self::REJECTED => 'danger',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

39
app/Enums/IsActive.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum IsActive: int implements HasColor, HasLabel
{
use WithComment, WithValue;
case ACTIVE = 1;
case INACTIVE = 2;
public function getLabel(): ?string
{
return match ($this) {
self::ACTIVE => 'Aktif',
self::INACTIVE => 'Tidak Aktif',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::ACTIVE => 'success',
self::INACTIVE => 'danger',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Colors\Color;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum IssueSentiment: string implements HasColor, HasLabel
{
use WithComment, WithValue;
case POSITIVE = 'positive';
case NEGATIVE = 'negative';
case NEUTRAL = 'neutral';
case CRISIS = 'krisis';
public function getLabel(): ?string
{
return match ($this) {
self::POSITIVE => 'Positif',
self::NEGATIVE => 'Negatif',
self::NEUTRAL => 'Netral',
self::CRISIS => 'Krisis',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::POSITIVE => Color::Green,
self::NEGATIVE => Color::Red,
self::NEUTRAL => Color::Gray,
self::CRISIS => Color::Orange,
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasLabel;
enum MediaClassification: int implements HasLabel
{
use WithComment, WithValue;
case LOCAL = 1;
case REGIONAL = 2;
case NATIONAL = 3;
public function getLabel(): ?string
{
return match ($this) {
self::LOCAL => 'Lokal',
self::REGIONAL => 'Regional',
self::NATIONAL => 'Nasional',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

34
app/Enums/MediaType.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasLabel;
enum MediaType: int implements HasLabel
{
use WithComment, WithValue;
case ONLINE = 1;
case PRINT = 2;
case RADIO = 3;
case TELEVISION = 4;
public function getLabel(): ?string
{
return match ($this) {
self::ONLINE => 'Online',
self::PRINT => 'Cetak',
self::RADIO => 'Radio',
self::TELEVISION => 'Televisi',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

41
app/Enums/NewsStatus.php Normal file
View File

@ -0,0 +1,41 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasLabel;
enum NewsStatus: int implements HasLabel
{
use WithComment, WithValue;
case DRAFT = 1;
case PUBLISHED = 2;
case ARCHIVED = 3;
public function getLabel(): ?string
{
return match ($this) {
self::DRAFT => 'Draft',
self::PUBLISHED => 'Publish',
self::ARCHIVED => 'Arsip',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::DRAFT => 'warning',
self::PUBLISHED => 'success',
self::ARCHIVED => 'gray',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

25
app/Enums/RoleEnum.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
enum RoleEnum: string
{
use WithComment, WithValue;
case DEVELOPER = 'Developer';
case ADMINISTRATOR = 'Administrator';
case ADMIN_MONITORING = 'Admin Monitoring';
case ADMIN_KONTEN = 'Admin Konten';
case ADMIN_KEUANGAN = 'Admin Keuangan';
case PERUSAHAAN = 'Perusahaan';
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->value])
->toArray();
}
}

31
app/Enums/SocialMedia.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use Filament\Support\Contracts\HasLabel;
enum SocialMedia: string implements HasLabel
{
use WithComment;
case PPID_DISKOMINFO_PURWAKARTA = 'PPID Diskominfo Purwakarta';
case DISKOMINFO_PURWAKARTA = 'Diskominfo Purwakarta';
case PURWAKARTA_SABER_HOAKS = 'Purwakarta Saber Hoaks';
public function getLabel(): ?string
{
return match ($this) {
self::PPID_DISKOMINFO_PURWAKARTA => 'PPID Diskominfo Purwakarta',
self::DISKOMINFO_PURWAKARTA => 'Diskominfo Purwakarta',
self::PURWAKARTA_SABER_HOAKS => 'Purwakarta Saber Hoaks',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum VerificationStatus: int implements HasColor, HasLabel
{
use WithComment, WithValue;
case PENDING = 1;
case APPROVED = 2;
case NEED_REVISION = 3;
case REJECTED = 4;
public function getLabel(): ?string
{
return match ($this) {
self::PENDING => 'Menunggu Verifikasi',
self::APPROVED => 'Disetujui',
self::NEED_REVISION => 'Perlu Revisi',
self::REJECTED => 'Ditolak',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::PENDING => 'warning',
self::APPROVED => 'success',
self::NEED_REVISION => 'info',
self::REJECTED => 'danger',
};
}
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()])
->toArray();
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Filament\Actions;
use Filament\Actions\Action;
class BackAction extends Action
{
public static function getDefaultName(): ?string
{
return 'back';
}
protected function setUp(): void
{
parent::setUp();
$this->label('Kembali')
->color('secondary')
->outlined();
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Filament\Actions\Cheerful;
use App\Filament\Support\CheerfulNotification;
use Filament\Actions\CreateAction as ActionsCreateAction;
class CreateAction extends ActionsCreateAction
{
protected function setUp(): void
{
parent::setUp();
$this->successNotification(
CheerfulNotification::create()
);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Filament\Actions\Cheerful;
use App\Filament\Support\CheerfulNotification;
use Filament\Actions\DeleteAction as ActionsDeleteAction;
class DeleteAction extends ActionsDeleteAction
{
protected function setUp(): void
{
parent::setUp();
$this->successNotification(
CheerfulNotification::delete()
);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Filament\Actions\Cheerful;
use App\Filament\Support\CheerfulNotification;
use Filament\Actions\EditAction as ActionsEditAction;
class EditAction extends ActionsEditAction
{
protected function setUp(): void
{
parent::setUp();
$this->successNotification(
CheerfulNotification::update()
);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Filament\Actions\Cheerful;
use App\Filament\Support\CheerfulNotification;
use Filament\Actions\ForceDeleteAction as ActionsForceDeleteAction;
class ForceDeleteAction extends ActionsForceDeleteAction
{
protected function setUp(): void
{
parent::setUp();
$this->successNotification(
CheerfulNotification::forceDelete()
);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Filament\Actions\Cheerful;
use App\Filament\Support\CheerfulNotification;
use Filament\Actions\RestoreAction as ActionsRestoreAction;
class RestoreAction extends ActionsRestoreAction
{
protected function setUp(): void
{
parent::setUp();
$this->successNotification(
CheerfulNotification::restore()
);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Filament\Actions;
use App\Filament\Support\CheerfulNotification;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
class DefaultBulkActions
{
public static function make(string $entity): array
{
return [
DeleteBulkAction::make()
->modalHeading("Hapus {$entity} yang dipilih")
->successNotification(
CheerfulNotification::bulkDelete()
),
ForceDeleteBulkAction::make()
->modalHeading("Hapus {$entity} yang dipilih")
->successNotification(
CheerfulNotification::bulkForceDelete()
),
RestoreBulkAction::make()
->modalHeading("Pulihkan {$entity} yang dipilih")
->successNotification(
CheerfulNotification::bulkRestore()
),
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Filament\Columns;
use Filament\Tables\Columns\TextColumn;
class TimestampColumns
{
public static function make(): array
{
return [
TextColumn::make('created_at')
->label('Dibuat')
->sortable()
->toggleable(isToggledHiddenByDefault: true)
->dateTime('l, d F Y H:i:s')
->wrap(),
TextColumn::make('updated_at')
->label('Diperbarui')
->sortable()
->toggleable(isToggledHiddenByDefault: true)
->dateTime('l, d F Y H:i:s')
->wrap(),
TextColumn::make('deleted_at')
->label('Dihapus')
->sortable()
->toggleable(isToggledHiddenByDefault: true)
->dateTime('l, d F Y H:i:s')
->wrap(),
];
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Filament\Exports;
use App\Enums\Channel;
use App\Enums\SocialMedia;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use pxlrbt\FilamentExcel\Columns\Column;
use pxlrbt\FilamentExcel\Exports\ExcelExport;
class ContentRecapExcelExport extends ExcelExport implements WithEvents, WithStyles
{
public function setUp()
{
$this->withFilename(fn ($resource) => $resource::getModelLabel().'-'.date('Y-m-d'));
$this->withColumns([
Column::make('title')
->heading('Judul'),
Column::make('type')
->heading('Tipe')
->formatStateUsing(fn ($state) => $state?->getLabel()),
Column::make('channel')
->heading('Kanal')
->formatStateUsing(fn ($state) => Channel::tryFrom($state)?->getLabel() ?? $state),
Column::make('social_media')
->heading('Media Sosial')
->formatStateUsing(fn ($state) => SocialMedia::tryFrom($state)?->getLabel() ?? $state),
Column::make('classification.name')
->heading('Klasifikasi'),
Column::make('link')
->heading('Tautan'),
Column::make('posting_date')
->heading('Tgl Posting')
->formatStateUsing(fn ($state) => $state?->translatedFormat('l, d F Y')),
]);
}
public function headings(): array
{
return [
['Data Rekap Konten'],
$this->getHeadings(),
];
}
public function styles(Worksheet $sheet)
{
return [
1 => [
'font' => ['bold' => true, 'size' => 14],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
],
2 => [
'font' => ['bold' => true],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
],
];
}
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$lastColumn = 'G'; // A-G
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->getStyle("A2:{$lastColumn}".$sheet->getHighestRow())
->getAlignment()
->setVertical(Alignment::VERTICAL_TOP)
->setWrapText(true);
for ($i = 3; $i <= $sheet->getHighestRow(); $i++) {
$sheet->getRowDimension($i)->setRowHeight(-1);
}
foreach (range('A', $lastColumn) as $columnID) {
$sheet->getColumnDimension($columnID)->setAutoSize(true);
}
},
];
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace App\Filament\Exports;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use pxlrbt\FilamentExcel\Columns\Column;
use pxlrbt\FilamentExcel\Exports\ExcelExport;
class CooperationExcelExport extends ExcelExport implements WithEvents, WithStyles
{
public function setUp()
{
$this->withFilename(fn ($resource) => $resource::getModelLabel().'-'.date('Y-m-d'));
$this->withColumns([
Column::make('title')
->heading('Judul'),
Column::make('partnerMedia.name')
->heading('Media')
->getStateUsing(fn ($record) => $record->partnerMedia->pluck('name')->join(', ')),
Column::make('initial_submission_date')
->heading('Tgl Pengajuan Awal')
->formatStateUsing(fn ($state) => $state?->translatedFormat('l, d F Y')),
Column::make('final_submission_date')
->heading('Tgl Pengajuan Akhir')
->formatStateUsing(fn ($state) => $state?->translatedFormat('l, d F Y')),
Column::make('status')
->heading('Status')
->formatStateUsing(fn ($state) => $state?->getLabel()),
Column::make('payment_amount')
->heading('Jumlah Pembayaran')
->formatStateUsing(fn ($state) => 'Rp '.number_format($state, 0, ',', '.')),
Column::make('payment_date')
->heading('Tanggal Pembayaran')
->formatStateUsing(fn ($state) => $state?->translatedFormat('l, d F Y')),
Column::make('description')
->heading('Deskripsi'),
]);
}
public function headings(): array
{
return [
['Data Kerja Sama'],
$this->getHeadings(),
];
}
public function styles(Worksheet $sheet)
{
return [
1 => [
'font' => ['bold' => true, 'size' => 14],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
],
2 => [
'font' => ['bold' => true],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
],
];
}
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$lastColumn = 'H'; // A-H
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->getStyle("A2:{$lastColumn}".$sheet->getHighestRow())
->getAlignment()
->setVertical(Alignment::VERTICAL_TOP)
->setWrapText(true);
for ($i = 3; $i <= $sheet->getHighestRow(); $i++) {
$sheet->getRowDimension($i)->setRowHeight(-1);
}
foreach (range('A', $lastColumn) as $columnID) {
$sheet->getColumnDimension($columnID)->setAutoSize(true);
}
},
];
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace App\Filament\Exports;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use pxlrbt\FilamentExcel\Columns\Column;
use pxlrbt\FilamentExcel\Exports\ExcelExport;
class IssueManagementExcelExport extends ExcelExport implements WithEvents, WithStyles
{
public function setUp()
{
$this->withFilename(fn ($resource) => $resource::getModelLabel().'-'.date('Y-m-d'));
$this->withColumns([
Column::make('mediaMonitoring.title')
->heading('Judul Berita'),
Column::make('issue')
->heading('Isu')
->formatStateUsing(fn ($state) => $state?->getLabel()),
Column::make('response')
->heading('Respon')
->formatStateUsing(fn ($state) => $state?->getLabel()),
Column::make('location.name')
->heading('Lokus'),
Column::make('subLocation.name')
->heading('Sub Lokus'),
Column::make('classification.name')
->heading('Klasifikasi'),
Column::make('subClassification.name')
->heading('Sub Klasifikasi'),
Column::make('description')
->heading('Keterangan'),
Column::make('mediaMonitoring.release_date')
->heading('Tgl Rilis')
->formatStateUsing(fn ($state) => $state?->translatedFormat('l, d F Y')),
]);
}
public function headings(): array
{
return [
['Data Manajemen Isu'],
$this->getHeadings(),
];
}
public function styles(Worksheet $sheet)
{
return [
1 => [
'font' => ['bold' => true, 'size' => 14],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
],
2 => [
'font' => ['bold' => true],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
],
];
}
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$lastColumn = 'I'; // A-I
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->getStyle("A2:{$lastColumn}".$sheet->getHighestRow())
->getAlignment()
->setVertical(Alignment::VERTICAL_TOP)
->setWrapText(true);
for ($i = 3; $i <= $sheet->getHighestRow(); $i++) {
$sheet->getRowDimension($i)->setRowHeight(-1);
}
foreach (range('A', $lastColumn) as $columnID) {
$sheet->getColumnDimension($columnID)->setAutoSize(true);
}
},
];
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace App\Filament\Exports;
use App\Enums\Channel;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use pxlrbt\FilamentExcel\Columns\Column;
use pxlrbt\FilamentExcel\Exports\ExcelExport;
class MediaMonitoringExcelExport extends ExcelExport implements WithEvents, WithStyles
{
public function setUp()
{
$this->withFilename(fn ($resource) => $resource::getModelLabel().'-'.date('Y-m-d'));
$this->withColumns([
Column::make('code')
->heading('Kode'),
Column::make('media_name')
->heading('Nama Media'),
Column::make('title')
->heading('Judul'),
Column::make('channel')
->heading('Kanal')
->formatStateUsing(fn ($state) => Channel::tryFrom($state)?->getLabel() ?? $state),
Column::make('writter')
->heading('Penulis'),
Column::make('link')
->heading('Tautan'),
Column::make('news_page')
->heading('Halaman Berita'),
Column::make('influencer')
->heading('Influencer'),
Column::make('keyword')
->heading('Kata Kunci'),
Column::make('themes')
->heading('Tema')
->getStateUsing(fn ($record) => $record->themes->pluck('name')->join(', ')),
Column::make('release_date')
->heading('Tgl Rilis'),
]);
}
public function headings(): array
{
return [
['Data Monitoring Media'],
$this->getHeadings(),
];
}
public function styles(Worksheet $sheet)
{
return [
1 => [
'font' => ['bold' => true, 'size' => 14],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
],
2 => [
'font' => ['bold' => true],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
],
];
}
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$lastColumn = 'K'; // Based on column count (A-K)
// Merge Title Row
// Merge Title Row
$sheet->mergeCells("A1:{$lastColumn}1");
// Alignment and Wrap Text
$sheet->getStyle("A2:{$lastColumn}".$sheet->getHighestRow())
->getAlignment()
->setVertical(Alignment::VERTICAL_TOP)
->setWrapText(true);
// Ensure data rows auto-expand to fit wrapped text ("jadi 2 row")
// We start from row 3 (after Title and Header)
for ($i = 3; $i <= $sheet->getHighestRow(); $i++) {
$sheet->getRowDimension($i)->setRowHeight(-1);
}
foreach (range('A', $lastColumn) as $columnID) {
$sheet->getColumnDimension($columnID)->setAutoSize(true);
}
},
];
}
}

View File

@ -0,0 +1,301 @@
<?php
namespace App\Filament\Pages;
use App\Enums\DecisionAdmin;
use App\Enums\VerificationStatus;
use App\Filament\Resources\Manage\Partners\Pages\ViewPartner;
use App\Filament\Support\CheerfulNotification;
use App\Models\User;
use App\Models\VerificationRequest;
use App\Notifications\BroadcastNotification;
use App\Services\VerificationService;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Pages\Page;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Log;
use UnitEnum;
class AdminVerification extends Page implements HasActions, HasForms
{
use HasPageShield, InteractsWithActions, InteractsWithForms;
public string $filterStatus = 'all';
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-user-group';
protected static ?string $navigationLabel = 'Verifikasi Rekanan';
protected static ?int $navigationSort = 6;
protected static ?string $slug = 'manage/admin-verification';
protected static ?string $title = 'Verifikasi Rekanan';
protected VerificationService $verificationService;
protected string $view = 'filament.pages.admin-verification';
public function boot(): void
{
$this->verificationService = app(VerificationService::class);
}
public static function getNavigationBadge(): ?string
{
return VerificationRequest::query()->pending()->count();
}
public function getVerifications(): LengthAwarePaginator
{
$query = VerificationRequest::with([
'company.user',
'company.partnerMedia',
'company.partnerMedia.journalists',
'submittedBy',
'reviews.reviewer',
])
->latest('updated_at');
// Apply status filter
if ($this->filterStatus !== 'all') {
$query->where('status', $this->filterStatus);
}
return $query->paginate(12);
}
public function setFilter(string $status): void
{
$this->filterStatus = $status;
}
public function getStatusCounts(): array
{
return [
'all' => VerificationRequest::count(),
'pending' => VerificationRequest::pending()->count(),
'revision' => VerificationRequest::needRevision()->count(),
'approved' => VerificationRequest::approved()->count(),
'rejected' => VerificationRequest::rejected()->count(),
];
}
/**
* Get company name from verification request
*/
public function getCompanyName(VerificationRequest $request): string
{
return $this->verificationService->getCompanyName($request);
}
/**
* Get detail URL for the company
*/
public function getDetailUrl(VerificationRequest $request): ?string
{
if (! $request->company || ! $request->company->user_id) {
return null;
}
return ViewPartner::getUrl(['record' => $request->company->user_id]);
}
/**
* Get verification summary data
*/
public function getVerificationSummary(VerificationRequest $request): array
{
return $this->verificationService->getVerificationSummary($request);
}
/**
* Get card data for verification request
*/
public function getCardData(VerificationRequest $verification): array
{
$latestReview = $verification->reviews->first();
$summary = $this->getVerificationSummary($verification);
$status = $verification->status;
return [
'companyName' => $this->getCompanyName($verification),
'detailUrl' => $this->getDetailUrl($verification),
'status' => $status,
'statusLabel' => $status->getLabel(),
'statusColor' => $status->getColor(),
'summary' => $summary,
'latestReview' => $latestReview,
'latestReviewNote' => $latestReview?->note,
'latestReviewDecision' => $latestReview?->decision,
'latestReviewDecisionLabel' => $latestReview?->decision?->getLabel(),
'latestReviewDecisionColor' => $latestReview?->decision?->getColor(),
'latestReviewReviewer' => $latestReview?->reviewer?->name ?? 'Admin',
'latestReviewCreatedAt' => $latestReview?->created_at,
'submittedBy' => $verification->submittedBy?->name ?? 'Unknown',
'reviewsCount' => $verification->reviews->count(),
'canApprove' => $status === VerificationStatus::PENDING,
'isPending' => $status === VerificationStatus::PENDING,
'isRevision' => $status === VerificationStatus::NEED_REVISION,
'isApproved' => $status === VerificationStatus::APPROVED,
'isRejected' => $status === VerificationStatus::REJECTED,
];
}
/**
* Approve verification action
*/
public function approveAction(): Action
{
return Action::make('approve')
->label('Setujui')
->color('success')
->icon('heroicon-o-check')
->size('sm')
->requiresConfirmation()
->modalHeading('Setujui Verifikasi')
->modalDescription('Apakah Anda yakin ingin menyetujui verifikasi ini? Pastikan data sudah benar dan lengkap.')
->modalSubmitActionLabel('Ya, Setujui')
->action(function (array $arguments, array $data): void {
$this->processReview($arguments['id'], DecisionAdmin::APPROVED, $data['note'] ?? null);
});
}
/**
* Revision verification action
*/
public function revisionAction(): Action
{
return Action::make('revision')
->label('Minta Revisi')
->color('warning')
->icon('heroicon-o-pencil-square')
->size('sm')
->modalHeading('Minta Revisi')
->modalDescription('Pengguna akan diminta untuk memperbaiki data sesuai catatan yang diberikan.')
->schema([
Textarea::make('note')
->label('Catatan Revisi')
->required()
->placeholder('Jelaskan bagian yang perlu diperbaiki (Perusahaan, Media, atau Jurnalis)...')
->autocomplete(false)
->autofocus(),
])
->action(function (array $arguments, array $data): void {
$this->processReview($arguments['id'], DecisionAdmin::NEED_REVISION, $data['note']);
});
}
/**
* Reject verification action
*/
public function rejectAction(): Action
{
return Action::make('reject')
->label('Tolak')
->color('danger')
->icon('heroicon-o-x-mark')
->size('sm')
->requiresConfirmation()
->modalHeading('Tolak Verifikasi')
->modalDescription('Pengajuan akan ditolak secara permanen. Pengguna tidak dapat mengajukan ulang untuk data ini.')
->modalSubmitActionLabel('Ya, Tolak')
->schema([
Textarea::make('note')
->label('Alasan Penolakan')
->required()
->placeholder('Jelaskan alasan pengajuan ditolak...')
->autocomplete(false)
->autofocus(),
])
->action(function (array $arguments, array $data): void {
$this->processReview($arguments['id'], DecisionAdmin::REJECTED, $data['note']);
});
}
/**
* Process review decision
*/
protected function processReview(int $requestId, DecisionAdmin $decision, ?string $note): void
{
$verificationRequest = VerificationRequest::with(['company.partnerMedia.journalists'])->find($requestId);
if (! $verificationRequest) {
CheerfulNotification::danger(
'Belum ketemu nih 🙌',
'Data verifikasi belum kami temukan. Coba sebentar lagi, ya.',
)
->send();
return;
}
try {
$this->verificationService->processReview($verificationRequest, $decision, $note);
$actionLabel = match ($decision) {
DecisionAdmin::APPROVED => 'disetujui',
DecisionAdmin::NEED_REVISION => 'diminta revisi',
DecisionAdmin::REJECTED => 'ditolak',
};
CheerfulNotification::success(
'Berhasil Diproses! ✨',
"Mantap! Verifikasi telah {$actionLabel} dan notifikasi sudah dikirim ke pengguna. Kerja bagus, Admin! 💪"
)
->send();
// Notify User
$partnerUser = $verificationRequest->company?->user;
if ($partnerUser) {
$statusTitle = match ($decision) {
DecisionAdmin::APPROVED => 'Horee! Verifikasi Disetujui 🎉',
DecisionAdmin::NEED_REVISION => 'Yah, Ada Yang Perlu Direvisi nih 📝',
DecisionAdmin::REJECTED => 'Maaf, Verifikasi Ditolak 😔',
};
$body = match ($decision) {
DecisionAdmin::APPROVED => 'Yeay! Pengajuan verifikasi perusahaan Anda telah disetujui. Sekarang Anda sudah resmi terverifikasi dan bekerja sama dengan kami! 🎊',
DecisionAdmin::NEED_REVISION => 'Ayo sedikit lagi! Admin meminta beberapa perbaikan pada pengajuan Anda agar bisa segera disetujui. 💪',
DecisionAdmin::REJECTED => 'Yah, maaf banget... pengajuan verifikasi perusahaan Anda belum bisa kami terima saat ini. 💔',
};
if ($note) {
$body .= "\n\nCatatan Admin: {$note}";
}
$partnerUser->notify(new BroadcastNotification([
'title' => $statusTitle,
'body' => $body,
'action' => [
Action::make('view')
->label('Lihat')
->url(Verification::getUrl()),
],
]));
}
} catch (\Throwable $e) {
Log::error('Verification processing failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'request_id' => $requestId,
'decision' => $decision->value,
]);
CheerfulNotification::danger(
'Belum ketemu nih 🙌',
'Data verifikasi belum kami temukan atau terjadi kesalahan sistem. Coba sebentar lagi, ya.',
)
->send();
}
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Filament\Pages\Auth;
use App\Filament\Support\CheerfulNotification;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Filament\Actions\Action;
use Filament\Auth\Pages\EmailVerification\EmailVerificationPrompt;
use Filament\Notifications\Notification;
class EmailVerification extends EmailVerificationPrompt
{
public function resendNotificationAction(): Action
{
return Action::make('resendNotification')
->link()
->label(__('filament-panels::auth/pages/email-verification/email-verification-prompt.actions.resend_notification.label').'.')
->size('sm')
->action(function (): void {
try {
$this->rateLimit(2);
} catch (TooManyRequestsException $exception) {
$this->getRateLimitedNotification($exception)?->send();
return;
}
$this->sendEmailVerificationNotification($this->getVerifiable());
CheerfulNotification::success(
'Mail Meluncur! 📧🚀',
'Tautan verifikasi baru sudah dikirim ke alamat surelmu. Cek inbox (atau spam) ya! 😉✨'
)->send();
});
}
protected function getRateLimitedNotification(TooManyRequestsException $exception): ?Notification
{
return CheerfulNotification::danger(
'Pelan-pelan ya ⏳',
'Permintaan terlalu sering. Tunggu sebentar lalu coba lagi dalam '.$exception->secondsUntilAvailable.' detik.'
)->send();
}
}

View File

@ -0,0 +1,151 @@
<?php
namespace App\Filament\Pages\Auth;
use App\Filament\Support\CheerfulNotification;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
use Filament\Auth\MultiFactor\Contracts\HasBeforeChallengeHook;
use Filament\Auth\Pages\Login as FilamentLogin;
use Filament\Facades\Filament;
use Filament\Forms\Components\TextInput;
use Filament\Models\Contracts\FilamentUser;
use Filament\Schemas\Schema;
use Illuminate\Auth\SessionGuard;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use Illuminate\Validation\ValidationException;
use SensitiveParameter;
class Login extends FilamentLogin
{
public function getHeading(): string|Htmlable
{
return '';
}
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('login')
->label('Nama Pengguna atau Alamat Surel')
->placeholder('johndoe@example.com')
->required()
->autocomplete(false)
->autofocus(),
TextInput::make('password')
->label('Kata Sandi')
->placeholder('********')
->password()
->revealable(filament()->arePasswordsRevealable())
->required()
->autocomplete(false)
->extraInputAttributes(['tabindex' => 2])
->hint(
filament()->hasPasswordReset()
? new HtmlString(Blade::render('<x-filament::link :href="filament()->getRequestPasswordResetUrl()" tabindex="3">{{ __(\'filament-panels::auth/pages/login.actions.request_password_reset.label\') }}</x-filament::link>'))
: null
),
]);
}
protected function getCredentialsFromFormData(#[SensitiveParameter] array $data): array
{
$loginType = filter_var($data['login'], FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
return [
$loginType => $data['login'],
'password' => $data['password'],
];
}
public function authenticate(): ?LoginResponse
{
try {
$this->rateLimit(5);
} catch (TooManyRequestsException $exception) {
$this->getRateLimitedNotification($exception)?->send();
return null;
}
$data = $this->form->getState();
/** @var SessionGuard $authGuard */
$authGuard = Filament::auth();
$authProvider = $authGuard->getProvider();
/** @phpstan-ignore-line */
$credentials = $this->getCredentialsFromFormData($data);
$user = $authProvider->retrieveByCredentials($credentials);
if ((! $user) || (! $authProvider->validateCredentials($user, $credentials))) {
$this->userUndertakingMultiFactorAuthentication = null;
$this->fireFailedEvent($authGuard, $user, $credentials);
$this->throwFailureValidationException();
}
if (
filled($this->userUndertakingMultiFactorAuthentication) &&
(decrypt($this->userUndertakingMultiFactorAuthentication) === $user->getAuthIdentifier())
) {
$this->multiFactorChallengeForm->validate();
} else {
foreach (Filament::getMultiFactorAuthenticationProviders() as $multiFactorAuthenticationProvider) {
if (! $multiFactorAuthenticationProvider->isEnabled($user)) {
continue;
}
$this->userUndertakingMultiFactorAuthentication = encrypt($user->getAuthIdentifier());
if ($multiFactorAuthenticationProvider instanceof HasBeforeChallengeHook) {
$multiFactorAuthenticationProvider->beforeChallenge($user);
}
break;
}
if (filled($this->userUndertakingMultiFactorAuthentication)) {
$this->multiFactorChallengeForm->fill();
return null;
}
}
if (! $authGuard->attemptWhen($credentials, function (Authenticatable $user): bool {
if (! ($user instanceof FilamentUser)) {
return true;
}
return $user->canAccessPanel(Filament::getCurrentOrDefaultPanel());
}, $data['remember'] ?? false)) {
$this->fireFailedEvent($authGuard, $user, $credentials);
$this->throwFailureValidationException();
}
session()->regenerate();
CheerfulNotification::success(
'Hore! Berhasil Masuk 🎉',
'Selamat datang kembali, '.$user->name.'! Siap untuk beraksi hari ini? 🚀✨'
)->send();
return app(LoginResponse::class);
}
protected function throwFailureValidationException(): never
{
CheerfulNotification::danger(
'Ups! Gagal Masuk 😅',
'Sepertinya ada yang salah nih. Coba cek email atau kata sandi mu lagi ya! 🤔🔐'
)->send();
throw ValidationException::withMessages([]);
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Filament\Pages\Auth;
use App\Filament\Resources\Manage\Partners\PartnerResource;
use App\Filament\Support\CheerfulNotification;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Auth\Pages\Register;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class Registration extends Register
{
public function getHeading(): string|Htmlable
{
return '';
}
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label('Nama')
->placeholder('John Doe')
->autocomplete(false)
->autofocus()
->required()
->maxLength(100),
TextInput::make('email')
->label('Alamat Surel')
->placeholder('johndoe@example.com')
->autocomplete(false)
->required()
->maxLength(254)
->unique('users', 'email')
->email()
->aboveContent('Pastikan alamat surel aktif, karena akan digunakan untuk verifikasi akun Anda.'),
TextInput::make('username')
->label('Nama Pengguna')
->placeholder('johndoe')
->autocomplete(false)
->required()
->minLength(5)
->maxLength(10)
->regex('/^[a-zA-Z0-9_.-]+$/')
->unique('users', 'username'),
TextInput::make('password')
->label('Kata Sandi')
->password()
->revealable(filament()->arePasswordsRevealable())
->required()
->rule(Password::default())
->autocomplete(false)
->dehydrated(fn ($state): bool => filled($state))
->dehydrateStateUsing(fn ($state): string => Hash::make($state))
->placeholder('********'),
]);
}
protected function handleRegistration(array $data): Model
{
$user = $this->getUserModel()::create($data);
$user->assignRole('Perusahaan');
CheerfulNotification::success(
'Selamat Datang! 👋🎉',
"Halooo, {$user->name}!, terima kasih sudah bergabung! Akunmu berhasil dibuat. Silakan verifikasi alamat surelmu! 🚀✨"
)
->send();
$this->getUserModel()::superAdmin()
->get()
->each(function ($admin) use ($user): void {
$admin->notify(new BroadcastNotification([
'title' => 'Warga Baru Nih! 👋🎉',
'body' => "Halo Admin! {$user->name} baru saja mendaftar. Yuk cek kelengkapannya! 🚀",
'action' => [
Action::make('view')
->label('Lihat')
->url(PartnerResource::getUrl('view', ['record' => $user->id])),
],
]));
});
return $user;
}
}

View File

@ -0,0 +1,481 @@
<?php
namespace App\Filament\Pages;
use App\Enums\DataChangeStatus;
use App\Enums\VerificationStatus;
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\Company as CompanyModel;
use App\Models\DataChangeRequest;
use App\Models\User;
use App\Models\VerificationRequest;
use App\Notifications\BroadcastNotification;
use Asmit\FilamentUpload\Forms\Components\AdvancedFileUpload;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Contracts\HasForms;
use Filament\Pages\Page;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use UnitEnum;
class Company extends Page implements HasForms
{
use HasPageShield;
public ?CompanyModel $company = null;
public array $data = [];
public array $originalDocuments = [];
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::BuildingOffice2;
protected static ?string $navigationLabel = 'Perusahaan';
protected static ?int $navigationSort = 2;
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $slug = 'manage/company';
protected static ?string $title = 'Perusahaan';
public ?VerificationRequest $verificationRequest = null;
protected string $view = 'filament.pages.company';
public function mount(): void
{
$company = auth()->user()->company;
if (! $company) {
return;
}
$this->verificationRequest = $company->verificationRequest()
->latest()
->first();
$this->company = $company;
$this->form->fill($company->toArray());
$documents = [
'director_nik',
'deed_incorporation',
'trade_license',
'tax_id_number',
'taxable_enterprise',
'annual_tax_return',
'domicile_certificate',
'profile',
];
$mediaItems = $company->getMedia('companies');
foreach ($documents as $docType) {
$media = $mediaItems
->where('custom_properties.doc_type', str($docType)->replace('_docs', '')->slug('-'))
->sortByDesc('created_at')
->first();
if ($media) {
$path = $media->getPathRelativeToRoot();
$this->data["{$docType}_docs"] = [$path];
$this->originalDocuments["{$docType}_docs"] = [$path];
}
}
}
public static function form(Schema $schema): Schema
{
$documents = [
[
'title' => 'Akta Pendirian',
'text_name' => 'deed_incorporation',
'placeholder' => '*****',
'max' => 100,
'max_size' => 1024 * 10,
'file_name' => 'deed_incorporation_docs',
'accept' => ['application/pdf'],
'folder' => 'companies/deed-incorporation/',
],
[
'title' => 'SIUP / NIB',
'text_name' => 'trade_license',
'placeholder' => '*****',
'max' => 100,
'max_size' => 1024 * 10,
'file_name' => 'trade_license_docs',
'accept' => ['application/pdf'],
'folder' => 'companies/trade-license/',
],
[
'title' => 'NPWP',
'text_name' => 'tax_id_number',
'placeholder' => '*****',
'max' => 100,
'max_size' => 1024 * 10,
'file_name' => 'tax_id_number_docs',
'accept' => ['application/pdf'],
'folder' => 'companies/tax-id-number/',
],
[
'title' => 'PKP',
'text_name' => 'taxable_enterprise',
'placeholder' => '*****',
'max' => 100,
'max_size' => 1024 * 10,
'file_name' => 'taxable_enterprise_docs',
'accept' => ['application/pdf'],
'folder' => 'companies/taxable-enterprise/',
],
[
'title' => 'SPT Tahunan',
'text_name' => 'annual_tax_return',
'placeholder' => '*****',
'max' => 100,
'max_size' => 1024 * 10,
'file_name' => 'annual_tax_return_docs',
'accept' => ['application/pdf'],
'folder' => 'companies/annual-tax-return/',
],
[
'title' => 'Suket Domisili',
'text_name' => 'domicile_certificate',
'placeholder' => '*****',
'max' => 100,
'max_size' => 1024 * 10,
'file_name' => 'domicile_certificate_docs',
'accept' => ['application/pdf'],
'folder' => 'companies/domicile-certificate/',
],
[
'title' => 'Profil Perusahaan',
'text_name' => 'profile',
'placeholder' => '*****',
'max' => 100,
'max_size' => 1024 * 10,
'file_name' => 'profile_docs',
'accept' => ['application/pdf'],
'folder' => 'companies/profile/',
],
];
return $schema
->components([
Section::make('Informasi')
->schema([
TextInput::make('name')
->label('Nama')
->placeholder('PT ABC Indonesia Abadi')
->autocomplete(false)
->autofocus()
->required()
->maxLength(100),
TextInput::make('email')
->label('Alamat Surel')
->placeholder('johndoe@example.com')
->autocomplete(false)
->required()
->maxLength(254),
Textarea::make('address')
->label('Alamat')
->placeholder('Jl. Kebontoro, Jakarta Pusat')
->autocomplete(false)
->required()
->columnSpanFull(),
TextInput::make('director_name')
->label('Nama Direktur')
->placeholder('John Doe')
->autocomplete(false)
->required()
->maxLength(100)
->columnSpanFull(),
])
->columns(2)
->columnSpan(2),
Group::make()
->schema(
array_merge(
[
Section::make('NIK Direktur')
->schema([
TextInput::make('director_nik')
->hiddenLabel()
->placeholder('32***')
->autocomplete(false)
->required()
->maxLength(16)
->rule('digits:16')
->inputMode('numeric')
->regex('/^[0-9]+$/'),
AdvancedFileUpload::make('director_nik_docs')
->hiddenLabel()
->disk(config('filesystems.default'))
->acceptedFileTypes(['image/*'])
->maxSize(1024 * 3)
->directory('companies/director-nik/'.now()->toDateString())
->required(),
]),
],
collect($documents)
->map(function (array $doc): Section {
return Section::make($doc['title'])
->schema([
TextInput::make($doc['text_name'])
->hiddenLabel()
->placeholder($doc['placeholder'])
->autocomplete(false)
->required()
->maxLength($doc['max']),
AdvancedFileUpload::make($doc['file_name'])
->hiddenLabel()
->disk(config('filesystems.default'))
->acceptedFileTypes($doc['accept'])
->maxSize($doc['max_size'])
->directory($doc['folder'].now()->toDateString())
->required(),
]);
})->toArray()
)
)
->columns(2),
])
->statePath('data');
}
public function save(): void
{
$this->mountAction('save');
}
public function saveAction(): Action
{
$needsReview = $this->company?->status === VerificationStatus::APPROVED;
return Action::make('save')
->label('Simpan')
->modalHeading('Ajukan Perubahan')
->modal($needsReview)
->schema(
$needsReview
? [
Textarea::make('change_reason')
->label('Alasan')
->placeholder('Jelaskan alasan perubahan data ini...')
->autocomplete(false)
->autofocus(),
]
: []
)
->action(function (array $data = []) use ($needsReview) {
try {
$this->performSave($data, $needsReview);
} catch (ValidationException $e) {
$this->unmountAction();
throw $e;
}
})
->modalWidth(Width::Large);
}
public function performSave(array $data, bool $needsReview = false): void
{
if ($needsReview) {
$existingRequest = DataChangeRequest::where('entity_type', CompanyModel::class)
->where('entity_id', $this->company->id)
->where('status', DataChangeStatus::PENDING)
->exists();
if ($existingRequest) {
CheerfulNotification::warning(
'Pengajuan Sudah Ada ⏳',
'Masih ada pengajuan perubahan data yang sedang menunggu verifikasi. Silakan tunggu hingga selesai diproses.'
)
->send();
return;
}
$state = $this->form->getState();
$data = array_merge($state, $data);
$fieldLabels = CompanyModel::dataChangeEntryLabels();
$editableFields = [
'name',
'email',
'address',
'director_name',
'director_nik',
'deed_incorporation',
'trade_license',
'tax_id_number',
'taxable_enterprise',
'annual_tax_return',
'domicile_certificate',
'profile',
];
$changedFields = [];
$oldData = [];
foreach ($editableFields as $field) {
if ($this->company->{$field} !== ($data[$field] ?? null)) {
$label = $fieldLabels[$field] ?? $field;
$changedFields[$label] = $data[$field] ?? null;
$oldData[$label] = $this->company->{$field};
}
}
$documents = [
'director_nik_docs',
'deed_incorporation_docs',
'trade_license_docs',
'tax_id_number_docs',
'taxable_enterprise_docs',
'annual_tax_return_docs',
'domicile_certificate_docs',
'profile_docs',
];
foreach ($documents as $field) {
$newValue = $this->data[$field] ?? [];
$oldValue = $this->originalDocuments[$field] ?? [];
// Compare as arrays since file uploads are often arrays
if (json_encode($newValue) !== json_encode($oldValue)) {
$label = $fieldLabels[$field] ?? $field;
$changedFields[$label] = $newValue;
$oldData[$label] = $oldValue;
}
}
if (empty($changedFields)) {
CheerfulNotification::info(
'Belum ada yang berubah ✨',
'Ubah data terlebih dulu lalu simpan kembali 💪'
)
->send();
return;
}
$dataChangeRequest = DataChangeRequest::create([
'user_id' => auth()->id(),
'entity_type' => CompanyModel::class,
'entity_id' => $this->company->id,
'old_data' => $oldData,
'new_data' => $changedFields,
'change_reason' => $data['change_reason'],
]);
CheerfulNotification::success(
'Permohonan Berhasil 🎉',
'Data sudah dikirim dan sedang menunggu verifikasi admin ⏳'
)
->send();
User::superAdmin()
->get()
->each(function ($admin) use ($dataChangeRequest): void {
$admin->notify(new BroadcastNotification([
'title' => 'Ada Pengajuan Perubahan Data ✨',
'body' => 'Halooo Admin! 👋 '.auth()->user()->name.' baru saja mengajukan perubahan data. Yuk, cek detailnya dan lakukan verifikasi ya! 🚀',
'action' => [
Action::make('view')
->label('Lihat')
->url(DataChangesResource::getUrl('view', ['record' => $dataChangeRequest->id])),
],
]));
});
} else {
$data = $this->form->getState();
$company = CompanyModel::updateOrCreate([
'id' => $this->company?->id,
], [
'user_id' => auth()->id(),
'name' => $data['name'],
'email' => $data['email'],
'address' => $data['address'],
'director_name' => $data['director_name'],
'director_nik' => $data['director_nik'],
'deed_incorporation' => $data['deed_incorporation'],
'trade_license' => $data['trade_license'],
'tax_id_number' => $data['tax_id_number'],
'taxable_enterprise' => $data['taxable_enterprise'],
'annual_tax_return' => $data['annual_tax_return'],
'domicile_certificate' => $data['domicile_certificate'],
'profile' => $data['profile'],
]);
$this->company = $company;
$documents = [
'director_nik_docs',
'deed_incorporation_docs',
'trade_license_docs',
'tax_id_number_docs',
'taxable_enterprise_docs',
'annual_tax_return_docs',
'domicile_certificate_docs',
'profile_docs',
];
foreach ($documents as $field) {
if (empty($this->data[$field])) {
continue;
}
foreach ((array) $this->data[$field] as $filePath) {
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
if (! file_exists($fullPath)) {
continue;
}
$company
->addMediaFromDisk($filePath, config('filesystems.default'))
->preservingOriginal()
->withCustomProperties([
'feature' => 'companies',
'date' => now()->toDateString(),
'doc_type' => str($field)
->replace('_docs', '')
->slug('-'),
])
->toMediaCollection('companies');
}
}
if ($this->company) {
CheerfulNotification::update()->send();
} else {
CheerfulNotification::create()->send();
}
}
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Filament\Pages;
use App\Enums\RoleEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Concerns\HasFiltersForm;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
class Dashboard extends BaseDashboard
{
use HasFiltersForm, HasPageShield;
public function filtersForm(Schema $schema): Schema
{
return $schema
->schema([
Fieldset::make('Filter')
->schema([
DatePicker::make('startDate')
->label('Tanggal Mulai')
->placeholder('Pilih tanggal mulai')
->native(false)
->displayFormat('l, d F Y'),
DatePicker::make('endDate')
->label('Tanggal Selesai')
->placeholder('Pilih tanggal selesai')
->native(false)
->displayFormat('l, d F Y'),
])
->columns(2)
->columnSpanFull()
->visible(fn () => auth()->user()->hasRole(RoleEnum::DEVELOPER->value) || auth()->user()->hasRole(RoleEnum::ADMINISTRATOR->value)),
]);
}
protected function getHeaderActions(): array
{
return [
Action::make('reset')
->label('Reset Filter')
->icon(Heroicon::XMark)
->color('danger')
->action(fn () => $this->resetFilters())
->visible(fn () => ! empty($this->filters['startDate']) || ! empty($this->filters['endDate'])),
];
}
public function resetFilters(): void
{
$this->filters['startDate'] = null;
$this->filters['endDate'] = null;
if (method_exists($this, 'getFiltersSessionKey')) {
session()->forget($this->getFiltersSessionKey());
}
}
}

View File

@ -0,0 +1,416 @@
<?php
namespace App\Filament\Pages;
use App\Enums\DataChangeStatus;
use App\Enums\MediaClassification;
use App\Enums\MediaType;
use App\Enums\RoleEnum;
use App\Enums\VerificationStatus;
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\Company;
use App\Models\DataChangeRequest;
use App\Models\PartnerMedia;
use App\Models\User;
use App\Models\VerificationRequest;
use App\Notifications\BroadcastNotification;
use Asmit\FilamentUpload\Forms\Components\AdvancedFileUpload;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Actions\Action;
use Filament\Forms\Components\Radio;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Contracts\HasForms;
use Filament\Pages\Page;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Contracts\HasLabel;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\Storage;
use UnitEnum;
class Media extends Page implements HasForms
{
use HasPageShield;
public ?Company $company = null;
public array $data = [];
public array $originalDocuments = [];
public ?PartnerMedia $media = null;
protected static ?string $model = PartnerMedia::class;
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::Newspaper;
protected static ?string $navigationLabel = 'Media';
protected static ?int $navigationSort = 3;
protected static ?string $recordTitleAttribute = 'name';
public bool $restricted = false;
protected static ?string $slug = 'manage/media';
protected static ?string $title = 'Media';
public ?VerificationRequest $verificationRequest = null;
protected string $view = 'filament.pages.media';
public function mount(): void
{
if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) && ! auth()->user()->company) {
$this->restricted = true;
return;
}
$company = auth()->user()->company;
if (! $company) {
$this->restricted = true;
return;
}
$this->company = $company;
$verificationRequest = $this->company->verificationRequest()
->latest()
->first();
$this->verificationRequest = $verificationRequest;
$media = $company->partnerMedia;
if (! $media) {
return;
}
$this->media = $media;
$this->form->fill($media->toArray());
$documents = [
'journalism_organization',
'press_council_certificate',
];
$mediaItems = $media->getMedia('partnerMedia');
foreach ($documents as $docType) {
$mediaItem = $mediaItems
->where('custom_properties.doc_type', str($docType)->replace('_docs', '')->slug('-'))
->sortByDesc('created_at')
->first();
if ($mediaItem) {
$path = $mediaItem->getPathRelativeToRoot();
$this->data["{$docType}_docs"] = [$path];
$this->originalDocuments["{$docType}_docs"] = [$path];
}
}
}
public static function form(Schema $schema): Schema
{
$documents = [
[
'title' => 'Organisasi Kewartawanan',
'text_name' => 'journalism_organization',
'placeholder' => '*****',
'max' => 100,
'max_size' => 1024 * 10,
'file_name' => 'journalism_organization_docs',
'accept' => ['application/pdf'],
'folder' => 'media/journalism-organization/',
],
[
'title' => 'Sertifikat Dewan Pers',
'text_name' => 'press_council_certificate',
'placeholder' => '*****',
'max' => 100,
'max_size' => 1024 * 10,
'file_name' => 'press_council_certificate_docs',
'accept' => ['application/pdf'],
'folder' => 'media/press-council-certificate/',
],
];
return $schema
->components([
Section::make('Informasi')
->schema([
TextInput::make('name')
->label('Nama')
->placeholder('PWK News')
->autocomplete(false)
->autofocus()
->required()
->maxLength(100),
TextInput::make('link')
->label('Tautan')
->placeholder('https://www.pwknews.com')
->autocomplete(false)
->nullable()
->maxLength(50)
->url(),
Textarea::make('address')
->label('Alamat')
->placeholder('Jl. Kebontoro, Jakarta Pusat')
->autocomplete(false)
->required()
->columnSpanFull(),
Radio::make('type')
->label('Jenis')
->options(MediaType::options())
->inline()
->required()
->in(implode(',', array_column(MediaType::cases(), 'value'))),
Radio::make('classification')
->label('Klasifikasi')
->options(MediaClassification::options())
->inline()
->required()
->in(implode(',', array_column(MediaClassification::cases(), 'value'))),
])
->columns(2)
->columnSpan(2),
Group::make()
->schema(
collect($documents)
->map(function (array $doc): Section {
return Section::make($doc['title'])
->schema([
TextInput::make($doc['text_name'])
->hiddenLabel()
->placeholder($doc['placeholder'])
->autocomplete(false)
->required()
->maxLength($doc['max']),
AdvancedFileUpload::make($doc['file_name'])
->hiddenLabel()
->disk(config('filesystems.default'))
->acceptedFileTypes($doc['accept'])
->maxSize($doc['max_size'])
->directory($doc['folder'].now()->toDateString()),
]);
})->toArray()
)
->columns(2),
])
->statePath('data');
}
public function save(): void
{
$this->mountAction('save');
}
public function saveAction(): Action
{
$needsReview = $this->media?->status === VerificationStatus::APPROVED;
return Action::make('save')
->label('Simpan')
->modalHeading('Ajukan Perubahan')
->modal($needsReview)
->schema(
$needsReview
? [
Textarea::make('change_reason')
->label('Alasan')
->required()
->placeholder('Jelaskan alasan perubahan data ini...')
->autocomplete(false)
->autofocus(),
]
: []
)
->action(function (array $data = []) use ($needsReview) {
$this->performSave($data, $needsReview);
})
->modalWidth(Width::Large);
}
public function performSave(array $data = [], bool $needsReview = false): void
{
if ($needsReview && $this->media !== null) {
$existingRequest = DataChangeRequest::where('entity_type', PartnerMedia::class)
->where('entity_id', $this->media->id)
->where('status', DataChangeStatus::PENDING)
->exists();
if ($existingRequest) {
CheerfulNotification::warning(
'Pengajuan Sudah Ada ⏳',
'Masih ada pengajuan perubahan data yang sedang menunggu verifikasi. Silakan tunggu hingga selesai diproses.'
)
->send();
return;
}
$state = $this->form->getState();
$data = array_merge($state, $data);
$fieldLabels = PartnerMedia::dataChangeEntryLabels();
$editableFields = [
'name',
'link',
'address',
'type',
'classification',
'journalism_organization',
'press_council_certificate',
];
$changedFields = [];
$oldDataArr = [];
$casts = $this->media->getCasts();
foreach ($editableFields as $field) {
$val = $data[$field];
$oldVal = $this->media->{$field};
// If value is a raw scalar but should be an enum, resolve it for display
if (isset($casts[$field]) && ! ($val instanceof BackedEnum) && enum_exists($casts[$field])) {
$enumClass = $casts[$field];
$val = $enumClass::tryFrom($val) ?? $val;
}
// Use labels for Enums if they implement HasLabel
$displayVal = ($val instanceof HasLabel) ? $val->getLabel() : ($val instanceof BackedEnum ? $val->value : $val);
$displayOldVal = ($oldVal instanceof HasLabel) ? $oldVal->getLabel() : ($oldVal instanceof BackedEnum ? $oldVal->value : $oldVal);
if ($displayOldVal !== $displayVal) {
$label = $fieldLabels[$field] ?? $field;
$changedFields[$label] = $displayVal;
$oldDataArr[$label] = $displayOldVal;
}
}
$documents = [
'journalism_organization_docs',
'press_council_certificate_docs',
];
foreach ($documents as $field) {
$newValue = $this->data[$field] ?? [];
$oldValue = $this->originalDocuments[$field] ?? [];
if (json_encode($newValue) !== json_encode($oldValue)) {
$label = $fieldLabels[$field] ?? $field;
$changedFields[$label] = $newValue;
$oldDataArr[$label] = $oldValue;
}
}
if (empty($changedFields)) {
CheerfulNotification::info(
'Belum ada yang berubah ✨',
'Ubah data terlebih dulu lalu simpan kembali 💪'
)
->send();
return;
}
$dataChangeRequest = DataChangeRequest::create([
'user_id' => auth()->id(),
'entity_type' => PartnerMedia::class,
'entity_id' => $this->media->id,
'old_data' => $oldDataArr,
'new_data' => $changedFields,
'change_reason' => $data['change_reason'],
]);
CheerfulNotification::success(
'Permohonan Berhasil 🎉',
'Data sudah dikirim dan sedang menunggu verifikasi admin ⏳'
)
->send();
User::superAdmin()
->get()
->each(function ($admin) use ($dataChangeRequest): void {
$admin->notify(new BroadcastNotification([
'title' => 'Ada Pengajuan Perubahan Data Media ✨',
'body' => 'Halooo Admin! 👋 '.auth()->user()->name.' baru saja mengajukan perubahan data media. Yuk, cek detailnya! 🚀',
'action' => [
Action::make('view')
->label('Lihat')
->url(DataChangesResource::getUrl('view', ['record' => $dataChangeRequest->id])),
],
]));
});
} else {
$state = $this->form->getState();
$media = PartnerMedia::updateOrCreate([
'id' => $this->media?->id,
], [
'company_id' => $this->company->id,
'name' => $state['name'],
'link' => $state['link'],
'address' => $state['address'],
'type' => $state['type'],
'classification' => $state['classification'],
'journalism_organization' => $state['journalism_organization'],
'press_council_certificate' => $state['press_council_certificate'],
]);
$this->media = $media;
$documents = [
'journalism_organization_docs',
'press_council_certificate_docs',
];
foreach ($documents as $field) {
if (empty($this->data[$field])) {
continue;
}
foreach ((array) $this->data[$field] as $filePath) {
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
if (! file_exists($fullPath)) {
continue;
}
$media
->addMediaFromDisk($filePath, config('filesystems.default'))
->preservingOriginal()
->withCustomProperties([
'feature' => 'media',
'date' => now()->toDateString(),
'doc_type' => str($field)
->replace('_docs', '')
->slug('-'),
])
->toMediaCollection('partnerMedia');
}
}
CheerfulNotification::create()->send();
}
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace App\Filament\Pages\Settings;
use App\Settings\GeneralSettings;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Pages\SettingsPage;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use UnitEnum;
class ManageGeneralSettings extends SettingsPage
{
use HasPageShield;
protected static string|UnitEnum|null $navigationGroup = 'Pengaturan';
protected static string|BackedEnum|null $navigationIcon = Heroicon::Cog6Tooth;
protected static ?string $navigationLabel = 'Umum';
protected static ?int $navigationSort = 1;
protected static string $settings = GeneralSettings::class;
protected static ?string $title = 'Pengaturan Umum';
public function form(Schema $schema): Schema
{
return $schema
->components([
Section::make('Informasi')
->schema([
TextInput::make('site_name')
->label('Nama Website')
->placeholder('Simedkom')
->autofocus()
->autocomplete(false)
->required()
->maxLength(255),
Textarea::make('site_description')
->label('Deskripsi Website')
->placeholder('Aplikasi Sistem Media Komunikasi Kabupaten Purwakarta')
->autocomplete(false)
->required(),
Textarea::make('address')
->label('Alamat')
->placeholder('Jl. Laks. Laut RE. Martadinata No.47')
->autocomplete(false)
->rows(3)
->required()
->maxLength(255),
TextInput::make('site_email')
->label('Email Website')
->placeholder('diskominfo@purwakartakab.go.id')
->email()
->required()
->maxLength(255)
->autocomplete(false),
TextInput::make('site_phone')
->label('Nomor Telepon')
->placeholder('(0264) 200222')
->required()
->maxLength(255)
->autocomplete(false),
RichEditor::make('about_us')
->label('Tentang Kami')
->placeholder('...')
->required()
->columnSpanFull(),
])->columnSpan(2),
Section::make('Lampiran')
->schema([
FileUpload::make('site_logo')
->label('Logo Website')
->required()
->image()
->directory('settings'),
FileUpload::make('site_icon')
->label('Icon Website')
->required()
->image()
->directory('settings'),
])->columnSpan(1),
])
->columns(3);
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Filament\Pages\Settings;
use App\Settings\SeoSettings;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Pages\SettingsPage;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use UnitEnum;
class ManageSeoSettings extends SettingsPage
{
use HasPageShield;
protected static string|UnitEnum|null $navigationGroup = 'Pengaturan';
protected static string|BackedEnum|null $navigationIcon = Heroicon::MagnifyingGlass;
protected static ?string $navigationLabel = 'SEO';
protected static ?int $navigationSort = 3;
protected static string $settings = SeoSettings::class;
protected static ?string $title = 'Pengaturan SEO';
public function form(Schema $schema): Schema
{
return $schema
->components([
Section::make('Search Engine Optimization')
->description('Atur bagaimana website Anda muncul di mesin pencari.')
->schema([
TextInput::make('title')
->label('SEO Title')
->placeholder('simedkom')
->autofocus()
->autocomplete(false)
->helperText('Judul yang muncul di tab browser dan hasil pencarian.'),
Textarea::make('description')
->label('SEO Description')
->placeholder('...')
->autocomplete(false)
->helperText('Deskripsi singkat website Anda.'),
TextInput::make('keywords')
->label('SEO Keywords')
->placeholder('keyword1, keyword2, keyword3')
->autocomplete(false)
->helperText('Gunakan koma untuk memisahkan kata kunci.'),
])
->columnSpanFull(),
]);
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Filament\Pages\Settings;
use App\Settings\SocialMediaSettings;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Forms\Components\TextInput;
use Filament\Pages\SettingsPage;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use UnitEnum;
class ManageSocialMediaSettings extends SettingsPage
{
use HasPageShield;
protected static string|UnitEnum|null $navigationGroup = 'Pengaturan';
protected static string|BackedEnum|null $navigationIcon = Heroicon::Share;
protected static ?string $navigationLabel = 'Media Sosial';
protected static ?int $navigationSort = 2;
protected static string $settings = SocialMediaSettings::class;
protected static ?string $title = 'Pengaturan Media Sosial';
public function form(Schema $schema): Schema
{
return $schema
->components([
Section::make('Link Media Sosial')
->description('Atur link media sosial resmi Anda.')
->schema([
Grid::make(2)
->schema([
TextInput::make('facebook')
->label('Facebook')
->placeholder('https://facebook.com/username')
->autofocus()
->autocomplete(false)
->url(),
TextInput::make('twitter')
->label('Twitter')
->placeholder('https://twitter.com/username')
->autocomplete(false)
->url(),
TextInput::make('instagram')
->label('Instagram')
->placeholder('https://instagram.com/username')
->autocomplete(false)
->url(),
TextInput::make('youtube')
->label('YouTube')
->placeholder('https://youtube.com/username')
->autocomplete(false)
->url(),
]),
])
->columnSpanFull(),
]);
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace App\Filament\Pages;
use App\Enums\VerificationStatus;
use App\Filament\Resources\Manage\Partners\PartnerResource;
use App\Models\Company;
use App\Models\User;
use App\Models\VerificationRequest;
use App\Notifications\BroadcastNotification;
use App\Services\VerificationService;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Support\Icons\Heroicon;
use UnitEnum;
class Verification extends Page implements HasActions, HasForms
{
use HasPageShield, InteractsWithActions, InteractsWithForms;
public ?Company $company = null;
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::CheckBadge;
protected static ?string $navigationLabel = 'Verifikasi';
protected static ?int $navigationSort = 5;
protected static ?string $slug = 'manage/verification';
protected static ?string $title = 'Verifikasi';
public ?VerificationRequest $verificationRequest = null;
protected VerificationService $verificationService;
protected string $view = 'filament.pages.verification';
public function boot(): void
{
$this->verificationService = app(VerificationService::class);
}
public function mount(): void
{
$user = auth()->user();
$this->company = $user->company;
// Load unified verification request
if ($this->company) {
$this->verificationRequest = $this->company->verificationRequest()
->with(['reviews.reviewer'])
->latest()
->first();
}
}
/**
* Get unified verification progress
*/
public function getVerificationProgress(): array
{
$progress = $this->verificationService->getVerificationProgress(
$this->company,
$this->verificationRequest
);
// Add additional data for view
$progress['missingDataButtons'] = $this->verificationService->getMissingDataButtons($this->company);
$progress['alert'] = $this->verificationService->getAlertData($this->verificationRequest);
$progress['lastSubmittedAt'] = $this->verificationRequest?->updated_at?->translatedFormat('l, d M Y H:i');
return $progress;
}
/**
* Submit unified verification action
*/
public function submitVerificationAction(): Action
{
$progress = $this->getVerificationProgress();
return Action::make('submitVerification')
->label(fn (): string => $this->verificationRequest && $this->verificationRequest->status === VerificationStatus::NEED_REVISION
? 'Kirim Ulang'
: 'Ajukan Verifikasi')
->icon('heroicon-o-paper-airplane')
->color('primary')
->requiresConfirmation()
->modalHeading('Ajukan Verifikasi')
->modalDescription(function () use ($progress): string {
if (! $progress['isDataComplete']) {
return 'Data belum lengkap. Silakan lengkapi terlebih dahulu: '.implode(', ', $progress['missingData']);
}
return 'Apakah Anda yakin ingin mengajukan verifikasi? Pastikan semua data sudah benar dan lengkap.';
})
->modalSubmitActionLabel('Ya, Ajukan')
->visible(fn (): bool => $progress['canSubmit'])
->disabled(fn (): bool => ! $progress['isDataComplete'])
->action(function () use ($progress): void {
if (! $progress['isDataComplete']) {
Notification::make()
->title('Data Belum Lengkap')
->body('Silakan lengkapi data terlebih dahulu: '.implode(', ', $progress['missingData']))
->danger()
->send();
return;
}
$isRevision = $this->verificationRequest && $this->verificationRequest->status === VerificationStatus::NEED_REVISION;
$this->verificationRequest = $this->verificationService->submitVerification(
$this->company,
$this->verificationRequest
);
Notification::make()
->title('Berhasil Diajukan! 🚀')
->body('Yeay! Pengajuan verifikasi Anda sudah terkirim ke Admin. Mohon ditunggu ya, semoga hasilnya memuaskan! ✨')
->success()
->send();
User::superAdmin()
->get()
->each(function ($admin) use ($isRevision): void {
$user = auth()->user();
$admin->notify(new BroadcastNotification([
'title' => $isRevision ? 'Pembaruan Data Verifikasi ✨' : 'Ada Pengajuan Verifikasi Baru! 🚀',
'body' => $isRevision
? "Cihuy! {$user->name} baru saja memperbarui data verifikasi untuk perusahaan {$user->company->name}. Yuk, cek perubahannya!"
: "Halo Admin! {$user->name} baru saja mengajukan verifikasi data untuk perusahaan {$user->company->name}. Segera diproses ya! 😊",
'action' => [
Action::make('view')
->label('Lihat')
->url(PartnerResource::getUrl('view', ['record' => $user])),
],
]));
});
});
}
public function getViewData(): array
{
return [
'verificationProgress' => $this->getVerificationProgress(),
];
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Filament\Resources\Manage\Contacts\Actions;
use App\Filament\Support\CheerfulNotification;
use App\Mail\ContactReplyMail;
use App\Models\Contact;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\Mail;
class ReplyAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Balas')
->icon(Heroicon::ChatBubbleLeftRight)
->color('success')
->modalHeading('Balas Pesan Kontak')
->modalDescription('Pesan balasan tidak bisa diubah setelah dikirim.')
->schema([
Textarea::make('reply_message')
->label('Pesan Balasan')
->placeholder(fn (Contact $record): string => "Halo {$record->name}, ...")
->required()
->rows(5)
->autocomplete(false)
->autofocus(),
])
->action(function (Contact $record, array $data): void {
Mail::to($record->email)->send(new ContactReplyMail($record, $data['reply_message']));
$record->update([
'reply_message' => $data['reply_message'],
'replied_at' => now(),
'replied_by' => auth()->id(),
]);
CheerfulNotification::success(
'Balasan Terkirim! 🚀✉️',
'Pesan balasan Anda sudah meluncur ke email pengirim. Semoga membantu mereka ya! 😊'
)->send();
})
->visible(fn (Contact $record): bool => $record->replied_at === null);
}
}

View File

@ -0,0 +1,171 @@
<?php
namespace App\Filament\Resources\Manage\Contacts;
use App\Filament\Actions\Cheerful\DeleteAction;
use App\Filament\Actions\DefaultBulkActions;
use App\Filament\Resources\Manage\Contacts\Actions\ReplyAction;
use App\Filament\Resources\Manage\Contacts\Pages\ManageContacts;
use App\Models\Contact;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\ViewAction;
use Filament\Infolists\Components\TextEntry;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
use UnitEnum;
class ContactResource extends Resource
{
protected static ?string $model = Contact::class;
protected static ?string $modelLabel = 'Kontak';
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::Envelope;
protected static ?string $navigationLabel = 'Kontak';
protected static ?int $navigationSort = 9;
protected static ?string $recordTitleAttribute = 'subject';
protected static ?string $slug = 'manage/contacts';
public static function getNavigationBadge(): ?string
{
return static::getModel()::where('replied_at', null)->count();
}
public static function infolist(Schema $schema): Schema
{
return $schema
->schema([
Section::make('Informasi Pengirim')
->schema([
Grid::make(4)
->schema([
TextEntry::make('name')
->label('Nama'),
TextEntry::make('email')
->label('Email'),
TextEntry::make('subject')
->label('Subyek'),
TextEntry::make('created_at')
->label('Diterima pada')
->date('l, d F Y H:i'),
]),
])
->columnSpanFull(),
Section::make('Pesan')
->schema([
TextEntry::make('message')
->label('Isi Pesan')
->columnSpanFull(),
])
->columnSpanFull(),
Section::make('Balasan')
->schema([
TextEntry::make('repliedBy.name')
->label('Dibalas Oleh')
->placeholder('Belum dibalas'),
TextEntry::make('replied_at')
->label('Dibalas pada')
->date('l, d F Y H:i')
->placeholder('Belum dibalas'),
TextEntry::make('reply_message')
->label('Pesan Balasan')
->placeholder('Belum dibalas'),
])
->columnSpanFull()
->visible(fn (Contact $record): bool => $record->replied_at !== null),
])
->columns(2);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Nama')
->searchable(),
TextColumn::make('email')
->label('Email')
->searchable(),
TextColumn::make('subject')
->label('Subyek')
->limit(30),
IconColumn::make('replied_at')
->label('Dibalas')
->boolean()
->getStateUsing(fn (Contact $record): bool => $record->replied_at !== null),
TextColumn::make('repliedBy.name')
->label('Dibalas Oleh')
->badge()
->placeholder('Belum dibalas')
->sortable(),
TextColumn::make('created_at')
->label('Diterima pada')
->date('l, d F Y H:i')
->sortable(),
])
->filters([
TernaryFilter::make('replied_at')
->label('Status Balasan')
->placeholder('Semua Pesan')
->trueLabel('Sudah Dibalas')
->falseLabel('Belum Dibalas')
->queries(
true: fn ($query) => $query->whereNotNull('replied_at'),
false: fn ($query) => $query->whereNull('replied_at'),
)
->native(false),
])
->recordActions([
ViewAction::make()
->label('Lihat'),
ReplyAction::make('reply'),
DeleteAction::make()
->label('Hapus'),
])
->toolbarActions([
BulkActionGroup::make([
...DefaultBulkActions::make('Kontak'),
]),
])
->emptyStateIcon(Heroicon::Envelope)
->emptyStateDescription('Belum ada kontak atau pesan masuk.')
->defaultSort('created_at', 'desc')
->deferFilters(false);
}
public static function getPages(): array
{
return [
'index' => ManageContacts::route('/'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Manage\Contacts\Pages;
use App\Filament\Resources\Manage\Contacts\ContactResource;
use Filament\Resources\Pages\ManageRecords;
class ManageContacts extends ManageRecords
{
protected static string $resource = ContactResource::class;
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Cooperation;
use App\Enums\ApprovalStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\Cooperation;
use App\Models\User;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Support\Icons\Heroicon;
use Illuminate\Database\Eloquent\Builder;
class AcceptAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Terima')
->icon(Heroicon::OutlinedCheck)
->color('success')
->action(function (Cooperation $record): void {
$cooperationMedia = $record->cooperationMedia()
->whereHas('partnerMedia', function (Builder $query): void {
$query->whereHas('company', function (Builder $subQuery): void {
$subQuery->where('user_id', auth()->id());
});
})
->first();
if ($cooperationMedia) {
$cooperationMedia->update(['status' => ApprovalStatus::ACCEPTED]);
CheerfulNotification::success(
'Berhasil Diterima! ✅✨',
'Mantap! Anda telah menerima penawaran kerja sama ini. Silakan lanjut ke tahap pengajuan proposal ya! 💪'
)->send();
// Notify Admins
User::superAdmin()
->get()
->each(function ($admin) use ($record, $cooperationMedia): void {
$admin->notify(new BroadcastNotification([
'title' => 'Kabar Baik! Tawaran Diterima ✨🤝',
'body' => "Media {$cooperationMedia->partnerMedia->name} baru saja menerima penawaran kerja sama \"{$record->title}\". Siap-siap cek proposal mereka ya! 😊",
'action' => [
Action::make('view')
->label('Lihat')
->url(CooperationResource::getUrl('view', ['record' => $record->id])),
],
]));
});
}
})
->visible(function (Cooperation $record): bool {
$user = auth()->user();
if (! $user || ! $user->hasRole(RoleEnum::PERUSAHAAN->value)) {
return false;
}
$cooperationMedia = $record->cooperationMedia()
->whereHas('partnerMedia', function (Builder $query): void {
$query->whereHas('company', function (Builder $subQuery): void {
$subQuery->where('user_id', auth()->id());
});
})
->first();
return $cooperationMedia && $cooperationMedia->status === ApprovalStatus::PENDING;
});
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Cooperation;
use App\Enums\CooperationStatus;
use App\Enums\RoleEnum;
use App\Filament\Support\CheerfulNotification;
use App\Models\Cooperation;
use Filament\Actions\Action;
use Filament\Support\Icons\Heroicon;
class CompleteCooperationAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Selesaikan Kerjasama')
->icon(Heroicon::CheckBadge)
->color('success')
->requiresConfirmation()
->modalHeading('Selesaikan Kerja Sama')
->modalDescription('Apakah Anda yakin ingin menyelesaikan kerja sama ini? Pastikan semua pembayaran kepada media yang bekerja sama telah lunas.')
->action(function (Cooperation $record): void {
// Verify all accepted media are paid
$acceptedMedia = $record->cooperationMedia()->accepted()->get();
if ($acceptedMedia->isEmpty()) {
CheerfulNotification::danger(
'Tidak Ada Media! ❌',
'Tidak ada media yang menerima kerja sama ini. Tidak dapat diselesaikan.'
)->send();
return;
}
$unpaidMedia = $acceptedMedia->filter(function ($media) {
return ! $media->payment()->exists();
});
if ($unpaidMedia->isNotEmpty()) {
CheerfulNotification::danger(
'Pembayaran Belum Lunas! ⚠️',
'Masih ada media yang belum dibayar. Harap selesaikan pembayaran untuk semua media yang menerima kerja sama.'
)->send();
$this->halt();
return;
}
// Calculate total payment from media payments
$totalPayment = $acceptedMedia->sum(fn ($media) => $media->payment->amount);
$record->update([
'payment_amount' => $totalPayment,
'payment_date' => now(),
'status' => CooperationStatus::COMPLETED,
]);
CheerfulNotification::success(
'Kerja Sama Selesai! 🎉✨',
'Alhamdulillah! Kerja sama ini telah resmi diselesaikan. Terima kasih atas kerja samanya! 🙌😊'
)->send();
})
->visible(function (Cooperation $record): bool {
$user = auth()->user();
return $user && ! $user->hasRole(RoleEnum::PERUSAHAAN->value)
&& $record->status === CooperationStatus::PAYMENT;
});
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Cooperation;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\Cooperation;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
class CreateTaskAssignmentAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Buat Penugasan')
->icon(Heroicon::Plus)
->color('primary')
->schema(function (Cooperation $record): array {
return [
DatePicker::make('start_date')
->label('Tanggal Mulai')
->placeholder(fn (): string => now()->translatedFormat('l, d F Y'))
->native(false)
->displayFormat('l, d F Y')
->required()
->minDate($record->final_submission_date),
DatePicker::make('end_date')
->label('Tanggal Selesai')
->placeholder(fn (): string => now()->addDays(30)->translatedFormat('l, d F Y'))
->native(false)
->displayFormat('l, d F Y')
->required()
->after('start_date')
->minDate($record->final_submission_date),
TextInput::make('report_amount')
->label('Jumlah Laporan')
->placeholder(10)
->numeric()
->minValue(1)
->required()
->autocomplete(false),
Textarea::make('task_description')
->label('Deskripsi Tugas')
->required()
->rows(4)
->placeholder('...')
->autocomplete(false),
];
})
->action(function (array $data, Cooperation $record): void {
$taskAssignment = $record->taskAssignment()->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,
]);
// Notify Partner
$partnerUser = $partnerMedia->company?->user;
if ($partnerUser) {
$partnerUser->notify(new BroadcastNotification([
'title' => 'Tugas Baru Telah Menanti! 📝✨',
'body' => "Halo! Admin sudah menetapkan tugas untuk kerja sama \"{$record->title}\". Yuk, cek detail tugasnya dan mulai buat laporannya! Semangat! 💪😊",
'action' => [
Action::make('view')
->label('Lihat')
->url(CooperationResource::getUrl('view', ['record' => $record->id, 'relation' => 2])),
],
]));
}
}
CheerfulNotification::success(
'Penugasan Berhasil Dibuat! 🚀✨',
'Mantap! Penugasan telah dibuat dan semua media terkait sudah diberi tahu. Mari kita pantau progresnya! 💪'
)->send();
})
->modalHeading('Buat Penugasan')
->modalSubmitActionLabel('Simpan')
->visible(function (Cooperation $record): bool {
$user = auth()->user();
return $user && ! $user->hasRole(RoleEnum::PERUSAHAAN->value)
&& ! $record->taskAssignment()->exists()
&& $record->status === CooperationStatus::ASSIGNMENT;
})
->modalWidth(Width::Large);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Cooperation;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Enums\RoleEnum;
use App\Filament\Support\CheerfulNotification;
use App\Models\Cooperation;
use Filament\Actions\Action;
use Filament\Support\Icons\Heroicon;
class ProceedToPaymentAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Lanjutkan ke Pembayaran')
->icon(Heroicon::Banknotes)
->color('success')
->requiresConfirmation()
->modalHeading('Lanjutkan ke Pembayaran?')
->modalDescription('Apakah Anda yakin ingin melanjutkan kerja sama ini ke tahap pembayaran? Pastikan semua laporan telah diverifikasi.')
->action(function (Cooperation $record): void {
$record->update(['status' => CooperationStatus::PAYMENT]);
CheerfulNotification::success(
'Tahap Pembayaran Dimulai! 💸✨',
'Kerja sama telah dipindahkan ke tahap pembayaran. Silakan proses pembayarannya ya! 😊'
)->send();
})
->visible(function (Cooperation $record): bool {
$user = auth()->user();
if (! $user || $user->hasRole(RoleEnum::PERUSAHAAN->value) || $record->status !== CooperationStatus::VERIFICATION) {
return false;
}
// Check if all reports are verified (either Accepted or Rejected, not Pending)
$reports = $record->reports;
if ($reports->isEmpty()) {
return true;
}
return $reports->every(fn ($report) => $report->status !== ApprovalStatus::PENDING);
});
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Cooperation;
use App\Enums\ApprovalStatus;
use App\Enums\RoleEnum;
use App\Filament\Support\CheerfulNotification;
use App\Models\Cooperation;
use App\Models\User;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Support\Icons\Heroicon;
class RejectAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Tolak')
->icon(Heroicon::XMark)
->color('danger')
->action(function (Cooperation $record): void {
$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]);
CheerfulNotification::info(
'Berhasil Ditolak 🙏',
'Terima kasih atas konfirmasinya. Pesan penolakan telah diteruskan ke Admin. 😊'
)->send();
// Notify Admins
User::superAdmin()
->get()
->each(function ($admin) use ($record, $cooperationMedia): void {
$admin->notify(new BroadcastNotification([
'title' => 'Yah, Tawaran Ditolak 😔🤝',
'body' => "Media {$cooperationMedia->partnerMedia->name} baru saja menolak penawaran kerja sama \"{$record->title}\". Mungkin di lain waktu ya! 🙏",
]));
});
}
})
->visible(function (Cooperation $record): bool {
$user = auth()->user();
if (! $user || ! $user->hasRole(RoleEnum::PERUSAHAAN->value)) {
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;
});
}
}

View File

@ -0,0 +1,144 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Cooperation;
use App\Enums\ApprovalStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\Cooperation;
use App\Models\User;
use App\Notifications\BroadcastNotification;
use Asmit\FilamentUpload\Forms\Components\AdvancedFileUpload;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Storage;
class SendProposalAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->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')
->autocomplete(false)
->required()
->rows(4)
->placeholder('...'),
AdvancedFileUpload::make('proposal_attachment')
->label('Lampiran')
->disk(config('filesystems.default'))
->acceptedFileTypes(['application/pdf'])
->maxSize(1024 * 10)
->directory(fn (): string => 'cooperations/proposal-attachment/'.now()->toDateString())
->required(),
])
->action(function (Cooperation $record, array $data): void {
$cooperationMedia = $record->cooperationMedia()
->whereHas('partnerMedia', function (Builder $query): void {
$query->whereHas('company', function (Builder $subQuery): void {
$subQuery->where('user_id', auth()->id());
});
})
->first();
if ($cooperationMedia) {
$existingProposal = $record->proposal()
->where('partner_media_id', $cooperationMedia->partner_media_id)
->first();
$isRevision = (bool) $existingProposal;
$proposal = $record->proposal()->create([
'partner_media_id' => $cooperationMedia->partner_media_id,
'description' => $data['description'],
'e_catalog' => $data['e_catalog'],
'status' => ApprovalStatus::PENDING->value,
]);
if (! empty($data['proposal_attachment'])) {
$filePath = collect($data['proposal_attachment'])->first();
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
if (file_exists($fullPath)) {
$proposal->addMediaFromDisk($filePath, config('filesystems.default'))
->preservingOriginal()
->withCustomProperties([
'feature' => 'cooperations',
'date' => now()->toDateString(),
'doc_type' => 'proposal-attachment',
])
->toMediaCollection('cooperations');
}
}
CheerfulNotification::success(
'Berhasil Diajukan! 🚀',
'Yeay! Proposal kerja sama Anda sudah terkirim ke Admin. Mohon ditunggu ya, semoga hasilnya memuaskan! ✨'
)->send();
// Notify Admin
User::superAdmin()
->get()
->each(function ($admin) use ($isRevision, $record, $cooperationMedia): void {
$user = auth()->user();
$admin->notify(new BroadcastNotification([
'title' => $isRevision ? 'Pembaruan Proposal Kerja Sama ✨' : 'Ada Proposal Kerja Sama Baru! 🚀',
'body' => $isRevision
? "Cihuy! {$user->name} baru saja memperbarui proposal kerja sama untuk media {$cooperationMedia->partnerMedia->name} pada kampanye {$record->title}. Yuk, cek perubahannya!"
: "Halo Admin! {$user->name} baru saja mengajukan proposal kerja sama baru untuk media {$cooperationMedia->partnerMedia->name} pada kampanye {$record->title}. Segera diproses ya! 😊",
'action' => [
Action::make('view')
->label('Lihat')
->url(CooperationResource::getUrl('view', ['record' => $record->id, 'relation' => 1])),
],
]));
});
}
})
->modalHeading('Proposal Kerja Sama')
->modalSubmitActionLabel('Kirim Proposal')
->visible(function (Cooperation $record): bool {
$user = auth()->user();
if (! $user->hasRole(RoleEnum::PERUSAHAAN->value)) {
return false;
}
$cooperationMedia = $record->cooperationMedia()
->whereHas('partnerMedia', function (Builder $query): void {
$query->whereHas('company', function (Builder $subQuery): void {
$subQuery->where('user_id', auth()->id());
});
})
->first();
return $cooperationMedia
&& $cooperationMedia->status === ApprovalStatus::ACCEPTED
&& ! $record->proposal()
->where('partner_media_id', $cooperationMedia->partner_media_id)
->where(fn (Builder $q): Builder => $q->accepted()->orWhere(fn (Builder $q): Builder => $q->pending()))
->exists();
})
->modalWidth(Width::Large);
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Media;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Filament\Support\CheerfulNotification;
use App\Models\CooperationMedia;
use Filament\Actions\Action;
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Filament\Support\RawJs;
class PayAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Bayar')
->icon(Heroicon::CreditCard)
->color('success')
->visible(fn (CooperationMedia $record): bool => $record->cooperation->status === CooperationStatus::PAYMENT && ! $record->payment()->exists() && $record->status === ApprovalStatus::ACCEPTED)
->schema([
TextInput::make('amount')
->label('Nominal Pembayaran')
->placeholder('1,000,000')
->autocomplete(false)
->autofocus()
->required()
->mask(RawJs::make('$money($input)'))
->prefix('Rp'),
SpatieMediaLibraryFileUpload::make('payment_proof')
->label('Bukti Pembayaran')
->disk(config('filesystems.default'))
->acceptedFileTypes(['image/*'])
->maxSize(1024 * 3)
->collection('cooperations')
->customProperties(fn (): array => [
'feature' => 'cooperations',
'date' => now()->toDateString(),
'doc_type' => 'payment-proof',
])
->image()
->required(),
Textarea::make('description')
->label('Keterangan')
->placeholder('...')
->autocomplete(false),
])
->action(function (CooperationMedia $record, array $data): void {
$record->payment()->create([
'amount' => str_replace(['Rp ', ','], '', $data['amount']),
'description' => $data['description'],
'payment_date' => now(),
]);
CheerfulNotification::success(
'Pembayaran Berhasil! 💸✨',
"Pembayaran untuk {$record->partnerMedia->name} telah berhasil dicatat."
)->send();
})
->modalWidth(Width::Large);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Proposal;
use App\Enums\ApprovalStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Models\CooperationProposal;
use App\Models\User;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Icons\Heroicon;
class AcceptAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Terima')
->icon(Heroicon::OutlinedCheck)
->color('success')
->action(function (CooperationProposal $record): void {
$record->update([
'status' => ApprovalStatus::ACCEPTED,
'responded_at' => now(),
]);
Notification::make()
->title('Berhasil Diproses! ✨')
->body("Mantap! Proposal dari {$record->partnerMedia->name} telah diterima. Notifikasi sudah dikirim! 💪")
->success()
->send();
// Notify User (Partner)
$partnerUser = $record->partnerMedia?->company?->user;
if ($partnerUser) {
$partnerUser->notify(new BroadcastNotification([
'title' => 'Horee! Proposal Disetujui 🎉',
'body' => 'Yeay! Proposal kerja sama Anda telah disetujui oleh Admin. Mari kita lanjut ke tahap berikutnya! 🎊',
'action' => [
Action::make('view')
->label('Lihat')
->url(CooperationResource::getUrl('view', ['record' => $record->cooperation_id])),
],
]));
}
})
->visible(fn (CooperationProposal $record): bool => ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) && $record->status === ApprovalStatus::PENDING);
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Proposal;
use App\Enums\ApprovalStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Models\CooperationProposal;
use App\Models\User;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
class RejectAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Tolak')
->icon(Heroicon::XMark)
->color('danger')
->schema([
Textarea::make('reason')
->label('Alasan Penolakan')
->placeholder('Jelaskan alasan pengajuan ditolak...')
->required(),
])
->action(function (CooperationProposal $record, array $data): void {
$record->update([
'status' => ApprovalStatus::REJECTED,
'responded_at' => now(),
]);
$record->rejectionReasons()->create(['reason' => $data['reason']]);
Notification::make()
->title('Berhasil Diproses! ✨')
->body("Proposal dari {$record->partnerMedia->name} telah ditolak. Notifikasi sudah dikirim! 🙏")
->warning()
->send();
// Notify User (Partner)
$partnerUser = $record->partnerMedia?->company?->user;
if ($partnerUser) {
$partnerUser->notify(new BroadcastNotification([
'title' => 'Maaf, Proposal Belum Diterima 😔',
'body' => "Yah, maaf banget... proposal kerja sama Anda belum bisa kami terima saat ini. 💔\n\nAlasan: {$data['reason']}",
'action' => [
Action::make('view')
->label('Lihat')
->url(CooperationResource::getUrl('view', ['record' => $record->cooperation_id])),
],
]));
}
})
->visible(fn (CooperationProposal $record): bool => ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) && $record->status === ApprovalStatus::PENDING)
->modalWidth(Width::Large);
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Report;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Models\Report;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Icons\Heroicon;
class AcceptAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Terima')
->icon(Heroicon::OutlinedCheck)
->color('success')
->action(function (Report $record): void {
$record->update([
'status' => ApprovalStatus::ACCEPTED,
]);
Notification::make()
->title('Berhasil Diproses! ✨')
->body("Laporan '{$record->title}' telah diterima. Notifikasi sudah dikirim ke media! 💪")
->success()
->send();
// Notify all partners in this cooperation (since reports might be per media even if shared in DB)
$partners = $record->taskAssignment?->cooperation?->partnerMedia()
->wherePivot('status', ApprovalStatus::ACCEPTED)
->get();
foreach ($partners as $media) {
$partnerUser = $media->company?->user;
if ($partnerUser) {
$partnerUser->notify(new BroadcastNotification([
'title' => 'Mantap! Laporan Diterima 🎊✨',
'body' => "Hore! Laporan \"{$record->title}\" pada kerja sama \"{$record->taskAssignment->cooperation->title}\" telah disetujui oleh Admin. Satu langkah lebih dekat! 🚀",
'action' => [
Action::make('view')
->label('Lihat')
->url(CooperationResource::getUrl('view', ['record' => $record->taskAssignment->cooperation_id, 'relation' => 3])),
],
]));
}
}
})
->visible(
fn (Report $record): bool => $record->taskAssignment?->cooperation?->status === CooperationStatus::VERIFICATION &&
! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) &&
$record->status === ApprovalStatus::PENDING
);
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Actions\Report;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Models\Report;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
class RejectAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Tolak')
->icon(Heroicon::XMark)
->color('danger')
->schema([
Textarea::make('reason')
->label('Alasan Penolakan')
->placeholder('Jelaskan alasan pengajuan ditolak...')
->required(),
])
->action(function (Report $record, array $data): void {
$record->update([
'status' => ApprovalStatus::REJECTED,
]);
$record->rejectionReasons()->create(['reason' => $data['reason']]);
Notification::make()
->title('Berhasil Diproses! ✨')
->body("Laporan '{$record->title}' telah ditolak. Media terkait akan segera mendapatkan notifikasi perbaikan. 🙏")
->warning()
->send();
// Notify all partners in this cooperation (since we can't tell which specific one)
$partners = $record->taskAssignment?->cooperation?->partnerMedia()
->wherePivot('status', ApprovalStatus::ACCEPTED)
->get();
foreach ($partners as $media) {
$partnerUser = $media->company?->user;
if ($partnerUser) {
$partnerUser->notify(new BroadcastNotification([
'title' => 'Yah, Laporan Perlu Diperbaiki nih 📝',
'body' => "Halo! Laporan \"{$record->title}\" pada kerja sama \"{$record->taskAssignment->cooperation->title}\" ditolak oleh Admin. 💪\n\nCatatan Admin: {$data['reason']}",
'action' => [
Action::make('view')
->label('Lihat')
->url(CooperationResource::getUrl('view', ['record' => $record->taskAssignment->cooperation_id])),
],
]));
}
}
})
->visible(
fn (Report $record): bool => $record->taskAssignment?->cooperation?->status === CooperationStatus::VERIFICATION &&
! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) &&
$record->status === ApprovalStatus::PENDING
)
->modalWidth(Width::Large);
}
}

View File

@ -0,0 +1,176 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\Pages\CreateCooperation;
use App\Filament\Resources\Manage\Cooperations\Pages\EditCooperation;
use App\Filament\Resources\Manage\Cooperations\Pages\ListCooperations;
use App\Filament\Resources\Manage\Cooperations\Pages\ViewCooperation;
use App\Filament\Resources\Manage\Cooperations\RelationManagers\CooperationMediaRelationManager;
use App\Filament\Resources\Manage\Cooperations\RelationManagers\CooperationProposalRelationManager;
use App\Filament\Resources\Manage\Cooperations\RelationManagers\ReportRelationManager;
use App\Filament\Resources\Manage\Cooperations\RelationManagers\TaskAssignmentRelationManager;
use App\Filament\Resources\Manage\Cooperations\Schemas\CooperationForm;
use App\Filament\Resources\Manage\Cooperations\Tables\CooperationsTable;
use App\Models\Cooperation;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Infolists\Components\ImageEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Joaopaulolndev\FilamentPdfViewer\Infolists\Components\PdfViewerEntry;
use UnitEnum;
class CooperationResource extends Resource
{
protected static ?string $model = Cooperation::class;
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::HandRaised;
protected static ?string $navigationLabel = 'Kerja Sama';
protected static ?int $navigationSort = 7;
protected static ?string $recordTitleAttribute = 'title';
protected static ?string $slug = 'manage/cooperations';
public static function getNavigationBadge(): ?string
{
return static::getModel()::when(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value), function (Builder $query): void {
$query->whereHas('partnerMedia', function (Builder $partnerMediaQuery): void {
$partnerMediaQuery->whereHas('company', function (Builder $companyQuery): void {
$companyQuery->where('user_id', auth()->id());
});
});
})
->count();
}
public static function form(Schema $schema): Schema
{
return CooperationForm::configure($schema);
}
public static function table(Table $table): Table
{
return CooperationsTable::configure($table)
->modifyQueryUsing(function (Builder $query): Builder {
if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) {
$query->whereHas('cooperationMedia', function (Builder $subQuery): void {
$subQuery->whereHas('partnerMedia', function (Builder $mediaQuery): void {
$mediaQuery->whereHas('company', function (Builder $companyQuery): void {
$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 (Cooperation $record): ?string => $record->status->getLabel())
->badge()
->color(fn (Cooperation $record): string|array|null => $record->status->getColor())
->disabled(),
])
->schema([
TextEntry::make('title')
->label('Judul'),
Grid::make(3)
->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('taskAssignment.start_date')
->label('Tanggal Mulai Penugasan')
->date('l, d F Y')
->placeholder('Belum ditentukan'),
TextEntry::make('taskAssignment.end_date')
->label('Tanggal Selesai Penugasan')
->date('l, d F Y')
->placeholder('Belum ditentukan'),
TextEntry::make('payment_date')
->label('Tanggal Pembayaran')
->date('l, d F Y')
->placeholder('Belum dibayar'),
]),
Grid::make(3)
->schema([
PdfViewerEntry::make('proposal_template')
->label('Template Proposal')
->getStateUsing(fn (Cooperation $record): ?string => optional($record->getMedia('cooperations')->where('custom_properties.doc_type', 'proposal-template')->sortByDesc('created_at')->first())->getPathRelativeToRoot())
->disk(config('filesystems.default'))
->columnSpan(2)
->visible(fn (Cooperation $record): bool => $record->getMedia('cooperations')->where('custom_properties.doc_type', 'proposal-template')->isNotEmpty()),
ImageEntry::make('banner')
->label('Banner')
->getStateUsing(fn (Cooperation $record): ?string => optional($record->getMedia('cooperations')->where('custom_properties.doc_type', 'banner')->sortByDesc('created_at')->first())->getPathRelativeToRoot())
->disk(config('filesystems.default'))
->visible(fn (Cooperation $record): bool => $record->getMedia('cooperations')->where('custom_properties.doc_type', 'banner')->isNotEmpty()),
])
->visible(fn (Cooperation $record): bool => $record->getMedia('cooperations')->where('custom_properties.doc_type', 'banner')->isNotEmpty() && $record->getMedia('cooperations')->where('custom_properties.doc_type', 'proposal-template')->isNotEmpty()),
TextEntry::make('description')
->label('Deskripsi'),
]),
])
->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,
]);
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Pages;
use App\Filament\Actions\BackAction;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Filament\Support\CheerfulNotification;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Facades\Storage;
class CreateCooperation extends CreateRecord
{
protected static string $resource = CooperationResource::class;
protected static ?string $title = 'Tambah Kerja Sama';
protected function getHeaderActions(): array
{
return [
BackAction::make()
->url(ListCooperations::getUrl()),
];
}
protected function getCreatedNotification(): ?Notification
{
return CheerfulNotification::create();
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
protected function afterCreate(): void
{
$record = $this->getRecord();
$data = $this->data;
if (empty($data['proposal_template'])) {
return;
}
$filePath = collect($data['proposal_template'])->first();
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
if (! file_exists($fullPath)) {
return;
}
$record
->addMediaFromDisk($filePath, config('filesystems.default'))
->preservingOriginal()
->withCustomProperties([
'feature' => 'cooperations',
'date' => now()->toDateString(),
'doc_type' => 'proposal-template',
])
->toMediaCollection('cooperations');
// Notify selected partners
$record->load('partnerMedia.company.user');
$record->partnerMedia->each(function ($media) use ($record) {
$partnerUser = $media->company?->user;
if ($partnerUser) {
$partnerUser->notify(new BroadcastNotification([
'title' => 'Ada Penawaran Kerja Sama Baru! 🤝✨',
'body' => "Halo! Admin baru saja menawarkan kerja sama \"{$record->title}\" untuk media {$media->name}. Yuk, cek detailnya dan berikan tanggapanmu! 😊",
'action' => [
Action::make('view')
->label('Lihat')
->url(CooperationResource::getUrl('view', ['record' => $record->id])),
],
]));
}
});
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Pages;
use App\Filament\Actions\BackAction;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\Cooperation;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Storage;
class EditCooperation extends EditRecord
{
protected static string $resource = CooperationResource::class;
public function mount(int|string $record): void
{
parent::mount($record);
$media = Cooperation::find($record)
->getMedia('cooperations')
->where('custom_properties.doc_type', 'proposal-template')
->sortByDesc('created_at')
->first();
if (! $media) {
return;
}
$this->data['proposal_template'] = [
$media->uuid => $media->getPathRelativeToRoot(),
];
}
protected function getHeaderActions(): array
{
return [
BackAction::make()
->url(ListCooperations::getUrl()),
];
}
protected function getSavedNotification(): ?Notification
{
return CheerfulNotification::update();
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
protected function afterSave(): void
{
$record = $this->getRecord();
$data = $this->data;
if (empty($data['proposal_template'])) {
return;
}
$filePath = collect($data['proposal_template'])->first();
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
if (! file_exists($fullPath)) {
return;
}
$record
->addMediaFromDisk($filePath, config('filesystems.default'))
->preservingOriginal()
->withCustomProperties([
'feature' => 'cooperations',
'doc_type' => 'proposal-template',
'date' => now()->toDateString(),
])
->toMediaCollection('cooperations');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Pages;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListCooperations extends ListRecords
{
protected static string $resource = CooperationResource::class;
protected static ?string $title = 'Kerja Sama';
protected function getHeaderActions(): array
{
$actions = [];
if (! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) {
$actions[] = CreateAction::make()
->label('Tambah');
}
return $actions;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Pages;
use App\Filament\Actions\BackAction;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use Filament\Resources\Pages\ViewRecord;
class ViewCooperation extends ViewRecord
{
protected static string $resource = CooperationResource::class;
protected function getHeaderActions(): array
{
return [
BackAction::make()
->url(ListCooperations::getUrl()),
];
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\RelationManagers;
use App\Enums\ApprovalStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\Actions\Media\PayAction;
use App\Models\CooperationMedia;
use Filament\Actions\ViewAction;
use Filament\Infolists\Components\ImageEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class CooperationMediaRelationManager extends RelationManager
{
protected static string $relationship = 'cooperationMedia';
protected static ?string $title = 'Media';
public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool
{
return auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value);
}
public static function getBadge(Model $ownerRecord, string $pageClass): ?string
{
return $ownerRecord->cooperationMedia()->count();
}
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('partnerMedia.name')
->label('Nama')
->searchable(),
TextColumn::make('payment.amount')
->label('Jumlah Pembayaran')
->money('IDR', decimalPlaces: 0)
->placeholder('Belum ditentukan'),
TextColumn::make('status')
->label('Status')
->badge(),
])
->filters([])
->headerActions([])
->recordActions([
PayAction::make('pay'),
ViewAction::make()
->visible(fn (CooperationMedia $record): bool => $record->status === ApprovalStatus::ACCEPTED && $record->payment()->exists()),
])
->recordAction(null);
}
public function infolist(Schema $infolist): Schema
{
return $infolist
->components([
Grid::make(2)
->schema([
TextEntry::make('payment.amount')
->label('Nominal')
->money('IDR'),
TextEntry::make('payment.payment_date')
->label('Tanggal Bayar')
->dateTime(),
]),
TextEntry::make('payment.description')
->label('Keterangan')
->visible(fn (CooperationMedia $record): bool => $record->description !== null),
ImageEntry::make('payment_proof')
->label('Bukti Pembayaran')
->getStateUsing(fn (CooperationMedia $record): ?string => optional($record->getMedia('cooperations')->where('custom_properties.doc_type', 'payment-proof')->sortByDesc('created_at')->first())->getPathRelativeToRoot())
->disk(config('filesystems.default')),
])
->columns(1);
}
}

View File

@ -0,0 +1,170 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\RelationManagers;
use App\Enums\ApprovalStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\Actions\Proposal\AcceptAction;
use App\Filament\Resources\Manage\Cooperations\Actions\Proposal\RejectAction;
use App\Models\CooperationProposal;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\ViewAction;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Joaopaulolndev\FilamentPdfViewer\Infolists\Components\PdfViewerEntry;
class CooperationProposalRelationManager extends RelationManager
{
protected static string $relationship = 'proposal';
protected static ?string $title = 'Proposal';
public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool
{
if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) {
return $ownerRecord->cooperationMedia()
->whereHas('partnerMedia', function ($query) {
$query->whereHas('company', function ($query) {
$query->where('user_id', auth()->id());
});
})
->where('status', ApprovalStatus::ACCEPTED)
->exists();
}
return true;
}
public static function getBadge(Model $ownerRecord, string $pageClass): ?string
{
if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) {
return null;
}
return (string) $ownerRecord->proposal()->count();
}
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('partnerMedia.name')
->label('Media')
->searchable()
->visible(! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)),
TextColumn::make('e_catalog')
->label('E-Catalog')
->url(fn (CooperationProposal $record): ?string => $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(),
AcceptAction::make('accept'),
RejectAction::make('reject'),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->successNotification(
Notification::make()
->title('Proposal Dihapus! 🗑️')
->body('Semua proposal yang dipilih telah berhasil dihapus. 👋')
->success()
),
]),
])
->modifyQueryUsing(function ($query) {
$user = auth()->user();
if (! $user->hasRole(RoleEnum::PERUSAHAAN->value)) {
return $query;
}
return $query->whereHas('partnerMedia.company', function ($q) use ($user) {
$q->where('user_id', $user->id);
});
});
}
public function infolist(Schema $infolist): Schema
{
return $infolist
->components([
Grid::make(2)
->schema([
TextEntry::make('partnerMedia.name')
->label('Media')
->visible(! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)),
TextEntry::make('status')
->label('Status')
->badge(),
TextEntry::make('submitted_at')
->label('Diajukan')
->dateTime('l, d F Y H:i:s'),
TextEntry::make('responded_at')
->label('Ditanggapi')
->dateTime('l, d F Y H:i:s'),
]),
TextEntry::make('e_catalog')
->label('E-Catalog')
->url(fn (CooperationProposal $record): ?string => $record->e_catalog)
->openUrlInNewTab()
->color('primary')
->visible(fn (CooperationProposal $record): ?string => $record->e_catalog),
TextEntry::make('description')
->label('Deskripsi')
->columnSpanFull(),
PdfViewerEntry::make('proposal_attachment')
->label('Lampiran Proposal')
->getStateUsing(fn (CooperationProposal $record): ?string => optional($record->getMedia('cooperations')->where('custom_properties.doc_type', 'proposal-attachment')->sortByDesc('created_at')->first())->getPathRelativeToRoot())
->disk(config('filesystems.default')),
TextEntry::make('rejectionReasons.reason')
->label('Alasan Penolakan')
->listWithLineBreaks()
->visible(fn (CooperationProposal $record): bool => $record->status === ApprovalStatus::REJECTED && $record->rejectionReasons->isNotEmpty())
->columnSpanFull(),
])
->columns(1);
}
}

View File

@ -0,0 +1,193 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\RelationManagers;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Cooperations\Actions\Report\AcceptAction;
use App\Filament\Resources\Manage\Cooperations\Actions\Report\RejectAction;
use App\Filament\Resources\Manage\Cooperations\CooperationResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\Report;
use App\Models\User;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class ReportRelationManager extends RelationManager
{
protected static string $relationship = 'reports';
protected static ?string $title = 'Laporan';
public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool
{
if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) {
return $ownerRecord->cooperationMedia()
->whereHas('partnerMedia', function ($query) {
$query->whereHas('company', function ($query) {
$query->where('user_id', auth()->id());
});
})
->where('status', ApprovalStatus::ACCEPTED)
->exists();
}
return true;
}
public function isReadOnly(): bool
{
return false;
}
public static function getBadge(Model $ownerRecord, string $pageClass): ?string
{
return $ownerRecord->reports()->count();
}
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('title')
->placeholder('Dirgahayu Purwakarta')
->required()
->maxLength(200)
->autocomplete(false)
->autofocus(),
DatePicker::make('publication_date')
->label('Tanggal Publikasi')
->placeholder(fn (): string => now()->translatedFormat('l, d F Y'))
->native(false)
->displayFormat('l, d F Y')
->required(),
TextInput::make('link')
->label('Tautan')
->placeholder('https://example.com')
->maxLength(50)
->url()
->required()
->autocomplete(false),
Textarea::make('description')
->label('Deskripsi')
->placeholder('....')
->required()
->autocomplete(false),
SpatieMediaLibraryFileUpload::make('image')
->label('Gambar')
->disk(config('filesystems.default'))
->acceptedFileTypes(['image/*'])
->maxSize(1024 * 3)
->collection('reports')
->image()
->required(),
])
->columns(1);
}
public function table(Table $table): Table
{
return $table
->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 (Report $record): string => $record->link)
->openUrlInNewTab(),
TextColumn::make('status')
->label('Status')
->badge(),
TextColumn::make('rejectionReasons.reason')
->label('Alasan Penolakan')
->searchable(),
])
->filters([])
->recordActions([
AcceptAction::make('accept'),
RejectAction::make('reject'),
])
->headerActions([
CreateAction::make()
->label('Buat Laporan')
->modalWidth(Width::Large)
->modalHeading('Buat Laporan')
->mutateDataUsing(function (array $data): array {
$data['task_assignment_id'] = $this->getOwnerRecord()->taskAssignment?->id;
return $data;
})
->successNotification(null)
->after(function (Report $record): void {
CheerfulNotification::success(
'Laporan Berhasil Terkirim! 🚀✨',
'Hore! Laporan Anda sudah masuk ke sistem. Admin akan segera memeriksanya. Terima kasih atas kerja kerasnya! 💪'
)->send();
// Notify Admins
User::superAdmin()
->get()
->each(function ($admin) use ($record): void {
$user = auth()->user();
$admin->notify(new BroadcastNotification([
'title' => 'Ada Laporan Baru! 📝✨',
'body' => "Halo Admin! {$user->name} baru saja mengirimkan laporan \"{$record->title}\" untuk kerja sama \"{$this->getOwnerRecord()->title}\". Yuk, dicek! 😊",
'action' => [
Action::make('view')
->label('Lihat')
->url(CooperationResource::getUrl('view', ['record' => $this->getOwnerRecord()->id, 'relation' => 3])),
],
]));
});
})
->visible(function (): bool {
$user = auth()->user();
if (! $user || ! $user->hasRole(RoleEnum::PERUSAHAAN->value)) {
return false;
}
$taskAssignment = $this->getOwnerRecord()->taskAssignment;
if (! $taskAssignment) {
return false;
}
return $this->getOwnerRecord()->status === CooperationStatus::ASSIGNMENT
&& $taskAssignment->start_date->toDateString() <= now()->toDateString()
&& $taskAssignment->end_date->toDateString() >= now()->toDateString()
&& $taskAssignment->reports()
->where('status', '!=', ApprovalStatus::REJECTED)
->count() < $taskAssignment->report_amount;
}),
])
->toolbarActions([]);
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\RelationManagers;
use App\Enums\ApprovalStatus;
use App\Enums\RoleEnum;
use App\Filament\Actions\Cheerful\CreateAction;
use App\Filament\Actions\Cheerful\DeleteAction;
use App\Filament\Actions\Cheerful\EditAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class TaskAssignmentRelationManager extends RelationManager
{
protected static string $relationship = 'taskAssignment';
protected static ?string $title = 'Penugasan';
public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool
{
if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) {
return $ownerRecord->cooperationMedia()
->whereHas('partnerMedia', function ($query) {
$query->whereHas('company', function ($query) {
$query->where('user_id', auth()->id());
});
})
->where('status', ApprovalStatus::ACCEPTED)
->exists();
}
return true;
}
public function table(Table $table): Table
{
return $table
->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'),
])
->filters([
//
])
->headerActions([
CreateAction::make()
->label('Buat Penugasan')
->visible(function (): bool {
$cooperation = $this->getOwnerRecord();
return $cooperation->proposal()->accepted()->exists()
&& ! $cooperation->taskAssignment()->exists();
})
->successNotification(
Notification::make()
->title('Penugasan Dibuat! 🚀✨')
->body('Tugas baru berhasil ditetapkan. Media terkait akan segera mengetahuinya! 💪')
->success()
),
])
->recordActions([
EditAction::make()
->successNotification(
Notification::make()
->title('Penugasan Diperbarui! ✅')
->body('Perubahan pada penugasan berhasil disimpan. 👍')
->success()
),
DeleteAction::make()
->successNotification(
Notification::make()
->title('Penugasan Dihapus! 🗑️')
->body('Penugasan telah berhasil dihapus dari sistem. 👋')
->success()
),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->successNotification(
Notification::make()
->title('Banyak Penugasan Dihapus! 🗑️👋')
->body('Semua penugasan yang dipilih telah dihapus. 🧹')
->success()
),
]),
]);
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Schemas;
use App\Models\PartnerMedia;
use Asmit\FilamentUpload\Forms\Components\AdvancedFileUpload;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Illuminate\Database\Eloquent\Builder;
class CooperationForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->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 (): string => now()->translatedFormat('l, d F Y'))
->native(false)
->displayFormat('l, d F Y')
->required()
->minDate(today())
->reactive(),
DatePicker::make('final_submission_date')
->label('Tanggal Pengajuan Akhir')
->placeholder(fn (): string => now()->addDays(7)->translatedFormat('l, d F Y'))
->native(false)
->displayFormat('l, d F Y')
->required()
->afterOrEqual('initial_submission_date')
->minDate(fn (Get $get) => $get('initial_submission_date')),
]),
Select::make('partner_media_ids')
->label('Media')
->relationship('partnerMedia', 'name', fn (Builder $query): Builder => $query->verified())
->multiple()
->preload()
->searchable()
->native(false)
->required()
->selectablePlaceholder(false)
->helperText('Pilih media yang akan diajak kerja sama.')
->suffixAction(
Action::make('select_all')
->label('Select All')
->icon(Heroicon::OutlinedCheckCircle)
->action(function (Set $set): void {
$allMediaIds = PartnerMedia::verified()->pluck('id')->toArray();
$set('partner_media_ids', $allMediaIds);
})
),
Textarea::make('description')
->label('Deskripsi')
->placeholder('...')
->required()
->rows(5)
->maxLength(65535)
->autocomplete(false),
])
->columnSpan(2),
Section::make([
SpatieMediaLibraryFileUpload::make('banner')
->label('Banner')
->disk(config('filesystems.default'))
->acceptedFileTypes(['image/*'])
->maxSize(1024 * 3)
->collection('cooperations')
->customProperties(fn (): array => [
'feature' => 'cooperations',
'doc_type' => 'banner',
'date' => now()->toDateString(),
])
->image(),
AdvancedFileUpload::make('proposal_template')
->label('Template Pengajuan')
->disk(config('filesystems.default'))
->acceptedFileTypes(['application/pdf'])
->maxSize(1024 * 10)
->directory(fn (): string => 'cooperations/proposal-template/'.now()->toDateString())
->required(),
]),
])
->columns(3);
}
}

View File

@ -0,0 +1,281 @@
<?php
namespace App\Filament\Resources\Manage\Cooperations\Tables;
use App\Enums\ApprovalStatus;
use App\Enums\CooperationStatus;
use App\Enums\RoleEnum;
use App\Filament\Actions\Cheerful\DeleteAction;
use App\Filament\Actions\Cheerful\EditAction;
use App\Filament\Actions\DefaultBulkActions;
use App\Filament\Columns\TimestampColumns;
use App\Filament\Exports\CooperationExcelExport;
use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\AcceptAction;
use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\CompleteCooperationAction;
use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\CreateTaskAssignmentAction;
use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\ProceedToPaymentAction;
use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\RejectAction;
use App\Filament\Resources\Manage\Cooperations\Actions\Cooperation\SendProposalAction;
use App\Models\Cooperation;
use Carbon\CarbonInterface;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use pxlrbt\FilamentExcel\Actions\Tables\ExportAction;
class CooperationsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')
->label('Judul')
->searchable()
->sortable()
->wrap(),
TextColumn::make('partnerMedia.name')
->label('Media')
->listWithLineBreaks()
->limitList(3)
->expandableLimitedList()
->badge()
->visible(! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)),
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->copy()->endOfDay())
? 'Waktu Pengajuan Habis'
: 'Sisa waktu pengajuan: '.now()->diffForHumans(
$record->final_submission_date->copy()->endOfDay(),
['syntax' => CarbonInterface::DIFF_ABSOLUTE]
)
),
TextColumn::make('status')
->searchable()
->sortable()
->badge(),
TextColumn::make('media_count')
->label('Tanggapan Media')
->html()
->getStateUsing(function (Cooperation $record): string {
$record->loadCount([
'partnerMedia as approved_count' => fn (Builder $query): Builder => $query->wherePivotAccepted(),
'partnerMedia as rejected_count' => fn (Builder $query): Builder => $query->wherePivotRejected(),
'partnerMedia as pending_count' => fn (Builder $query): Builder => $query->wherePivotPending(),
]);
$totalCount = $record->partnerMedia()->count();
return "Menerima: {$record->approved_count}<br>
Menolak: {$record->rejected_count}<br>
Belum Menanggapi: {$record->pending_count}<br>
Total: {$totalCount}";
})
->visible(! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)),
TextColumn::make('status_tanggapan')
->label('Tanggapan')
->getStateUsing(function (Cooperation $record): ?ApprovalStatus {
if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)) {
$cooperationMedia = $record->cooperationMedia()
->whereHas('partnerMedia', function (Builder $query): void {
$query->whereHas('company', function (Builder $subQuery): void {
$subQuery->where('user_id', auth()->id());
});
})
->first();
return $cooperationMedia?->status;
}
return null;
})
->badge()
->visible(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)),
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),
SelectFilter::make('status')
->label('Status')
->options(CooperationStatus::class),
SelectFilter::make('partnerMedia')
->label('Media')
->relationship('partnerMedia', 'name')
->searchable()
->preload(),
Filter::make('title')
->schema([
TextInput::make('title')
->label('Nama Kegiatan')
->placeholder('Cari nama kegiatan...'),
])
->query(function (Builder $query, array $data): Builder {
return $query->when(
$data['title'],
fn (Builder $query, $title): Builder => $query->where('title', 'like', "%{$title}%"),
);
}),
Filter::make('date_filter')
->schema([
Select::make('date_type')
->label('Jenis Filter Tanggal')
->options([
'date' => 'Tanggal Penyerahan Awal',
'month' => 'Bulan & Tahun',
'year' => 'Tahun',
'range' => 'Rentang Tanggal',
])
->default('date')
->live(),
DatePicker::make('specific_date')
->label('Tanggal')
->visible(fn (Get $get) => $get('date_type') === 'date'),
Select::make('month')
->label('Bulan')
->options([
1 => 'Januari',
2 => 'Februari',
3 => 'Maret',
4 => 'April',
5 => 'Mei',
6 => 'Juni',
7 => 'Juli',
8 => 'Agustus',
9 => 'September',
10 => 'Oktober',
11 => 'November',
12 => 'Desember',
])
->visible(fn (Get $get) => $get('date_type') === 'month'),
Select::make('year')
->label('Tahun')
->options(function () {
$years = range(date('Y'), 2020);
return array_combine($years, $years);
})
->visible(fn (Get $get) => in_array($get('date_type'), ['month', 'year'])),
DatePicker::make('start_date')
->label('Mulai Tanggal')
->visible(fn (Get $get) => $get('date_type') === 'range'),
DatePicker::make('end_date')
->label('Sampai Tanggal')
->visible(fn (Get $get) => $get('date_type') === 'range'),
])
->query(function (Builder $query, array $data): Builder {
$type = $data['date_type'] ?? null;
$column = 'initial_submission_date';
if ($type === 'date' && ! empty($data['specific_date'])) {
return $query->whereDate($column, $data['specific_date']);
}
if ($type === 'month' && ! empty($data['month']) && ! empty($data['year'])) {
return $query->whereMonth($column, $data['month'])
->whereYear($column, $data['year']);
}
if ($type === 'year' && ! empty($data['year'])) {
return $query->whereYear($column, $data['year']);
}
if ($type === 'range' && ! empty($data['start_date']) && ! empty($data['end_date'])) {
return $query->whereBetween($column, [$data['start_date'], $data['end_date']]);
}
return $query;
}),
])
->recordActions([
ViewAction::make(),
AcceptAction::make('accept'),
RejectAction::make('reject'),
SendProposalAction::make('sendProposal'),
CreateTaskAssignmentAction::make('createTaskAssignment'),
ProceedToPaymentAction::make('proceedToPayment'),
CompleteCooperationAction::make('completeCooperation'),
EditAction::make()
->visible(fn (): bool => ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)),
DeleteAction::make()
->visible(fn (): bool => ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)),
])
->headerActions([
ExportAction::make()
->exports([
CooperationExcelExport::make(),
])
->color('success'),
])
->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);
}
}

View File

@ -0,0 +1,162 @@
<?php
namespace App\Filament\Resources\Manage\DataChanges\Actions\DataChangeRequest;
use App\Enums\DataChangeDecision;
use App\Enums\DataChangeStatus;
use App\Enums\RoleEnum;
use App\Filament\Support\CheerfulNotification;
use App\Models\DataChangeRequest;
use App\Models\DataChangeReview;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Support\Contracts\HasLabel;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\Storage;
class AcceptAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Setujui')
->icon(Heroicon::Check)
->color('success')
->requiresConfirmation()
->modalHeading('Setujui Perubahan Data')
->modalDescription('Apakah Anda yakin ingin menyetujui dan menerapkan perubahan data ini?')
->action(function (DataChangeRequest $record): void {
$entity = $record->entity;
$newData = $record->new_data;
if (isset($newData['__delete_request__']) && $newData['__delete_request__'] === true) {
$entity->delete();
$record->update(['status' => DataChangeStatus::APPROVED]);
DataChangeReview::create([
'data_change_request_id' => $record->id,
'reviewer_id' => auth()->id(),
'decision' => DataChangeDecision::APPROVED,
]);
CheerfulNotification::success(
'Penghapusan Disetujui! ✅',
'Data telah berhasil dihapus sesuai dengan permohonan.'
)->send();
// Notify User
$record->user?->notify(new BroadcastNotification([
'title' => 'Permohonan Penghapusan Disetujui ✨',
'body' => "Halo! Permohonan penghapusan data Anda untuk {$record->change_reason} telah disetujui oleh admin. 🚀",
]));
return;
}
if (! $entity) {
CheerfulNotification::danger(
'Entitas Tidak Ditemukan ❌',
'Data asli untuk permohonan ini tidak dapat ditemukan.'
)->send();
return;
}
$fields = [];
$docs = [];
$reverseLabels = [];
if (method_exists($record->entity_type, 'dataChangeEntryLabels')) {
$reverseLabels = array_flip(($record->entity_type)::dataChangeEntryLabels());
}
$casts = $entity->getCasts();
foreach ($newData as $key => $value) {
$actualKey = $reverseLabels[$key] ?? $key;
if (str_ends_with($actualKey, '_docs')) {
$docs[$actualKey] = $value;
} else {
// Handle Enum labels reverse mapping
if (isset($casts[$actualKey])) {
$castType = $casts[$actualKey];
if (is_string($castType) && enum_exists($castType)) {
if (is_subclass_of($castType, HasLabel::class)) {
foreach ($castType::cases() as $case) {
if ($case->getLabel() === $value) {
$value = $case->value;
break;
}
}
}
// If it's an int-backed enum but we have a string (e.g. from older data or failed mapping)
if (is_string($value) && is_numeric($value)) {
$reflection = new \ReflectionEnum($castType);
if ($reflection->isBacked() && $reflection->getBackingType()?->getName() === 'int') {
$value = (int) $value;
}
}
}
}
$fields[$actualKey] = $value;
}
}
// Update model fields
$entity->update($fields);
// Update media/documents
foreach ($docs as $field => $filePaths) {
if (empty($filePaths)) {
continue;
}
$docType = str($field)->replace('_docs', '')->slug('-');
$collection = match ($record->entity_type) {
'App\Models\PartnerMedia' => 'partnerMedia',
default => str($record->entity_type)->afterLast('\\')->plural()->lower()->toString(),
};
foreach ((array) $filePaths as $filePath) {
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
if (! file_exists($fullPath)) {
continue;
}
$entity->addMediaFromDisk($filePath, config('filesystems.default'))
->preservingOriginal()
->withCustomProperties([
'feature' => $collection,
'date' => now()->toDateString(),
'doc_type' => $docType,
])
->toMediaCollection($collection);
}
}
$record->update(['status' => DataChangeStatus::APPROVED]);
DataChangeReview::create([
'data_change_request_id' => $record->id,
'reviewer_id' => auth()->id(),
'decision' => DataChangeDecision::APPROVED,
]);
CheerfulNotification::success(
'Perubahan Disetujui! ✅',
'Data telah berhasil diperbarui sesuai dengan permohonan.'
)->send();
// Notify User
$record->user?->notify(new BroadcastNotification([
'title' => 'Permohonan Perubahan Disetujui ✨',
'body' => "Halo! Permohonan perubahan data Anda untuk {$record->change_reason} telah disetujui oleh admin. Cek sekarang ya! 🚀",
]));
})
->visible(fn (DataChangeRequest $record): bool => $record->status === DataChangeStatus::PENDING && ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value));
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Filament\Resources\Manage\DataChanges\Actions\DataChangeRequest;
use App\Enums\DataChangeDecision;
use App\Enums\DataChangeStatus;
use App\Enums\RoleEnum;
use App\Filament\Support\CheerfulNotification;
use App\Models\DataChangeRequest;
use App\Models\DataChangeReview;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
class RejectAction extends Action
{
protected function setUp(): void
{
parent::setUp();
$this->label('Tolak')
->icon(Heroicon::XMark)
->color('danger')
->schema([
Textarea::make('rejection_reason')
->label('Alasan Penolakan')
->placeholder('Berikan alasan mengapa permohonan ini ditolak...')
->required(),
])
->modalHeading('Tolak Perubahan Data')
->action(function (DataChangeRequest $record, array $data): void {
$record->update([
'status' => DataChangeStatus::REJECTED,
]);
DataChangeReview::create([
'data_change_request_id' => $record->id,
'reviewer_id' => auth()->id(),
'decision' => DataChangeDecision::REJECTED,
'note' => $data['rejection_reason'],
]);
// If it was a new addition (empty old_data), delete the entity
$entity = $record->entity;
if (empty($record->old_data) && $entity) {
$entity->forceDelete();
}
CheerfulNotification::info(
'Permohonan Ditolak 🛑',
'Permohonan perubahan data telah ditolak. 😔'
)->send();
// Notify User
$record->user?->notify(new BroadcastNotification([
'title' => 'Permohonan Perubahan Ditolak 🛑',
'body' => "Halo! Mohon maaf, permohonan perubahan data Anda ({$record->change_reason}) ditolak oleh admin. Alasan: {$data['rejection_reason']} 😔",
]));
})
->visible(fn (DataChangeRequest $record): bool => $record->status === DataChangeStatus::PENDING && ! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value))
->modalWidth(Width::Large);
}
}

View File

@ -0,0 +1,282 @@
<?php
namespace App\Filament\Resources\Manage\DataChanges;
use App\Enums\DataChangeStatus;
use App\Enums\RoleEnum;
use App\Filament\Actions\DefaultBulkActions;
use App\Filament\Resources\Manage\DataChanges\Actions\DataChangeRequest\AcceptAction;
use App\Filament\Resources\Manage\DataChanges\Actions\DataChangeRequest\RejectAction;
use App\Filament\Resources\Manage\DataChanges\Pages\ManageDataChanges;
use App\Filament\Resources\Manage\DataChanges\Pages\ViewDataChange;
use App\Models\Company;
use App\Models\DataChangeRequest;
use App\Models\Journalist;
use App\Models\PartnerMedia;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\ViewAction;
use Filament\Infolists\Components\TextEntry;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Storage;
use UnitEnum;
class DataChangesResource extends Resource
{
protected static ?string $model = DataChangeRequest::class;
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::DocumentText;
protected static ?string $navigationLabel = 'Permohonan Perubahan Data';
protected static ?int $navigationSort = 8;
protected static ?string $recordTitleAttribute = 'change_reason';
protected static ?string $slug = 'manage/data-changes';
public static function getNavigationBadge(): string
{
return static::getModel()::pending()
->when(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value), function (Builder $query): void {
$query->where('user_id', auth()->id());
})
->count();
}
public static function infolist(Schema $schema): Schema
{
return $schema
->schema([
Section::make('Perbandingan Data')
->schema([
TextEntry::make('comparison')
->hiddenLabel()
->html()
->state(function (DataChangeRequest $record): string {
$old = $record->old_data ?? [];
$new = $record->new_data ?? [];
if (! is_array($old) || ! is_array($new)) {
return '<em>Format data tidak valid.</em>';
}
$renderValue = function ($value) {
if (empty($value)) {
return '<em>Kosong</em>';
}
$paths = is_array($value) ? $value : [$value];
$isProbablyPath = false;
foreach ($paths as $path) {
if (is_string($path) && (str_contains($path, '/') || str_contains($path, '\\'))) {
$isProbablyPath = true;
break;
}
}
if ($isProbablyPath) {
return collect($paths)
->map(function ($path) {
if (! is_string($path)) {
return e(json_encode($path));
}
$url = Storage::disk(config('filesystems.default'))->url($path);
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if (in_array($extension, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'])) {
return "<a href='{$url}' target='_blank'><img src='{$url}' style='max-height:100px; border-radius:8px; margin-top:4px; border:1px solid #e5e7eb;'></a>";
}
if ($extension === 'pdf') {
return "
<div style='margin-top:4px;'>
<iframe src='{$url}' style='width:100%; height:300px; border:1px solid #e5e7eb; border-radius:8px;'></iframe>
<a href='{$url}' target='_blank' style='display:block; margin-top:4px; color:#3b82f6; text-decoration:underline; font-size:0.75rem;'>↗️ Buka PDF di Tab Baru</a>
</div>
";
}
return "<a href='{$url}' target='_blank' style='color:#3b82f6; text-decoration:underline; font-size:0.875rem;'>📎 Buka Dokumen (".strtoupper($extension).')</a>';
})
->implode('<br>');
}
return e(is_scalar($value) ? $value : json_encode($value));
};
// 1. Deletion Request
if (isset($new['__delete_request__']) && $new['__delete_request__'] === true) {
return "
<div style='background-color:#fee2e2; border:1px solid #f87171; color:#991b1b; padding:16px; border-radius:8px; margin-bottom:16px;'>
<div style='font-weight:700; font-size:1.125rem; margin-bottom:4px;'>🚮 Permohonan Penghapusan Data</div>
<p style='font-size:0.875rem;'>User mengajukan penghapusan permanen untuk data ini. Semua informasi terkait akan dihapus setelah disetujui.</p>
</div>
";
}
// 2. New Addition Request
if (empty($old)) {
$rows = collect($new)
->map(function ($value, $key) use ($renderValue) {
return sprintf(
"<div style='margin-bottom:12px; border-bottom:1px solid #f3f4f6; padding-bottom:8px'>
<div style='font-weight:600; color:#374151; margin-bottom:4px'>%s</div>
<div style='color:#16a34a; font-weight:500;'>[BARU]</div>
<div style='margin-top:4px'>%s</div>
</div>",
e($key),
$renderValue($value)
);
})
->implode('');
return "
<div style='background-color:#f0fdf4; border:1px solid #bbf7d0; color:#166534; padding:16px; border-radius:8px; margin-bottom:16px;'>
<div style='font-weight:700; font-size:1.125rem; margin-bottom:4px;'> Permohonan Penambahan Data</div>
<p style='font-size:0.875rem;'>Berikut adalah data baru yang akan dibuat.</p>
</div>
{$rows}
";
}
// 3. Standard Comparison (Update)
return collect($new)
->map(function ($newValue, $key) use ($old, $renderValue) {
$oldValue = $old[$key] ?? null;
if ($oldValue === $newValue) {
return null;
}
return sprintf(
"<div style='margin-bottom:16px; border-bottom:1px solid #f3f4f6; padding-bottom:12px'>
<div style='font-weight:600; color:#374151; margin-bottom:4px'>%s</div>
<div style='display:grid; grid-template-columns: 1fr 1fr; gap:16px'>
<div>
<span style='font-size:0.75rem; font-weight:700; color:#dc2626; text-transform:uppercase'>LAMA</span>
<div style='margin-top:2px'>%s</div>
</div>
<div>
<span style='font-size:0.75rem; font-weight:700; color:#16a34a; text-transform:uppercase'>BARU</span>
<div style='margin-top:2px'>%s</div>
</div>
</div>
</div>",
e($key),
$renderValue($oldValue),
$renderValue($newValue)
);
})
->filter()
->implode('');
})
->columnSpanFull(),
]),
])
->columns(1);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('user.name')
->label('Pemohon')
->searchable()
->sortable()
->description(fn (DataChangeRequest $record): ?string => $record?->entity?->name)
->visible(! auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)),
TextColumn::make('entity_type')
->label('Jenis Data')
->searchable()
->sortable()
->description(fn (DataChangeRequest $record): ?string => match (true) {
isset($record->new_data['__delete_request__']) => '🗑️ Penghapusan',
empty($record->old_data) => '✨ Penambahan',
default => '📝 Perubahan',
})
->formatStateUsing(fn (DataChangeRequest $record): ?string => match ($record->entity_type) {
Company::class => 'Perusahaan',
PartnerMedia::class => 'Media',
Journalist::class => 'Jurnalis',
default => 'Lainnya',
})
->color(fn (DataChangeRequest $record): ?string => match ($record->entity_type) {
Company::class => 'success',
PartnerMedia::class => 'info',
Journalist::class => 'warning',
default => 'secondary',
})
->badge(),
TextColumn::make('status')
->label('Status')
->searchable()
->sortable()
->badge()
->formatStateUsing(fn (DataChangeStatus $state): string => $state->getLabel())
->color(fn (DataChangeStatus $state): string => $state->getColor())
->description(fn (DataChangeRequest $record): ?string => $record?->review ? 'Oleh: '.$record->review?->reviewer?->name : ''),
TextColumn::make('change_reason')
->label('Alasan Perubahan')
->limit(50),
TextColumn::make('review.note')
->label('Alasan Penolakan')
->limit(50),
TextColumn::make('created_at')
->label('Tgl Permohonan')
->date()
->sortable()
->dateTime('l, d F Y H:i:s')
->wrap(),
])
->filters([
TrashedFilter::make()
->native(false)
->visible(fn () => auth()->user()?->hasRole('Developer')),
])
->recordActions([
ViewAction::make(),
AcceptAction::make('accept'),
RejectAction::make('reject'),
])
->toolbarActions([
BulkActionGroup::make([
...DefaultBulkActions::make('permohonan perubahan data'),
]),
])
->emptyStateIcon(Heroicon::DocumentText)
->emptyStateDescription('Belum ada permohonan perubahan data.')
->defaultSort('created_at', 'desc')
->deferFilters(false)
->modifyQueryUsing(function (Builder $query): void {
$query->when(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value), function (Builder $q): Builder {
return $q->where('user_id', auth()->id());
});
});
}
public static function getPages(): array
{
return [
'index' => ManageDataChanges::route('/'),
'view' => ViewDataChange::route('/{record}'),
];
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Filament\Resources\Manage\DataChanges\Pages;
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
use Filament\Resources\Pages\ListRecords;
class ManageDataChanges extends ListRecords
{
protected static string $resource = DataChangesResource::class;
protected static ?string $title = 'Permohonan Perubahan Data';
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Manage\DataChanges\Pages;
use App\Filament\Actions\BackAction;
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
use Filament\Resources\Pages\ViewRecord;
class ViewDataChange extends ViewRecord
{
protected static string $resource = DataChangesResource::class;
protected function getHeaderActions(): array
{
return [
BackAction::make()
->url(DataChangesResource::getUrl()),
];
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace App\Filament\Resources\Manage\Journalists\Actions;
use App\Enums\VerificationStatus;
use App\Filament\Actions\Cheerful\CreateAction;
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\DataChangeRequest;
use App\Models\Journalist;
use App\Models\User;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Support\Enums\Width;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class CreateJournalistAction extends CreateAction
{
protected function setUp(): void
{
parent::setUp();
$this->label('Tambah')
->modalHeading('Tambah Jurnalis')
->modalSubmitActionLabel('Simpan')
->modalCancelActionLabel('Batal')
->successNotification(null)
->extraModalFooterActions(fn (CreateAction $action): array => [
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
->label('Simpan dan Tambah Lagi'),
])
->modalWidth(Width::Large)
->using(function (array $data, string $model): Model {
$data['partner_media_id'] = auth()->user()->company?->partnerMedia?->id;
$journalist = $model::create($data);
$documents = [
'press_card_docs',
'ukw_certificate_docs',
];
foreach ($documents as $field) {
if (empty($data[$field])) {
continue;
}
foreach ((array) $data[$field] as $filePath) {
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
if (! file_exists($fullPath)) {
continue;
}
$journalist
->addMediaFromDisk($filePath, config('filesystems.default'))
->preservingOriginal()
->withCustomProperties([
'feature' => 'journalists',
'date' => now()->toDateString(),
'doc_type' => str($field)
->replace('_docs', '')
->slug('-'),
])
->toMediaCollection('journalists');
}
}
// If company is already approved, addition of new journalist needs permohonan/review
if (auth()->user()->company?->status === VerificationStatus::APPROVED) {
$fieldLabels = Journalist::dataChangeEntryLabels();
$newFields = [];
foreach (['name', 'email', 'phone_number', 'press_card', 'ukw_certificate'] as $f) {
$label = $fieldLabels[$f] ?? $f;
$newFields[$label] = $data[$f] ?? '-';
}
foreach (['press_card_docs', 'ukw_certificate_docs'] as $df) {
$label = $fieldLabels[$df] ?? $df;
$newFields[$label] = $data[$df] ?? [];
}
$dataChangeRequest = DataChangeRequest::create([
'user_id' => auth()->id(),
'entity_type' => Journalist::class,
'entity_id' => $journalist->id,
'old_data' => [],
'new_data' => $newFields,
'change_reason' => 'Penambahan jurnalis baru',
]);
CheerfulNotification::success(
'Permohonan Berhasil 🎉',
'Data jurnalis baru telah dikirim dan sedang menunggu verifikasi admin ⏳'
)->send();
User::superAdmin()
->get()
->each(function ($admin) use ($dataChangeRequest): void {
$admin->notify(new BroadcastNotification([
'title' => 'Ada Penambahan Jurnalis Baru ✨',
'body' => 'Halooo Admin! 👋 '.auth()->user()->name.' baru saja menambahkan jurnalis baru. Yuk, cek detailnya! 🚀',
'action' => [
Action::make('view')
->label('Lihat')
->url(DataChangesResource::getUrl('view', ['record' => $dataChangeRequest->id])),
],
]));
});
} else {
CheerfulNotification::create()->send();
}
return $journalist;
});
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace App\Filament\Resources\Manage\Journalists\Actions;
use App\Enums\DataChangeStatus;
use App\Enums\RoleEnum;
use App\Enums\VerificationStatus;
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\DataChangeRequest;
use App\Models\Journalist;
use App\Models\User;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Textarea;
class DeleteJournalistAction extends DeleteAction
{
protected function setUp(): void
{
parent::setUp();
$this->successNotification(null);
$this->modalHeading(fn () => auth()->user()->company?->status === VerificationStatus::APPROVED && auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) ? 'Ajukan Penghapusan Jurnalis' : 'Hapus Jurnalis');
$this->schema(fn () => auth()->user()->company?->status === VerificationStatus::APPROVED && auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) ? [
Textarea::make('deletion_reason')
->label('Alasan Penghapusan')
->placeholder('Jelaskan alasan mengapa jurnalis ini dihapus...')
->required(),
] : []);
$this->action(function (Journalist $record, array $data): void {
$user = auth()->user();
$needsReview = $user->company?->status === VerificationStatus::APPROVED &&
$user->hasRole(RoleEnum::PERUSAHAAN->value);
if ($needsReview) {
$existingRequest = DataChangeRequest::where('entity_type', Journalist::class)
->where('entity_id', $record->id)
->where('status', DataChangeStatus::PENDING)
->exists();
if ($existingRequest) {
CheerfulNotification::warning(
'Pengajuan Sudah Ada ⏳',
'Masih ada pengajuan penghapusan jurnalis yang sedang menunggu verifikasi.'
)->send();
return;
}
$dataChangeRequest = DataChangeRequest::create([
'user_id' => $user->id,
'entity_type' => Journalist::class,
'entity_id' => $record->id,
'old_data' => $record->toArray(),
'new_data' => ['__delete_request__' => true],
'change_reason' => $data['deletion_reason'] ?? 'Penghapusan jurnalis',
]);
CheerfulNotification::success(
'Permohonan Berhasil 🎉',
'Permohonan penghapusan jurnalis telah dikirim dan sedang menunggu verifikasi admin ⏳'
)->send();
User::superAdmin()
->get()
->each(function ($admin) use ($user, $record, $dataChangeRequest): void {
$admin->notify(new BroadcastNotification([
'title' => 'Ada Pengajuan Penghapusan Jurnalis ✨',
'body' => 'Halooo Admin! 👋 '.$user->name.' baru saja mengajukan penghapusan jurnalis '.$record->name.'. Yuk, cek detailnya! 🚀',
'action' => [
Action::make('view')
->label('Lihat')
->url(DataChangesResource::getUrl('view', ['record' => $dataChangeRequest->id])),
],
]));
});
return;
}
$record->delete();
CheerfulNotification::delete()->send();
});
$this->visible(function (): bool {
$user = auth()->user();
if (! $user->hasRole(RoleEnum::PERUSAHAAN->value)) {
return true;
}
if (! $user->company?->partnerMedia) {
return false;
}
return true;
});
}
}

View File

@ -0,0 +1,234 @@
<?php
namespace App\Filament\Resources\Manage\Journalists\Actions;
use App\Enums\DataChangeStatus;
use App\Enums\RoleEnum;
use App\Enums\VerificationStatus;
use App\Filament\Resources\Manage\DataChanges\DataChangesResource;
use App\Filament\Support\CheerfulNotification;
use App\Models\DataChangeRequest;
use App\Models\Journalist;
use App\Models\User;
use App\Notifications\BroadcastNotification;
use Filament\Actions\Action;
use Filament\Actions\EditAction;
use Filament\Support\Contracts\HasLabel;
use Filament\Support\Enums\Width;
use Illuminate\Support\Facades\Storage;
class EditJournalistAction extends EditAction
{
protected function setUp(): void
{
parent::setUp();
$this->modalWidth(Width::Large)
->successNotification(null)
->fillForm(function (Journalist $journalist): array {
$data = $journalist->attributesToArray();
$documents = [
'press_card_docs',
'ukw_certificate_docs',
];
$mediaItems = $journalist->getMedia('journalists');
foreach ($documents as $docField) {
$docType = str($docField)->replace('_docs', '')->slug('-');
$media = $mediaItems
->where('custom_properties.doc_type', $docType)
->sortByDesc('created_at')
->first();
if ($media) {
$data[$docField] = [$media->getPathRelativeToRoot()];
}
}
return $data;
})
->using(function (Journalist $record, array $data): Journalist {
$needsReview = auth()->user()->company?->status === VerificationStatus::APPROVED &&
auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value);
if ($needsReview) {
$existingRequest = DataChangeRequest::where('entity_type', Journalist::class)
->where('entity_id', $record->id)
->where('status', DataChangeStatus::PENDING)
->exists();
if ($existingRequest) {
CheerfulNotification::warning(
'Pengajuan Sudah Ada ⏳',
'Masih ada pengajuan perubahan data yang sedang menunggu verifikasi.'
)->send();
return $record;
}
}
$fieldLabels = Journalist::dataChangeEntryLabels();
$editableFields = ['name', 'email', 'phone_number', 'press_card', 'ukw_certificate'];
$changedFields = [];
$oldDataArr = [];
$casts = $record->getCasts();
foreach ($editableFields as $field) {
$val = $data[$field] ?? null;
$oldVal = $record->{$field};
// If value is a raw scalar but should be an enum, resolve it for display
if (isset($casts[$field]) && ! ($val instanceof \BackedEnum) && enum_exists($casts[$field])) {
$enumClass = $casts[$field];
$val = $enumClass::tryFrom($val) ?? $val;
}
$displayVal = ($val instanceof HasLabel) ? $val->getLabel() : ($val instanceof \BackedEnum ? $val->value : $val);
$displayOldVal = ($oldVal instanceof HasLabel) ? $oldVal->getLabel() : ($oldVal instanceof \BackedEnum ? $oldVal->value : $oldVal);
if ($displayOldVal !== $displayVal) {
$label = $fieldLabels[$field] ?? $field;
$changedFields[$label] = $displayVal;
$oldDataArr[$label] = $displayOldVal;
}
}
$documentFields = ['press_card_docs', 'ukw_certificate_docs'];
$mediaItems = $record->getMedia('journalists');
foreach ($documentFields as $field) {
$newDocs = array_values((array) ($data[$field] ?? []));
sort($newDocs);
$docType = str($field)->replace('_docs', '')->slug('-');
$currentMedia = $mediaItems
->where('custom_properties.doc_type', $docType)
->sortByDesc('created_at')
->first();
$oldDocs = $currentMedia ? [$currentMedia->getPathRelativeToRoot()] : [];
$oldDocs = array_values($oldDocs);
sort($oldDocs);
if (json_encode($newDocs) !== json_encode($oldDocs)) {
$label = $fieldLabels[$field] ?? $field;
$changedFields[$label] = $newDocs;
$oldDataArr[$label] = $oldDocs;
}
}
if (empty($changedFields)) {
CheerfulNotification::info(
'Belum ada yang berubah ✨',
'Ubah data terlebih dulu lalu simpan kembali 💪'
)->send();
return $record;
}
if ($needsReview) {
$dataChangeRequest = DataChangeRequest::create([
'user_id' => auth()->id(),
'entity_type' => Journalist::class,
'entity_id' => $record->id,
'old_data' => $oldDataArr,
'new_data' => $changedFields,
'change_reason' => $data['change_reason'] ?? '-',
]);
CheerfulNotification::success(
'Permohonan Berhasil 🎉',
'Data sudah dikirim dan sedang menunggu verifikasi admin ⏳'
)->send();
User::superAdmin()
->get()
->each(function ($admin) use ($dataChangeRequest): void {
$admin->notify(new BroadcastNotification([
'title' => 'Ada Pengajuan Perubahan Jurnalis ✨',
'body' => 'Halooo Admin! 👋 '.auth()->user()->name.' baru saja mengajukan perubahan data jurnalis. Yuk, cek detailnya! 🚀',
'action' => [
Action::make('view')
->label('Lihat')
->url(DataChangesResource::getUrl('view', ['record' => $dataChangeRequest->id])),
],
]));
});
return $record;
}
$this->performSave($record, $data);
CheerfulNotification::update()->send(); // Manual success notification
return $record;
})
->visible(function (?Journalist $record): bool {
$user = auth()->user();
if (! $user->hasRole(RoleEnum::PERUSAHAAN->value)) {
return true;
}
if (! $user->company?->partnerMedia) {
return false;
}
return true;
});
}
private function performSave(Journalist $record, array $data): void
{
$record->update([
'name' => $data['name'],
'email' => $data['email'],
'phone_number' => $data['phone_number'],
'press_card' => $data['press_card'],
'ukw_certificate' => $data['ukw_certificate'],
]);
$documents = [
'press_card_docs',
'ukw_certificate_docs',
];
$existingMedia = $record->getMedia('journalists');
$existingPaths = $existingMedia->map(fn ($m) => $m->getPathRelativeToRoot())->toArray();
foreach ($documents as $field) {
if (empty($data[$field])) {
continue;
}
foreach ((array) $data[$field] as $filePath) {
// If this path is already from one of our media items, skip re-adding it
if (in_array($filePath, $existingPaths)) {
continue;
}
$fullPath = Storage::disk(config('filesystems.default'))->path($filePath);
if (! file_exists($fullPath)) {
continue;
}
$record
->addMediaFromDisk($filePath, config('filesystems.default'))
->preservingOriginal()
->withCustomProperties([
'feature' => 'journalists',
'date' => now()->toDateString(),
'doc_type' => str($field)
->replace('_docs', '')
->slug('-'),
])
->toMediaCollection('journalists');
}
}
}
}

View File

@ -0,0 +1,252 @@
<?php
namespace App\Filament\Resources\Manage\Journalists;
use App\Enums\RoleEnum;
use App\Enums\VerificationStatus;
use App\Filament\Actions\Cheerful\ForceDeleteAction;
use App\Filament\Actions\Cheerful\RestoreAction;
use App\Filament\Columns\TimestampColumns;
use App\Filament\Pages\Media;
use App\Filament\Resources\Manage\Journalists\Actions\DeleteJournalistAction;
use App\Filament\Resources\Manage\Journalists\Actions\EditJournalistAction;
use App\Filament\Resources\Manage\Journalists\Pages\ManageJournalists;
use App\Models\Journalist;
use Asmit\FilamentUpload\Forms\Components\AdvancedFileUpload;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use UnitEnum;
class JournalistResource extends Resource
{
protected static ?string $model = Journalist::class;
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::Identification;
protected static ?string $navigationLabel = 'Jurnalis';
protected static ?int $navigationSort = 4;
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $slug = 'manage/journalists';
public static function getNavigationBadge(): ?string
{
return static::getModel()::pending()
->when(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value), function (Builder $query): void {
$query->whereHas('partnerMedia.company', function (Builder $q): void {
$q->where('user_id', auth()->id());
});
})
->count() ?: null;
}
public static function form(Schema $schema): Schema
{
$documents = [
[
'title' => 'Dokumen Pers',
'text_name' => 'press_card',
'placeholder' => '*****',
'max' => 100,
'max_size' => 1024 * 10,
'file_name' => 'press_card_docs',
'accept' => ['application/pdf'],
'folder' => 'press-card/',
],
[
'title' => 'Sertifikat UKW',
'text_name' => 'ukw_certificate',
'placeholder' => '*****',
'max' => 100,
'max_size' => 1024 * 10,
'file_name' => 'ukw_certificate_docs',
'accept' => ['application/pdf'],
'folder' => 'ukw-certificate/',
],
];
return $schema
->components([
TextInput::make('name')
->label('Nama')
->placeholder('John Doe')
->autocomplete(false)
->autofocus()
->required()
->maxLength(100),
TextInput::make('email')
->label('Alamat Surel')
->placeholder('johndoe@example.com')
->autocomplete(false)
->required()
->maxLength(254)
->unique('journalists', 'email', ignoreRecord: true)
->email(),
TextInput::make('phone_number')
->label('Nomor Telepon')
->placeholder('08xx xxxx xxxx')
->autocomplete(false)
->required()
->maxLength(20)
->tel(),
Group::make()
->schema(
collect($documents)
->map(function (array $doc): Section {
return Section::make($doc['title'])
->schema([
TextInput::make($doc['text_name'])
->hiddenLabel()
->placeholder($doc['placeholder'])
->autocomplete(false)
->required()
->maxLength($doc['max']),
AdvancedFileUpload::make($doc['file_name'])
->hiddenLabel()
->disk(config('filesystems.default'))
->acceptedFileTypes($doc['accept'])
->directory(fn (): string => 'journalists/'.$doc['folder'].now()->toDateString())
->maxSize($doc['max_size'])
->required(),
]);
})
->toArray()
),
Section::make('Alasan Perubahan')
->schema([
Textarea::make('change_reason')
->label('Alasan')
->placeholder('Jelaskan alasan perubahan data ini...')
->rows(3)
->required(),
])
->visible(fn () => auth()->user()->company?->status === VerificationStatus::APPROVED && auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value)),
])
->columns(1);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Nama')
->searchable()
->sortable(),
TextColumn::make('email')
->label('Alamat Surel')
->searchable()
->sortable(),
TextColumn::make('phone_number')
->label('Nomor Telepon')
->searchable()
->sortable(),
TextColumn::make('press_card')
->label('Dokumen Pers')
->searchable()
->sortable()
->wrap(),
TextColumn::make('ukw_certificate')
->label('Sertifikat UKW')
->searchable()
->sortable()
->wrap(),
...TimestampColumns::make(),
])
->filters([
TrashedFilter::make()
->native(false)
->visible(fn () => auth()->user()?->hasRole('Developer')),
])
->recordActions([
EditJournalistAction::make('edit'),
DeleteJournalistAction::make('delete'),
ForceDeleteAction::make(),
RestoreAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
ForceDeleteBulkAction::make(),
RestoreBulkAction::make(),
]),
])
->emptyStateIcon(Heroicon::Identification)
->emptyStateDescription('Setelah Anda membubat data pertama, maka akan muncul disini.')
->defaultSort('created_at', 'desc')
->deferFilters(false)
->modifyQueryUsing(function (Builder $query): void {
if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) && ! auth()->user()->company?->partnerMedia) {
$query->whereRaw('1 = 0');
}
$query->when(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value), function (Builder $q): Builder {
return $q->whereHas('partnerMedia.company', function (Builder $q): void {
$q->where('user_id', auth()->id());
});
});
})
->when(auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) && ! auth()->user()->company?->partnerMedia, function (Table $table): Table {
return $table
->emptyStateHeading('Akses Dibatasi')
->emptyStateDescription('Silakan lengkapi data media Anda terlebih dahulu untuk dapat mengelola informasi jurnalis.')
->emptyStateIcon(Heroicon::LockClosed)
->emptyStateActions([
Action::make('complete_media')
->label('Lengkapi Data Media')
->url(Media::getUrl())
->button(),
]);
});
}
public static function getPages(): array
{
return [
'index' => ManageJournalists::route('/'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Filament\Resources\Manage\Journalists\Pages;
use App\Enums\RoleEnum;
use App\Filament\Resources\Manage\Journalists\Actions\CreateJournalistAction;
use App\Filament\Resources\Manage\Journalists\JournalistResource;
use Filament\Resources\Pages\ManageRecords;
class ManageJournalists extends ManageRecords
{
protected static string $resource = JournalistResource::class;
protected static ?string $title = 'Jurnalis';
protected function getHeaderActions(): array
{
if (auth()->user()->hasRole(RoleEnum::PERUSAHAAN->value) && ! auth()->user()->company?->partnerMedia) {
return [];
}
return [
CreateJournalistAction::make(),
];
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Filament\Resources\Manage\Partners\Pages;
use App\Filament\Resources\Manage\Partners\PartnerResource;
use Filament\Resources\Pages\ManageRecords;
class ManagePartners extends ManageRecords
{
protected static string $resource = PartnerResource::class;
protected static ?string $title = 'Rekanan';
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Manage\Partners\Pages;
use App\Filament\Actions\BackAction;
use App\Filament\Resources\Manage\Partners\PartnerResource;
use Filament\Resources\Pages\ViewRecord;
class ViewPartner extends ViewRecord
{
protected static string $resource = PartnerResource::class;
protected function getHeaderActions(): array
{
return [
BackAction::make()
->url(url()->previous()),
];
}
}

View File

@ -0,0 +1,369 @@
<?php
namespace App\Filament\Resources\Manage\Partners;
use App\Filament\Actions\DefaultBulkActions;
use App\Filament\Resources\Manage\Partners\Pages\ManagePartners;
use App\Filament\Resources\Manage\Partners\Pages\ViewPartner;
use App\Models\User;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\ViewAction;
use Filament\Infolists\Components\RepeatableEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\HtmlString;
use Joaopaulolndev\FilamentPdfViewer\Infolists\Components\PdfViewerEntry;
use UnitEnum;
class PartnerResource extends Resource
{
protected static ?string $model = User::class;
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::BuildingOffice2;
protected static ?string $navigationLabel = 'Rekanan';
protected static ?int $navigationSort = 1;
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $slug = 'manage/partners';
public static function infolist(Schema $schema): Schema
{
return $schema
->schema([
Section::make('Akun')
->schema([
Grid::make(3)
->schema([
TextEntry::make('name')
->label('Nama'),
TextEntry::make('email')
->label('Email'),
TextEntry::make('username')
->label('Nama Pengguna'),
TextEntry::make('is_active')
->label('Status')
->badge(),
TextEntry::make('email_verified_at')
->label('Email Verifikasi')
->placeholder('Akun Belum Diverifikasi')
->dateTime('l, d F Y H:i'),
]),
]),
Section::make('Perusahaan')
->schema([
Grid::make(3)
->schema([
TextEntry::make('company.name')
->label('Nama'),
TextEntry::make('company.email')
->label('Email'),
TextEntry::make('company.address')
->label('Alamat'),
TextEntry::make('company.director_name')
->label('Nama Direktur'),
TextEntry::make('company.validated_at')
->label('Tanggal Validasi')
->dateTime('d F Y H:i'),
TextEntry::make('company.rejection_reason')
->label('Alasan Penolakan')
->visible(fn (User $record): ?string => $record->company?->rejection_reason),
]),
Grid::make(3)
->schema([
Section::make('NIK Direktur')
->schema([
TextEntry::make('company.director_nik')
->hiddenLabel(),
TextEntry::make('company.director_nik_docs')
->hiddenLabel()
->html()
->getStateUsing(function (User $record): string {
$media = $record->company
?->getMedia('companies')
->where('custom_properties.doc_type', 'director-nik')
->sortByDesc('created_at')
->first();
if (! $media) {
return 'Tidak ada dokumen';
}
$url = $media->getUrl();
$mime = $media->mime_type;
if (str_contains($mime, 'image')) {
return '<img src="'.$url.'" alt="Dokumen NIK Direktur" style="max-width: 100%; height: auto; border-radius: 0.5rem;" />';
}
if (str_contains($mime, 'pdf')) {
return '<iframe src="'.$url.'" style="width: 100%; height: 500px; border: none;"></iframe>';
}
return '<a href="'.$url.'" target="_blank" class="text-primary-600 hover:text-primary-500 underline">Unduh Dokumen</a>';
}),
]),
Section::make('Akta Pendirian')
->schema([
TextEntry::make('company.deed_incorporation')
->hiddenLabel(),
self::fileEntry('company.deed_incorporation'),
]),
Section::make('SIUP')
->schema([
TextEntry::make('company.trade_license')
->hiddenLabel(),
self::fileEntry('company.trade_license'),
]),
Section::make('NPWP')
->schema([
TextEntry::make('company.tax_id_number')
->hiddenLabel(),
self::fileEntry('company.tax_id_number'),
]),
Section::make('PKP')
->schema([
TextEntry::make('company.taxable_enterprise')
->hiddenLabel(),
self::fileEntry('company.taxable_enterprise'),
]),
Section::make('SPT Tahunan')
->schema([
TextEntry::make('company.annual_tax_return')
->hiddenLabel(),
self::fileEntry('company.annual_tax_return'),
]),
Section::make('SK Domisili')
->schema([
TextEntry::make('company.domicile_certificate')
->hiddenLabel(),
self::fileEntry('company.domicile_certificate'),
]),
Section::make('Profil Perusahaan')
->schema([
TextEntry::make('company.profile')
->hiddenLabel(),
self::fileEntry('company.profile'),
]),
]),
]),
Section::make('Media')
->schema([
Grid::make(4)
->schema([
TextEntry::make('company.partnerMedia.name')
->label('Nama'),
TextEntry::make('company.partnerMedia.link')
->label('Tautan')
->url(fn (User $record): ?string => $record->company?->partnerMedia?->link, true),
TextEntry::make('company.partnerMedia.type')
->label('Jenis')
->badge(),
TextEntry::make('company.partnerMedia.classification')
->label('Klasifikasi')
->badge(),
TextEntry::make('company.partnerMedia.address')
->label('Alamat')
->columnSpanFull(),
]),
Grid::make(2)
->schema([
Section::make('Sertifikat Organisasi Kewartawanan')
->schema([
TextEntry::make('company.partnerMedia.journalism_organization')
->hiddenLabel(),
self::fileEntry('company.partnerMedia.journalism_organization', 'company.partnerMedia', 'partnerMedia'),
]),
Section::make('Sertifikat Dewan Pers')
->schema([
TextEntry::make('company.partnerMedia.press_council_certificate')
->hiddenLabel(),
self::fileEntry('company.partnerMedia.press_council_certificate', 'company.partnerMedia', 'partnerMedia'),
]),
]),
]),
Section::make('Jurnalis')
->schema([
RepeatableEntry::make('company.partnerMedia.journalists')
->hiddenLabel()
->schema([
Grid::make(3)
->schema([
TextEntry::make('name')
->label('Nama'),
TextEntry::make('email')
->label('Email'),
TextEntry::make('phone_number')
->label('No. Telepon'),
]),
Grid::make(2)
->schema([
Section::make('Kartu Pers')
->schema([
TextEntry::make('press_card')
->hiddenLabel(),
self::fileEntry('press_card', null, 'journalists'),
]),
Section::make('Sertifikat UKW')
->schema([
TextEntry::make('ukw_certificate')
->hiddenLabel(),
self::fileEntry('ukw_certificate', null, 'journalists'),
]),
]),
]),
]),
])
->columns(1);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('company.name')
->label('Perusahaan')
->searchable()
->sortable()
->description(
fn (User $record) => new HtmlString(
'Akun: '.$record->name.
'<br/>Direktur: '.$record->company?->director_name
)
),
TextColumn::make('company.partnerMedia.name')
->label('Media')
->searchable()
->sortable(),
TextColumn::make('company.journalists.name')
->label('Jurnalis')
->searchable()
->sortable()
->badge(),
TextColumn::make('company.status')
->label('Status')
->badge(),
TextColumn::make('created_at')
->label('Tgl Daftar')
->date()
->sortable()
->searchable()
->dateTime('l, d F Y H:i:s')
->wrap(),
])
->filters([
TrashedFilter::make()
->native(false)
->visible(fn () => auth()->user()?->hasRole('Developer')),
])
->recordActions([
ViewAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
...DefaultBulkActions::make('rekanan'),
]),
])
->emptyStateIcon(Heroicon::BuildingOffice2)
->emptyStateDescription('Setelah Anda membubat data pertama, maka akan muncul disini.')
->defaultSort('created_at', 'desc')
->deferFilters(false)
->modifyQueryUsing(function (Builder $query): void {
$query->role('Perusahaan')
->whereHas('company');
});
}
public static function getPages(): array
{
return [
'index' => ManagePartners::route('/'),
'view' => ViewPartner::route('/{record}'),
];
}
private static function fileEntry(
string $name,
?string $mediaOwner = 'company',
string $collection = 'companies'
): PdfViewerEntry {
return PdfViewerEntry::make($name)
->hiddenLabel()
->getStateUsing(function (Model $record) use ($name, $mediaOwner, $collection): ?string {
$owner = data_get($record, $mediaOwner);
if (! $owner) {
return null;
}
$docType = str($name)
->afterLast('.')
->slug('-');
return $owner->getMedia($collection)
->where('custom_properties.doc_type', (string) $docType)
->sortByDesc('created_at')
->first()
?->getUrl();
});
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace App\Filament\Resources\Master\Categories;
use App\Enums\IsActive;
use App\Filament\Actions\Cheerful\DeleteAction;
use App\Filament\Actions\Cheerful\EditAction;
use App\Filament\Actions\Cheerful\ForceDeleteAction;
use App\Filament\Actions\Cheerful\RestoreAction;
use App\Filament\Actions\DefaultBulkActions;
use App\Filament\Columns\TimestampColumns;
use App\Filament\Resources\Master\Categories\Pages\ManageCategories;
use App\Filament\Support\CheerfulNotification;
use App\Models\Category;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use UnitEnum;
class CategoryResource extends Resource
{
protected static ?string $model = Category::class;
protected static string|UnitEnum|null $navigationGroup = 'Master';
protected static string|BackedEnum|null $navigationIcon = Heroicon::ListBullet;
protected static ?string $navigationLabel = 'Kategori';
protected static ?int $navigationSort = 5;
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $slug = 'master/categories';
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label('Nama')
->placeholder('Ekonomi')
->autocomplete(false)
->autofocus()
->required()
->maxLength(50),
])
->columns(1);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Nama')
->searchable()
->sortable(),
ToggleColumn::make('is_active')
->label('Aktif?')
->sortable()
->getStateUsing(fn (Category $record): bool => $record->is_active === IsActive::ACTIVE)
->updateStateUsing(function (Category $record, bool $state): void {
$record->is_active = $state ? IsActive::ACTIVE : IsActive::INACTIVE;
$record->save();
CheerfulNotification::statusUpdated()->send();
}),
...TimestampColumns::make(),
])
->filters([
TrashedFilter::make()
->native(false)
->visible(fn () => auth()->user()?->hasRole('Developer')),
])
->recordActions([
EditAction::make()
->modalWidth(Width::Large),
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
...DefaultBulkActions::make('kategori'),
]),
])
->emptyStateIcon(Heroicon::ListBullet)
->emptyStateDescription('Setelah Anda membubat data pertama, maka akan muncul disini.')
->defaultSort('created_at', 'desc')
->deferFilters(false)
->reorderable('sort_order');
}
public static function getPages(): array
{
return [
'index' => ManageCategories::route('/'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Filament\Resources\Master\Categories\Pages;
use App\Filament\Actions\Cheerful\CreateAction;
use App\Filament\Resources\Master\Categories\CategoryResource;
use Filament\Resources\Pages\ManageRecords;
use Filament\Support\Enums\Width;
class ManageCategories extends ManageRecords
{
protected static string $resource = CategoryResource::class;
protected static ?string $title = 'Kategori';
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Tambah')
->modalHeading('Tambah Kategori')
->modalSubmitActionLabel('Simpan')
->modalCancelActionLabel('Batal')
->extraModalFooterActions(fn (CreateAction $action): array => [
$action->makeModalSubmitAction('createAknother', arguments: ['another' => true])
->label('Simpan dan Tambah Lagi'),
])
->modalWidth(Width::Large),
];
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace App\Filament\Resources\Master\Classifications;
use App\Enums\IsActive;
use App\Filament\Actions\Cheerful\DeleteAction;
use App\Filament\Actions\Cheerful\EditAction;
use App\Filament\Actions\Cheerful\ForceDeleteAction;
use App\Filament\Actions\Cheerful\RestoreAction;
use App\Filament\Actions\DefaultBulkActions;
use App\Filament\Columns\TimestampColumns;
use App\Filament\Resources\Master\Classifications\Pages\ManageClassifications;
use App\Filament\Support\CheerfulNotification;
use App\Models\Classification;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use UnitEnum;
class ClassificationResource extends Resource
{
protected static ?string $model = Classification::class;
protected static string|UnitEnum|null $navigationGroup = 'Master';
protected static string|BackedEnum|null $navigationIcon = Heroicon::BookOpen;
protected static ?string $navigationLabel = 'Klasifikasi';
protected static ?int $navigationSort = 1;
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $slug = 'master/classifications';
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label('Nama')
->placeholder('Pelaporan')
->autocomplete(false)
->autofocus()
->required()
->maxLength(50),
])
->columns(1);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Nama')
->searchable()
->sortable(),
ToggleColumn::make('is_active')
->label('Aktif?')
->sortable()
->getStateUsing(fn (Classification $record): bool => $record->is_active === IsActive::ACTIVE)
->updateStateUsing(function (Classification $record, bool $state): void {
$record->is_active = $state ? IsActive::ACTIVE : IsActive::INACTIVE;
$record->save();
CheerfulNotification::statusUpdated()->send();
}),
...TimestampColumns::make(),
])
->filters([
TrashedFilter::make()
->native(false)
->visible(fn () => auth()->user()?->hasRole('Developer')),
])
->recordActions([
EditAction::make()
->modalWidth(Width::Large),
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
...DefaultBulkActions::make('klasifikasi'),
]),
])
->emptyStateIcon(Heroicon::BookOpen)
->emptyStateDescription('Setelah Anda membubat data pertama, maka akan muncul disini.')
->defaultSort('created_at', 'desc')
->deferFilters(false)
->reorderable('sort_order');
}
public static function getPages(): array
{
return [
'index' => ManageClassifications::route('/'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Filament\Resources\Master\Classifications\Pages;
use App\Filament\Actions\Cheerful\CreateAction;
use App\Filament\Resources\Master\Classifications\ClassificationResource;
use Filament\Resources\Pages\ManageRecords;
use Filament\Support\Enums\Width;
class ManageClassifications extends ManageRecords
{
protected static string $resource = ClassificationResource::class;
protected static ?string $title = 'Klasifikasi';
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Tambah')
->modalHeading('Tambah Klasifikasi')
->modalSubmitActionLabel('Simpan')
->modalCancelActionLabel('Batal')
->extraModalFooterActions(fn (CreateAction $action): array => [
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
->label('Simpan dan Tambah Lagi'),
])
->modalWidth(Width::Large),
];
}
}

View File

@ -0,0 +1,134 @@
<?php
namespace App\Filament\Resources\Master\Departments;
use App\Enums\IsActive;
use App\Filament\Actions\Cheerful\DeleteAction;
use App\Filament\Actions\Cheerful\EditAction;
use App\Filament\Actions\Cheerful\ForceDeleteAction;
use App\Filament\Actions\Cheerful\RestoreAction;
use App\Filament\Actions\DefaultBulkActions;
use App\Filament\Columns\TimestampColumns;
use App\Filament\Resources\Master\Departments\Pages\ManageDepartments;
use App\Filament\Support\CheerfulNotification;
use App\Models\Department;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use UnitEnum;
class DepartmentResource extends Resource
{
protected static ?string $model = Department::class;
protected static string|UnitEnum|null $navigationGroup = 'Master';
protected static string|BackedEnum|null $navigationIcon = Heroicon::BuildingLibrary;
protected static ?string $navigationLabel = 'OPD';
protected static ?int $navigationSort = 6;
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $slug = 'master/departments';
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label('Nama')
->placeholder('Dinas Komunikasi dan Informatika')
->autocomplete(false)
->autofocus()
->required()
->maxLength(100),
TextInput::make('alias')
->placeholder('Diskominfo')
->autocomplete(false)
->required()
->maxLength(20),
])
->columns(1);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Nama')
->searchable()
->sortable(),
TextColumn::make('alias')
->searchable()
->sortable(),
ToggleColumn::make('is_active')
->label('Aktif?')
->sortable()
->getStateUsing(fn (Department $record): bool => $record->is_active === IsActive::ACTIVE)
->updateStateUsing(function (Department $record, bool $state): void {
$record->is_active = $state ? IsActive::ACTIVE : IsActive::INACTIVE;
$record->save();
CheerfulNotification::statusUpdated()->send();
}),
...TimestampColumns::make(),
])
->filters([
TrashedFilter::make()
->native(false)
->visible(fn () => auth()->user()?->hasRole('Developer')),
])
->recordActions([
EditAction::make()
->modalWidth(Width::Large),
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
...DefaultBulkActions::make('OPD'),
]),
])
->emptyStateIcon(Heroicon::BuildingLibrary)
->emptyStateDescription('Setelah Anda membubat data pertama, maka akan muncul disini.')
->defaultSort('created_at', 'desc')
->deferFilters(false)
->reorderable('sort_order');
}
public static function getPages(): array
{
return [
'index' => ManageDepartments::route('/'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Filament\Resources\Master\Departments\Pages;
use App\Filament\Actions\Cheerful\CreateAction;
use App\Filament\Resources\Master\Departments\DepartmentResource;
use Filament\Resources\Pages\ManageRecords;
use Filament\Support\Enums\Width;
class ManageDepartments extends ManageRecords
{
protected static string $resource = DepartmentResource::class;
protected static ?string $title = 'OPD';
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Tambah')
->modalHeading('Tambah OPD')
->modalSubmitActionLabel('Simpan')
->modalCancelActionLabel('Batal')
->extraModalFooterActions(fn (CreateAction $action): array => [
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
->label('Simpan dan Tambah Lagi'),
])
->modalWidth(Width::Large),
];
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace App\Filament\Resources\Master\Locations;
use App\Enums\IsActive;
use App\Filament\Actions\Cheerful\DeleteAction;
use App\Filament\Actions\Cheerful\EditAction;
use App\Filament\Actions\Cheerful\ForceDeleteAction;
use App\Filament\Actions\Cheerful\RestoreAction;
use App\Filament\Actions\DefaultBulkActions;
use App\Filament\Columns\TimestampColumns;
use App\Filament\Resources\Master\Locations\Pages\ManageLocations;
use App\Filament\Support\CheerfulNotification;
use App\Models\Location;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use UnitEnum;
class LocationResource extends Resource
{
protected static ?string $model = Location::class;
protected static string|UnitEnum|null $navigationGroup = 'Master';
protected static string|BackedEnum|null $navigationIcon = Heroicon::MapPin;
protected static ?string $navigationLabel = 'Lokus';
protected static ?int $navigationSort = 3;
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $slug = 'master/locations';
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label('Nama')
->placeholder('Purwakarta')
->autocomplete(false)
->autofocus()
->required()
->maxLength(50),
])
->columns(1);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Nama')
->searchable()
->sortable(),
ToggleColumn::make('is_active')
->label('Aktif?')
->sortable()
->getStateUsing(fn (Location $record): bool => $record->is_active === IsActive::ACTIVE)
->updateStateUsing(function (Location $record, bool $state): void {
$record->is_active = $state ? IsActive::ACTIVE : IsActive::INACTIVE;
$record->save();
CheerfulNotification::statusUpdated()->send();
}),
...TimestampColumns::make(),
])
->filters([
TrashedFilter::make()
->native(false)
->visible(fn () => auth()->user()?->hasRole('Developer')),
])
->recordActions([
EditAction::make()
->modalWidth(Width::Large),
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
...DefaultBulkActions::make('Lokus'),
]),
])
->emptyStateIcon(Heroicon::MapPin)
->emptyStateDescription('Setelah Anda membubat data pertama, maka akan muncul disini.')
->defaultSort('created_at', 'desc')
->deferFilters(false)
->reorderable('sort_order');
}
public static function getPages(): array
{
return [
'index' => ManageLocations::route('/'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Filament\Resources\Master\Locations\Pages;
use App\Filament\Actions\Cheerful\CreateAction;
use App\Filament\Resources\Master\Locations\LocationResource;
use Filament\Resources\Pages\ManageRecords;
use Filament\Support\Enums\Width;
class ManageLocations extends ManageRecords
{
protected static string $resource = LocationResource::class;
protected static ?string $title = 'Lokus';
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Tambah')
->modalHeading('Tambah Lokus')
->modalSubmitActionLabel('Simpan')
->modalCancelActionLabel('Batal')
->extraModalFooterActions(fn (CreateAction $action): array => [
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
->label('Simpan dan Tambah Lagi'),
])
->modalWidth(Width::Large),
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Filament\Resources\Master\SubClassifications\Pages;
use App\Filament\Actions\Cheerful\CreateAction;
use App\Filament\Resources\Master\SubClassifications\SubClassificationResource;
use Filament\Resources\Pages\ManageRecords;
use Filament\Support\Enums\Width;
class ManageSubClassifications extends ManageRecords
{
protected static string $resource = SubClassificationResource::class;
protected static ?string $title = 'Sub Klasifikasi';
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Tambah')
->modalHeading('Tambah Sub Klasifikasi')
->modalSubmitActionLabel('Simpan')
->modalCancelActionLabel('Batal')
->extraModalFooterActions(fn (CreateAction $action): array => [
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
->label('Simpan dan Tambah Lagi'),
])
->modalWidth(Width::Large),
];
}
}

View File

@ -0,0 +1,143 @@
<?php
namespace App\Filament\Resources\Master\SubClassifications;
use App\Enums\IsActive;
use App\Filament\Actions\Cheerful\DeleteAction;
use App\Filament\Actions\Cheerful\EditAction;
use App\Filament\Actions\Cheerful\ForceDeleteAction;
use App\Filament\Actions\Cheerful\RestoreAction;
use App\Filament\Actions\DefaultBulkActions;
use App\Filament\Columns\TimestampColumns;
use App\Filament\Resources\Master\SubClassifications\Pages\ManageSubClassifications;
use App\Models\Classification;
use App\Models\SubClassification;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use UnitEnum;
class SubClassificationResource extends Resource
{
protected static ?string $model = SubClassification::class;
protected static string|UnitEnum|null $navigationGroup = 'Master';
protected static string|BackedEnum|null $navigationIcon = Heroicon::QueueList;
protected static ?string $navigationLabel = 'Sub Klasifikasi';
protected static ?int $navigationSort = 2;
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $slug = 'master/sub-classifications';
public static function form(Schema $schema): Schema
{
return $schema
->components([
Select::make('classification_id')
->label('Klasifikasi')
->options(Classification::query()->active()->pluck('name', 'id'))
->searchable()
->native(false)
->required()
->rules(['exists:classifications,id'])
->autofocus(),
TextInput::make('name')
->label('Nama')
->placeholder('Korupsi')
->autocomplete(false)
->required()
->maxLength(50),
])
->columns(1);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('classification.name')
->label('Klasifikasi')
->searchable()
->sortable(),
TextColumn::make('name')
->label('Nama')
->searchable()
->sortable(),
ToggleColumn::make('is_active')
->label('Aktif?')
->sortable()
->getStateUsing(fn (SubClassification $record): bool => $record->is_active === IsActive::ACTIVE)
->updateStateUsing(function (SubClassification $record, bool $state): void {
$record->is_active = $state ? IsActive::ACTIVE : IsActive::INACTIVE;
$record->save();
Notification::make()
->title('Status Berubah! 🔄✨')
->body('Status data berhasil diperbarui. Perubahan langsung aktif ya! 👍')
->success()
->send();
}),
...TimestampColumns::make(),
])
->filters([
TrashedFilter::make()
->native(false)
->visible(fn () => auth()->user()?->hasRole('Developer')),
])
->recordActions([
EditAction::make()
->modalWidth(Width::Large),
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
...DefaultBulkActions::make('Sub Klasifikasi'),
]),
])
->emptyStateIcon(Heroicon::QueueList)
->emptyStateDescription('Setelah Anda membubat data pertama, maka akan muncul disini.')
->defaultSort('created_at', 'desc')
->deferFilters(false)
->reorderable('sort_order');
}
public static function getPages(): array
{
return [
'index' => ManageSubClassifications::route('/'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Filament\Resources\Master\SubLocations\Pages;
use App\Filament\Actions\Cheerful\CreateAction;
use App\Filament\Resources\Master\SubLocations\SubLocationResource;
use Filament\Resources\Pages\ManageRecords;
use Filament\Support\Enums\Width;
class ManageSubLocations extends ManageRecords
{
protected static string $resource = SubLocationResource::class;
protected static ?string $title = 'Sub Lokus';
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Tambah')
->modalHeading('Tambah Sub Lokus')
->modalSubmitActionLabel('Simpan')
->modalCancelActionLabel('Batal')
->extraModalFooterActions(fn (CreateAction $action): array => [
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
->label('Simpan dan Tambah Lagi'),
])
->modalWidth(Width::Large),
];
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace App\Filament\Resources\Master\SubLocations;
use App\Enums\IsActive;
use App\Filament\Actions\Cheerful\DeleteAction;
use App\Filament\Actions\Cheerful\EditAction;
use App\Filament\Actions\Cheerful\ForceDeleteAction;
use App\Filament\Actions\Cheerful\RestoreAction;
use App\Filament\Actions\DefaultBulkActions;
use App\Filament\Columns\TimestampColumns;
use App\Filament\Resources\Master\SubLocations\Pages\ManageSubLocations;
use App\Filament\Support\CheerfulNotification;
use App\Models\Location;
use App\Models\SubLocation;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use UnitEnum;
class SubLocationResource extends Resource
{
protected static ?string $model = SubLocation::class;
protected static string|UnitEnum|null $navigationGroup = 'Master';
protected static string|BackedEnum|null $navigationIcon = Heroicon::NumberedList;
protected static ?string $navigationLabel = 'Sub Lokus';
protected static ?int $navigationSort = 4;
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $slug = 'master/sub-locations';
public static function form(Schema $schema): Schema
{
return $schema
->components([
Select::make('location_id')
->label('Lokus')
->options(Location::query()->pluck('name', 'id'))
->searchable()
->native(false)
->required()
->rules(['exists:locations,id'])
->autofocus(),
TextInput::make('name')
->label('Nama')
->placeholder('Nagri Kaler')
->autocomplete(false)
->required()
->maxLength(50),
])
->columns(1);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('location.name')
->label('Lokus')
->searchable()
->sortable(),
TextColumn::make('name')
->label('Nama')
->searchable()
->sortable(),
ToggleColumn::make('is_active')
->label('Aktif?')
->sortable()
->getStateUsing(fn (SubLocation $record): bool => $record->is_active === IsActive::ACTIVE)
->updateStateUsing(function (SubLocation $record, bool $state): void {
$record->is_active = $state ? IsActive::ACTIVE : IsActive::INACTIVE;
$record->save();
CheerfulNotification::statusUpdated()->send();
}),
...TimestampColumns::make(),
])
->filters([
TrashedFilter::make()
->native(false)
->visible(fn () => auth()->user()?->hasRole('Developer')),
])
->recordActions([
EditAction::make()
->modalWidth(Width::Large),
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
...DefaultBulkActions::make('Sub Lokus'),
]),
])
->emptyStateIcon(Heroicon::NumberedList)
->emptyStateDescription('Setelah Anda membubat data pertama, maka akan muncul disini.')
->defaultSort('created_at', 'desc')
->deferFilters(false)
->reorderable('sort_order');
}
public static function getPages(): array
{
return [
'index' => ManageSubLocations::route('/'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Filament\Resources\Master\Themes\Pages;
use App\Filament\Actions\Cheerful\CreateAction;
use App\Filament\Resources\Master\Themes\ThemeResource;
use Filament\Resources\Pages\ManageRecords;
use Filament\Support\Enums\Width;
class ManageThemes extends ManageRecords
{
protected static string $resource = ThemeResource::class;
protected static ?string $title = 'Tema';
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Tambah')
->modalHeading('Tambah Tema')
->modalSubmitActionLabel('Simpan')
->modalCancelActionLabel('Batal')
->extraModalFooterActions(fn (CreateAction $action): array => [
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
->label('Simpan dan Tambah Lagi'),
])
->modalWidth(Width::Large),
];
}
}

Some files were not shown because too many files have changed in this diff Show More