ifnex/04_Laravel/resources/views/orders/create.blade.php
Kazem Alghasi 24fc56feaf feat(order): implement order creation flow and pricing API
Introduce order management functionality including web routes for creating orders and a new API endpoint for price calculations. This change also cleans up the codebase by removing various debug scripts used during the development of the shipping rate logic.

- Add `OrderController` to handle order creation and success redirection.
- Add `POST /v1/calculate` endpoint for pricing calculations.
- Define web routes for order creation flow (`/order`).
- Remove multiple debug PHP scripts from the `04_Laravel` directory.
- Add base layout and order view directories.
2026-08-04 07:59:23 +03:30

406 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('layouts.app')
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-5 border-b border-gray-200">
<h1 class="text-2xl font-bold text-gray-900">فرم ثبت سفارش</h1>
<p class="mt-1 text-sm text-gray-500">لطفاً اطلاعات مرسوله و کالاهای گمرکی را وارد کنید</p>
</div>
<form action="{{ route('orders.store') }}" method="POST" id="orderForm" class="p-6 space-y-8">
@csrf
<input type="hidden" name="direction" id="direction" value="export">
<input type="hidden" name="type" id="type" value="DOC_NORMAL">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="space-y-6">
<h2 class="text-lg font-semibold text-gray-900 border-b pb-2">اطلاعات مسیر</h2>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">نوع سرویس</label>
<select id="serviceType" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
<option value="DOC_NORMAL" selected>Document Normal</option>
<option value="DOC_ECONOMY">Document Economy</option>
<option value="PARCEL">Parcel</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">جهت ارسال</label>
<select id="directionSelect" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
<option value="export" selected>صادرات (از ایران)</option>
<option value="import">واردات (به ایران)</option>
</select>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">کشور مبدأ</label>
<select name="from_country_id" id="fromCountry" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" required>
<option value="">انتخاب کنید</option>
@foreach($countries as $country)
<option value="{{ $country->id }}" data-iso="{{ $country->iso_code }}" {{ old('from_country_id') == $country->id ? 'selected' : '' }}>{{ $country->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">کشور مقصد</label>
<select name="to_country_id" id="toCountry" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" required>
<option value="">انتخاب کنید</option>
@foreach($countries as $country)
<option value="{{ $country->id }}" data-iso="{{ $country->iso_code }}" {{ old('to_country_id') == $country->id ? 'selected' : '' }}>{{ $country->name }}</option>
@endforeach
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">فراوئر</label>
<input type="text" name="forwarder" value="{{ old('forwarder') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="مثال: DHL, FedEx">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">وزن واقعی (kg)</label>
<input type="number" step="0.01" name="weight" id="weight" value="{{ old('weight') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">وزن حجمی (kg)</label>
<input type="number" step="0.01" name="volumetric_weight" id="volumetricWeight" value="{{ old('volumetric_weight') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">ابعاد (طول × عرض × ارتفاع به cm)</label>
<input type="text" name="dimensions" value="{{ old('dimensions') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="مثال: 30×20×10">
</div>
<div id="spotRateWarning" class="hidden bg-yellow-50 border border-yellow-200 rounded-md p-4">
<p class="text-sm text-yellow-700">مرسولات بالای ۳۰ کیلوگرم نیاز به استعلام قیمت ویژه دارند. کارشناسان ما با شما تماس می‌گیرند.</p>
</div>
</div>
<div class="space-y-6">
<h2 class="text-lg font-semibold text-gray-900 border-b pb-2">اطلاعات فرستنده</h2>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">نام <span class="text-red-500">*</span></label>
<input type="text" name="sender_name" value="{{ old('sender_name') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">شرکت</label>
<input type="text" name="sender_company" value="{{ old('sender_company') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">تلفن <span class="text-red-500">*</span></label>
<input type="text" name="sender_phone" value="{{ old('sender_phone') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">ایمیل</label>
<input type="email" name="sender_email" value="{{ old('sender_email') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">آدرس <span class="text-red-500">*</span></label>
<input type="text" name="sender_address" value="{{ old('sender_address') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" required>
</div>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">شهر <span class="text-red-500">*</span></label>
<input type="text" name="sender_city" value="{{ old('sender_city') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">کد پستی</label>
<input type="text" name="sender_zip" value="{{ old('sender_zip') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">شماره شناسنامه</label>
<input type="text" name="sender_id_number" value="{{ old('sender_id_number') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
</div>
</div>
</div>
</div>
<div class="border-t border-gray-200 pt-8">
<h2 class="text-lg font-semibold text-gray-900 border-b pb-2 mb-6">اطلاعات گیرنده</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">نام <span class="text-red-500">*</span></label>
<input type="text" name="receiver_name" value="{{ old('receiver_name') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">شرکت</label>
<input type="text" name="receiver_company" value="{{ old('receiver_company') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">تلفن <span class="text-red-500">*</span></label>
<input type="text" name="receiver_phone" value="{{ old('receiver_phone') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">ایمیل</label>
<input type="email" name="receiver_email" value="{{ old('receiver_email') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">آدرس <span class="text-red-500">*</span></label>
<input type="text" name="receiver_address" value="{{ old('receiver_address') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" required>
</div>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">شهر <span class="text-red-500">*</span></label>
<input type="text" name="receiver_city" value="{{ old('receiver_city') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">کد پستی</label>
<input type="text" name="receiver_zip" value="{{ old('receiver_zip') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">شماره شناسنامه</label>
<input type="text" name="receiver_id_number" value="{{ old('receiver_id_number') }}" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
</div>
</div>
</div>
</div>
</div>
<div class="border-t border-gray-200 pt-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-semibold text-gray-900 border-b pb-2">کالاهای گمرکی</h2>
<button type="button" id="addItemBtn" class="inline-flex items-center px-4 py-2 bg-orange-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-orange-700 focus:bg-orange-700 active:bg-orange-900 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 transition ease-in-out duration-150">
+ افزودن ردیف
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase w-16">ردیف</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">شرح کالا</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase w-32">کد HS</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase w-24">تعداد</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase w-32">قیمت واحد (USD)</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase w-36">جمع (USD)</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase w-20">عملیات</th>
</tr>
</thead>
<tbody id="itemsTableBody" class="bg-white divide-y divide-gray-200">
<tr class="item-row">
<td class="px-4 py-3"><input type="text" value="1" readonly class="w-full rounded border-gray-300 bg-gray-100 text-center text-sm"></td>
<td class="px-4 py-3"><input type="text" name="items[0][description]" class="w-full rounded border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500 text-sm" placeholder="شرح کالا" required></td>
<td class="px-4 py-3"><input type="text" name="items[0][hs_code]" class="w-full rounded border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500 text-sm" placeholder="HS Code" required></td>
<td class="px-4 py-3"><input type="number" name="items[0][quantity]" class="w-full rounded border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500 text-sm qty-input" placeholder="1" min="1" required></td>
<td class="px-4 py-3"><input type="number" step="0.01" name="items[0][unit_price]" class="w-full rounded border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500 text-sm price-input" placeholder="0.00" min="0" required></td>
<td class="px-4 py-3"><input type="number" step="0.01" name="items[0][total_usd]" class="w-full rounded border-gray-300 bg-gray-50 text-left text-sm total-input" placeholder="0.00" readonly></td>
<td class="px-4 py-3 text-center"></td>
</tr>
</tbody>
</table>
</div>
<div class="mt-4 flex justify-end">
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div class="text-sm text-gray-600">جمع کل فاکتور (USD):</div>
<div class="text-2xl font-bold text-gray-900" id="invoiceTotal">0.00</div>
</div>
</div>
</div>
<div id="priceResult" class="hidden border-t border-gray-200 pt-8">
<h2 class="text-lg font-semibold text-gray-900 border-b pb-2 mb-6">قیمت محاسبه شده</h2>
<div class="bg-orange-50 border border-orange-200 rounded-lg p-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<div class="text-sm text-gray-600">قیمت پایه</div>
<div class="text-lg font-semibold" id="basePrice"></div>
</div>
<div>
<div class="text-sm text-gray-600">قیمت نهایی (ریال)</div>
<div class="text-lg font-semibold text-orange-600" id="totalFee"></div>
</div>
<div>
<div class="text-sm text-gray-600">زون</div>
<div class="text-lg font-semibold" id="zone"></div>
</div>
</div>
</div>
</div>
<div class="border-t border-gray-200 pt-6 flex justify-end">
<button type="submit" id="submitBtn" class="inline-flex items-center px-6 py-3 bg-orange-600 border border-transparent rounded-md font-semibold text-white hover:bg-orange-700 focus:bg-orange-700 active:bg-orange-900 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 transition ease-in-out duration-150">
ثبت سفارش
</button>
</div>
</form>
</div>
</div>
@endsection
@push('scripts')
<script>
let itemCount = 1;
const maxItems = 9;
function updateRowNumbers() {
document.querySelectorAll('.item-row').forEach((row, index) => {
row.querySelector('td:first-child input').value = index + 1;
row.querySelectorAll('input').forEach(input => {
if (input.name) {
input.name = input.name.replace(/items\[\d+\]/, `items[${index}]`);
}
});
});
}
function calculateRowTotal(row) {
const qty = parseFloat(row.querySelector('.qty-input').value) || 0;
const price = parseFloat(row.querySelector('.price-input').value) || 0;
const total = qty * price;
row.querySelector('.total-input').value = total.toFixed(2);
updateInvoiceTotal();
}
function updateInvoiceTotal() {
let total = 0;
document.querySelectorAll('.total-input').forEach(input => {
total += parseFloat(input.value) || 0;
});
document.getElementById('invoiceTotal').textContent = total.toFixed(2);
}
function addItemRow() {
if (itemCount >= maxItems) {
alert('حداکثر ۹ ردیف می‌توانید اضافه کنید.');
return;
}
const tbody = document.getElementById('itemsTableBody');
const row = document.createElement('tr');
row.className = 'item-row';
row.innerHTML = `
<td class="px-4 py-3"><input type="text" value="${itemCount + 1}" readonly class="w-full rounded border-gray-300 bg-gray-100 text-center text-sm"></td>
<td class="px-4 py-3"><input type="text" name="items[${itemCount}][description]" class="w-full rounded border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500 text-sm" placeholder="شرح کالا" required></td>
<td class="px-4 py-3"><input type="text" name="items[${itemCount}][hs_code]" class="w-full rounded border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500 text-sm" placeholder="HS Code" required></td>
<td class="px-4 py-3"><input type="number" name="items[${itemCount}][quantity]" class="w-full rounded border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500 text-sm qty-input" placeholder="1" min="1" required></td>
<td class="px-4 py-3"><input type="number" step="0.01" name="items[${itemCount}][unit_price]" class="w-full rounded border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500 text-sm price-input" placeholder="0.00" min="0" required></td>
<td class="px-4 py-3"><input type="number" step="0.01" name="items[${itemCount}][total_usd]" class="w-full rounded border-gray-300 bg-gray-50 text-left text-sm total-input" placeholder="0.00" readonly></td>
<td class="px-4 py-3 text-center">
<button type="button" onclick="removeItemRow(this)" class="text-red-600 hover:text-red-800 text-sm">حذف</button>
</td>
`;
tbody.appendChild(row);
itemCount++;
row.querySelector('.qty-input').addEventListener('input', () => calculateRowTotal(row));
row.querySelector('.price-input').addEventListener('input', () => calculateRowTotal(row));
}
function removeItemRow(btn) {
const row = btn.closest('tr');
row.remove();
updateRowNumbers();
updateInvoiceTotal();
}
document.getElementById('addItemBtn').addEventListener('click', addItemRow);
document.querySelectorAll('.qty-input, .price-input').forEach(input => {
input.addEventListener('input', () => calculateRowTotal(input.closest('tr')));
});
async function calculatePrice() {
const direction = document.getElementById('directionSelect').value;
const type = document.getElementById('serviceType').value;
const fromCountry = document.getElementById('fromCountry').value;
const toCountry = document.getElementById('toCountry').value;
const weight = parseFloat(document.getElementById('weight').value) || 0;
const volumetricWeight = parseFloat(document.getElementById('volumetricWeight').value) || weight;
document.getElementById('direction').value = direction;
document.getElementById('type').value = type;
if (!fromCountry || !toCountry || weight <= 0) {
document.getElementById('priceResult').classList.add('hidden');
return;
}
if (weight > 30) {
document.getElementById('spotRateWarning').classList.remove('hidden');
document.getElementById('priceResult').classList.add('hidden');
return;
} else {
document.getElementById('spotRateWarning').classList.add('hidden');
}
const chargeableWeight = Math.max(weight, volumetricWeight);
const toCountryIso = document.getElementById('toCountry').selectedOptions[0]?.dataset?.iso || '';
try {
const response = await fetch('/api/v1/calculate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
direction: direction === 'export' ? 'Outbound' : 'Inbound',
type: type,
country_iso: toCountryIso,
weight: chargeableWeight,
volumetric_weight: chargeableWeight,
}),
});
const data = await response.json();
if (response.ok) {
document.getElementById('basePrice').textContent = data.base_price?.toFixed(2) + ' درهم';
document.getElementById('totalFee').textContent = Number(data.total_fee).toLocaleString('fa-IR') + ' ریال';
document.getElementById('zone').textContent = data.zone;
document.getElementById('priceResult').classList.remove('hidden');
} else {
document.getElementById('priceResult').classList.add('hidden');
}
} catch (error) {
console.error('Price calculation error:', error);
document.getElementById('priceResult').classList.add('hidden');
}
}
document.getElementById('directionSelect').addEventListener('change', calculatePrice);
document.getElementById('serviceType').addEventListener('change', calculatePrice);
document.getElementById('fromCountry').addEventListener('change', calculatePrice);
document.getElementById('toCountry').addEventListener('change', calculatePrice);
document.getElementById('weight').addEventListener('input', calculatePrice);
document.getElementById('volumetricWeight').addEventListener('input', calculatePrice);
document.getElementById('orderForm').addEventListener('submit', function(e) {
const itemCount = document.querySelectorAll('.item-row').length;
if (itemCount < 1) {
e.preventDefault();
alert('حداقل یک کالای گمرکی باید وارد شود.');
return;
}
document.querySelectorAll('.total-input').forEach((input, index) => {
input.name = `items[${index}][total_usd]`;
});
});
</script>
@endpush