From c493d066f4783dd83647a5e4ddcd8f1aa85ece13 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 26 Mar 2026 09:25:03 +0700 Subject: [PATCH] feat: Implement a new Analytic dashboard page with several data visualization widgets, add Company Status Chart, and expand Filament dashboard color options. --- app/Filament/Pages/Analytic.php | 114 ++++++++++++++++++ app/Filament/Pages/Dashboard.php | 57 ++------- app/Filament/Widgets/CompanyStatusChart.php | 61 ++++++++++ app/Filament/Widgets/OverviewStats.php | 106 +++++++++------- .../Filament/DashboardPanelProvider.php | 13 ++ database/seeders/ShieldSeeder.php | 16 +++ 6 files changed, 274 insertions(+), 93 deletions(-) create mode 100644 app/Filament/Pages/Analytic.php create mode 100644 app/Filament/Widgets/CompanyStatusChart.php diff --git a/app/Filament/Pages/Analytic.php b/app/Filament/Pages/Analytic.php new file mode 100644 index 0000000..af2a105 --- /dev/null +++ b/app/Filament/Pages/Analytic.php @@ -0,0 +1,114 @@ +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::OutlinedXMark) + ->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()); + } + } + + public function getWidgets(): array + { + return [ + CooperationStatusChart::class, + IssueLocationChart::class, + JournalistRegistrationChart::class, + MediaClassificationChart::class, + MediaCooperationRankingChart::class, + MediaRegistrationChart::class, + MediaTypeChart::class, + PopularNewsChart::class, + TaskRealizationChart::class, + TopMediaJournalistChart::class, + VerificationPerformanceChart::class, + VisitorTrafficChart::class, + ]; + } +} diff --git a/app/Filament/Pages/Dashboard.php b/app/Filament/Pages/Dashboard.php index cecc9fd..6e74b95 100644 --- a/app/Filament/Pages/Dashboard.php +++ b/app/Filament/Pages/Dashboard.php @@ -2,68 +2,27 @@ namespace App\Filament\Pages; -use App\Enums\RoleEnum; +use App\Filament\Widgets\CompanyStatusChart; +use App\Filament\Widgets\OverviewStats; 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; + use HasPageShield; + + protected static ?string $pollingInterval = null; public function getColumns(): int|array { return 2; } - 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 + public function getWidgets(): array { return [ - Action::make('reset') - ->label('Reset Filter') - ->icon(Heroicon::OutlinedXMark) - ->color('danger') - ->action(fn () => $this->resetFilters()) - ->visible(fn () => ! empty($this->filters['startDate']) || ! empty($this->filters['endDate'])), + OverviewStats::class, + CompanyStatusChart::class, ]; } - - public function resetFilters(): void - { - $this->filters['startDate'] = null; - $this->filters['endDate'] = null; - - if (method_exists($this, 'getFiltersSessionKey')) { - session()->forget($this->getFiltersSessionKey()); - } - } } diff --git a/app/Filament/Widgets/CompanyStatusChart.php b/app/Filament/Widgets/CompanyStatusChart.php new file mode 100644 index 0000000..5cc0375 --- /dev/null +++ b/app/Filament/Widgets/CompanyStatusChart.php @@ -0,0 +1,61 @@ +selectRaw('status, count(*) as total') + ->groupBy('status') + ->get(); + + $notSubmittedCount = Company::doesntHave('verificationRequest')->count(); + + $labels = ['Belum Mengajukan']; + $counts = [$notSubmittedCount]; + $colors = ['#94a3b8']; // Slate 400 + + foreach (VerificationStatus::cases() as $status) { + $labels[] = $status->getLabel(); + $counts[] = $data->where('status', $status)->first()?->total ?? 0; + $colors[] = match ($status) { + VerificationStatus::PENDING => '#f59e0b', // Amber 500 + VerificationStatus::APPROVED => '#10b981', // Emerald 500 + VerificationStatus::NEED_REVISION => '#0ea5e9', // Sky 500 + VerificationStatus::REJECTED => '#f43f5e', // Rose 500 + }; + } + + return [ + 'datasets' => [ + [ + 'label' => 'Jumlah Perusahaan', + 'data' => $counts, + 'backgroundColor' => $colors, + ], + ], + 'labels' => $labels, + ]; + } + + protected function getType(): string + { + return 'pie'; + } +} diff --git a/app/Filament/Widgets/OverviewStats.php b/app/Filament/Widgets/OverviewStats.php index 662580c..05e9d5a 100644 --- a/app/Filament/Widgets/OverviewStats.php +++ b/app/Filament/Widgets/OverviewStats.php @@ -2,93 +2,111 @@ namespace App\Filament\Widgets; -use App\Models\Company; +use App\Enums\IsActive; +use App\Models\Classification; use App\Models\DataChangeRequest; +use App\Models\Department; use App\Models\Journalist; +use App\Models\Location; use App\Models\PartnerMedia; +use App\Models\Theme; use App\Models\VerificationRequest; use App\Models\Visitor; use BezhanSalleh\FilamentShield\Traits\HasWidgetShield; use Filament\Support\Icons\Heroicon; -use Filament\Widgets\Concerns\InteractsWithPageFilters; use Filament\Widgets\StatsOverviewWidget as BaseWidget; use Filament\Widgets\StatsOverviewWidget\Stat; class OverviewStats extends BaseWidget { - use HasWidgetShield, InteractsWithPageFilters; + use HasWidgetShield; protected static ?int $sort = 1; protected int|string|array $columnSpan = 'full'; + protected ?string $pollingInterval = null; + protected function getStats(): array { - $startDate = $this->filters['startDate'] ?? null; - $endDate = $this->filters['endDate'] ?? null; - - $totalJournalists = Journalist::when($startDate, fn ($q) => $q->whereDate('created_at', '>=', $startDate)) - ->when($endDate, fn ($q) => $q->whereDate('created_at', '<=', $endDate)) - ->count(); - - $totalMedia = PartnerMedia::when($startDate, fn ($q) => $q->whereDate('created_at', '>=', $startDate)) - ->when($endDate, fn ($q) => $q->whereDate('created_at', '<=', $endDate)) - ->count(); - + $totalJournalists = Journalist::count(); + $totalMedia = PartnerMedia::count(); $average = $totalMedia > 0 ? round($totalJournalists / $totalMedia, 1) : 0; return [ - Stat::make('Total Perusahaan', Company::when($startDate, fn ($q) => $q->whereDate('created_at', '>=', $startDate)) - ->when($endDate, fn ($q) => $q->whereDate('created_at', '<=', $endDate)) - ->count()) - ->description('Perusahaan terdaftar') - ->descriptionIcon(Heroicon::OutlinedBuildingOffice2) - ->color('primary'), + Stat::make('Total Klasifikasi', Classification::active()->count()) + ->description('Klasifikasi aktif') + ->descriptionIcon(Heroicon::OutlinedBookOpen) + ->color($this->getRandomColor()), + + Stat::make('Total Lokus', Location::active()->count()) + ->description('Lokus aktif') + ->descriptionIcon(Heroicon::OutlinedMapPin) + ->color($this->getRandomColor()), + + Stat::make('Total OPD', Department::where('is_active', IsActive::ACTIVE)->count()) + ->description('OPD aktif') + ->descriptionIcon(Heroicon::OutlinedBuildingLibrary) + ->color($this->getRandomColor()), + + Stat::make('Total Tema', Theme::active()->count()) + ->description('Tema aktif') + ->descriptionIcon(Heroicon::OutlinedPuzzlePiece) + ->color($this->getRandomColor()), Stat::make('Media Terverifikasi', PartnerMedia::whereHas('company.verificationRequest', function ($q) { $q->approved(); - }) - ->when($startDate, fn ($q) => $q->whereDate('created_at', '>=', $startDate)) - ->when($endDate, fn ($q) => $q->whereDate('created_at', '<=', $endDate)) - ->count()) + })->count()) ->description('Media aktif') ->descriptionIcon(Heroicon::OutlinedCheckBadge) - ->color('success'), + ->color($this->getRandomColor()), - Stat::make('Total Jurnalis', Journalist::when($startDate, fn ($q) => $q->whereDate('created_at', '>=', $startDate)) - ->when($endDate, fn ($q) => $q->whereDate('created_at', '<=', $endDate)) - ->count()) + Stat::make('Total Jurnalis', $totalJournalists) ->description('Jurnalis terdaftar') ->descriptionIcon(Heroicon::OutlinedIdentification) - ->color('info'), + ->color($this->getRandomColor()), - Stat::make('Antrean Verifikasi', VerificationRequest::pending() - ->when($startDate, fn ($q) => $q->whereDate('created_at', '>=', $startDate)) - ->when($endDate, fn ($q) => $q->whereDate('created_at', '<=', $endDate)) - ->count()) + Stat::make('Antrean Verifikasi', VerificationRequest::pending()->count()) ->description('Butuh tindakan') ->descriptionIcon(Heroicon::OutlinedPencilSquare) - ->color('warning'), + ->color($this->getRandomColor()), - Stat::make('Antrean Perubahan', DataChangeRequest::pending() - ->when($startDate, fn ($q) => $q->whereDate('created_at', '>=', $startDate)) - ->when($endDate, fn ($q) => $q->whereDate('created_at', '<=', $endDate)) - ->count()) + Stat::make('Antrean Perubahan', DataChangeRequest::pending()->count()) ->description('Perubahan data') ->descriptionIcon(Heroicon::OutlinedDocumentText) - ->color('warning'), + ->color($this->getRandomColor()), - Stat::make('Total Pengunjung', Visitor::when($startDate, fn ($q) => $q->whereDate('date', '>=', $startDate)) - ->when($endDate, fn ($q) => $q->whereDate('date', '<=', $endDate)) - ->count()) + Stat::make('Total Pengunjung', Visitor::count()) ->description('Total traffic') ->descriptionIcon(Heroicon::OutlinedChartBar) - ->color('info'), + ->color($this->getRandomColor()), Stat::make('Rata-rata Jurnalis / Media', $average) ->description('Rasio sebaran jurnalis') ->descriptionIcon(Heroicon::OutlinedUsers) - ->color('primary'), + ->color($this->getRandomColor()), ]; } + + private array $availableColors = [ + 'primary', + 'success', + 'warning', + 'danger', + 'info', + 'fuchsia', + 'indigo', + 'violet', + 'orange', + 'cyan', + 'emerald', + 'amber', + 'rose', + 'lime', + ]; + + private function getRandomColor(): string + { + return $this->availableColors[array_rand($this->availableColors)]; + } } diff --git a/app/Providers/Filament/DashboardPanelProvider.php b/app/Providers/Filament/DashboardPanelProvider.php index 0915342..16485cc 100644 --- a/app/Providers/Filament/DashboardPanelProvider.php +++ b/app/Providers/Filament/DashboardPanelProvider.php @@ -60,7 +60,20 @@ public function panel(Panel $panel): Panel ->emailVerification(EmailVerification::class) ->colors([ 'primary' => Color::Blue, + 'success' => Color::Emerald, + 'warning' => Color::Amber, + 'danger' => Color::Rose, + 'info' => Color::Sky, 'slate' => Color::Slate, + 'fuchsia' => Color::Fuchsia, + 'indigo' => Color::Indigo, + 'violet' => Color::Violet, + 'orange' => Color::Orange, + 'cyan' => Color::Cyan, + 'emerald' => Color::Emerald, + 'amber' => Color::Amber, + 'rose' => Color::Rose, + 'lime' => Color::Lime, ]) ->breadcrumbs(false) ->maxContentWidth(Width::Full) diff --git a/database/seeders/ShieldSeeder.php b/database/seeders/ShieldSeeder.php index 94b0c08..602f0bc 100644 --- a/database/seeders/ShieldSeeder.php +++ b/database/seeders/ShieldSeeder.php @@ -18,6 +18,8 @@ public function run(): void "name": "Developer", "guard_name": "web", "permissions": [ + "View:Analytic", + "View:AdminVerification", "ViewAny:Announcement", @@ -44,6 +46,8 @@ public function run(): void "Replicate:Category", "Reorder:Category", + "View:CompanyStatusChart", + "ViewAny:Classification", "View:Classification", "Create:Classification", @@ -277,6 +281,8 @@ public function run(): void "name": "Administrator", "guard_name": "web", "permissions": [ + "View:Analytic", + "View:AdminVerification", "ViewAny:Announcement", @@ -303,6 +309,8 @@ public function run(): void "Replicate:Category", "Reorder:Category", + "View:CompanyStatusChart", + "ViewAny:Classification", "View:Classification", "Create:Classification", @@ -522,6 +530,8 @@ public function run(): void "name": "Admin Monitoring", "guard_name": "web", "permissions": [ + "View:Analytic", + "ViewAny:Classification", "View:Classification", "Create:Classification", @@ -625,6 +635,8 @@ public function run(): void "name": "Admin Konten", "guard_name": "web", "permissions": [ + "View:Analytic", + "ViewAny:Category", "View:Category", @@ -681,6 +693,8 @@ public function run(): void "name": "Admin Keuangan", "guard_name": "web", "permissions": [ + "View:Analytic", + "ViewAny:Cooperation", "View:Cooperation", @@ -691,6 +705,8 @@ public function run(): void "name": "Perusahaan", "guard_name": "web", "permissions": [ + "View:Analytic", + "ViewAny:Cooperation", "View:Cooperation",