@extends('layouts.app') @section('content') @php /* |-------------------------------------------------------------------------- | Sinkron Nominal Laporan Omset |-------------------------------------------------------------------------- | Aturan: | - Minimal order/item Rp10.000 hanya untuk meter/m² | - Selain meter/m² hanya dibulatkan ke atas Rp1.000 | - Omset bersih tidak termasuk PPN | - PPN tetap dipisah | - Tidak memakai total mentah jika data item tersedia */ $minimumOrder = 10000; $roundTo = 1000; $syncRoundAmount = function ($amount, bool $applyMinimum = false) use ($minimumOrder, $roundTo) { $amount = (float) ($amount ?? 0); if ($amount <= 0) { return 0; } // Minimal Rp10.000 hanya berlaku untuk item meter/m². if ($applyMinimum && $amount < $minimumOrder) { $amount = $minimumOrder; } return ceil($amount / $roundTo) * $roundTo; }; $syncNormalizeUnit = function ($unit) { $unit = strtolower(trim((string) $unit)); if (in_array($unit, ['m2', 'm²', 'meter2', 'meter persegi', 'per meter persegi'], true)) { return 'm2'; } if (in_array($unit, ['meter', 'm', 'per meter'], true)) { return 'meter'; } return $unit ?: 'lembar'; }; $syncOrderItems = function ($order) { if (!$order) { return collect(); } if (method_exists($order, 'relationLoaded') && $order->relationLoaded('items')) { return collect($order->items ?? []); } if (isset($order->items) && is_iterable($order->items)) { return collect($order->items); } return collect(); }; $syncItemSubtotal = function ($item) use ($syncRoundAmount, $syncNormalizeUnit) { if (!$item) { return 0; } $unit = $syncNormalizeUnit($item->size_unit ?? $item->unit ?? 'lembar'); $isMeterItem = in_array($unit, ['m2', 'meter'], true); $rawSubtotal = $item->display_subtotal ?? $item->report_subtotal ?? $item->subtotal ?? null; if ($rawSubtotal === null) { $qty = max((float) ($item->qty ?? 1), 0); $price = max((float) ($item->price ?? $item->unit_price ?? 0), 0); $sizeValue = max((float) ($item->size_value ?? 0), 0); if ($isMeterItem) { $rawSubtotal = $price * max($sizeValue, 1) * max($qty, 1); } else { $rawSubtotal = $price * max($qty, 1); } } return $syncRoundAmount($rawSubtotal, $isMeterItem); }; $syncOrderNetTotal = function ($order) use ($syncOrderItems, $syncItemSubtotal, $syncRoundAmount) { if (!$order) { return 0; } $items = $syncOrderItems($order); if ($items->count() > 0) { return (float) $items->sum(fn ($item) => $syncItemSubtotal($item)); } $rawTotal = (float) ( $order->display_total_price ?? $order->report_total_price ?? $order->total_before_ppn ?? $order->subtotal ?? $order->total_price ?? 0 ); $usePpn = (bool) ($order->use_ppn ?? false); $ppnAmount = (float) ($order->ppn_amount ?? 0); // Jika total lama sudah termasuk PPN, keluarkan dulu PPN agar omset bersih tidak ikut pajak. if ($usePpn && $ppnAmount > 0 && $rawTotal >= $ppnAmount) { $rawTotal -= $ppnAmount; } return $syncRoundAmount($rawTotal, false); }; $syncOrderPpn = function ($order, $netTotal) { if (!$order || !(bool) ($order->use_ppn ?? false)) { return 0; } $storedPpn = (float) ($order->ppn_amount ?? 0); if ($storedPpn > 0) { return $storedPpn; } $rate = (float) ($order->ppn_rate ?? 0); if ($rate <= 0) { return 0; } return round(((float) $netTotal) * ($rate / 100)); }; $syncPaidAmount = function ($order) { if (!$order) { return 0; } if (method_exists($order, 'relationLoaded') && $order->relationLoaded('payments')) { return (float) collect($order->payments ?? [])->sum('amount'); } if (isset($order->payments) && is_iterable($order->payments)) { return (float) collect($order->payments)->sum('amount'); } return (float) ($order->paid_amount ?? 0); }; $syncOrderDisplay = function ($order) use ($syncOrderNetTotal, $syncOrderPpn, $syncPaidAmount) { $netTotal = (float) $syncOrderNetTotal($order); $ppnAmount = (float) $syncOrderPpn($order, $netTotal); $invoiceTotal = $netTotal + $ppnAmount; $paidAmount = (float) $syncPaidAmount($order); $remaining = max($invoiceTotal - $paidAmount, 0); return [ 'net_total' => $netTotal, 'ppn_amount' => $ppnAmount, 'invoice_total' => $invoiceTotal, 'paid_amount' => $paidAmount, 'remaining' => $remaining, 'is_receivable' => $remaining > 0, 'revenue_amount' => $netTotal, ]; }; $dailyData = collect($dailyData ?? []); $orders = $orders ?? collect(); $machines = collect($machines ?? []); $categories = collect($categories ?? []); $ordersCollection = method_exists($orders, 'getCollection') ? collect($orders->getCollection()) : collect($orders ?? []); $canRebuildSummaryFromOrders = $ordersCollection->count() > 0 && (!method_exists($orders, 'total') || (int) $orders->total() === (int) $ordersCollection->count()); if ($canRebuildSummaryFromOrders) { $displayOrders = $ordersCollection->map(function ($order) use ($syncOrderDisplay) { $display = $syncOrderDisplay($order); return (object) [ 'order' => $order, 'date' => $order->paid_at ?? $order->created_at ?? null, 'customer_name' => $order->customer_name ?? '-', 'net_total' => $display['net_total'], 'ppn_amount' => $display['ppn_amount'], 'invoice_total' => $display['invoice_total'], 'remaining' => $display['remaining'], 'is_receivable' => $display['is_receivable'], 'revenue_amount' => $display['revenue_amount'], 'qty' => (float) ($order->report_total_qty ?? $order->total_qty ?? 0), ]; }); $paidRows = $displayOrders->filter(fn ($row) => !$row->is_receivable); $receivableRows = $displayOrders->filter(fn ($row) => $row->is_receivable); $paidRevenue = (float) $paidRows->sum('net_total'); $receivableRevenue = (float) $receivableRows->sum('net_total'); $totalRevenue = $paidRevenue + $receivableRevenue; $totalPpn = (float) $displayOrders->sum('ppn_amount'); $paidPpn = (float) $paidRows->sum('ppn_amount'); $totalInvoice = (float) $displayOrders->sum('invoice_total'); $totalOrders = (int) $displayOrders->count(); $paidOrders = (int) $paidRows->count(); $receivableOrders = (int) $receivableRows->count(); $totalQty = (float) $displayOrders->sum('qty'); $paidCustomers = $paidRows ->groupBy('customer_name') ->map(fn ($rows, $name) => [ 'customer_name' => $name, 'total_orders' => collect($rows)->count(), 'total_amount' => collect($rows)->sum('net_total'), ]) ->sortByDesc('total_amount') ->values(); $receivableCustomers = $receivableRows ->groupBy('customer_name') ->map(fn ($rows, $name) => [ 'customer_name' => $name, 'total_orders' => collect($rows)->count(), 'total_amount' => collect($rows)->sum('net_total'), ]) ->sortByDesc('total_amount') ->values(); $dailyData = $displayOrders ->groupBy(function ($row) { return !empty($row->date) ? \Carbon\Carbon::parse($row->date)->format('Y-m-d') : 'unknown'; }) ->map(function ($rows, $date) { return (object) [ 'date' => $date === 'unknown' ? null : $date, 'total_revenue' => collect($rows)->sum('net_total'), 'total_orders' => collect($rows)->count(), ]; }) ->sortByDesc('date') ->values(); } else { $paidRevenue = (float) ($summary['paid_revenue'] ?? 0); $receivableRevenue = (float) ($summary['receivable_revenue'] ?? 0); $totalRevenue = (float) ($summary['total_revenue'] ?? 0); $totalPpn = (float) ($summary['total_ppn'] ?? 0); $paidPpn = (float) ($summary['paid_ppn'] ?? 0); $totalInvoice = (float) ($summary['total_invoice'] ?? ($totalRevenue + $totalPpn)); $totalOrders = (int) ($summary['total_orders'] ?? 0); $paidOrders = (int) ($summary['paid_orders'] ?? 0); $receivableOrders = (int) ($summary['receivable_orders'] ?? 0); $totalQty = (float) ($summary['total_qty'] ?? 0); $paidCustomers = collect($paidCustomers ?? []); $receivableCustomers = collect($receivableCustomers ?? []); } $paidCustomers = collect($paidCustomers ?? []); $receivableCustomers = collect($receivableCustomers ?? []); $paidCustomersCount = (int) ($summary['paid_customers'] ?? $paidCustomers->count()); $receivableCustomersCount = (int) ($summary['receivable_customers'] ?? $receivableCustomers->count()); $paidPercent = $totalRevenue > 0 ? round(($paidRevenue / $totalRevenue) * 100, 1) : 0; $receivablePercent = $totalRevenue > 0 ? round(($receivableRevenue / $totalRevenue) * 100, 1) : 0; $avgOrderValue = $totalOrders > 0 ? $totalRevenue / $totalOrders : 0; $bestDay = $dailyData->sortByDesc(fn ($item) => (float) ($item->total_revenue ?? 0))->first(); $bestDayLabel = $bestDay && !empty($bestDay->date) ? \Carbon\Carbon::parse($bestDay->date)->translatedFormat('d M Y') : '-'; $bestDayRevenue = (float) ($bestDay->total_revenue ?? 0); $bestDayOrders = (int) ($bestDay->total_orders ?? 0); $monthlyData = $dailyData ->groupBy(function ($item) { return !empty($item->date) ? \Carbon\Carbon::parse($item->date)->format('Y-m') : 'unknown'; }) ->map(function ($items, $monthKey) { $totalRevenueMonth = collect($items)->sum(fn ($row) => (float) ($row->total_revenue ?? 0)); $totalOrdersMonth = collect($items)->sum(fn ($row) => (int) ($row->total_orders ?? 0)); $totalDaysMonth = collect($items)->count(); return (object) [ 'month_key' => $monthKey, 'month_label' => $monthKey !== 'unknown' ? \Carbon\Carbon::createFromFormat('Y-m', $monthKey)->translatedFormat('F Y') : '-', 'total_revenue' => $totalRevenueMonth, 'total_orders' => $totalOrdersMonth, 'total_days' => $totalDaysMonth, 'avg_daily_revenue' => $totalDaysMonth > 0 ? $totalRevenueMonth / $totalDaysMonth : 0, ]; }) ->sortByDesc('month_key') ->values(); $bestMonth = $monthlyData->sortByDesc('total_revenue')->first(); $bestMonthLabel = $bestMonth->month_label ?? '-'; $bestMonthRevenue = (float) ($bestMonth->total_revenue ?? 0); $topPaidCustomer = $paidCustomers->first(); $topPaidCustomerName = is_array($topPaidCustomer) ? ($topPaidCustomer['customer_name'] ?? '-') : ($topPaidCustomer->customer_name ?? '-'); $topPaidCustomerAmount = (float) ( is_array($topPaidCustomer) ? ($topPaidCustomer['total_amount'] ?? 0) : ($topPaidCustomer->total_amount ?? 0) ); $topReceivableCustomer = $receivableCustomers->first(); $topReceivableCustomerName = is_array($topReceivableCustomer) ? ($topReceivableCustomer['customer_name'] ?? '-') : ($topReceivableCustomer->customer_name ?? '-'); $topReceivableCustomerAmount = (float) ( is_array($topReceivableCustomer) ? ($topReceivableCustomer['total_amount'] ?? 0) : ($topReceivableCustomer->total_amount ?? 0) ); @endphp @endphp
{{-- Ringkasan utama --}}
Dashboard Omset Sinkron

Ringkasan Omset Periode

Nilai omset utama pada halaman ini sudah menjadi omset bersih tanpa PPN. PPN dipisahkan sebagai pajak keluaran agar laporan penjualan tidak tercampur pajak.

Omset Bersih
Rp {{ number_format($totalRevenue, 0, ',', '.') }}
Tidak termasuk PPN
PPN Keluaran
Rp {{ number_format($totalPpn, 0, ',', '.') }}
Dipisah dari omset
Total Order
{{ number_format($totalOrders, 0, ',', '.') }}
Total Qty
{{ number_format($totalQty, 0, ',', '.') }}
Rata-rata / Order
Rp {{ number_format($avgOrderValue, 0, ',', '.') }}
Bersih tanpa PPN
Sudah Lunas
Rp {{ number_format($paidRevenue, 0, ',', '.') }}
@if($paidPpn > 0)
PPN terpisah: Rp {{ number_format($paidPpn, 0, ',', '.') }}
@endif
{{ $paidOrders }} order
{{ $paidPercent }}%
{{ $paidCustomersCount }} pelanggan
Piutang
Rp {{ number_format($receivableRevenue, 0, ',', '.') }}
{{ $receivableOrders }} order
{{ $receivablePercent }}%
{{ $receivableCustomersCount }} pelanggan
{{-- Filter --}}

Filter Laporan

Saring data berdasarkan periode, mesin, dan kategori.

@foreach($machines as $machine) @endforeach
@foreach($categories as $category) @endforeach
Reset
{{-- Harian dan Bulanan --}}

Omset Harian

Ringkasan omset per hari pada periode terpilih.

{{ $dailyData->count() }} hari
Hari Terbaik
{{ $bestDayLabel }}
Rp {{ number_format($bestDayRevenue, 0, ',', '.') }}
{{ number_format($bestDayOrders, 0, ',', '.') }} order
Rata-rata Harian
Rp {{ number_format($dailyData->count() > 0 ? $dailyData->avg(fn ($row) => (float) ($row->total_revenue ?? 0)) : 0, 0, ',', '.') }}
@forelse($dailyData as $day) @empty @endforelse
Tanggal Order Omset Bersih
{{ !empty($day->date) ? \Carbon\Carbon::parse($day->date)->translatedFormat('d M Y') : '-' }} {{ (int) ($day->total_orders ?? 0) }} Rp {{ number_format((float) ($day->total_revenue ?? 0), 0, ',', '.') }}
Tidak ada data harian.

Omset Bulanan

Rekap bulan dari data harian pada periode yang dipilih.

{{ $monthlyData->count() }} bulan
Bulan Terbaik
{{ $bestMonthLabel }}
Rp {{ number_format($bestMonthRevenue, 0, ',', '.') }}
Rata-rata Bulanan
Rp {{ number_format($monthlyData->count() > 0 ? $monthlyData->avg('total_revenue') : 0, 0, ',', '.') }}
@forelse($monthlyData as $month) @empty @endforelse
Bulan Hari Order Omset Bersih
{{ $month->month_label }} {{ $month->total_days }} {{ number_format($month->total_orders, 0, ',', '.') }} Rp {{ number_format($month->total_revenue, 0, ',', '.') }}
Tidak ada data bulanan.
{{-- Insight pelanggan --}}

Pelanggan Lunas

Pelanggan dengan transaksi lunas pada periode ini.

{{ $paidCustomersCount }} pelanggan
Top Customer
{{ $topPaidCustomerName }}
Rp {{ number_format($topPaidCustomerAmount, 0, ',', '.') }}
@forelse($paidCustomers->take(6) as $customer) @php $customerName = is_array($customer) ? ($customer['customer_name'] ?? '-') : ($customer->customer_name ?? '-'); $customerOrders = (int) (is_array($customer) ? ($customer['total_orders'] ?? 0) : ($customer->total_orders ?? 0)); $customerTotal = (float) (is_array($customer) ? ($customer['total_amount'] ?? 0) : ($customer->total_amount ?? 0)); @endphp
{{ $customerName }}
{{ $customerOrders }} order
Rp {{ number_format($customerTotal, 0, ',', '.') }}
@empty
Belum ada data pelanggan lunas.
@endforelse

Pelanggan Piutang

Pelanggan dengan sisa piutang pada periode ini.

{{ $receivableCustomersCount }} pelanggan
Top Customer
{{ $topReceivableCustomerName }}
Rp {{ number_format($topReceivableCustomerAmount, 0, ',', '.') }}
@forelse($receivableCustomers->take(6) as $customer) @php $customerName = is_array($customer) ? ($customer['customer_name'] ?? '-') : ($customer->customer_name ?? '-'); $customerOrders = (int) (is_array($customer) ? ($customer['total_orders'] ?? 0) : ($customer->total_orders ?? 0)); $customerTotal = (float) (is_array($customer) ? ($customer['total_amount'] ?? 0) : ($customer->total_amount ?? 0)); @endphp
{{ $customerName }}
{{ $customerOrders }} order
Rp {{ number_format($customerTotal, 0, ',', '.') }}
@empty
Belum ada data pelanggan piutang.
@endforelse
{{-- Detail order --}}

Detail Order Penjualan

Tabel detail untuk melihat kontribusi per order pada periode yang dipilih.

Cetak
@forelse($orders as $order) @php $displayOrder = $syncOrderDisplay($order); $remainingBalance = (float) $displayOrder['remaining']; $orderTotal = (float) $displayOrder['invoice_total']; $nominalOmset = (float) $displayOrder['net_total']; $isReceivable = $remainingBalance > 0; $statusLabel = $isReceivable ? 'Piutang' : 'Lunas'; $statusClass = $isReceivable ? 'status-rec' : 'status-paid'; $tanggalTampil = $order->paid_at ?? $order->created_at ?? null; @endphp @empty @endforelse
Tanggal Invoice Pelanggan Status Nilai Invoice Nominal Omset
{{ $tanggalTampil ? $tanggalTampil->format('d/m/Y') : '-' }} {{ $order->invoice_number }} {{ $order->customer_name }} {{ $statusLabel }} Rp {{ number_format($orderTotal, 0, ',', '.') }} Rp {{ number_format($nominalOmset, 0, ',', '.') }}
Tidak ada data pada filter ini.
@if(method_exists($orders, 'hasPages') && $orders->hasPages())
{{ $orders->links() }}
@endif
@endsection