@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
{{ $order->cancel_reason }}
Dibatalkan oleh {{ $order->canceller->name ?? '-' }} pada {{ $order->cancelled_at?->format('d M Y H:i') }}
Menunggu approval pembatalan order dari supervisor
{{ $displayItem['material_name'] }}
@endifPreview Upload
Klik gambar untuk lihat ukuran besar.
{{ $cancelledItemsCount }} item dibatalkan
@endif @if($order->isPaid())Order Dibuat
{{ $order->created_at->format('d M Y H:i') }}
oleh {{ $order->designer->name ?? '-' }}
Pembayaran
{{ $order->paid_at?->format('d M Y H:i') }}
oleh {{ $order->cashier->name ?? '-' }}
SPK Dicetak
{{ $order->spk_printed_at?->format('d M Y H:i') }}
Produksi Dimulai
{{ $order->started_at->format('d M Y H:i') }}
oleh {{ $order->operator->name ?? '-' }}
Selesai
{{ $order->finished_at->format('d M Y H:i') }}
Diambil
{{ $order->picked_up_at->format('d M Y H:i') }}
Dibatalkan
{{ $order->cancelled_at?->format('d M Y H:i') }}
oleh {{ $order->canceller->name ?? '-' }}
@if($order->cancel_reason){{ $order->cancel_reason }}
@endif