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.
This commit is contained in:
Kazem Alghasi 2026-08-04 07:59:23 +03:30
parent 3aaa9a5481
commit 24fc56feaf
15 changed files with 601 additions and 233 deletions

View File

@ -0,0 +1,109 @@
<?php
namespace App\Http\Controllers;
use App\Enums\ShipmentDirection;
use App\Enums\ShipmentType;
use App\Models\Country;
use App\Models\Shipment;
use App\Models\ShipmentItem;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class OrderController extends Controller
{
public function create()
{
$countries = Country::orderBy('name')->get();
$directions = ShipmentDirection::cases();
$types = ShipmentType::cases();
return view('orders.create', compact('countries', 'directions', 'types'));
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'direction' => ['required', 'in:export,import'],
'type' => ['required', 'in:DOC_NORMAL,DOC_ECONOMY,PARCEL'],
'from_country_id' => ['required', 'exists:countries,id'],
'to_country_id' => ['required', 'exists:countries,id'],
'forwarder' => ['nullable', 'string', 'max:255'],
'weight' => ['required', 'numeric', 'min:0.1'],
'volumetric_weight' => ['nullable', 'numeric', 'min:0'],
'dimensions' => ['nullable', 'string', 'max:255'],
'sender_name' => ['required', 'string', 'max:255'],
'sender_company' => ['nullable', 'string', 'max:255'],
'sender_phone' => ['required', 'string', 'max:50'],
'sender_email' => ['nullable', 'email', 'max:255'],
'sender_address' => ['required', 'string', 'max:500'],
'sender_city' => ['required', 'string', 'max:255'],
'sender_zip' => ['nullable', 'string', 'max:20'],
'sender_id_number' => ['nullable', 'string', 'max:50'],
'receiver_name' => ['required', 'string', 'max:255'],
'receiver_company' => ['nullable', 'string', 'max:255'],
'receiver_phone' => ['required', 'string', 'max:50'],
'receiver_email' => ['nullable', 'email', 'max:255'],
'receiver_address' => ['required', 'string', 'max:500'],
'receiver_city' => ['required', 'string', 'max:255'],
'receiver_zip' => ['nullable', 'string', 'max:20'],
'receiver_id_number' => ['nullable', 'string', 'max:50'],
'items' => ['required', 'array', 'min:1', 'max:9'],
'items.*.description' => ['required', 'string', 'max:500'],
'items.*.hs_code' => ['required', 'string', 'max:50'],
'items.*.quantity' => ['required', 'numeric', 'min:1'],
'items.*.unit_price' => ['required', 'numeric', 'min:0'],
'items.*.total_usd' => ['required', 'numeric', 'min:0'],
]);
$data = $validator->validated();
$shipment = Shipment::create([
'awb_no' => 'IFNEX-' . strtoupper(uniqid()),
'direction' => $data['direction'],
'type' => $data['type'],
'from_country_id' => $data['from_country_id'],
'to_country_id' => $data['to_country_id'],
'forwarder' => $data['forwarder'] ?? null,
'weight' => $data['weight'],
'volumetric_weight' => $data['volumetric_weight'] ?? $data['weight'],
'chargeable_weight' => max($data['weight'], $data['volumetric_weight'] ?? $data['weight']),
'dimensions' => $data['dimensions'] ?? null,
'sender_name' => $data['sender_name'],
'sender_company' => $data['sender_company'] ?? null,
'sender_phone' => $data['sender_phone'],
'sender_email' => $data['sender_email'] ?? null,
'sender_address' => $data['sender_address'],
'sender_city' => $data['sender_city'],
'sender_zip' => $data['sender_zip'] ?? null,
'sender_id_number' => $data['sender_id_number'] ?? null,
'receiver_name' => $data['receiver_name'],
'receiver_company' => $data['receiver_company'] ?? null,
'receiver_phone' => $data['receiver_phone'],
'receiver_email' => $data['receiver_email'] ?? null,
'receiver_address' => $data['receiver_address'],
'receiver_city' => $data['receiver_city'],
'receiver_zip' => $data['receiver_zip'] ?? null,
'receiver_id_number' => $data['receiver_id_number'] ?? null,
'status' => 'processed',
]);
foreach ($data['items'] as $index => $item) {
$shipment->items()->create([
'row_number' => $index + 1,
'description' => $item['description'],
'hs_code' => $item['hs_code'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
'total_usd' => $item['total_usd'],
]);
}
return redirect()->route('orders.success', $shipment)->with('success', 'سفارش شما با موفقیت ثبت شد.');
}
public function success(Shipment $shipment)
{
return view('orders.success', compact('shipment'));
}
}

