Fix merge master to main
This commit is contained in:
commit
179ebee693
18
.editorconfig
Normal file
18
.editorconfig
Normal 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
65
.env.example
Normal 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
11
.gitattributes
vendored
Normal 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
26
.gitignore
vendored
Normal 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
320
COOPERATION_MODULE.md
Normal 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
|
||||
@ -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
91
README_CLEAN_CODE.md
Normal 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.
|
||||
61
app/Console/Commands/CrawlNewsCommand.php
Normal file
61
app/Console/Commands/CrawlNewsCommand.php
Normal 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}");
|
||||
}
|
||||
}
|
||||
130
app/Console/Commands/SimulateCooperationTime.php
Normal file
130
app/Console/Commands/SimulateCooperationTime.php
Normal 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;
|
||||
}
|
||||
}
|
||||
88
app/Console/Commands/UpdateCooperationStatusCommand.php
Normal file
88
app/Console/Commands/UpdateCooperationStatusCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
39
app/Enums/AnnouncementType.php
Normal file
39
app/Enums/AnnouncementType.php
Normal 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();
|
||||
}
|
||||
}
|
||||
42
app/Enums/ApprovalStatus.php
Normal file
42
app/Enums/ApprovalStatus.php
Normal 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
37
app/Enums/Channel.php
Normal 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
34
app/Enums/ContentType.php
Normal 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();
|
||||
}
|
||||
}
|
||||
48
app/Enums/CooperationStatus.php
Normal file
48
app/Enums/CooperationStatus.php
Normal 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();
|
||||
}
|
||||
}
|
||||
39
app/Enums/DataChangeDecision.php
Normal file
39
app/Enums/DataChangeDecision.php
Normal 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();
|
||||
}
|
||||
}
|
||||
42
app/Enums/DataChangeStatus.php
Normal file
42
app/Enums/DataChangeStatus.php
Normal 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();
|
||||
}
|
||||
}
|
||||
42
app/Enums/DecisionAdmin.php
Normal file
42
app/Enums/DecisionAdmin.php
Normal 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
39
app/Enums/IsActive.php
Normal 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();
|
||||
}
|
||||
}
|
||||
46
app/Enums/IssueSentiment.php
Normal file
46
app/Enums/IssueSentiment.php
Normal 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();
|
||||
}
|
||||
}
|
||||
32
app/Enums/MediaClassification.php
Normal file
32
app/Enums/MediaClassification.php
Normal 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
34
app/Enums/MediaType.php
Normal 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
41
app/Enums/NewsStatus.php
Normal 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
25
app/Enums/RoleEnum.php
Normal 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
31
app/Enums/SocialMedia.php
Normal 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();
|
||||
}
|
||||
}
|
||||
45
app/Enums/VerificationStatus.php
Normal file
45
app/Enums/VerificationStatus.php
Normal 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();
|
||||
}
|
||||
}
|
||||
22
app/Filament/Actions/BackAction.php
Normal file
22
app/Filament/Actions/BackAction.php
Normal 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();
|
||||
}
|
||||
}
|
||||
18
app/Filament/Actions/Cheerful/CreateAction.php
Normal file
18
app/Filament/Actions/Cheerful/CreateAction.php
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
18
app/Filament/Actions/Cheerful/DeleteAction.php
Normal file
18
app/Filament/Actions/Cheerful/DeleteAction.php
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
18
app/Filament/Actions/Cheerful/EditAction.php
Normal file
18
app/Filament/Actions/Cheerful/EditAction.php
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
18
app/Filament/Actions/Cheerful/ForceDeleteAction.php
Normal file
18
app/Filament/Actions/Cheerful/ForceDeleteAction.php
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
18
app/Filament/Actions/Cheerful/RestoreAction.php
Normal file
18
app/Filament/Actions/Cheerful/RestoreAction.php
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
34
app/Filament/Actions/DefaultBulkActions.php
Normal file
34
app/Filament/Actions/DefaultBulkActions.php
Normal 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()
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Filament/Columns/TimestampColumns.php
Normal file
34
app/Filament/Columns/TimestampColumns.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
95
app/Filament/Exports/ContentRecapExcelExport.php
Normal file
95
app/Filament/Exports/ContentRecapExcelExport.php
Normal 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);
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
98
app/Filament/Exports/CooperationExcelExport.php
Normal file
98
app/Filament/Exports/CooperationExcelExport.php
Normal 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);
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
98
app/Filament/Exports/IssueManagementExcelExport.php
Normal file
98
app/Filament/Exports/IssueManagementExcelExport.php
Normal 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);
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
109
app/Filament/Exports/MediaMonitoringExcelExport.php
Normal file
109
app/Filament/Exports/MediaMonitoringExcelExport.php
Normal 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);
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
301
app/Filament/Pages/AdminVerification.php
Normal file
301
app/Filament/Pages/AdminVerification.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
44
app/Filament/Pages/Auth/EmailVerification.php
Normal file
44
app/Filament/Pages/Auth/EmailVerification.php
Normal 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();
|
||||
}
|
||||
}
|
||||
151
app/Filament/Pages/Auth/Login.php
Normal file
151
app/Filament/Pages/Auth/Login.php
Normal 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([]);
|
||||
}
|
||||
}
|
||||
96
app/Filament/Pages/Auth/Registration.php
Normal file
96
app/Filament/Pages/Auth/Registration.php
Normal 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;
|
||||
}
|
||||
}
|
||||
481
app/Filament/Pages/Company.php
Normal file
481
app/Filament/Pages/Company.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
app/Filament/Pages/Dashboard.php
Normal file
64
app/Filament/Pages/Dashboard.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
416
app/Filament/Pages/Media.php
Normal file
416
app/Filament/Pages/Media.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
101
app/Filament/Pages/Settings/ManageGeneralSettings.php
Normal file
101
app/Filament/Pages/Settings/ManageGeneralSettings.php
Normal 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);
|
||||
}
|
||||
}
|
||||
61
app/Filament/Pages/Settings/ManageSeoSettings.php
Normal file
61
app/Filament/Pages/Settings/ManageSeoSettings.php
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
70
app/Filament/Pages/Settings/ManageSocialMediaSettings.php
Normal file
70
app/Filament/Pages/Settings/ManageSocialMediaSettings.php
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
160
app/Filament/Pages/Verification.php
Normal file
160
app/Filament/Pages/Verification.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
171
app/Filament/Resources/Manage/Contacts/ContactResource.php
Normal file
171
app/Filament/Resources/Manage/Contacts/ContactResource.php
Normal 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('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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])),
|
||||
],
|
||||
]));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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()),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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([]);
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
@ -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()),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
252
app/Filament/Resources/Manage/Journalists/JournalistResource.php
Normal file
252
app/Filament/Resources/Manage/Journalists/JournalistResource.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
20
app/Filament/Resources/Manage/Partners/Pages/ViewPartner.php
Normal file
20
app/Filament/Resources/Manage/Partners/Pages/ViewPartner.php
Normal 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()),
|
||||
];
|
||||
}
|
||||
}
|
||||
369
app/Filament/Resources/Manage/Partners/PartnerResource.php
Normal file
369
app/Filament/Resources/Manage/Partners/PartnerResource.php
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
124
app/Filament/Resources/Master/Categories/CategoryResource.php
Normal file
124
app/Filament/Resources/Master/Categories/CategoryResource.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
134
app/Filament/Resources/Master/Departments/DepartmentResource.php
Normal file
134
app/Filament/Resources/Master/Departments/DepartmentResource.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
124
app/Filament/Resources/Master/Locations/LocationResource.php
Normal file
124
app/Filament/Resources/Master/Locations/LocationResource.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
31
app/Filament/Resources/Master/Themes/Pages/ManageThemes.php
Normal file
31
app/Filament/Resources/Master/Themes/Pages/ManageThemes.php
Normal 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
Loading…
Reference in New Issue
Block a user