feat: Implement a new Analytic dashboard page with several data visualization widgets, add Company Status Chart, and expand Filament dashboard color options.
This commit is contained in:
parent
3dd098759c
commit
c493d066f4
114
app/Filament/Pages/Analytic.php
Normal file
114
app/Filament/Pages/Analytic.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Enums\RoleEnum;
|
||||
use App\Filament\Widgets\CooperationStatusChart;
|
||||
use App\Filament\Widgets\IssueLocationChart;
|
||||
use App\Filament\Widgets\JournalistRegistrationChart;
|
||||
use App\Filament\Widgets\MediaClassificationChart;
|
||||
use App\Filament\Widgets\MediaCooperationRankingChart;
|
||||
use App\Filament\Widgets\MediaRegistrationChart;
|
||||
use App\Filament\Widgets\MediaTypeChart;
|
||||
use App\Filament\Widgets\PopularNewsChart;
|
||||
use App\Filament\Widgets\TaskRealizationChart;
|
||||
use App\Filament\Widgets\TopMediaJournalistChart;
|
||||
use App\Filament\Widgets\VerificationPerformanceChart;
|
||||
use App\Filament\Widgets\VisitorTrafficChart;
|
||||
use BackedEnum;
|
||||
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 Analytic extends BaseDashboard
|
||||
{
|
||||
use HasFiltersForm, HasPageShield;
|
||||
|
||||
protected static string $routePath = '/analytic';
|
||||
|
||||
protected static ?string $slug = 'analytic';
|
||||
|
||||
protected static ?string $title = 'Statistik';
|
||||
|
||||
protected static ?string $navigationLabel = 'Statistik';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChartBar;
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
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
|
||||
{
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
app/Filament/Widgets/CompanyStatusChart.php
Normal file
61
app/Filament/Widgets/CompanyStatusChart.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Enums\VerificationStatus;
|
||||
use App\Models\Company;
|
||||
use App\Models\VerificationRequest;
|
||||
use BezhanSalleh\FilamentShield\Traits\HasWidgetShield;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
|
||||
class CompanyStatusChart extends ChartWidget
|
||||
{
|
||||
use HasWidgetShield;
|
||||
|
||||
protected ?string $heading = 'Status Verifikasi Perusahaan';
|
||||
|
||||
protected static ?int $sort = 2;
|
||||
|
||||
protected int|string|array $columnSpan = 1;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$data = VerificationRequest::query()
|
||||
->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';
|
||||
}
|
||||
}
|
||||
@ -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)];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user