diff --git a/app/Filament/Pages/Analytic.php b/app/Filament/Pages/Analytic.php index cc42bb5..87c08e9 100644 --- a/app/Filament/Pages/Analytic.php +++ b/app/Filament/Pages/Analytic.php @@ -4,6 +4,7 @@ use App\Enums\RoleEnum; use App\Filament\Support\CheerfulNotification; +use App\Filament\Widgets\Charts\BudgetRealizationChart; use App\Filament\Widgets\Charts\CompanyStatusChart; use App\Filament\Widgets\Charts\CooperationStatusChart; use App\Filament\Widgets\Charts\IssueDepartmentChart; @@ -15,6 +16,7 @@ use App\Filament\Widgets\Charts\MediaRegistrationChart; use App\Filament\Widgets\Charts\MediaTypeChart; use App\Filament\Widgets\Charts\PopularNewsChart; +use App\Filament\Widgets\Charts\SentimentTrendChart; use App\Filament\Widgets\Charts\TaskRealizationChart; use App\Filament\Widgets\Charts\TopMediaJournalistChart; use App\Filament\Widgets\Charts\VerificationPerformanceChart; @@ -123,6 +125,8 @@ public function getWidgets(): array TaskRealizationChart::class, TopMediaJournalistChart::class, VerificationPerformanceChart::class, + BudgetRealizationChart::class, + SentimentTrendChart::class, VisitorTrafficChart::class, ]; } diff --git a/app/Filament/Widgets/Charts/BudgetRealizationChart.php b/app/Filament/Widgets/Charts/BudgetRealizationChart.php new file mode 100644 index 0000000..cde4ce3 --- /dev/null +++ b/app/Filament/Widgets/Charts/BudgetRealizationChart.php @@ -0,0 +1,75 @@ +filters['startDate'] ?? Carbon::now()->startOfYear(); + $endDate = $this->filters['endDate'] ?? Carbon::now()->endOfYear(); + + $startDate = Carbon::parse($startDate)->startOfDay(); + $endDate = Carbon::parse($endDate)->endOfDay(); + + $data = CooperationPayment::query() + ->select( + DB::raw("DATE_FORMAT(payment_date, '%Y-%m') as month"), + DB::raw('SUM(amount) as total') + ) + ->whereBetween('payment_date', [$startDate, $endDate]) + ->groupBy('month') + ->orderBy('month') + ->get(); + + $labels = []; + $values = []; + + $current = clone $startDate; + while ($current <= $endDate) { + $monthStr = $current->format('Y-m'); + $labels[] = $current->translatedFormat('M Y'); + + $monthData = $data->where('month', $monthStr)->first(); + $values[] = $monthData ? (int) $monthData->total : 0; + + $current->addMonth(); + } + + return [ + 'datasets' => [ + [ + 'label' => 'Total Pembayaran (Rp)', + 'data' => $values, + 'fill' => 'start', + 'backgroundColor' => 'rgba(59, 130, 246, 0.1)', + 'borderColor' => '#3B82F6', + 'tension' => 0.3, + ], + ], + 'labels' => $labels, + ]; + } + + protected function getType(): string + { + return 'line'; + } +} diff --git a/app/Filament/Widgets/Charts/SentimentTrendChart.php b/app/Filament/Widgets/Charts/SentimentTrendChart.php new file mode 100644 index 0000000..5795992 --- /dev/null +++ b/app/Filament/Widgets/Charts/SentimentTrendChart.php @@ -0,0 +1,123 @@ +filters['startDate'] ?? Carbon::now()->subDays(30); + $endDate = $this->filters['endDate'] ?? Carbon::now(); + + $startDate = Carbon::parse($startDate)->startOfDay(); + $endDate = Carbon::parse($endDate)->endOfDay(); + + $query = IssueManagement::query() + ->select( + DB::raw('DATE(created_at) as date'), + 'issue', + DB::raw('count(*) as total') + ) + ->whereBetween('created_at', [$startDate, $endDate]) + ->groupBy('date', 'issue') + ->orderBy('date') + ->get(); + + $labels = []; + $current = clone $startDate; + while ($current <= $endDate) { + $labels[] = $current->translatedFormat('d M'); + $current->addDay(); + } + + $datasets = []; + foreach (IssueSentiment::cases() as $sentiment) { + $data = []; + $current = clone $startDate; + while ($current <= $endDate) { + $dateStr = $current->toDateString(); + $count = $query->where('date', $dateStr) + ->where('issue', $sentiment->value) + ->first(); + $data[] = $count ? $count->total : 0; + $current->addDay(); + } + + $datasets[] = [ + 'label' => $sentiment->getLabel(), + 'data' => $data, + 'fill' => 'origin', + 'backgroundColor' => $this->getHexColorRGBA($sentiment, 0.2), + 'borderColor' => $this->getHexColor($sentiment), + 'tension' => 0.4, + ]; + } + + return [ + 'datasets' => $datasets, + 'labels' => $labels, + ]; + } + + private function getHexColor(IssueSentiment $sentiment): string + { + return match ($sentiment) { + IssueSentiment::POSITIVE => '#10B981', + IssueSentiment::NEGATIVE => '#EF4444', + IssueSentiment::NEUTRAL => '#6B7280', + IssueSentiment::CRISIS => '#F97316', + }; + } + + private function getHexColorRGBA(IssueSentiment $sentiment, float $opacity): string + { + return match ($sentiment) { + IssueSentiment::POSITIVE => "rgba(16, 185, 129, {$opacity})", + IssueSentiment::NEGATIVE => "rgba(239, 68, 68, {$opacity})", + IssueSentiment::NEUTRAL => "rgba(107, 114, 128, {$opacity})", + IssueSentiment::CRISIS => "rgba(249, 115, 22, {$opacity})", + }; + } + + protected function getType(): string + { + return 'line'; + } + + protected function getOptions(): array + { + return [ + 'plugins' => [ + 'legend' => [ + 'display' => true, + ], + ], + 'scales' => [ + 'y' => [ + 'beginAtZero' => true, + 'ticks' => [ + 'stepSize' => 1, + ], + ], + ], + ]; + } +} diff --git a/app/Filament/Widgets/Stats/OverviewStats.php b/app/Filament/Widgets/Stats/OverviewStats.php index ccbbd66..80d55e9 100644 --- a/app/Filament/Widgets/Stats/OverviewStats.php +++ b/app/Filament/Widgets/Stats/OverviewStats.php @@ -3,9 +3,12 @@ namespace App\Filament\Widgets\Stats; use App\Enums\IsActive; +use App\Enums\IssueSentiment; use App\Models\Classification; +use App\Models\CooperationPayment; use App\Models\DataChangeRequest; use App\Models\Department; +use App\Models\IssueManagement; use App\Models\Journalist; use App\Models\Location; use App\Models\PartnerMedia; @@ -13,7 +16,6 @@ use App\Models\VerificationRequest; use App\Models\Visitor; use BezhanSalleh\FilamentShield\Traits\HasWidgetShield; -use Filament\Support\Icons\Heroicon; use Filament\Widgets\StatsOverviewWidget as BaseWidget; use Filament\Widgets\StatsOverviewWidget\Stat; @@ -36,42 +38,57 @@ protected function getStats(): array return [ Stat::make('Total Klasifikasi', Classification::active()->count()) ->description('Klasifikasi aktif') - ->descriptionIcon(Heroicon::OutlinedBookOpen) + ->descriptionIcon('heroicon-o-book-open') ->color($this->getRandomColor()), Stat::make('Total Lokus', Location::active()->count()) ->description('Lokus aktif') - ->descriptionIcon(Heroicon::OutlinedMapPin) + ->descriptionIcon('heroicon-o-map-pin') ->color($this->getRandomColor()), Stat::make('Total OPD', Department::where('is_active', IsActive::ACTIVE)->count()) ->description('OPD aktif') - ->descriptionIcon(Heroicon::OutlinedBuildingLibrary) + ->descriptionIcon('heroicon-o-building-library') ->color($this->getRandomColor()), Stat::make('Total Tema', Theme::active()->count()) ->description('Tema aktif') - ->descriptionIcon(Heroicon::OutlinedPuzzlePiece) + ->descriptionIcon('heroicon-o-puzzle-piece') + ->color($this->getRandomColor()), + + Stat::make('Total Anggaran', 'Rp '.number_format(CooperationPayment::sum('amount'), 0, ',', '.')) + ->description('Realisasi pembayaran') + ->descriptionIcon('heroicon-o-banknotes') + ->color('success'), + + Stat::make('Rasio Jurnalis', $average.' per Media') + ->description('Rata-rata jurnalis') + ->descriptionIcon('heroicon-o-users') ->color($this->getRandomColor()), Stat::make('Total Jurnalis', $totalJournalists) ->description('Jurnalis terdaftar') - ->descriptionIcon(Heroicon::OutlinedIdentification) + ->descriptionIcon('heroicon-o-identification') ->color($this->getRandomColor()), + Stat::make('Isu Kritis', IssueManagement::whereIn('issue', [IssueSentiment::NEGATIVE, IssueSentiment::CRISIS])->count()) + ->description('Perlu perhatian') + ->descriptionIcon('heroicon-o-exclamation-triangle') + ->color('danger'), + Stat::make('Antrean Verifikasi', VerificationRequest::pending()->count()) ->description('Butuh tindakan') - ->descriptionIcon(Heroicon::OutlinedPencilSquare) + ->descriptionIcon('heroicon-o-pencil-square') ->color($this->getRandomColor()), Stat::make('Antrean Perubahan', DataChangeRequest::pending()->count()) ->description('Perubahan data') - ->descriptionIcon(Heroicon::OutlinedDocumentText) + ->descriptionIcon('heroicon-o-document-text') ->color($this->getRandomColor()), Stat::make('Total Pengunjung', Visitor::count()) ->description('Total traffic') - ->descriptionIcon(Heroicon::OutlinedChartBar) + ->descriptionIcon('heroicon-o-chart-bar') ->color($this->getRandomColor()), ]; } diff --git a/database/seeders/ShieldSeeder.php b/database/seeders/ShieldSeeder.php index 64aa799..a3b0368 100644 --- a/database/seeders/ShieldSeeder.php +++ b/database/seeders/ShieldSeeder.php @@ -34,6 +34,8 @@ public function run(): void "Replicate:Announcement", "Reorder:Announcement", + "View:BudgetRealizationChart", + "ViewAny:Category", "View:Category", "Create:Category", @@ -212,6 +214,8 @@ public function run(): void "Replicate:Role", "Reorder:Role", + "View:SentimentTrendChart", + "ViewAny:SubClassification", "View:SubClassification", "Create:SubClassification", @@ -301,6 +305,8 @@ public function run(): void "Replicate:Announcement", "Reorder:Announcement", + "View:BudgetRealizationChart", + "ViewAny:Category", "View:Category", "Create:Category", @@ -465,6 +471,8 @@ public function run(): void "View:PopularNewsChart", + "View:SentimentTrendChart", + "ViewAny:SubClassification", "View:SubClassification", "Create:SubClassification",