layer-chicken/app/Filament/Pages/Analisys.php

316 lines
15 KiB
PHP

<?php
namespace App\Filament\Pages;
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 BezhanSalleh\FilamentShield\Traits\HasPageShield;
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\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\DB;
class Analisys extends Page implements HasForms
{
use HasPageShield, 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([
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('to_date')
->label('Sampai Tanggal')
->native(false)
->displayFormat('d F Y')
->live()
->afterStateUpdated(function ($set) {
$set('period', 'custom');
$this->dispatch('stats-updated');
}),
])
->compact()
->columns(3),
])
->statePath('data');
}
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, ',', '.');
$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. Customer Count
$customerCount = Customer::count();
// 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')
->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')
->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. 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. 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')
->groupBy('units.name')
->get()
->map(fn ($item) => [
'unit_name' => $item->unit_name,
'total' => $formatNumber($item->total),
]);
// 5. Financial Totals in Range
$delayedTotals = DelayedGood::whereBetween('stored_date', [$fromDate, $toDate])
->selectRaw('sum(total_amount) as total, sum(paid_amount) as paid')
->first();
$delayedRevenue = (float) ($delayedTotals->total ?? 0);
$paidAmountInRange = (float) ($delayedTotals->paid ?? 0);
$totalRevenue = $orderRevenue + $delayedRevenue;
$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');
$grossProfit = $totalRevenue - $totalFeedPurchase;
$netProfit = $grossProfit - $totalExpenses - $totalPayroll;
$totalReceivables = $delayedRevenue - $paidAmountInRange;
$mainStats = [
['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'],
];
// 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()
->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 (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();
$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) ($productionDataMap[$d]->good ?? 0))->values(),
'broken' => $rangeQueryDays->map(fn ($d) => (float) ($productionDataMap[$d]->broken ?? 0))->values(),
];
// 8. Monthly Financial Trends (Optimized aggregates)
$months = collect(range(0, 5))->map(fn ($i) => Carbon::parse($toDate)->subMonths($i)->format('Y-m'))->reverse();
$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(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();
// 9. Distribution Charts
$expenseDistributionData = [
'labels' => ['Gaji Karyawan', 'Belanja Pakan', 'Pengeluaran Umum'],
'data' => [(float) $totalPayroll, (float) $totalFeedPurchase, (float) $totalExpenses],
];
$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();
$debtStatusData = [
'paid' => (float) $paidAmountInRange,
'unpaid' => (float) ($delayedRevenue - $paidAmountInRange),
];
$feedStocks = Feed::query()
->join('units', 'feeds.unit_id', '=', 'units.id')
->select('feeds.name', 'feeds.stock', 'units.alias as unit_alias')
->get();
$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();
$revenueSplitData = [
'orders' => (float) $orderRevenue,
'delayed' => (float) $delayedRevenue,
];
return [
'mainStats' => $mainStats,
'totalEggsByUnit' => $totalEggsByUnit,
'totalDelayedByUnit' => $delayedStatsByUnit,
'topCustomers' => $topCustomers,
'productionChart' => $productionChartData,
'financialChart' => $financialChartData,
'expenseDistribution' => $expenseDistributionData,
'salesByUnit' => [
'labels' => $salesByUnit->pluck('name')->values(),
'data' => $salesByUnit->pluck('total')->map(fn ($v) => (float) $v)->values(),
],
'debtStatus' => $debtStatusData,
'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' => [$revenueSplitData['orders'], $revenueSplitData['delayed']],
],
'formatNumber' => $formatNumber,
];
}
}