feat: Implement role-based dashboard content visibility and consolidate view permissions under 'Analisys'.

This commit is contained in:
Yoga Pangestu 2026-03-09 19:32:05 +07:00
parent 1eba57cb6b
commit 9619912f2f
4 changed files with 229 additions and 204 deletions

View File

@ -12,6 +12,7 @@
use App\Models\Order; use App\Models\Order;
use App\Models\Payroll; use App\Models\Payroll;
use BackedEnum; use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Carbon\Carbon; use Carbon\Carbon;
use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
@ -25,7 +26,7 @@
class Analisys extends Page implements HasForms class Analisys extends Page implements HasForms
{ {
use InteractsWithForms; use HasPageShield, InteractsWithForms;
public ?array $data = []; public ?array $data = [];

View File

@ -2,6 +2,7 @@
namespace App\Filament\Pages; namespace App\Filament\Pages;
use App\Enums\RoleEnum;
use App\Models\Customer; use App\Models\Customer;
use App\Models\DelayedGood; use App\Models\DelayedGood;
use App\Models\EggCollectionItem; use App\Models\EggCollectionItem;
@ -10,6 +11,7 @@
use App\Models\Order; use App\Models\Order;
use App\Models\Payroll; use App\Models\Payroll;
use Filament\Pages\Dashboard as BaseDashboard; use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Widgets\AccountWidget;
class Dashboard extends BaseDashboard class Dashboard extends BaseDashboard
{ {
@ -17,6 +19,13 @@ class Dashboard extends BaseDashboard
protected string $view = 'filament.pages.dashboard'; protected string $view = 'filament.pages.dashboard';
public function getHeaderWidgets(): array
{
return [
AccountWidget::class,
];
}
public function getViewData(): array public function getViewData(): array
{ {
$formatNumber = fn ($val) => number_format((float) $val, $val == floor($val) ? 0 : 2, ',', '.'); $formatNumber = fn ($val) => number_format((float) $val, $val == floor($val) ? 0 : 2, ',', '.');
@ -118,6 +127,7 @@ public function getViewData(): array
->get(); ->get();
return [ return [
'isAdministrator' => auth()->user()->hasRole(RoleEnum::ADMINISTRATOR->value),
'mainStats' => $mainStats, 'mainStats' => $mainStats,
'totalEggsByUnit' => $totalEggsByUnit, 'totalEggsByUnit' => $totalEggsByUnit,
'totalDelayedByUnit' => $delayedStatsByUnit, 'totalDelayedByUnit' => $delayedStatsByUnit,

View File

@ -22,11 +22,7 @@ public function run(): void
"name": "Developer", "name": "Developer",
"guard_name": "web", "guard_name": "web",
"permissions" : [ "permissions" : [
"View:StatsOverview", "View:Analisys",
"View:EggProductionChart",
"View:OrderTrendChart",
"View:RevenueExpenseChart",
"View:TopCustomersChart",
"ViewAny:Customer", "ViewAny:Customer",
"View:Customer", "View:Customer",
@ -189,11 +185,7 @@ public function run(): void
"name": "Pemilik", "name": "Pemilik",
"guard_name": "web", "guard_name": "web",
"permissions" : [ "permissions" : [
"View:StatsOverview", "View:Analisys",
"View:EggProductionChart",
"View:OrderTrendChart",
"View:RevenueExpenseChart",
"View:TopCustomersChart",
"ViewAny:Customer", "ViewAny:Customer",
"View:Customer", "View:Customer",

View File

@ -1,220 +1,242 @@
<x-filament-panels::page> <x-filament-panels::page>
<div class="space-y-6 pb-12"> @if (!$isAdministrator)
{{-- Main Stats Grid --}}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4"> <div class="space-y-6 pb-12">
@foreach ($mainStats as $stat) {{-- Main Stats Grid --}}
<div <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-4 transition hover:shadow-md border-b-2"> @foreach ($mainStats as $stat)
<div class="flex flex-col gap-1"> <div
<span class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-4 transition hover:shadow-md border-b-2">
class="text-[10px] font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider">{{ $stat['label'] }}</span> <div class="flex flex-col gap-1">
<div class="flex items-baseline gap-1"> <span
@if ($stat['prefix']) class="text-[10px] font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider">{{ $stat['label'] }}</span>
<span <div class="flex items-baseline gap-1">
class="text-xs font-semibold text-gray-400 dark:text-gray-500">{{ $stat['prefix'] }}</span> @if ($stat['prefix'])
@endif <span
<span class="text-xl font-bold text-gray-900 dark:text-white">{{ $stat['value'] }}</span> class="text-xs font-semibold text-gray-400 dark:text-gray-500">{{ $stat['prefix'] }}</span>
@endif
<span class="text-xl font-bold text-gray-900 dark:text-white">{{ $stat['value'] }}</span>
</div>
<span
class="text-[9px] text-gray-900 dark:text-gray-600 uppercase">{{ $stat['desc'] }}</span>
</div> </div>
<span class="text-[9px] text-gray-900 dark:text-gray-600 uppercase">{{ $stat['desc'] }}</span> </div>
@endforeach
</div>
{{-- Distribution Row --}}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
<x-heroicon-o-arrows-right-left class="w-4 h-4 text-primary-500" />
Sumber Pemasukan
</h3>
<div class="h-64 flex flex-col items-center">
<canvas id="revenueSplitChart"></canvas>
</div> </div>
</div> </div>
@endforeach
</div>
{{-- Distribution Row --}}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
<x-heroicon-o-arrows-right-left class="w-4 h-4 text-primary-500" />
Sumber Pemasukan
</h3>
<div class="h-64 flex flex-col items-center">
<canvas id="revenueSplitChart"></canvas>
</div>
</div>
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
<x-heroicon-o-chart-pie class="w-4 h-4 text-primary-500" />
Alokasi Pengeluaran
</h3>
<div class="h-64 flex flex-col items-center">
<canvas id="expenseChart"></canvas>
</div>
</div>
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
<x-heroicon-o-shopping-bag class="w-4 h-4 text-primary-500" />
Penjualan per Satuan
</h3>
<div class="h-64 flex flex-col items-center">
<canvas id="salesUnitChart"></canvas>
</div>
</div>
</div>
{{-- Units Breakdown --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
{{-- Production --}}
<div
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm overflow-hidden">
<div <div
class="px-5 py-3 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50 flex items-center justify-between"> class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Produksi (Unit)</h3> <h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
<span class="text-[10px] font-bold text-gray-400 uppercase">Hari Ini</span> <x-heroicon-o-chart-pie class="w-4 h-4 text-primary-500" />
Alokasi Pengeluaran
</h3>
<div class="h-64 flex flex-col items-center">
<canvas id="expenseChart"></canvas>
</div>
</div> </div>
<div class="divide-y divide-gray-100 dark:divide-gray-800"> <div
@forelse($totalEggsByUnit as $item) class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6">
<div <h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-6 flex items-center gap-2">
class="px-5 py-3 flex justify-between items-center transition hover:bg-gray-50 dark:hover:bg-gray-800/40"> <x-heroicon-o-shopping-bag class="w-4 h-4 text-primary-500" />
<span Penjualan per Satuan
class="text-sm text-gray-600 dark:text-gray-400 font-medium">{{ $item['unit_name'] }}</span> </h3>
<div class="flex gap-4"> <div class="h-64 flex flex-col items-center">
<canvas id="salesUnitChart"></canvas>
</div>
</div>
</div>
{{-- Units Breakdown --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
{{-- Production --}}
<div
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm overflow-hidden">
<div
class="px-5 py-3 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50 flex items-center justify-between">
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Produksi (Unit)</h3>
<span class="text-[10px] font-bold text-gray-400 uppercase">Hari Ini</span>
</div>
<div class="divide-y divide-gray-100 dark:divide-gray-800">
@forelse($totalEggsByUnit as $item)
<div
class="px-5 py-3 flex justify-between items-center transition hover:bg-gray-50 dark:hover:bg-gray-800/40">
<span
class="text-sm text-gray-600 dark:text-gray-400 font-medium">{{ $item['unit_name'] }}</span>
<div class="flex gap-4">
<div class="text-right">
<span class="text-[10px] text-gray-400 font-bold uppercase block">Bagus</span>
<span
class="text-sm font-bold text-emerald-600 dark:text-emerald-400">{{ $item['total_good'] }}</span>
</div>
<div class="text-right border-l border-gray-100 dark:border-gray-800 pl-4">
<span class="text-[10px] text-gray-400 font-bold uppercase block">Pecah</span>
<span
class="text-sm font-bold text-rose-600 dark:text-rose-400">{{ $item['total_broken'] }}</span>
</div>
</div>
</div>
@empty
<div class="px-5 py-10 text-center text-gray-400 text-sm italic">Belum ada data produksi
</div>
@endforelse
</div>
</div>
{{-- Delayed --}}
<div
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm overflow-hidden">
<div
class="px-5 py-3 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50 flex items-center justify-between">
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Tertunda (Unit)</h3>
<span class="text-[10px] font-bold text-gray-400 uppercase">Hari Ini</span>
</div>
<div class="divide-y divide-gray-100 dark:divide-gray-800">
@forelse($totalDelayedByUnit as $item)
<div
class="px-5 py-3 flex justify-between items-center transition hover:bg-gray-50 dark:hover:bg-gray-800/40">
<span
class="text-sm text-gray-600 dark:text-gray-400 font-medium">{{ $item['unit_name'] }}</span>
<span
class="text-sm font-bold text-gray-900 dark:text-white">{{ $item['total'] }}</span>
</div>
@empty
<div class="px-5 py-10 text-center text-gray-400 text-sm italic">Tidak ada barang tertunda
hari
ini</div>
@endforelse
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{{-- Top Customers Today --}}
<div
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm overflow-hidden flex flex-col">
<div
class="px-5 py-3 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50 flex items-center gap-2">
<x-heroicon-o-star class="w-4 h-4 text-amber-500" />
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Top Pelanggan Hari Ini</h3>
</div>
<div class="flex-1 divide-y divide-gray-100 dark:divide-gray-800">
@forelse ($topCustomers as $customer)
<div
class="px-5 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-800/40 transition">
<div class="flex flex-col">
<span
class="text-sm font-bold text-gray-800 dark:text-gray-200">{{ $customer->name }}</span>
<span
class="text-xs text-gray-400 tracking-tight">{{ $customer->phone_number ?? 'Telp tidak ada' }}</span>
</div>
<div class="text-right"> <div class="text-right">
<span class="text-[10px] text-gray-400 font-bold uppercase block">Bagus</span>
<span <span
class="text-sm font-bold text-emerald-600 dark:text-emerald-400">{{ $item['total_good'] }}</span> class="text-[10px] font-semibold text-gray-400 uppercase block leading-none mb-1">Transaksi</span>
</div> <span class="text-sm font-black text-primary-600">Rp
<div class="text-right border-l border-gray-100 dark:border-gray-800 pl-4"> {{ $formatNumber($customer->total_spent) }}</span>
<span class="text-[10px] text-gray-400 font-bold uppercase block">Pecah</span>
<span
class="text-sm font-bold text-rose-600 dark:text-rose-400">{{ $item['total_broken'] }}</span>
</div> </div>
</div> </div>
</div> @empty
@empty <div class="px-5 py-10 text-center text-gray-400 text-sm italic">Belum ada transaksi hari
<div class="px-5 py-10 text-center text-gray-400 text-sm italic">Belum ada data produksi</div> ini
@endforelse </div>
</div> @endforelse
</div> </div>
{{-- Delayed --}}
<div
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm overflow-hidden">
<div
class="px-5 py-3 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50 flex items-center justify-between">
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Tertunda (Unit)</h3>
<span class="text-[10px] font-bold text-gray-400 uppercase">Hari Ini</span>
</div>
<div class="divide-y divide-gray-100 dark:divide-gray-800">
@forelse($totalDelayedByUnit as $item)
<div
class="px-5 py-3 flex justify-between items-center transition hover:bg-gray-50 dark:hover:bg-gray-800/40">
<span
class="text-sm text-gray-600 dark:text-gray-400 font-medium">{{ $item['unit_name'] }}</span>
<span class="text-sm font-bold text-gray-900 dark:text-white">{{ $item['total'] }}</span>
</div>
@empty
<div class="px-5 py-10 text-center text-gray-400 text-sm italic">Tidak ada barang tertunda hari
ini</div>
@endforelse
</div> </div>
</div> </div>
</div> </div>
@endif
@if (!$isAdministrator)
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
{{-- Top Customers Today --}} document.addEventListener('DOMContentLoaded', () => {
<div const chartOptions = {
class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm overflow-hidden flex flex-col"> responsive: true,
<div maintainAspectRatio: false,
class="px-5 py-3 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50 flex items-center gap-2"> plugins: {
<x-heroicon-o-star class="w-4 h-4 text-amber-500" /> legend: {
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Top Pelanggan Hari Ini</h3> position: 'bottom',
</div> labels: {
<div class="flex-1 divide-y divide-gray-100 dark:divide-gray-800"> boxWidth: 10,
@forelse ($topCustomers as $customer) font: {
<div size: 10
class="px-5 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-800/40 transition"> }
<div class="flex flex-col">
<span
class="text-sm font-bold text-gray-800 dark:text-gray-200">{{ $customer->name }}</span>
<span
class="text-xs text-gray-400 tracking-tight">{{ $customer->phone_number ?? 'Telp tidak ada' }}</span>
</div>
<div class="text-right">
<span
class="text-[10px] font-semibold text-gray-400 uppercase block leading-none mb-1">Transaksi</span>
<span class="text-sm font-black text-primary-600">Rp
{{ $formatNumber($customer->total_spent) }}</span>
</div>
</div>
@empty
<div class="px-5 py-10 text-center text-gray-400 text-sm italic">Belum ada transaksi hari ini
</div>
@endforelse
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 10,
font: {
size: 10
} }
} }
} }
};
// Revenue Split Chart
const revenueSplitCtx = document.getElementById('revenueSplitChart');
if (revenueSplitCtx) {
new Chart(revenueSplitCtx, {
type: 'doughnut',
data: {
labels: {!! json_encode($revenueSplit['labels']) !!},
datasets: [{
data: {!! json_encode($revenueSplit['data']) !!},
backgroundColor: ['#6366f1', '#94a3b8'],
}]
},
options: {
...chartOptions,
cutout: '70%'
}
});
} }
};
// Revenue Split Chart // Expense Category Chart
new Chart(document.getElementById('revenueSplitChart'), { const expenseCtx = document.getElementById('expenseChart');
type: 'doughnut', if (expenseCtx) {
data: { new Chart(expenseCtx, {
labels: {!! json_encode($revenueSplit['labels']) !!}, type: 'doughnut',
datasets: [{ data: {
data: {!! json_encode($revenueSplit['data']) !!}, labels: {!! json_encode($expenseDistribution['labels']) !!},
backgroundColor: ['#6366f1', '#94a3b8'], datasets: [{
}] data: {!! json_encode($expenseDistribution['data']) !!},
}, backgroundColor: ['#f59e0b', '#64748b'],
options: { borderWidth: 0
...chartOptions, }]
cutout: '70%' },
options: {
...chartOptions,
cutout: '70%'
}
});
} }
});
// Expense Category Chart // Sales by Unit Chart
new Chart(document.getElementById('expenseChart'), { const salesUnitCtx = document.getElementById('salesUnitChart');
type: 'doughnut', if (salesUnitCtx) {
data: { new Chart(salesUnitCtx, {
labels: {!! json_encode($expenseDistribution['labels']) !!}, type: 'pie',
datasets: [{ data: {
data: {!! json_encode($expenseDistribution['data']) !!}, labels: {!! json_encode($salesByUnit['labels']) !!},
backgroundColor: ['#f59e0b', '#64748b'], datasets: [{
borderWidth: 0 data: {!! json_encode($salesByUnit['data']) !!},
}] backgroundColor: ['#3b82f6', '#06b6d4', '#8b5cf6', '#ec4899'],
}, }]
options: { },
...chartOptions, options: chartOptions
cutout: '70%' });
} }
});
// Sales by Unit Chart
new Chart(document.getElementById('salesUnitChart'), {
type: 'pie',
data: {
labels: {!! json_encode($salesByUnit['labels']) !!},
datasets: [{
data: {!! json_encode($salesByUnit['data']) !!},
backgroundColor: ['#3b82f6', '#06b6d4', '#8b5cf6', '#ec4899'],
}]
},
options: chartOptions
}); });
</script>
}); @endif
</script>
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&display=swap');