feat: Add new Filament Analisys page to display various business statistics and breakdowns.

This commit is contained in:
Yoga Pangestu 2026-03-09 13:49:51 +07:00
parent 3a3158a024
commit 8813249154
2 changed files with 212 additions and 0 deletions

View File

@ -0,0 +1,106 @@
<?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 BackedEnum;
use Filament\Pages\Page;
use Filament\Support\Icons\Heroicon;
class Analisys extends Page
{
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChartPie;
protected static ?string $navigationLabel = 'Analisis';
protected static ?string $slug = 'analisys';
protected static ?string $title = 'Analisis';
protected string $view = 'filament.pages.analisys';
public function getViewData(): array
{
$formatNumber = fn ($val) => number_format((float) $val, $val == floor($val) ? 0 : 2, ',', '.');
// 1. Count customers
$countPelanggan = Customer::count();
// 2. Count total eggs (by unit)
$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')
->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),
]);
// 3. Count total orders
$countPesanan = Order::count();
// 4. Count total delayed goods (by unit)
$totalDelayedByUnit = DelayedGood::query()
->join('units', 'delayed_goods.unit_id', '=', 'units.id')
->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),
]);
// 5. Revenue (from orders and delayed goods)
$pendapatanPesanan = Order::sum('total_amount');
$pendapatanTertunda = DelayedGood::sum('total_amount');
$totalPendapatan = $pendapatanPesanan + $pendapatanTertunda;
// 6. Expenses
$totalPengeluaran = Expense::sum('amount');
// 7. Payroll
$totalPenggajian = Payroll::sum('total_salary');
// 8. Feed purchases
$totalBelanjaPakan = FeedPurchase::sum('total_price');
// 9. Gross profit (Revenue - COGS/Feed)
$labaKotor = $totalPendapatan - $totalBelanjaPakan;
// 10. Net profit (Gross Profit - Expenses - Payroll)
$labaBersih = $labaKotor - $totalPengeluaran - $totalPenggajian;
// 11. Unpaid accounts receivable (delayed goods)
$totalPiutang = DelayedGood::sum('total_amount') - DelayedGood::sum('paid_amount');
$mainStats = [
['label' => 'Total Pelanggan', 'desc' => 'Jumlah pelanggan terdaftar', 'value' => $countPelanggan, 'prefix' => ''],
['label' => 'Total Pesanan', 'desc' => 'Jumlah transaksi pesanan', 'value' => $countPesanan, 'prefix' => ''],
['label' => 'Laba Bersih', 'desc' => 'Setelah dikurangi beban operasional', 'value' => $formatNumber($labaBersih), 'prefix' => 'Rp'],
['label' => 'Total Piutang', 'desc' => 'Piutang barang tertunda', 'value' => $formatNumber($totalPiutang), 'prefix' => 'Rp'],
['label' => 'Total Pendapatan', 'desc' => 'Akumulasi semua pemasukan', 'value' => $formatNumber($totalPendapatan), 'prefix' => 'Rp'],
['label' => 'Laba Kotor', 'desc' => 'Pendapatan dikurangi biaya pakan', 'value' => $formatNumber($labaKotor), 'prefix' => 'Rp'],
['label' => 'Total Pengeluaran', 'desc' => 'Biaya operasional & umum', 'value' => $formatNumber($totalPengeluaran), 'prefix' => 'Rp'],
['label' => 'Total Penggajian', 'desc' => 'Total gaji karyawan', 'value' => $formatNumber($totalPenggajian), 'prefix' => 'Rp'],
['label' => 'Belanja Pakan', 'desc' => 'Total pembelian pakan ayam', 'value' => $formatNumber($totalBelanjaPakan), 'prefix' => 'Rp'],
];
return [
'mainStats' => $mainStats,
'totalEggsByUnit' => $totalEggsByUnit,
'totalDelayedByUnit' => $totalDelayedByUnit,
];
}
}

View File

@ -0,0 +1,106 @@
<x-filament-panels::page>
<div class="space-y-6">
{{-- Main Stats Grid --}}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 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-5 transition hover:shadow-md">
<div class="flex flex-col gap-1">
<span
class="text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-tight">{{ $stat['label'] }}</span>
<div class="flex items-baseline gap-1">
@if ($stat['prefix'])
<span
class="text-sm font-semibold text-gray-400 dark:text-gray-500">{{ $stat['prefix'] }}</span>
@endif
<span class="text-2xl font-bold text-gray-900 dark:text-white">{{ $stat['value'] }}</span>
</div>
<span class="text-xs text-gray-400 dark:text-gray-600">{{ $stat['desc'] }}</span>
</div>
</div>
@endforeach
</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">
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Produksi Telur (By Unit)</h3>
</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">
<div class="flex flex-col">
<span
class="text-sm text-gray-600 dark:text-gray-400 font-medium">{{ $item['unit_name'] }}</span>
</div>
<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">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">
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300">Barang Tertunda (By Unit)</h3>
</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">Tidak ada barang tertunda</div>
@endforelse
</div>
</div>
</div>
</div>
<style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&display=swap');
:root {
--fi-page-content-padding: 2rem;
}
.fi-section {
background: transparent !important;
box-shadow: none !important;
border: none !important;
}
* {
font-family: 'Plus Jakarta Sans', sans-serif;
}
.tracking-tighter {
letter-spacing: -0.05em;
}
.tracking-tight {
letter-spacing: -0.025em;
}
</style>
</x-filament-panels::page>