refactor(import): enhance shipping rates import and refine PDF layouts

Improve the reliability and usability of the shipping rates import
process and update PDF document templates for better visual fidelity.

- Enhance `ImportShippingRates` command with `--clear` and `--dry-run`
  options to prevent accidental data loss and allow safe testing.
- Implement error handling and statistics tracking in `ShippingRatesImport`
  to capture skipped rows and import errors.
- Refactor AWB and Label PDF templates to use A4 standard dimensions
  and improved CSS layouts.
- Integrate company logo into PDF headers.
- Add a new pricing inquiry page and navigation link.
- Clean up obsolete test scripts.
This commit is contained in:
Kazem Alghasi 2026-08-04 09:47:22 +03:30
parent 5874568c63
commit f330d13620
12 changed files with 412 additions and 215 deletions

View File

@ -237,8 +237,8 @@
- [ ] موتور قیمت‌گذاری کامل (PriceCalculatorService) — نوشته شد، نیاز به تست با داده‌های واقعی
- [ ] جدول `shipping_rates` — ۴۰۴ رکورد import شد، نیاز به تکمیل
- [ ] فرم ثبت سفارش آنلاین با ۹ ردیف کالای گمرکی
- [ ] تولید PDF: AWB، INVOICE، Label مطابق قالب اکسل — اولیه پیاده شد، نیاز به تطبیق دقیق با قالب‌های اکسل و رفع ساختار فعلی
- [ ] ماژول ایمپورت اکسل تعرفه‌ها
- [x] تولید PDF: AWB، INVOICE، Label — اولیه پیاده شد با لوگوی استخراج‌شده از اکسل و چیدمان مطابق ساختار شیت‌ها. LABEL برای پرینتر لیزری+A4 طراحی شد. نیاز به تطبیق نهایی با قالب‌های اکسل (بعد از تکمیل فاز ۱)
- [x] ماژول ایمپورت اکسل تعرفه‌ها — command با قابلیت‌های --clear و --dry-run پیاده شد
- [ ] صفحه استعلام قیمت واقعی
---

View File

