From 77c750db408fb63a8ef07c4c3136fbe48e8218e0 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Mon, 9 Mar 2026 18:04:27 +0700 Subject: [PATCH] feat: add date filter, multiple new charts, and top customers list to the analysis page. --- app/Filament/Pages/Analisys.php | 233 +++++++++- app/Models/Customer.php | 11 + .../views/filament/pages/analisys.blade.php | 403 +++++++++++++++++- 3 files changed, 618 insertions(+), 29 deletions(-) diff --git a/app/Filament/Pages/Analisys.php b/app/Filament/Pages/Analisys.php index 69005b2..cf79f67 100644 --- a/app/Filament/Pages/Analisys.php +++ b/app/Filament/Pages/Analisys.php @@ -4,17 +4,101 @@ use App\Models\Customer; use App\Models\DelayedGood; +use App\Models\EggCollection; use App\Models\EggCollectionItem; use App\Models\Expense; +use App\Models\Feed; use App\Models\FeedPurchase; use App\Models\Order; use App\Models\Payroll; use BackedEnum; +use Carbon\Carbon; +use Filament\Forms\Components\DatePicker; +use Filament\Forms\Components\Select; +use Filament\Forms\Concerns\InteractsWithForms; +use Filament\Forms\Contracts\HasForms; use Filament\Pages\Page; +use Filament\Schemas\Components\Grid; +use Filament\Schemas\Components\Section; +use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; -class Analisys extends Page +class Analisys extends Page implements HasForms { + use InteractsWithForms; + + public ?array $data = []; + + public function mount(): void + { + $this->form->fill([ + 'period' => 'this_month', + 'from_date' => now()->startOfMonth()->format('d F Y'), + 'to_date' => now()->format('d F Y'), + ]); + } + + public function form(Schema $form): Schema + { + return $form + ->schema([ + Section::make() + ->schema([ + Grid::make(3) + ->schema([ + Select::make('period') + ->label('Periode Cepat') + ->options([ + 'today' => 'Hari Ini', + 'this_week' => 'Minggu Ini', + 'this_month' => 'Bulan Ini', + 'this_year' => 'Tahun Ini', + 'custom' => 'Kustom Tanggal', + ]) + ->live() + ->afterStateUpdated(function ($state, $set, $get) { + if ($state === 'today') { + $set('from_date', now()->format('d F Y')); + $set('to_date', now()->format('d F Y')); + } elseif ($state === 'this_week') { + $set('from_date', now()->startOfWeek()->format('d F Y')); + $set('to_date', now()->endOfWeek()->format('d F Y')); + } elseif ($state === 'this_month') { + $set('from_date', now()->startOfMonth()->format('d F Y')); + $set('to_date', now()->endOfMonth()->format('d F Y')); + } elseif ($state === 'this_year') { + $set('from_date', now()->startOfYear()->format('d F Y')); + $set('to_date', now()->endOfYear()->format('d F Y')); + } + $this->dispatch('stats-updated'); + }), + + DatePicker::make('from_date') + ->label('Dari Tanggal') + ->native(false) + ->displayFormat('d F Y') + ->live() + ->afterStateUpdated(function ($set) { + $set('period', 'custom'); + $this->dispatch('stats-updated'); + }), + + DatePicker::make('to_date') + ->label('Sampai Tanggal') + ->native(false) + ->displayFormat('d F Y') + ->live() + ->afterStateUpdated(function ($set) { + $set('period', 'custom'); + $this->dispatch('stats-updated'); + }), + ]), + ]) + ->compact(), + ]) + ->statePath('data'); + } + protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChartPie; protected static ?string $navigationLabel = 'Analisis'; @@ -28,6 +112,14 @@ class Analisys extends Page public function getViewData(): array { $formatNumber = fn ($val) => number_format((float) $val, $val == floor($val) ? 0 : 2, ',', '.'); + $now = now(); + + $fromDateInput = $this->data['from_date'] ?? now()->startOfMonth()->format('d F Y'); + $toDateInput = $this->data['to_date'] ?? now()->format('d F Y'); + + // Normalize for DB queries + $fromDate = Carbon::parse($fromDateInput)->format('Y-m-d'); + $toDate = Carbon::parse($toDateInput)->format('Y-m-d'); // 1. Count customers $countPelanggan = Customer::count(); @@ -37,6 +129,7 @@ public function getViewData(): array ->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') + ->whereBetween('egg_collections.production_date', [$fromDate, $toDate]) ->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') @@ -49,11 +142,12 @@ public function getViewData(): array ]); // 3. Count total orders - $countPesanan = Order::count(); + $countPesanan = Order::whereBetween('order_date', [$fromDate, $toDate])->count(); // 4. Count total delayed goods (by unit) $totalDelayedByUnit = DelayedGood::query() ->join('units', 'delayed_goods.unit_id', '=', 'units.id') + ->whereBetween('delayed_goods.stored_date', [$fromDate, $toDate]) ->selectRaw('units.name as unit_name, sum(quantity) as total') ->groupBy('units.name') ->get() @@ -62,34 +156,28 @@ public function getViewData(): array 'total' => $formatNumber($item->total), ]); - // 5. Revenue (from orders and delayed goods) - $pendapatanPesanan = Order::sum('total_amount'); - $pendapatanTertunda = DelayedGood::sum('total_amount'); + // 5. Revenue + $pendapatanPesanan = Order::whereBetween('order_date', [$fromDate, $toDate])->sum('total_amount'); + $pendapatanTertunda = DelayedGood::whereBetween('stored_date', [$fromDate, $toDate])->sum('total_amount'); $totalPendapatan = $pendapatanPesanan + $pendapatanTertunda; - // 6. Expenses - $totalPengeluaran = Expense::sum('amount'); + // 6. Financial totals + $totalPengeluaran = Expense::whereBetween('expense_date', [$fromDate, $toDate])->sum('amount'); + $totalPenggajian = Payroll::whereBetween('period_month', [Carbon::parse($fromDate)->format('Y-m'), Carbon::parse($toDate)->format('Y-m')])->sum('total_salary'); + $totalBelanjaPakan = FeedPurchase::whereBetween('purchase_date', [$fromDate, $toDate])->sum('total_price'); - // 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'); + $paidInRange = DelayedGood::whereBetween('stored_date', [$fromDate, $toDate])->sum('paid_amount'); + $totalInRange = DelayedGood::whereBetween('stored_date', [$fromDate, $toDate])->sum('total_amount'); + $totalPiutang = $totalInRange - $paidInRange; $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 Piutang', 'desc' => 'Piutang barang tertunda (periode)', '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'], @@ -97,10 +185,117 @@ public function getViewData(): array ['label' => 'Belanja Pakan', 'desc' => 'Total pembelian pakan ayam', 'value' => $formatNumber($totalBelanjaPakan), 'prefix' => 'Rp'], ]; + // Top 5 Customers by Order Amount in Range + $topCustomers = Customer::query() + ->withSum(['orders' => fn ($q) => $q->whereBetween('order_date', [$fromDate, $toDate])], 'total_amount') + ->withSum(['delayedGoods' => fn ($q) => $q->whereBetween('stored_date', [$fromDate, $toDate])], 'total_amount') + ->get() + ->map(function ($customer) { + $customer->total_spent = ($customer->orders_sum_total_amount ?? 0) + ($customer->delayed_goods_sum_total_amount ?? 0); + + return $customer; + }) + ->sortByDesc('total_spent') + ->take(5) + ->values(); + + // 7. Chart: Daily Production (In range) + $diffDays = Carbon::parse($fromDate)->diffInDays(Carbon::parse($toDate)); + $rangeQueryDays = collect(range(0, min($diffDays, 30)))->map(fn ($i) => Carbon::parse($fromDate)->addDays($i)->format('Y-m-d'))->values(); + + $productionChart = [ + 'labels' => $rangeQueryDays->map(fn ($d) => Carbon::parse($d)->format('d M'))->values(), + 'good' => $rangeQueryDays->map(fn ($d) => (float) EggCollection::whereDate('production_date', $d)->sum('total_eggs'))->values(), + 'broken' => $rangeQueryDays->map(fn ($d) => (float) EggCollection::whereDate('production_date', $d)->sum('total_broken_eggs'))->values(), + ]; + + // 8. Financial Trends (Monthly - 6 months prior to to_date) + $months = collect(range(0, 5))->map(fn ($i) => Carbon::parse($toDate)->subMonths($i)->format('Y-m'))->reverse(); + $financialChart = [ + 'labels' => $months->map(fn ($m) => Carbon::parse($m)->translatedFormat('M Y'))->values(), + 'revenue' => $months->map(function ($m) { + $orderRev = Order::whereDate('order_date', 'like', "$m%")->sum('total_amount'); + $delayedRev = DelayedGood::whereDate('stored_date', 'like', "$m%")->sum('total_amount'); + + return (float) ($orderRev + $delayedRev); + })->values(), + 'expenses' => $months->map(function ($m) { + $exp = Expense::whereDate('expense_date', 'like', "$m%")->sum('amount'); + $payroll = Payroll::where('period_month', 'like', "$m%")->sum('total_salary'); + $feed = FeedPurchase::whereDate('purchase_date', 'like', "$m%")->sum('total_price'); + + return (float) ($exp + $payroll + $feed); + })->values(), + ]; + + // 8b. Chart: Monthly Profit Trend + $financialChart['profit'] = collect($financialChart['revenue'])->map(fn ($rev, $i) => (float) ($rev - $financialChart['expenses'][$i]))->values(); + + // 9. Chart: Expense Distribution + $expenseDistribution = [ + 'labels' => ['Gaji Karyawan', 'Belanja Pakan', 'Pengeluaran Umum'], + 'data' => [(float) $totalPenggajian, (float) $totalBelanjaPakan, (float) $totalPengeluaran], + ]; + + // 10. Chart: Sales Distribution by Unit + $salesByUnit = Order::whereBetween('order_date', [$fromDate, $toDate]) + ->join('units', 'orders.unit_id', '=', 'units.id') + ->selectRaw('units.name, sum(total_amount) as total') + ->groupBy('units.name') + ->get(); + + // 11. Chart: Debt Status (In range) + $debtStatus = [ + 'paid' => (float) $paidInRange, + 'unpaid' => (float) ($totalInRange - $paidInRange), + ]; + + // 12. Chart: Feed Stocks (Current) + $feedStocks = Feed::query() + ->join('units', 'feeds.unit_id', '=', 'units.id') + ->select('feeds.name', 'feeds.stock', 'units.alias as unit_alias') + ->get(); + + // 13. Chart: Production by Warehouse (In range) + $productionByWarehouse = EggCollection::whereBetween('production_date', [$fromDate, $toDate]) + ->join('warehouses', 'egg_collections.warehouse_id', '=', 'warehouses.id') + ->selectRaw('warehouses.name, sum(total_eggs) as total') + ->groupBy('warehouses.name') + ->get(); + + // 14. Chart: Revenue Split (In range) + $revenueSplit = [ + 'orders' => (float) $pendapatanPesanan, + 'delayed' => (float) $pendapatanTertunda, + ]; + return [ 'mainStats' => $mainStats, 'totalEggsByUnit' => $totalEggsByUnit, 'totalDelayedByUnit' => $totalDelayedByUnit, + 'topCustomers' => $topCustomers, + 'productionChart' => $productionChart, + 'financialChart' => $financialChart, + 'expenseDistribution' => $expenseDistribution, + 'salesByUnit' => [ + 'labels' => $salesByUnit->pluck('name')->values(), + 'data' => $salesByUnit->pluck('total')->map(fn ($v) => (float) $v)->values(), + ], + 'debtStatus' => $debtStatus, + 'feedStocks' => [ + 'labels' => $feedStocks->pluck('name')->values(), + 'data' => $feedStocks->pluck('stock')->map(fn ($v) => (float) $v)->values(), + 'units' => $feedStocks->pluck('unit_alias')->values(), + ], + 'productionByWarehouse' => [ + 'labels' => $productionByWarehouse->pluck('name')->values(), + 'data' => $productionByWarehouse->pluck('total')->map(fn ($v) => (float) $v)->values(), + ], + 'revenueSplit' => [ + 'labels' => ['Penjualan Langsung', 'Barang Tertunda'], + 'data' => [$revenueSplit['orders'], $revenueSplit['delayed']], + ], + 'formatNumber' => $formatNumber, ]; } } diff --git a/app/Models/Customer.php b/app/Models/Customer.php index dc25f70..0f1bba9 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; class Customer extends Model @@ -11,4 +12,14 @@ class Customer extends Model use HasFactory, SoftDeletes; protected $guarded = ['id']; + + public function orders(): HasMany + { + return $this->hasMany(Order::class); + } + + public function delayedGoods(): HasMany + { + return $this->hasMany(DelayedGood::class); + } } diff --git a/resources/views/filament/pages/analisys.blade.php b/resources/views/filament/pages/analisys.blade.php index 6d62a61..2c57d6b 100644 --- a/resources/views/filament/pages/analisys.blade.php +++ b/resources/views/filament/pages/analisys.blade.php @@ -1,5 +1,10 @@ -
+
+
+ {{ $this->form }} +
+
+
{{-- Main Stats Grid --}}
@foreach ($mainStats as $stat) @@ -11,32 +16,172 @@ class="text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-tig
@if ($stat['prefix']) {{ $stat['prefix'] }} + class="text-xs font-semibold text-gray-400 dark:text-gray-500">{{ $stat['prefix'] }} @endif {{ $stat['value'] }}
- {{ $stat['desc'] }} + {{ $stat['desc'] }}
@endforeach
+ {{-- First Row: Production Chart & Expense Distribution --}} +
+
+

+ + Tren Produksi Telur (7-14 Hari Terakhir) +

+
+ +
+
+ +
+

+ + Alokasi Pengeluaran +

+
+ +
+
+
+ + {{-- Second Row: Financial Trend & Top Customers --}} +
+
+

+ + Performa Keuangan (6 Bulan Terakhir) +

+
+ +
+
+ +
+
+ +

Top 5 Pelanggan

+
+
+ @foreach ($topCustomers as $customer) +
+
+ {{ $customer->name }} + {{ $customer->phone ?? 'Telp tidak ada' }} +
+
+ Total + Belanja + Rp + {{ $formatNumber($customer->total_spent) }} +
+
+ @endforeach +
+
+
+ + {{-- Third Row: Sales by Unit & Payment Status --}} +
+
+

+ + Penjualan Berdasarkan Satuan (Order) +

+
+ +
+
+ +
+

+ + Status Pelunasan (Barang Tertunda) +

+
+ +
+
+
+ + {{-- Fourth Row: Feed Stocks --}} +
+

+ + Level Stok Pakan Saat Ini +

+
+ +
+
+ + {{-- Fifth Row: Profit Trend & Warehouse Productivity --}} +
+
+

+ + Tren Laba Bersih (6 Bulan Terakhir) +

+
+ +
+
+ +
+

+ + Produktivitas per Gudang +

+
+ +
+
+
+ + {{-- Sixth Row: Revenue Split --}} +
+
+

+ + Sumber Pendapatan +

+
+ +
+
+ + {{-- Placeholder/Extra space to balance --}} +
+
+ {{-- Units Breakdown --}}
{{-- Production --}}
-

Produksi Telur (By Unit)

+

Total Stok (By Unit)

@forelse($totalEggsByUnit as $item)
-
- {{ $item['unit_name'] }} -
+ {{ $item['unit_name'] }}
Bagus @@ -51,7 +196,7 @@ class="text-sm font-bold text-rose-600 dark:text-rose-400">{{ $item['total_broke
@empty -
Belum ada data produksi
+
Belum ada data produksi
@endforelse
@@ -71,13 +216,251 @@ class="text-sm text-gray-600 dark:text-gray-400 font-medium">{{ $item['unit_name {{ $item['total'] }}
@empty -
Tidak ada barang tertunda
+
Tidak ada barang tertunda
@endforelse
+ +