@extends('layouts.app') @section('content') @php $normalizeUnitLabel = function ($unit) { $unit = strtolower(trim((string) $unit)); return match ($unit) { 'm2', 'm²' => 'm²', 'meter', 'm' => 'meter', 'pcs', 'pc', 'piece', 'lembar' => 'lembar', default => $unit !== '' ? $unit : 'lembar', }; }; $normalizeUnitKey = function ($unit) { $unit = strtolower(trim((string) $unit)); return match ($unit) { 'm2', 'm²' => 'm2', 'meter', 'm' => 'meter', 'pcs', 'pc', 'piece', 'lembar' => 'lembar', default => $unit !== '' ? $unit : 'lembar', }; }; $formatNumberClean = function ($value, $decimals = 2) { $formatted = number_format((float) $value, $decimals, ',', '.'); $formatted = rtrim($formatted, '0'); $formatted = rtrim($formatted, ','); return $formatted === '' ? '0' : $formatted; }; $parseTwoDimensions = function ($text) { $clean = strtolower(trim((string) $text)); if ($clean === '') { return null; } $clean = preg_replace('/\s+/', '', $clean); $clean = str_replace(['×', '*'], 'x', $clean); if (!preg_match('/^(\d+(?:[.,]\d+)?)x(\d+(?:[.,]\d+)?)(cm|m)?$/', $clean, $matches)) { return null; } $a = (float) str_replace(',', '.', $matches[1]); $b = (float) str_replace(',', '.', $matches[2]); $suffix = $matches[3] ?? ''; if ($suffix === 'cm') { return [$a, $b, 'cm']; } if ($suffix === 'm') { return [$a, $b, 'm']; } $unit = ($a >= 10 && $b >= 10) ? 'cm' : 'm'; return [$a, $b, $unit]; }; $resolveAreaValue = function ($sizeValue, $sizeDescription, $basePrice, $qty, $storedSubtotal) use ($parseTwoDimensions) { $sizeValue = (float) $sizeValue; $basePrice = (float) $basePrice; $qty = max((float) $qty, 0); $storedSubtotal = (float) $storedSubtotal; if ($sizeValue > 0) { return round($sizeValue, 4); } $dims = $parseTwoDimensions($sizeDescription); if ($dims) { [$a, $b, $parsedUnit] = $dims; if ($parsedUnit === 'cm') { return round(($a * $b) / 10000, 4); } return round($a * $b, 4); } if ($storedSubtotal > 0 && $basePrice > 0 && $qty > 0) { return round($storedSubtotal / ($basePrice * $qty), 4); } return 1; }; $resolveMeterValue = function ($sizeValue, $sizeDescription, $material, $basePrice, $qty, $storedSubtotal) use ($parseTwoDimensions) { $sizeValue = (float) $sizeValue; $basePrice = (float) $basePrice; $qty = max((float) $qty, 0); $storedSubtotal = (float) $storedSubtotal; if ($sizeValue > 0) { return round($sizeValue, 4); } $dims = $parseTwoDimensions($sizeDescription); if ($dims) { [$a, $b, $parsedUnit] = $dims; $aCm = $parsedUnit === 'cm' ? $a : ($a * 100); $bCm = $parsedUnit === 'cm' ? $b : ($b * 100); $rollWidthCm = (float) ($material->roll_width_cm ?? 0); $allowRotation = (bool) ($material->allow_rotation ?? true); if ($rollWidthCm > 0) { $candidates = []; if ($aCm <= $rollWidthCm) { $candidates[] = $bCm; } if ($allowRotation && $bCm <= $rollWidthCm) { $candidates[] = $aCm; } if (!empty($candidates)) { return round(min($candidates) / 100, 4); } return round(max($aCm, $bCm) / 100, 4); } return round(min($aCm, $bCm) / 100, 4); } if ($storedSubtotal > 0 && $basePrice > 0 && $qty > 0) { return round($storedSubtotal / ($basePrice * $qty), 4); } return 1; }; $resolveSizeValue = function ($sizeValue, $sizeDescription, $unit, $material = null, $basePrice = 0, $qty = 1, $storedSubtotal = 0) use ( $normalizeUnitKey, $resolveAreaValue, $resolveMeterValue ) { $unitKey = $normalizeUnitKey($unit); if ($unitKey === 'm2') { return $resolveAreaValue($sizeValue, $sizeDescription, $basePrice, $qty, $storedSubtotal); } if ($unitKey === 'meter') { return $resolveMeterValue($sizeValue, $sizeDescription, $material, $basePrice, $qty, $storedSubtotal); } return 1; }; $applyRoundingOnly = function ($amount) { $amount = max((float) $amount, 0); $roundTo = 1000; if ($amount <= 0) { return 0; } return (float) (ceil($amount / $roundTo) * $roundTo); }; $applyMeterMinimumAndRounding = function ($amount) { $amount = max((float) $amount, 0); $minimumOrder = 10000; $roundTo = 1000; if ($amount <= 0) { return 0; } if ($amount < $minimumOrder) { $amount = $minimumOrder; } return (float) (ceil($amount / $roundTo) * $roundTo); }; $calculateDisplaySubtotal = function ($basePrice, $qty, $unit, $resolvedSizeValue, $storedSubtotal = 0) use ($normalizeUnitKey, $applyRoundingOnly, $applyMeterMinimumAndRounding) { $unitKey = $normalizeUnitKey($unit); $basePrice = max((float) $basePrice, 0); $qty = max((float) $qty, 0); $resolvedSizeValue = max((float) $resolvedSizeValue, 0); $storedSubtotal = max((float) $storedSubtotal, 0); // Wajib utamakan subtotal final yang tersimpan. // Setelah ada minimal order + pembulatan, angka tampilan juga dipaksa rapi: // minimal Rp 10.000 dan pembulatan ke atas Rp 1.000. if ($storedSubtotal > 0) { return in_array($unitKey, ['m2', 'meter'], true) ? round($applyMeterMinimumAndRounding($storedSubtotal), 2) : round($applyRoundingOnly($storedSubtotal), 2); } if (in_array($unitKey, ['m2', 'meter'], true)) { return round($applyMeterMinimumAndRounding($basePrice * $resolvedSizeValue * $qty), 2); } return round($applyRoundingOnly($basePrice * $qty), 2); }; $rawItems = method_exists($order, 'items') ? ($order->relationLoaded('items') ? $order->items : $order->items()->with('material')->get()) : collect(); $displayItems = $rawItems->isNotEmpty() ? $rawItems->map(function ($item) use ($normalizeUnitLabel, $normalizeUnitKey, $resolveSizeValue, $calculateDisplaySubtotal, $order) { $sizeUnit = $item->size_unit ?? 'lembar'; $sizeUnitKey = $normalizeUnitKey($sizeUnit); $basePrice = (float) ($item->base_price ?? 0); $qty = (float) ($item->qty ?? 1); $storedSubtotal = (float) ($item->system_subtotal ?? $item->subtotal ?? 0); $resolvedSizeValue = $resolveSizeValue( $item->size_value ?? 0, $item->size_description ?? '', $sizeUnit, $item->material ?? null, $basePrice, $qty, $storedSubtotal ); $displaySubtotal = $calculateDisplaySubtotal( $basePrice, $qty, $sizeUnit, $resolvedSizeValue, $storedSubtotal ); return [ 'model' => $item, 'id' => $item->id ?? null, 'product_name' => $item->product_name ?? '-', 'material_name' => $item->material_name ?? null, 'file_name' => $item->file_name ?? '-', 'size_description' => $item->size_description ?? '-', 'size_value' => (float) ($item->size_value ?? 0), 'resolved_size_value' => $resolvedSizeValue, 'size_unit' => $sizeUnitKey, 'size_unit_label' => $normalizeUnitLabel($sizeUnit), 'qty' => $qty, 'sisi' => (int) ($item->sisi ?? 1), 'base_price' => $basePrice, 'stored_subtotal' => $storedSubtotal, 'display_subtotal' => $displaySubtotal, 'finishing_note' => $item->finishing_note ?? null, 'file_url' => $item->file_url ?? $item->file_path_url ?? null, 'preview_image_path' => $item->preview_image_path ?? null, 'preview_image_url' => !empty($item->preview_image_path) ? route('orders.items.preview', [$order, $item]) : null, 'is_cancelled' => method_exists($item, 'isCancelled') ? $item->isCancelled() : (bool) ($item->is_cancelled ?? false), 'has_pending_cancellation' => method_exists($item, 'hasPendingCancellationRequest') ? $item->hasPendingCancellationRequest() : false, 'cancel_reason' => $item->cancel_reason ?? null, 'can_cancel' => method_exists($item, 'canBeCancelled') && auth()->check() && method_exists(auth()->user(), 'canRequestCancellations') ? ($item->canBeCancelled() && !$item->hasPendingCancellationRequest() && auth()->user()->canRequestCancellations()) : false, ]; })->values() : collect([ (function () use ( $order, $normalizeUnitLabel, $normalizeUnitKey, $resolveSizeValue, $calculateDisplaySubtotal ) { $sizeUnit = $order->size_unit ?? 'lembar'; $sizeUnitKey = $normalizeUnitKey($sizeUnit); $basePrice = (float) ($order->base_price ?? 0); $qty = (float) ($order->qty ?? 1); $storedSubtotal = (float) ($order->total_price ?? 0); $resolvedSizeValue = $resolveSizeValue( $order->size_value ?? 0, $order->size ?? '', $sizeUnit, null, $basePrice, $qty, $storedSubtotal ); $displaySubtotal = $calculateDisplaySubtotal( $basePrice, $qty, $sizeUnit, $resolvedSizeValue, $storedSubtotal ); return [ 'model' => null, 'id' => null, 'product_name' => $order->product_name ?? '-', 'material_name' => $order->material_name ?? null, 'file_name' => $order->file_name ?? '-', 'size_description' => $order->size ?? '-', 'size_value' => (float) ($order->size_value ?? 0), 'resolved_size_value' => $resolvedSizeValue, 'size_unit' => $sizeUnitKey, 'size_unit_label' => $normalizeUnitLabel($sizeUnit), 'qty' => $qty, 'sisi' => (int) ($order->sisi ?? 1), 'base_price' => $basePrice, 'stored_subtotal' => $storedSubtotal, 'display_subtotal' => $displaySubtotal, 'finishing_note' => $order->finishing_note ?? null, 'file_url' => null, 'preview_image_path' => null, 'preview_image_url' => null, 'is_cancelled' => false, 'has_pending_cancellation' => false, 'cancel_reason' => null, 'can_cancel' => false, ]; })(), ]); $activeItems = $displayItems->where('is_cancelled', false)->values(); $activeItemsCount = $activeItems->count(); $cancelledItemsCount = $displayItems->where('is_cancelled', true)->count(); $displayGrandTotal = (float) $activeItems->sum(fn ($item) => (float) $item['display_subtotal']); // Sinkron tampilan pembayaran dengan total final yang sudah kena minimal order + pembulatan. // Jangan lagi pakai change_amount lama apa adanya, karena data lama bisa masih menyimpan // kembalian dari total sebelum pembulatan. $displayPaidAmount = (float) ($order->paid_amount ?? 0); if ($displayPaidAmount <= 0 && isset($order->payments)) { $displayPaidAmount = (float) $order->payments->sum('amount'); } $displayChangeAmount = 0; if ($order->isPaid()) { $displayChangeAmount = max($displayPaidAmount - $displayGrandTotal, 0); // Jika paid_amount sudah berupa nilai bersih setelah dikurangi kembalian, // maka kembalian wajib 0 agar tidak muncul angka lama seperti Rp 600. if ($displayPaidAmount <= $displayGrandTotal) { $displayChangeAmount = 0; } } @endphp @if(session('success'))
{{ session('success') }}
@endif @if(session('error'))
{{ session('error') }}
@endif @if($order->isCancelled())

Order Dibatalkan

{{ $order->cancel_reason }}

Dibatalkan oleh {{ $order->canceller->name ?? '-' }} pada {{ $order->cancelled_at?->format('d M Y H:i') }}

@endif @if(!$order->isCancelled() && $order->hasPendingCancellationRequest())

Menunggu approval pembatalan order dari supervisor

@endif

{{ $order->invoice_number }}

@if($order->isCancelled()) Dibatalkan @endif @if($order->is_credit) Piutang @endif {{ $order->getPaymentStatusLabel() }} @if(!$order->isCancelled()) {{ $order->getProductionStatusLabel() }} @endif {{ $order->getPickupStatusLabel() }}

Data Pelanggan

Nama
{{ $order->customer_name }}
@if($order->customer_phone)
Telepon
{{ $order->customer_phone }}
@endif

Detail Order

Tanggal
{{ $order->created_at->format('d M Y H:i') }}
Designer
{{ $order->designer->name ?? '-' }}
Jumlah Item
{{ $displayItems->count() }} item
@if($order->notes)
Catatan Order
{{ $order->notes }}
@endif

Item Order

@foreach($displayItems as $index => $displayItem) @php $itemModel = $displayItem['model']; $isCancelled = $displayItem['is_cancelled']; $hasPendingCancellation = $displayItem['has_pending_cancellation']; $canCancelItem = $displayItem['can_cancel'] && $itemModel; $qtyDisplay = $formatNumberClean($displayItem['qty']); $sizeValueDisplay = $displayItem['size_unit'] === 'lembar' ? '1' : $formatNumberClean($displayItem['resolved_size_value'], 4); $displaySubtotal = (float) $displayItem['display_subtotal']; @endphp
@if($isCancelled)
Dibatalkan
@endif @if(!$isCancelled && $hasPendingCancellation)
Menunggu Approval
@endif
{{ $index + 1 }}
{{ $displayItem['product_name'] }}
@if($displayItem['material_name'])

{{ $displayItem['material_name'] }}

@endif
Rp {{ number_format($displaySubtotal, 0, ',', '.') }} @if($canCancelItem) @endif
Nama File
{{ $displayItem['file_name'] }}
Ukuran
{{ $displayItem['size_description'] }}
Sisi
@if($displayItem['size_unit'] === 'm2') - @elseif(($displayItem['sisi'] ?? 1) == 2) 2 sisi @else 1 sisi @endif
Harga/{{ $displayItem['size_unit_label'] }}
Rp {{ number_format((float) $displayItem['base_price'], 0, ',', '.') }}
Qty
{{ $qtyDisplay }}
@if($displayItem['finishing_note'])
Finishing: {{ $displayItem['finishing_note'] }}
@endif @if($displayItem['file_url']) @endif @if(!empty($displayItem['preview_image_url']))

Preview Upload

Klik gambar untuk lihat ukuran besar.

@endif @if($isCancelled && $displayItem['cancel_reason'])
Alasan: {{ $displayItem['cancel_reason'] }}
@endif
Rp {{ number_format((float) $displayItem['base_price'], 0, ',', '.') }} × {{ $sizeValueDisplay }} {{ $displayItem['size_unit_label'] }} × {{ $qtyDisplay }} = Rp {{ number_format($displaySubtotal, 0, ',', '.') }}
@endforeach
Grand Total ({{ $activeItemsCount }} item aktif) Rp {{ number_format($displayGrandTotal, 0, ',', '.') }}
@if($cancelledItemsCount > 0)

{{ $cancelledItemsCount }} item dibatalkan

@endif @if($order->isPaid())
Dibayar Rp {{ number_format($displayPaidAmount, 0, ',', '.') }}
Kembalian Rp {{ number_format($displayChangeAmount, 0, ',', '.') }}
@endif

Aksi

@if(!$order->isCancelled()) @if($order->canPrintSPK() && auth()->user()->canPrintSPK()) Cetak SPK @endif @if($order->isPaid() && auth()->user()->canProcessPayments()) Cetak Nota Cetak Faktur @endif @if(auth()->user()->can('edit orders')) Edit Order @endif @if($order->canMarkAsPickedUp())
@csrf
@endif @if($order->canBeCancelled() && !$order->hasPendingCancellationRequest() && auth()->user()->canRequestCancellations()) @endif @if($order->customer_phone) @php $phone = preg_replace('/[^0-9]/', '', $order->customer_phone); if (str_starts_with($phone, '08')) { $phone = '62' . substr($phone, 1); } elseif (str_starts_with($phone, '8')) { $phone = '62' . $phone; } elseif (str_starts_with($phone, '+62')) { $phone = substr($phone, 1); } $waMessage = urlencode(\App\Models\Setting::buildWaMessage($order)); @endphp Kirim WA @endif @endif Kembali

Riwayat

Order Dibuat

{{ $order->created_at->format('d M Y H:i') }}

oleh {{ $order->designer->name ?? '-' }}

@if($order->isPaid())

Pembayaran

{{ $order->paid_at?->format('d M Y H:i') }}

oleh {{ $order->cashier->name ?? '-' }}

@endif @if($order->spk_printed)

SPK Dicetak

{{ $order->spk_printed_at?->format('d M Y H:i') }}

@endif @if($order->started_at)

Produksi Dimulai

{{ $order->started_at->format('d M Y H:i') }}

oleh {{ $order->operator->name ?? '-' }}

@endif @if($order->finished_at)

Selesai

{{ $order->finished_at->format('d M Y H:i') }}

@endif @if($order->picked_up_at)

Diambil

{{ $order->picked_up_at->format('d M Y H:i') }}

@endif @if($order->isCancelled())

Dibatalkan

{{ $order->cancelled_at?->format('d M Y H:i') }}

oleh {{ $order->canceller->name ?? '-' }}

@if($order->cancel_reason)

{{ $order->cancel_reason }}

@endif
@endif
{{-- Modal Preview Gambar --}} @endsection {{-- Preview gambar tersedia via $item->preview_image_path --}}