@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
Tidak termasuk PPN
PPN Keluaran
Dipisah dari omset
Rata-rata / Order
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.
{{-- 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, ',', '.') }}
| Tanggal |
Order |
Omset Bersih |
@forelse($dailyData as $day)
|
{{ !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, ',', '.') }}
|
@empty
|
Tidak ada data harian.
|
@endforelse
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, ',', '.') }}
| Bulan |
Hari |
Order |
Omset Bersih |
@forelse($monthlyData as $month)
|
{{ $month->month_label }}
|
{{ $month->total_days }}
|
{{ number_format($month->total_orders, 0, ',', '.') }}
|
Rp {{ number_format($month->total_revenue, 0, ',', '.') }}
|
@empty
|
Tidak ada data bulanan.
|
@endforelse
{{-- 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
| Tanggal |
Invoice |
Pelanggan |
Status |
Nilai Invoice |
Nominal Omset |
@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
|
{{ $tanggalTampil ? $tanggalTampil->format('d/m/Y') : '-' }}
|
{{ $order->invoice_number }}
|
{{ $order->customer_name }}
|
{{ $statusLabel }}
|
Rp {{ number_format($orderTotal, 0, ',', '.') }}
|
Rp {{ number_format($nominalOmset, 0, ',', '.') }}
|
@empty
|
Tidak ada data pada filter ini.
|
@endforelse
@if(method_exists($orders, 'hasPages') && $orders->hasPages())
{{ $orders->links() }}
@endif
@endsection