feat: Add new Filament Analisys page to display various business statistics and breakdowns.
This commit is contained in:
parent
3a3158a024
commit
8813249154
106
app/Filament/Pages/Analisys.php
Normal file
106
app/Filament/Pages/Analisys.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
106
resources/views/filament/pages/analisys.blade.php
Normal file
106
resources/views/filament/pages/analisys.blade.php
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user