View File

@ -1,27 +0,0 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
$sheet = $spreadsheet->getSheetByName('Assumptions');
echo "=== Assumptions sheet ===" . PHP_EOL;
echo "Total rows: " . $sheet->getHighestRow() . PHP_EOL;
echo "Total cols: " . $sheet->getHighestColumn() . PHP_EOL;
for ($row = 1; $row <= $sheet->getHighestRow(); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:Z{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}
echo PHP_EOL . "=== Zone sheet ===" . PHP_EOL;
$sheet = $spreadsheet->getSheetByName('Zone');
for ($row = 1; $row <= $sheet->getHighestRow(); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:Z{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}

View File

@ -1,32 +0,0 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$ss = IOFactory::load($file);
$sheet = $ss->getSheetByName('DocNor');
echo "=== Raw Excel values (DocNor) ===" . PHP_EOL;
for ($r = 9; $r <= 11; $r++) {
$row = $sheet->rangeToArray("A{$r}:C{$r}")[0];
echo "Row {$r}: weight={$row[0]}, zone={$row[1]}, raw={$row[2]}, /455000=" . ($row[2] / 455000) . PHP_EOL;
}
echo PHP_EOL . "=== Stored values (DB) ===" . PHP_EOL;
$records = \App\Models\ShippingRate::where('direction', 'export')
->where('type', 'DOC_NORMAL')
->where('weight', 1)
->first(['zone_1', 'zone_2', 'zone_3', 'zone_4', 'zone_5', 'zone_6', 'zone_7']);
echo json_encode($records, JSON_PRETTY_PRINT) . PHP_EOL;
echo PHP_EOL . "=== Raw Excel values (Export Rate) ===" . PHP_EOL;
$sheet2 = $ss->getSheetByName('Export Rate');
for ($r = 6; $r <= 9; $r++) {
$row = $sheet2->rangeToArray("A{$r}:J{$r}")[0];
echo "Row {$r}: " . implode(' | ', array_map(function($c) { return $c === null ? '(null)' : $c; }, $row)) . PHP_EOL;
}
echo PHP_EOL . "=== System Settings ===" . PHP_EOL;
echo "aed_to_irr: " . \App\Models\SystemSetting::get('aed_to_irr', 455000) . PHP_EOL;
echo "profit_margin: " . \App\Models\SystemSetting::get('profit_margin', 1.25) . PHP_EOL;
echo "vat_rate: " . \App\Models\SystemSetting::get('vat_rate', 0.09) . PHP_EOL;

View File

@ -1,25 +0,0 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
$sheet = $spreadsheet->getSheetByName('DocEco');
echo "=== DocEco: looking for weight 2, zone 3 ===" . PHP_EOL;
for ($row = 1; $row <= $sheet->getHighestRow(); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:C{$row}")[0];
if ((float) $rowData[0] == 2 && (int) $rowData[1] == 3) {
echo "Row $row: Weight={$rowData[0]}, Zone={$rowData[1]}, Value={$rowData[2]}" . PHP_EOL;
echo "Divided by 455000: " . ($rowData[2] / 455000) . PHP_EOL;
echo "Divided by 37000: " . ($rowData[2] / 37000) . PHP_EOL;
echo "Divided by 30000: " . ($rowData[2] / 30000) . PHP_EOL;
break;
}
}
echo PHP_EOL . "=== DocEco: all rows ===" . PHP_EOL;
for ($row = 2; $row <= $sheet->getHighestRow(); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:C{$row}")[0];
echo "Row $row: {$rowData[0]} | {$rowData[1]} | {$rowData[2]}" . PHP_EOL;
}

View File

@ -1,37 +0,0 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
// Check all sheet names
echo "Sheet names:" . PHP_EOL;
foreach ($spreadsheet->getSheetNames() as $name) {
echo " - $name" . PHP_EOL;
}
// Check DocNor for any notes
$sheet = $spreadsheet->getSheetByName('DocNor');
echo PHP_EOL . "=== DocNor notes/comments ===" . PHP_EOL;
$comments = $sheet->getComments();
foreach ($comments as $cell => $comment) {
echo "Cell $cell: " . $comment->getText() . PHP_EOL;
}
// Check for any cells with formulas
echo PHP_EOL . "=== DocNor formulas ===" . PHP_EOL;
for ($row = 1; $row <= 5; $row++) {
for ($col = 1; $col <= 3; $col++) {
$cell = $sheet->getCellByColumnAndRow($col, $row);
if ($cell->getDataType() == \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_FORMULA) {
echo "Cell " . $cell->getCoordinate() . ": " . $cell->getValue() . " = " . $cell->getCalculatedValue() . PHP_EOL;
}
}
}
// Check if there's any metadata
echo PHP_EOL . "=== DocNor first cell values ===" . PHP_EOL;
echo "A1: " . $sheet->getCell('A1')->getValue() . PHP_EOL;
echo "B1: " . $sheet->getCell('B1')->getValue() . PHP_EOL;
echo "C1: " . $sheet->getCell('C1')->getValue() . PHP_EOL;

View File

@ -1,29 +0,0 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
$sheets = ['DocNor', 'DocEco', 'Parcel', 'Import Rate', 'Export Rate'];
foreach ($sheets as $sheetName) {
$sheet = $spreadsheet->getSheetByName($sheetName);
if (!$sheet) {
echo "Sheet '$sheetName' not found" . PHP_EOL;
continue;
}
echo "=== Sheet: $sheetName ===" . PHP_EOL;
echo "Total rows: " . $sheet->getHighestRow() . PHP_EOL;
echo "Total cols: " . $sheet->getHighestColumn() . PHP_EOL;
// Print first 10 rows
for ($row = 1; $row <= min(10, $sheet->getHighestRow()); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:J{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}
echo PHP_EOL;
}

View File

@ -1,19 +0,0 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
// Search all sheets for exchange rate or currency info
foreach ($spreadsheet->getSheetNames() as $sheetName) {
$sheet = $spreadsheet->getSheetByName($sheetName);
echo "=== $sheetName ===" . PHP_EOL;
foreach ($sheet->getCellCollection() as $cell) {
$value = $sheet->getCell($cell)->getValue();
if (is_string($value) && (stripos($value, 'rate') !== false || stripos($value, 'exchange') !== false || stripos($value, 'currency') !== false || stripos($value, 'aed') !== false || stripos($value, 'irr') !== false || stripos($value, 'rial') !== false || stripos($value, 'dirham') !== false)) {
echo " Cell {$cell}: {$value}" . PHP_EOL;
}
}
}

View File

@ -1,19 +0,0 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
foreach (['Start', 'Form', 'DATES'] as $sheetName) {
$sheet = $spreadsheet->getSheetByName($sheetName);
if (!$sheet) continue;
echo "=== $sheetName ===" . PHP_EOL;
for ($row = 1; $row <= min(10, $sheet->getHighestRow()); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:Z{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}
echo PHP_EOL;
}

View File

@ -1,25 +0,0 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
// Check Export Rate sheet more thoroughly
$sheet = $spreadsheet->getSheetByName('Export Rate');
echo "=== Export Rate full structure ===" . PHP_EOL;
for ($row = 1; $row <= 15; $row++) {
$rowData = $sheet->rangeToArray("A{$row}:L{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}
echo PHP_EOL . "=== Import Rate full structure ===" . PHP_EOL;
$sheet = $spreadsheet->getSheetByName('Import Rate');
for ($row = 1; $row <= 15; $row++) {
$rowData = $sheet->rangeToArray("A{$row}:K{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}

View File

@ -1,20 +0,0 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$ss = IOFactory::load($file);
echo "=== Export Rate structure ===" . PHP_EOL;
$sheet = $ss->getSheetByName('Export Rate');
for ($r = 4; $r <= 14; $r++) {
$row = $sheet->rangeToArray("A{$r}:L{$r}")[0];
echo "Row {$r}: " . implode(' | ', array_map(function($c) { return $c === null ? '(null)' : $c; }, $row)) . PHP_EOL;
}
echo PHP_EOL . "=== Import Rate structure ===" . PHP_EOL;
$sheet = $ss->getSheetByName('Import Rate');
for ($r = 4; $r <= 14; $r++) {
$row = $sheet->rangeToArray("A{$r}:K{$r}")[0];
echo "Row {$r}: " . implode(' | ', array_map(function($c) { return $c === null ? '(null)' : $c; }, $row)) . PHP_EOL;
}

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ config('app.name', 'IFNEX Logistics') }} ثبت سفارش</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-50 text-gray-800 font-sans">
<nav class="bg-white shadow-sm border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<a href="/" class="text-xl font-bold text-orange-600">IFNEX Logistics</a>
</div>
<div class="flex items-center gap-4">
<a href="{{ route('orders.create') }}" class="text-gray-600 hover:text-orange-600 px-3 py-2 rounded-md text-sm font-medium">ثبت سفارش</a>
</div>
</div>
</div>
</nav>
<main class="py-8">
@if(session('success'))
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-6">
<div class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-md">
{{ session('success') }}
</div>
</div>
@endif
@if($errors->any())
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-6">
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md">
<ul class="list-disc list-inside">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
@endif
@yield('content')
</main>
<footer class="bg-white border-t border-gray-200 mt-auto">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<p class="text-center text-sm text-gray-500">© {{ date('Y') }} IFNEX Logistics. All rights reserved.</p>
</div>
</footer>
@stack('scripts')
</body>
</html>

View File

@ -0,0 +1,405 @@
@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

View File

@ -0,0 +1,24 @@
@extends('layouts.app')
@section('content')
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="bg-white shadow rounded-lg p-8 text-center">
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-6">
<svg class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-900 mb-2">سفارش شما با موفقیت ثبت شد</h2>
<p class="text-gray-600 mb-6">کد پیگیری سفارش شما:</p>
<div class="bg-gray-50 rounded-lg p-4 mb-6">
<span class="text-2xl font-mono font-bold text-orange-600">{{ $shipment->awb_no ?? 'N/A' }}</span>
</div>
<p class="text-sm text-gray-500 mb-8">کارشناسان ما در اسرع وقت با شما تماس خواهند گرفت.</p>
<div class="flex justify-center gap-4">
<a href="{{ route('orders.create') }}" class="inline-flex items-center px-4 py-2 bg-orange-600 border border-transparent rounded-md font-semibold text-white hover:bg-orange-700">
ثبت سفارش جدید
</a>
</div>
</div>
</div>
@endsection

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Http\Controllers\Api\PricingController;
use App\Http\Controllers\Api\TrackController; use App\Http\Controllers\Api\TrackController;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@ -8,3 +9,5 @@ use App\Http\Middleware\ApiKeyMiddleware;
Route::middleware([ApiKeyMiddleware::class])->prefix('v1')->group(function () { Route::middleware([ApiKeyMiddleware::class])->prefix('v1')->group(function () {
Route::get('/track/{awb_no}', [TrackController::class, 'show']); Route::get('/track/{awb_no}', [TrackController::class, 'show']);
}); });
Route::post('/v1/calculate', [PricingController::class, 'calculate']);

View File

@ -1,7 +1,12 @@
<?php <?php
use App\Http\Controllers\OrderController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', function () { Route::get('/', function () {
return view('welcome'); return view('welcome');
}); });
Route::get('/order', [OrderController::class, 'create'])->name('orders.create');
Route::post('/order', [OrderController::class, 'store'])->name('orders.store');
Route::get('/order/success/{shipment}', [OrderController::class, 'success'])->name('orders.success');