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:
parent
1c063c4c79
commit
29948fb547
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
75
app/Filament/Widgets/Charts/BudgetRealizationChart.php
Normal file
75
app/Filament/Widgets/Charts/BudgetRealizationChart.php
Normal 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';
|
||||
}
|
||||
}
|
||||
123
app/Filament/Widgets/Charts/SentimentTrendChart.php
Normal file
123
app/Filament/Widgets/Charts/SentimentTrendChart.php
Normal 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,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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()),
|
||||
];
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user