feat: Add Budget Realization and Sentiment Trend charts to the analytics page, enhancing data visualization capabilities for budget and sentiment analysis.

This commit is contained in:
Yoga Pangestu 2026-03-27 10:58:20 +07:00
parent 1c063c4c79
commit 29948fb547
5 changed files with 236 additions and 9 deletions

View File

@ -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,
];
}

View File

@ -0,0 +1,75 @@
<?php
namespace App\Filament\Widgets\Charts;
use App\Models\CooperationPayment;
use BezhanSalleh\FilamentShield\Traits\HasWidgetShield;
use Filament\Widgets\ChartWidget;
use Filament\Widgets\Concerns\InteractsWithPageFilters;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class BudgetRealizationChart extends ChartWidget
{
use HasWidgetShield, InteractsWithPageFilters;
protected ?string $heading = 'Realisasi Anggaran Kerjasama';
protected static ?int $sort = 10;
protected int|string|array $columnSpan = 1;
protected ?string $pollingInterval = null;
protected function getData(): array
{
$startDate = $this->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';
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace App\Filament\Widgets\Charts;
use App\Enums\IssueSentiment;
use App\Models\IssueManagement;
use BezhanSalleh\FilamentShield\Traits\HasWidgetShield;
use Filament\Widgets\ChartWidget;
use Filament\Widgets\Concerns\InteractsWithPageFilters;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class SentimentTrendChart extends ChartWidget
{
use HasWidgetShield, InteractsWithPageFilters;
protected ?string $heading = 'Dinamika Sentimen Isu (Area)';
protected static ?int $sort = 18;
protected int|string|array $columnSpan = 1;
protected ?string $pollingInterval = null;
protected function getData(): array
{
$startDate = $this->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,
],
],
],
];
}
}

View File

@ -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()),
];
}

View File

@ -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",