diff --git a/app/Filament/Pages/Analisys.php b/app/Filament/Pages/Analisys.php index cf79f67..95ff37c 100644 --- a/app/Filament/Pages/Analisys.php +++ b/app/Filament/Pages/Analisys.php @@ -18,10 +18,10 @@ 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; +use Illuminate\Support\Facades\DB; class Analisys extends Page implements HasForms { @@ -44,57 +44,56 @@ public function form(Schema $form): Schema ->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'); - }), + 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'); + }) + ->native(false), - 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('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'); - }), - ]), + 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(), + ->compact() + ->columns(3), ]) ->statePath('data'); } @@ -121,10 +120,10 @@ public function getViewData(): array $fromDate = Carbon::parse($fromDateInput)->format('Y-m-d'); $toDate = Carbon::parse($toDateInput)->format('Y-m-d'); - // 1. Count customers - $countPelanggan = Customer::count(); + // 1. Customer Count + $customerCount = Customer::count(); - // 2. Count total eggs (by unit) + // 2. Egg Production by Unit - Optimized join query $totalEggsByUnit = EggCollectionItem::query() ->join('egg_collections', 'egg_collection_items.egg_collection_id', '=', 'egg_collections.id') ->join('units', 'egg_collection_items.unit_id', '=', 'units.id') @@ -141,11 +140,15 @@ public function getViewData(): array 'total_broken' => $formatNumber($item->total_broken), ]); - // 3. Count total orders - $countPesanan = Order::whereBetween('order_date', [$fromDate, $toDate])->count(); + // 3. Orders Stats - Combined count and sum + $orderStats = Order::whereBetween('order_date', [$fromDate, $toDate]) + ->selectRaw('count(*) as count, sum(total_amount) as total') + ->first(); + $orderCount = (int) ($orderStats->count ?? 0); + $orderRevenue = (float) ($orderStats->total ?? 0); - // 4. Count total delayed goods (by unit) - $totalDelayedByUnit = DelayedGood::query() + // 4. Delayed Goods Stats - Combined by unit + $delayedStatsByUnit = 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') @@ -156,37 +159,38 @@ public function getViewData(): array 'total' => $formatNumber($item->total), ]); - // 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; + // 5. Financial Totals in Range + $delayedTotals = DelayedGood::whereBetween('stored_date', [$fromDate, $toDate]) + ->selectRaw('sum(total_amount) as total, sum(paid_amount) as paid') + ->first(); - // 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'); + $delayedRevenue = (float) ($delayedTotals->total ?? 0); + $paidAmountInRange = (float) ($delayedTotals->paid ?? 0); + $totalRevenue = $orderRevenue + $delayedRevenue; - $labaKotor = $totalPendapatan - $totalBelanjaPakan; - $labaBersih = $labaKotor - $totalPengeluaran - $totalPenggajian; + $totalExpenses = (float) Expense::whereBetween('expense_date', [$fromDate, $toDate])->sum('amount'); + $totalPayroll = (float) Payroll::whereBetween('period_month', [Carbon::parse($fromDate)->format('Y-m'), Carbon::parse($toDate)->format('Y-m')])->sum('total_salary'); + $totalFeedPurchase = (float) FeedPurchase::whereBetween('purchase_date', [$fromDate, $toDate])->sum('total_price'); - $paidInRange = DelayedGood::whereBetween('stored_date', [$fromDate, $toDate])->sum('paid_amount'); - $totalInRange = DelayedGood::whereBetween('stored_date', [$fromDate, $toDate])->sum('total_amount'); - $totalPiutang = $totalInRange - $paidInRange; + $grossProfit = $totalRevenue - $totalFeedPurchase; + $netProfit = $grossProfit - $totalExpenses - $totalPayroll; + $totalReceivables = $delayedRevenue - $paidAmountInRange; $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 (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'], - ['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'], + ['label' => 'Total Pelanggan', 'desc' => 'Jumlah pelanggan terdaftar', 'value' => $customerCount, 'prefix' => ''], + ['label' => 'Total Pesanan', 'desc' => 'Jumlah transaksi pesanan', 'value' => $orderCount, 'prefix' => ''], + ['label' => 'Laba Bersih', 'desc' => 'Setelah dikurangi beban operasional', 'value' => $formatNumber($netProfit), 'prefix' => 'Rp'], + ['label' => 'Total Piutang', 'desc' => 'Piutang barang tertunda (periode)', 'value' => $formatNumber($totalReceivables), 'prefix' => 'Rp'], + ['label' => 'Total Pendapatan', 'desc' => 'Akumulasi semua pemasukan', 'value' => $formatNumber($totalRevenue), 'prefix' => 'Rp'], + ['label' => 'Laba Kotor', 'desc' => 'Pendapatan dikurangi biaya pakan', 'value' => $formatNumber($grossProfit), 'prefix' => 'Rp'], + ['label' => 'Total Pengeluaran', 'desc' => 'Biaya operasional & umum', 'value' => $formatNumber($totalExpenses), 'prefix' => 'Rp'], + ['label' => 'Total Penggajian', 'desc' => 'Total gaji karyawan', 'value' => $formatNumber($totalPayroll), 'prefix' => 'Rp'], + ['label' => 'Belanja Pakan', 'desc' => 'Total pembelian pakan ayam', 'value' => $formatNumber($totalFeedPurchase), 'prefix' => 'Rp'], ]; - // Top 5 Customers by Order Amount in Range + // 6. Top 5 Customers - Optimized summing $topCustomers = Customer::query() + ->select('id', 'name', 'phone_number') ->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() @@ -199,89 +203,98 @@ public function getViewData(): array ->take(5) ->values(); - // 7. Chart: Daily Production (In range) + // 7. Chart: Daily Production (Optimized query) $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 = [ + $productionDataMap = EggCollection::whereBetween('production_date', [$fromDate, $toDate]) + ->selectRaw('production_date, sum(total_eggs) as good, sum(total_broken_eggs) as broken') + ->groupBy('production_date') + ->get() + ->keyBy('production_date'); + + $productionChartData = [ '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(), + 'good' => $rangeQueryDays->map(fn ($d) => (float) ($productionDataMap[$d]->good ?? 0))->values(), + 'broken' => $rangeQueryDays->map(fn ($d) => (float) ($productionDataMap[$d]->broken ?? 0))->values(), ]; - // 8. Financial Trends (Monthly - 6 months prior to to_date) + // 8. Monthly Financial Trends (Optimized aggregates) $months = collect(range(0, 5))->map(fn ($i) => Carbon::parse($toDate)->subMonths($i)->format('Y-m'))->reverse(); - $financialChart = [ + + $monthlyOrderRev = Order::whereIn(DB::raw("DATE_FORMAT(order_date, '%Y-%m')"), $months) + ->selectRaw("DATE_FORMAT(order_date, '%Y-%m') as month, sum(total_amount) as total") + ->groupBy('month')->pluck('total', 'month'); + + $monthlyDelayedRev = DelayedGood::whereIn(DB::raw("DATE_FORMAT(stored_date, '%Y-%m')"), $months) + ->selectRaw("DATE_FORMAT(stored_date, '%Y-%m') as month, sum(total_amount) as total") + ->groupBy('month')->pluck('total', 'month'); + + $monthlyExpenses = Expense::whereIn(DB::raw("DATE_FORMAT(expense_date, '%Y-%m')"), $months) + ->selectRaw("DATE_FORMAT(expense_date, '%Y-%m') as month, sum(amount) as total") + ->groupBy('month')->pluck('total', 'month'); + + $monthlyPayroll = Payroll::whereIn('period_month', $months) + ->selectRaw('period_month as month, sum(total_salary) as total') + ->groupBy('month')->pluck('total', 'month'); + + $monthlyFeed = FeedPurchase::whereIn(DB::raw("DATE_FORMAT(purchase_date, '%Y-%m')"), $months) + ->selectRaw("DATE_FORMAT(purchase_date, '%Y-%m') as month, sum(total_price) as total") + ->groupBy('month')->pluck('total', 'month'); + + $financialChartData = [ '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(), + 'revenue' => $months->map(fn ($m) => (float) (($monthlyOrderRev[$m] ?? 0) + ($monthlyDelayedRev[$m] ?? 0)))->values(), + 'expenses' => $months->map(fn ($m) => (float) (($monthlyExpenses[$m] ?? 0) + ($monthlyPayroll[$m] ?? 0) + ($monthlyFeed[$m] ?? 0)))->values(), ]; + $financialChartData['profit'] = $financialChartData['revenue']->map(fn ($rev, $i) => (float) ($rev - $financialChartData['expenses'][$i]))->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 = [ + // 9. Distribution Charts + $expenseDistributionData = [ 'labels' => ['Gaji Karyawan', 'Belanja Pakan', 'Pengeluaran Umum'], - 'data' => [(float) $totalPenggajian, (float) $totalBelanjaPakan, (float) $totalPengeluaran], + 'data' => [(float) $totalPayroll, (float) $totalFeedPurchase, (float) $totalExpenses], ]; - // 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), + $debtStatusData = [ + 'paid' => (float) $paidAmountInRange, + 'unpaid' => (float) ($delayedRevenue - $paidAmountInRange), ]; - // 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, + $revenueSplitData = [ + 'orders' => (float) $orderRevenue, + 'delayed' => (float) $delayedRevenue, ]; return [ 'mainStats' => $mainStats, 'totalEggsByUnit' => $totalEggsByUnit, - 'totalDelayedByUnit' => $totalDelayedByUnit, + 'totalDelayedByUnit' => $delayedStatsByUnit, 'topCustomers' => $topCustomers, - 'productionChart' => $productionChart, - 'financialChart' => $financialChart, - 'expenseDistribution' => $expenseDistribution, + 'productionChart' => $productionChartData, + 'financialChart' => $financialChartData, + 'expenseDistribution' => $expenseDistributionData, 'salesByUnit' => [ 'labels' => $salesByUnit->pluck('name')->values(), 'data' => $salesByUnit->pluck('total')->map(fn ($v) => (float) $v)->values(), ], - 'debtStatus' => $debtStatus, + 'debtStatus' => $debtStatusData, 'feedStocks' => [ 'labels' => $feedStocks->pluck('name')->values(), 'data' => $feedStocks->pluck('stock')->map(fn ($v) => (float) $v)->values(), @@ -293,7 +306,7 @@ public function getViewData(): array ], 'revenueSplit' => [ 'labels' => ['Penjualan Langsung', 'Barang Tertunda'], - 'data' => [$revenueSplit['orders'], $revenueSplit['delayed']], + 'data' => [$revenueSplitData['orders'], $revenueSplitData['delayed']], ], 'formatNumber' => $formatNumber, ]; diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 94873db..98b65d7 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -5,6 +5,7 @@ use AchyutN\FilamentLogViewer\FilamentLogViewer; use App\Enums\RoleEnum; use App\Filament\Pages\Auth\Login; +use App\Filament\Pages\Dashboard; use App\Filament\Pages\Profile; use BezhanSalleh\FilamentShield\FilamentShieldPlugin; use DiogoGPinto\AuthUIEnhancer\AuthUIEnhancerPlugin; @@ -12,7 +13,6 @@ use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; -use Filament\Pages\Dashboard; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; diff --git a/resources/views/filament/pages/analisys.blade.php b/resources/views/filament/pages/analisys.blade.php index 2c57d6b..50879c3 100644 --- a/resources/views/filament/pages/analisys.blade.php +++ b/resources/views/filament/pages/analisys.blade.php @@ -1,9 +1,8 @@ -
-
- {{ $this->form }} -
-
+
+ {{ $this->form }} +
+
{{-- Main Stats Grid --}}
@@ -21,7 +20,7 @@ class="text-xs font-semibold text-gray-400 dark:text-gray-500">{{ $stat['prefix' {{ $stat['value'] }}
{{ $stat['desc'] }} + class="text-[10px] text-gray-900 dark:text-gray-600 italic tracking-wide uppercase">{{ $stat['desc'] }}
@endforeach @@ -79,7 +78,7 @@ class="px-5 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:b {{ $customer->name }} {{ $customer->phone ?? 'Telp tidak ada' }} + class="text-xs text-gray-400 tracking-tight">{{ $customer->phone_number ?? 'Telp tidak ada' }}
{{ $customer->name }} {{ $customer->phone ?? 'Telp tidak ada' }} + class="text-xs text-gray-400 tracking-tight">{{ $customer->phone_number ?? 'Telp tidak ada' }}