feat: Create Dashboard page with comprehensive business statistics, including egg production, order counts, revenue, expenses, and top customers.
This commit is contained in:
parent
77c750db40
commit
b424d119fc
134
app/Filament/Pages/Dashboard.php
Normal file
134
app/Filament/Pages/Dashboard.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\DelayedGood;
|
||||
use App\Models\EggCollectionItem;
|
||||
use App\Models\Expense;
|
||||
use App\Models\FeedPurchase;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payroll;
|
||||
use Filament\Pages\Dashboard as BaseDashboard;
|
||||
|
||||
class Dashboard extends BaseDashboard
|
||||
{
|
||||
protected static ?string $title = 'Dasbor';
|
||||
|
||||
protected string $view = 'filament.pages.dashboard';
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
$formatNumber = fn ($val) => number_format((float) $val, $val == floor($val) ? 0 : 2, ',', '.');
|
||||
$now = now();
|
||||
$today = $now->format('Y-m-d');
|
||||
|
||||
// 1. Egg Production by Unit - Single optimized query with joins
|
||||
$totalEggsByUnit = EggCollectionItem::query()
|
||||
->join('egg_collections', 'egg_collection_items.egg_collection_id', 'egg_collections.id')
|
||||
->join('units', 'egg_collection_items.unit_id', 'units.id')
|
||||
->whereNull('egg_collections.deleted_at')
|
||||
->where('egg_collections.production_date', $today)
|
||||
->selectRaw('units.name as unit_name,
|
||||
sum(case when is_broken = 0 then quantity else 0 end) as total_good,
|
||||
sum(case when is_broken = 1 then quantity else 0 end) as total_broken')
|
||||
->groupBy('units.name')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'unit_name' => $item->unit_name,
|
||||
'total_good' => $formatNumber($item->total_good),
|
||||
'total_broken' => $formatNumber($item->total_broken),
|
||||
]);
|
||||
|
||||
// 2. Orders Stats - Combined count and sum
|
||||
$orderStats = Order::whereDate('order_date', $today)
|
||||
->selectRaw('count(*) as count, sum(total_amount) as total')
|
||||
->first();
|
||||
$orderCount = (int) ($orderStats->count ?? 0);
|
||||
$orderRevenue = (float) ($orderStats->total ?? 0);
|
||||
|
||||
// 3. Delayed Goods Stats - Combined sum and paid_amount
|
||||
$delayedStatsByUnit = DelayedGood::query()
|
||||
->join('units', 'delayed_goods.unit_id', 'units.id')
|
||||
->whereDate('delayed_goods.stored_date', $today)
|
||||
->selectRaw('units.name as unit_name, sum(quantity) as total')
|
||||
->groupBy('units.name')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'unit_name' => $item->unit_name,
|
||||
'total' => $formatNumber($item->total),
|
||||
]);
|
||||
|
||||
$delayedStatsTotals = DelayedGood::whereDate('stored_date', $today)
|
||||
->selectRaw('sum(total_amount) as total, sum(paid_amount) as paid')
|
||||
->first();
|
||||
|
||||
$delayedRevenue = (float) ($delayedStatsTotals->total ?? 0);
|
||||
$paidToday = (float) ($delayedStatsTotals->paid ?? 0);
|
||||
$totalRevenue = $orderRevenue + $delayedRevenue;
|
||||
|
||||
// 4. Expenses & Financials Today
|
||||
$totalExpenses = (float) Expense::whereDate('expense_date', $today)->sum('amount');
|
||||
$monthlyPayroll = (float) Payroll::where('period_month', $now->format('Y-m'))->sum('total_salary');
|
||||
$totalFeedPurchase = (float) FeedPurchase::whereDate('purchase_date', $today)->sum('total_price');
|
||||
|
||||
$grossProfit = $totalRevenue - $totalFeedPurchase;
|
||||
$netProfit = $grossProfit - $totalExpenses - $monthlyPayroll;
|
||||
$totalReceivablesToday = $delayedRevenue - $paidToday;
|
||||
|
||||
$mainStats = [
|
||||
['label' => 'Pesanan', 'desc' => 'Jumlah transaksi', 'value' => $orderCount, 'prefix' => ''],
|
||||
['label' => 'Laba Bersih', 'desc' => 'Setelah dikurangi pengeluaran harian', 'value' => $formatNumber($netProfit), 'prefix' => 'Rp'],
|
||||
['label' => 'Piutang', 'desc' => 'Piutang barang tertunda', 'value' => $formatNumber($totalReceivablesToday), 'prefix' => 'Rp'],
|
||||
['label' => 'Pendapatan', 'desc' => 'Akumulasi pemasukan', 'value' => $formatNumber($totalRevenue), 'prefix' => 'Rp'],
|
||||
['label' => 'Belanja Pakan', 'desc' => 'Baru dibeli', 'value' => $formatNumber($totalFeedPurchase), 'prefix' => 'Rp'],
|
||||
];
|
||||
|
||||
// 5. Top Customers Today - Optimized summing
|
||||
$topCustomers = Customer::query()
|
||||
->select('id', 'name', 'phone_number')
|
||||
->withSum(['orders' => fn ($q) => $q->whereDate('order_date', $today)], 'total_amount')
|
||||
->withSum(['delayedGoods' => fn ($q) => $q->whereDate('stored_date', $today)], 'total_amount')
|
||||
->get()
|
||||
->map(function ($customer) {
|
||||
$customer->total_spent = ($customer->orders_sum_total_amount ?? 0) + ($customer->delayed_goods_sum_total_amount ?? 0);
|
||||
|
||||
return $customer;
|
||||
})
|
||||
->filter(fn ($c) => $c->total_spent > 0)
|
||||
->sortByDesc('total_spent')
|
||||
->take(5)
|
||||
->values();
|
||||
|
||||
// 6. Distribution Charts Data
|
||||
$revenueSplitData = [
|
||||
'labels' => ['Penjualan Langsung', 'Barang Tertunda'],
|
||||
'data' => [$orderRevenue, $delayedRevenue],
|
||||
];
|
||||
|
||||
$expenseDistributionData = [
|
||||
'labels' => ['Belanja Pakan', 'Pengeluaran Umum'],
|
||||
'data' => [$totalFeedPurchase, $totalExpenses],
|
||||
];
|
||||
|
||||
$salesByUnit = Order::whereDate('order_date', $today)
|
||||
->join('units', 'orders.unit_id', 'units.id')
|
||||
->selectRaw('units.name, sum(total_amount) as total')
|
||||
->groupBy('units.name')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'mainStats' => $mainStats,
|
||||
'totalEggsByUnit' => $totalEggsByUnit,
|
||||
'totalDelayedByUnit' => $delayedStatsByUnit,
|
||||
'topCustomers' => $topCustomers,
|
||||
'revenueSplit' => $revenueSplitData,
|
||||
'expenseDistribution' => $expenseDistributionData,
|
||||
'salesByUnit' => [
|
||||
'labels' => $salesByUnit->pluck('name')->values(),
|
||||
'data' => $salesByUnit->pluck('total')->map(fn ($v) => (float) $v)->values(),
|
||||
],
|
||||
'formatNumber' => $formatNumber,
|
||||
];
|
||||
}
|
||||
}
|
||||
226
resources/views/filament/pages/dashboard.blade.php
Normal file
226
resources/views/filament/pages/dashboard.blade.php
Normal file
@ -0,0 +1,226 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="space-y-6 pb-12">
|
||||
{{-- Main Stats Grid --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
|
||||
@foreach ($mainStats as $stat)
|
||||
<div
|
||||
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">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span
|
||||
class="text-[10px] font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider">{{ $stat['label'] }}</span>
|
||||
<div class="flex items-baseline gap-1">
|
||||
@if ($stat['prefix'])
|
||||
<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>
|
||||
@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
|
||||
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 ?? '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
|
||||
new Chart(document.getElementById('revenueSplitChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: {!! json_encode($revenueSplit['labels']) !!},
|
||||
datasets: [{
|
||||
data: {!! json_encode($revenueSplit['data']) !!},
|
||||
backgroundColor: ['#6366f1', '#94a3b8'],
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...chartOptions,
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
|
||||
// Expense Category Chart
|
||||
new Chart(document.getElementById('expenseChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: {!! json_encode($expenseDistribution['labels']) !!},
|
||||
datasets: [{
|
||||
data: {!! json_encode($expenseDistribution['data']) !!},
|
||||
backgroundColor: ['#f59e0b', '#64748b'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
},
|
||||
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>
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&display=swap');
|
||||
|
||||
* {
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
}
|
||||
</style>
|
||||
</x-filament-panels::page>
|
||||
Loading…
Reference in New Issue
Block a user