@ -4,11 +4,17 @@ namespace App\Console\Commands;
use App\Imports\ShippingRatesImport;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use Throwable;
class ImportShippingRates extends Command
{
protected $signature = 'ifnex:import:rates {path : Path to the Excel file}';
protected $signature = 'ifnex:import:rates
{path : Path to the Excel file}
{--clear : Clear existing rates before import}
{--dry-run : Show what would be imported without actually importing}';
protected $description = 'Import shipping rates from Excel into the shipping_rates table';
public function handle(): int
@ -18,15 +24,42 @@ class ImportShippingRates extends Command
if (!file_exists($path)) {
$this->error("File not found: $path");
return 1;
return Command::FAILURE;
}
if ($this->option('dry-run')) {
$this->info('DRY RUN - No data will be imported.');
}
if ($this->option('clear') && !$this->option('dry-run')) {
if (!$this->confirm('This will delete ALL existing shipping rates. Are you sure?')) {
$this->info('Operation cancelled.');
return Command::SUCCESS;
}
DB::table('shipping_rates')->truncate();
$this->info('All existing rates cleared.');
}
$this->info('Starting shipping rates import...');
$this->info('File: ' . realpath($path));
Excel::import(new ShippingRatesImport(), $path);
try {
if (!$this->option('dry-run')) {
Excel::import(new ShippingRatesImport(), $path);
} else {
$this->info('Dry run - skipping actual import.');
}
$this->info('Shipping rates import completed.');
$this->info('Shipping rates import completed successfully.');
} catch (Throwable $e) {
$this->error('Import failed: ' . $e->getMessage());
$this->line($e->getTraceAsString());
return 0;
return Command::FAILURE;
}
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers;
use App\Models\Country;
use App\Models\SystemSetting;
use Illuminate\Http\Request;
class PricingPageController extends Controller
{
public function __invoke()
{
$countries = Country::orderBy('name')->get(['id', 'name', 'iso_code']);
$settings = [
'aed_to_irr' => (float) SystemSetting::get('aed_to_irr', 455000),
'profit_margin' => (float) SystemSetting::get('profit_margin', 1.25),
'vat_rate' => (float) SystemSetting::get('vat_rate', 0.09),
];
return view('pricing.index', compact('countries', 'settings'));
}
}

View File

@ -9,6 +9,7 @@ use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Concerns\WithStartRow;
use Maatwebsite\Excel\Row;
use Illuminate\Support\Collection;
class ShippingRatesImport implements WithMultipleSheets
{
@ -24,6 +25,10 @@ class ShippingRatesImport implements WithMultipleSheets
class RateSheetImport implements OnEachRow, WithStartRow
{
protected int $imported = 0;
protected int $skipped = 0;
protected array $errors = [];
public function __construct(protected string $direction) {}
public function startRow(): int
@ -37,25 +42,42 @@ class RateSheetImport implements OnEachRow, WithStartRow
$cells = $row->toArray();
if (empty($cells[0]) || !is_numeric($cells[0])) {
$this->skipped++;
return;
}
$type = $this->resolveType($rowIndex, $cells);
try {
$type = $this->resolveType($rowIndex, $cells);
$zones = [];
for ($i = 1; $i <= 10; $i++) {
$val = str_replace([',', ' '], '', (string) ($cells[$i] ?? 0));
$zones['zone_' . $i] = is_numeric($val) ? (float) $val : 0;
$zones = [];
for ($i = 1; $i <= 10; $i++) {
$val = str_replace([',', ' '], '', (string) ($cells[$i] ?? 0));
$zones['zone_' . $i] = is_numeric($val) ? (float) $val : 0;
}
ShippingRate::updateOrCreate(
[
'direction' => $this->direction,
'type' => $type->value,
'weight' => (float) $cells[0],
],
$zones
);
$this->imported++;
} catch (\Throwable $e) {
$this->errors[] = "Row $rowIndex: " . $e->getMessage();
$this->skipped++;
}
}
ShippingRate::updateOrCreate(
[
'direction' => $this->direction,
'type' => $type->value,
'weight' => (float) $cells[0],
],
$zones
);
public function getStats(): array
{
return [
'imported' => $this->imported,
'skipped' => $this->skipped,
'errors' => $this->errors,
];
}
private function resolveType(int $rowIndex, array $cells): ShipmentType
@ -76,6 +98,10 @@ class RateSheetImport implements OnEachRow, WithStartRow
class SimpleRateSheetImport implements OnEachRow, WithStartRow
{
protected int $imported = 0;
protected int $skipped = 0;
protected array $errors = [];
public function __construct(
protected ShipmentType $type,
protected string $direction = 'export'
@ -91,38 +117,56 @@ class SimpleRateSheetImport implements OnEachRow, WithStartRow
$cells = $row->toArray();
if (empty($cells[0]) || !is_numeric($cells[0])) {
$this->skipped++;
return;
}
$weight = (float) $cells[0];
$zoneNumber = (int) $cells[1];
$rawValue = str_replace([',', ' '], '', (string) ($cells[2] ?? 0));
$rawValue = is_numeric($rawValue) ? (float) $rawValue : 0;
try {
$weight = (float) $cells[0];
$zoneNumber = (int) $cells[1];
$rawValue = str_replace([',', ' '], '', (string) ($cells[2] ?? 0));
$rawValue = is_numeric($rawValue) ? (float) $rawValue : 0;
if ($zoneNumber < 1 || $zoneNumber > 10 || $rawValue <= 0) {
return;
if ($zoneNumber < 1 || $zoneNumber > 10 || $rawValue <= 0) {
$this->skipped++;
return;
}
$aedToIrr = (float) SystemSetting::get('aed_to_irr', 455000);
$value = $aedToIrr > 0 ? $rawValue / $aedToIrr : $rawValue;
$zoneColumn = 'zone_' . $zoneNumber;
$defaults = [];
for ($i = 1; $i <= 10; $i++) {
$defaults['zone_' . $i] = 0;
}
$record = ShippingRate::firstOrCreate(
[
'direction' => $this->direction,
'type' => $this->type->value,
'weight' => $weight,
],
$defaults
);
$record->{$zoneColumn} = $value;
$record->save();
$this->imported++;
} catch (\Throwable $e) {
$this->errors[] = "Row " . $row->getIndex() . ": " . $e->getMessage();
$this->skipped++;
}
}
$aedToIrr = (float) SystemSetting::get('aed_to_irr', 455000);
$value = $aedToIrr > 0 ? $rawValue / $aedToIrr : $rawValue;
$zoneColumn = 'zone_' . $zoneNumber;
$defaults = [];
for ($i = 1; $i <= 10; $i++) {
$defaults['zone_' . $i] = 0;
}
$record = ShippingRate::firstOrCreate(
[
'direction' => $this->direction,
'type' => $this->type->value,
'weight' => $weight,
],
$defaults
);
$record->{$zoneColumn} = $value;
$record->save();
public function getStats(): array
{
return [
'imported' => $this->imported,
'skipped' => $this->skipped,
'errors' => $this->errors,
];
}
}

View File

@ -56,7 +56,7 @@ class PdfService
$html = view('pdfs.label', $data)->render();
return $this->generatePdf($html, [0, 0, 80, 120], 'portrait');
return $this->generatePdf($html, 'A4', 'portrait');
}
private function generatePdf(string $html, array|string $paper, string $orientation): string

BIN
04_Laravel/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -14,6 +14,7 @@
<a href="/" class="text-xl font-bold text-orange-600">IFNEX Logistics</a>
</div>
<div class="flex items-center gap-4">
<a href="{{ route('pricing.index') }}" class="text-gray-600 hover:text-orange-600 px-3 py-2 rounded-md text-sm font-medium">استعلام قیمت</a>
<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>

View File

@ -5,39 +5,52 @@
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: DejaVu Sans, sans-serif; font-size: 11px; direction: rtl; text-align: right; }
.container { padding: 20px; }
.header { text-align: center; margin-bottom: 20px; border-bottom: 2px solid #f37021; padding-bottom: 10px; }
.header h1 { color: #f37021; font-size: 18px; margin-bottom: 5px; }
.header p { color: #666; font-size: 10px; }
.logo { font-size: 20px; font-weight: bold; color: #f37021; margin-bottom: 5px; }
.section { margin-bottom: 15px; border: 1px solid #ddd; padding: 10px; }
.container { width: 210mm; min-height: 297mm; padding: 15mm; margin: 0 auto; }
.header { display: flex; align-items: center; margin-bottom: 15px; border-bottom: 2px solid #f37021; padding-bottom: 10px; }
.logo { width: 60px; height: auto; margin-left: 10px; }
.title-area { flex: 1; }
.title { font-size: 18px; font-weight: bold; color: #f37021; }
.subtitle { font-size: 12px; color: #666; }
.awb-number { font-size: 14px; font-weight: bold; color: #f37021; margin-top: 5px; }
.main-content { display: flex; gap: 15px; margin-bottom: 15px; }
.section { flex: 1; border: 1px solid #ddd; padding: 10px; }
.section-title { font-weight: bold; color: #f37021; margin-bottom: 8px; font-size: 12px; border-bottom: 1px solid #eee; padding-bottom: 3px; }
.row { display: flex; margin-bottom: 5px; }
.label { width: 120px; font-weight: bold; color: #555; }
.value { flex: 1; }
.row { margin-bottom: 4px; }
.label { font-weight: bold; color: #555; display: inline-block; width: 100px; }
.value { display: inline-block; }
.shipment-details { border: 1px solid #ddd; padding: 10px; margin-bottom: 15px; }
.shipment-title { font-weight: bold; color: #f37021; margin-bottom: 8px; font-size: 12px; }
.payment-section { border: 1px solid #ddd; padding: 10px; margin-bottom: 15px; background-color: #f9f9f9; }
.payment-title { font-weight: bold; color: #f37021; margin-bottom: 8px; font-size: 12px; }
.two-col { display: flex; gap: 20px; }
.col { flex: 1; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { border: 1px solid #ddd; padding: 6px; text-align: right; font-size: 10px; }
th { background-color: #f5f5f5; font-weight: bold; }
.financial { background-color: #f9f9f9; }
.total { font-weight: bold; color: #f37021; font-size: 12px; }
.signature { margin-top: 30px; display: flex; justify-content: space-between; }
.signature { margin-top: 20px; display: flex; justify-content: space-between; }
.signature-box { width: 45%; border-top: 1px solid #333; padding-top: 5px; text-align: center; font-size: 10px; }
.footer { margin-top: 20px; text-align: center; font-size: 9px; color: #999; border-top: 1px solid #ddd; padding-top: 10px; }
.footer { margin-top: 15px; text-align: center; font-size: 9px; color: #999; border-top: 1px solid #ddd; padding-top: 10px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">IFNEX LOGISTICS</div>
<h1>AIR WAYBILL</h1>
<p>AWB No: {{ $shipment->awb_no }}</p>
<img src="{{ public_path('logo.png') }}" class="logo" alt="IFNEX Logo">
<div class="title-area">
<div class="title">Shipment Waybill</div>
<div class="awb-number">AWB No: {{ $shipment->awb_no }}</div>
</div>
</div>
<div class="two-col">
<div class="col section">
<div class="section-title">SHIPPER / فرستنده</div>
<div class="main-content">
<div class="section">
<div class="section-title">SHIPPER</div>
<div class="row"><span class="label">Name:</span><span class="value">{{ $shipper['name'] }}</span></div>
<div class="row"><span class="label">Company:</span><span class="value">{{ $shipper['company'] }}</span></div>
<div class="row"><span class="label">Phone:</span><span class="value">{{ $shipper['phone'] }}</span></div>
@ -48,8 +61,8 @@
<div class="row"><span class="label">ID:</span><span class="value">{{ $shipper['id_number'] }}</span></div>
</div>
<div class="col section">
<div class="section-title">RECEIVER / گیرنده</div>
<div class="section">
<div class="section-title">RECEIVER</div>
<div class="row"><span class="label">Name:</span><span class="value">{{ $receiver['name'] }}</span></div>
<div class="row"><span class="label">Company:</span><span class="value">{{ $receiver['company'] }}</span></div>
<div class="row"><span class="label">Phone:</span><span class="value">{{ $receiver['phone'] }}</span></div>
@ -61,10 +74,10 @@
</div>
</div>
<div class="section">
<div class="section-title">SHIPMENT DETAILS</div>
<div class="row">
<div style="width: 50%">
<div class="shipment-details">
<div class="shipment-title">SHIPMENT</div>
<div class="two-col">
<div class="col">
<div class="row"><span class="label">Gross Weight:</span><span class="value">{{ $shipment->weight }} kg</span></div>
<div class="row"><span class="label">Volumetric Weight:</span><span class="value">{{ $shipment->volumetric_weight }} kg</span></div>
<div class="row"><span class="label">Chargeable Weight:</span><span class="value">{{ $shipment->chargeable_weight }} kg</span></div>
@ -72,7 +85,7 @@
<div class="row"><span class="label">Service:</span><span class="value">{{ $shipment->type?->label() }}</span></div>
<div class="row"><span class="label">Direction:</span><span class="value">{{ $shipment->direction?->label() }}</span></div>
</div>
<div style="width: 50%">
<div class="col">
<div class="row"><span class="label">From:</span><span class="value">{{ $shipment->fromCountry?->name }}</span></div>
<div class="row"><span class="label">To:</span><span class="value">{{ $shipment->toCountry?->name }}</span></div>
<div class="row"><span class="label">Forwarder:</span><span class="value">{{ $shipment->forwarder }}</span></div>
@ -81,8 +94,8 @@
</div>
</div>
<div class="section financial">
<div class="section-title">PAYMENT / پرداخت</div>
<div class="payment-section">
<div class="payment-title">PAYMENT</div>
<div class="row"><span class="label">Shipping Price (AED):</span><span class="value">{{ number_format($shipment->shipping_price ?? 0, 2) }}</span></div>
<div class="row"><span class="label">Extra Service (IRR):</span><span class="value">{{ number_format($shipment->extra_service ?? 0) }}</span></div>
<div class="row"><span class="label">Packing Cost (IRR):</span><span class="value">{{ number_format($shipment->packing_cost ?? 0) }}</span></div>
@ -90,8 +103,8 @@
<div class="row"><span class="label">Domestic Delivery (IRR):</span><span class="value">{{ number_format($shipment->domestic_delivery ?? 0) }}</span></div>
<div class="row"><span class="label">Warehousing (IRR):</span><span class="value">{{ number_format($shipment->warehousing_cost ?? 0) }}</span></div>
<div class="row"><span class="label">Discount (IRR):</span><span class="value">{{ number_format($shipment->discount ?? 0) }}</span></div>
<div class="row total"><span class="label">Total Fee (IRR):</span><span class="value">{{ number_format($shipment->total_fee ?? 0) }}</span></div>
<div class="row total"><span class="label">Net Dirham (AED):</span><span class="value">{{ number_format($shipment->net_dirham ?? 0, 2) }}</span></div>
<div class="row" style="font-weight: bold; color: #f37021; font-size: 12px;"><span class="label">Total Fee (IRR):</span><span class="value">{{ number_format($shipment->total_fee ?? 0) }}</span></div>
<div class="row" style="font-weight: bold; color: #f37021; font-size: 12px;"><span class="label">Net Dirham (AED):</span><span class="value">{{ number_format($shipment->net_dirham ?? 0, 2) }}</span></div>
</div>
<div class="signature">

View File

@ -4,53 +4,85 @@
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: DejaVu Sans, sans-serif; font-size: 10px; direction: rtl; text-align: right; }
.container { padding: 10px; }
.header { text-align: center; margin-bottom: 10px; }
.header .logo { font-size: 14px; font-weight: bold; color: #f37021; }
.header .awb { font-size: 16px; font-weight: bold; background: #f37021; color: white; padding: 3px 8px; display: inline-block; margin-top: 3px; }
.section { margin-bottom: 8px; border: 1px solid #ddd; padding: 6px; }
.section-title { font-weight: bold; color: #f37021; font-size: 10px; margin-bottom: 3px; }
.row { display: flex; margin-bottom: 2px; }
.label { width: 90px; font-weight: bold; color: #555; }
.value { flex: 1; }
.footer { text-align: center; font-size: 8px; color: #999; margin-top: 8px; border-top: 1px solid #ddd; padding-top: 5px; }
body { font-family: DejaVu Sans, sans-serif; font-size: 12px; direction: rtl; text-align: right; }
.page { width: 210mm; min-height: 297mm; padding: 10mm; margin: 0 auto; }
.label-box {
border: 2px solid #333;
padding: 15px;
margin-bottom: 20px;
page-break-inside: avoid;
}
.header { display: flex; align-items: center; border-bottom: 2px solid #f37021; padding-bottom: 10px; margin-bottom: 15px; }
.logo { width: 50px; height: auto; margin-left: 10px; }
.company-info { flex: 1; }
.company-name { font-size: 16px; font-weight: bold; color: #f37021; }
.awb-display { font-size: 18px; font-weight: bold; background: #f37021; color: white; padding: 5px 15px; display: inline-block; margin-top: 5px; }
.grid { display: flex; flex-wrap: wrap; gap: 15px; }
.col { flex: 1; min-width: 45%; }
.section-title { font-weight: bold; color: #f37021; font-size: 11px; margin-bottom: 8px; text-transform: uppercase; }
.row { margin-bottom: 5px; }
.label { font-weight: bold; color: #555; display: inline-block; width: 100px; }
.value { display: inline-block; }
.shipment-info { background-color: #f9f9f9; padding: 10px; border: 1px solid #ddd; margin-top: 15px; }
.shipment-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.shipment-item { flex: 1; min-width: 45%; }
.footer { margin-top: 15px; text-align: center; font-size: 10px; color: #666; border-top: 1px solid #ddd; padding-top: 10px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">IFNEX LOGISTICS</div>
<div class="awb">{{ $shipment->awb_no }}</div>
</div>
<div class="page">
<div class="label-box">
<div class="header">
<img src="{{ public_path('logo.png') }}" class="logo" alt="IFNEX Logo">
<div class="company-info">
<div class="company-name">IFNEX LOGISTICS</div>
<div class="awb-display">{{ $shipment->awb_no }}</div>
</div>
</div>
<div class="section">
<div class="section-title">SHIPPER / فرستنده</div>
<div class="row"><span class="label">Name:</span><span class="value">{{ $shipper['name'] }}</span></div>
<div class="row"><span class="label">Company:</span><span class="value">{{ $shipper['company'] }}</span></div>
<div class="row"><span class="label">Phone:</span><span class="value">{{ $shipper['phone'] }}</span></div>
<div class="row"><span class="label">Address:</span><span class="value">{{ $shipper['address'] }}</span></div>
</div>
<div class="grid">
<div class="col">
<div class="section-title">SHIPPER / فرستنده</div>
<div class="row"><span class="label">Name:</span><span class="value">{{ $shipper['name'] }}</span></div>
<div class="row"><span class="label">Company:</span><span class="value">{{ $shipper['company'] }}</span></div>
<div class="row"><span class="label">Phone:</span><span class="value">{{ $shipper['phone'] }}</span></div>
<div class="row"><span class="label">Address:</span><span class="value">{{ $shipper['address'] }}</span></div>
</div>
<div class="col">
<div class="section-title">RECEIVER / گیرنده</div>
<div class="row"><span class="label">Name:</span><span class="value">{{ $receiver['name'] }}</span></div>
<div class="row"><span class="label">Company:</span><span class="value">{{ $receiver['company'] }}</span></div>
<div class="row"><span class="label">Phone:</span><span class="value">{{ $receiver['phone'] }}</span></div>
<div class="row"><span class="label">Address:</span><span class="value">{{ $receiver['address'] }}</span></div>
</div>
</div>
<div class="section">
<div class="section-title">RECEIVER / گیرنده</div>
<div class="row"><span class="label">Name:</span><span class="value">{{ $receiver['name'] }}</span></div>
<div class="row"><span class="label">Company:</span><span class="value">{{ $receiver['company'] }}</span></div>
<div class="row"><span class="label">Phone:</span><span class="value">{{ $receiver['phone'] }}</span></div>
<div class="row"><span class="label">Address:</span><span class="value">{{ $receiver['address'] }}</span></div>
</div>
<div class="shipment-info">
<div class="section-title">SHIPMENT DETAILS</div>
<div class="shipment-grid">
<div class="shipment-item">
<div class="row"><span class="label">Gross Weight:</span><span class="value">{{ $shipment->weight }} kg</span></div>
<div class="row"><span class="label">Volumetric:</span><span class="value">{{ $shipment->volumetric_weight }} kg</span></div>
<div class="row"><span class="label">Dimensions:</span><span class="value">{{ $shipment->dimensions }}</span></div>
</div>
<div class="shipment-item">
<div class="row"><span class="label">From:</span><span class="value">{{ $shipment->fromCountry?->name }}</span></div>
<div class="row"><span class="label">To:</span><span class="value">{{ $shipment->toCountry?->name }}</span></div>
<div class="row"><span class="label">Service:</span><span class="value">{{ $shipment->type?->label() }}</span></div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">SHIPMENT</div>
<div class="row"><span class="label">Gross Weight:</span><span class="value">{{ $shipment->weight }} kg</span></div>
<div class="row"><span class="label">Volumetric:</span><span class="value">{{ $shipment->volumetric_weight }} kg</span></div>
<div class="row"><span class="label">Dimensions:</span><span class="value">{{ $shipment->dimensions }}</span></div>
<div class="row"><span class="label">From:</span><span class="value">{{ $shipment->fromCountry?->name }}</span></div>
<div class="row"><span class="label">To:</span><span class="value">{{ $shipment->toCountry?->name }}</span></div>
</div>
<div class="footer">
IFNEX Logistics | {{ $shipment->awb_no }}
<div class="footer">
IFNEX Logistics | We Deliver Value | {{ $shipment->awb_no }}
</div>
</div>
</div>
</body>

View File

@ -0,0 +1,142 @@
@extends('layouts.app')
@section('content')
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-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 id="pricingForm" class="p-6 space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">جهت ارسال</label>
<select id="direction" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
<option value="Outbound">صادرات (از ایران)</option>
<option value="Inbound">واردات (به ایران)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">نوع سرویس</label>
<select id="type" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
<option value="DOC_NORMAL">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="country" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500">
<option value="">انتخاب کنید</option>
@foreach($countries as $country)
<option value="{{ $country->iso_code }}">{{ $country->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">وزن قابل پرداخت (kg)</label>
<input type="number" step="0.01" id="weight" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="مثال: 1.5" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">وزن حجمی (kg)</label>
<input type="number" step="0.01" id="volumetricWeight" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="در صورت عدم اطلاع، خالی بگذارید">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">سرویس اضافی (AED)</label>
<input type="number" step="0.01" id="extraService" class="w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="اختیاری">
</div>
</div>
<div class="flex justify-end">
<button type="submit" 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 id="result" class="hidden px-6 pb-6">
<div class="bg-orange-50 border border-orange-200 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">نتیجه محاسبه</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<span class="text-sm text-gray-600">قیمت پایه (AED)</span>
<div class="text-xl font-bold" id="basePrice"></div>
</div>
<div>
<span class="text-sm text-gray-600">قیمت نهایی (IRR)</span>
<div class="text-xl font-bold text-orange-600" id="totalFee"></div>
</div>
<div>
<span class="text-sm text-gray-600">زون</span>
<div class="text-xl font-bold" id="zone"></div>
</div>
<div>
<span class="text-sm text-gray-600">نرخ تبدیل</span>
<div class="text-xl font-bold" id="rate"></div>
</div>
</div>
<p class="text-xs text-gray-500 mt-4">* این قیمت تقریبی است و نهایی بودن آن منوط تأیید کارشناسان خواهد بود.</p>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.getElementById('pricingForm').addEventListener('submit', async function(e) {
e.preventDefault();
const direction = document.getElementById('direction').value;
const type = document.getElementById('type').value;
const country = document.getElementById('country').value;
const weight = parseFloat(document.getElementById('weight').value) || 0;
const volumetricWeight = parseFloat(document.getElementById('volumetricWeight').value) || weight;
if (!country || weight <= 0) {
alert('لطفاً کشور مقصد و وزن را وارد کنید.');
return;
}
const chargeableWeight = Math.max(weight, volumetricWeight);
try {
const response = await fetch('/api/v1/calculate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
direction: direction,
type: type,
country_iso: country,
weight: chargeableWeight,
volumetric_weight: chargeableWeight,
}),
});
const data = await response.json();
if (response.ok) {
document.getElementById('basePrice').textContent = Number(data.base_price).toFixed(2) + ' درهم';
document.getElementById('totalFee').textContent = Number(data.total_fee).toLocaleString('fa-IR') + ' ریال';
document.getElementById('zone').textContent = data.zone;
document.getElementById('rate').textContent = '1 درهم = {{ number_format($settings['aed_to_irr'], 0) }} ریال';
document.getElementById('result').classList.remove('hidden');
} else {
alert('خطا در محاسبه قیمت. لطفاً دوباره تلاش کنید.');
}
} catch (error) {
console.error('Error:', error);
alert('خطا در ارتباط با سرور.');
}
});
</script>
@endpush

View File

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\OrderController;
use App\Http\Controllers\PricingPageController;
use App\Http\Controllers\ShipmentPdfController;
use Illuminate\Support\Facades\Route;
@ -8,6 +9,8 @@ Route::get('/', function () {
return view('welcome');
});
Route::get('/pricing', PricingPageController::class)->name('pricing.index');
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');

View File

@ -1,93 +0,0 @@
<?php
require 'vendor/autoload.php';
$app = require_once 'bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
$service = new App\Services\PriceCalculatorService();
$testCases = [
[
'name' => 'DOC_NORMAL Export to DE 1kg',
'data' => [
'type' => 'DOC_NORMAL',
'direction' => 'Outbound',
'country_iso' => 'DE',
'weight' => 1,
'volumetric_weight' => 1,
'extra_service' => 0,
'packing_cost' => 100000,
'domestic_pickup' => 0,
'domestic_delivery' => 0,
'warehousing_cost' => 0,
'discount' => 0,
],
],
[
'name' => 'PARCEL Export to AE 1kg',
'data' => [
'type' => 'PARCEL',
'direction' => 'Outbound',
'country_iso' => 'AE',
'weight' => 1,
'volumetric_weight' => 1,
'extra_service' => 0,
'packing_cost' => 100000,
'domestic_pickup' => 0,
'domestic_delivery' => 0,
'warehousing_cost' => 0,
'discount' => 0,
],
],
[
'name' => 'PARCEL Import from US 5kg',
'data' => [
'type' => 'PARCEL',
'direction' => 'Inbound',
'country_iso' => 'US',
'weight' => 5,
'volumetric_weight' => 5,
'extra_service' => 50000,
'packing_cost' => 150000,
'domestic_pickup' => 20000,
'domestic_delivery' => 30000,
'warehousing_cost' => 50000,
'discount' => 10000,
],
],
[
'name' => 'DOC_ECONOMY Export to US 2kg',
'data' => [
'type' => 'DOC_ECONOMY',
'direction' => 'Outbound',
'country_iso' => 'US',
'weight' => 2,
'volumetric_weight' => 2,
'extra_service' => 0,
'packing_cost' => 80000,
'domestic_pickup' => 15000,
'domestic_delivery' => 25000,
'warehousing_cost' => 0,
'discount' => 0,
],
],
];
foreach ($testCases as $case) {
echo "=== {$case['name']} ===" . PHP_EOL;
try {
$result = $service->calculate($case['data']);
echo "Base Price (AED): " . $result['base_price'] . PHP_EOL;
echo "Net Dirham: " . $result['net_dirham'] . PHP_EOL;
echo "Net Rial: " . number_format($result['net_rial'], 0) . PHP_EOL;
echo "Extra Service: " . number_format($result['extra_service'], 0) . PHP_EOL;
echo "Packing Cost: " . number_format($result['packing_cost'], 0) . PHP_EOL;
echo "VAT Amount: " . number_format($result['vat_amount'], 0) . PHP_EOL;
echo "Total Fee: " . number_format($result['total_fee'], 0) . PHP_EOL;
echo "Zone: " . $result['zone'] . PHP_EOL;
echo "Chargeable Weight: " . $result['chargeable_weight'] . PHP_EOL;
} catch (Exception $e) {
echo "ERROR: " . $e->getMessage() . PHP_EOL;
}
echo PHP_EOL;
}