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:
parent
5874568c63
commit
f330d13620
@ -237,8 +237,8 @@
|
||||
- [ ] موتور قیمتگذاری کامل (PriceCalculatorService) — نوشته شد، نیاز به تست با دادههای واقعی
|
||||
- [ ] جدول `shipping_rates` — ۴۰۴ رکورد import شد، نیاز به تکمیل
|
||||
- [ ] فرم ثبت سفارش آنلاین با ۹ ردیف کالای گمرکی
|
||||
- [ ] تولید PDF: AWB، INVOICE، Label مطابق قالب اکسل — اولیه پیاده شد، نیاز به تطبیق دقیق با قالبهای اکسل و رفع ساختار فعلی
|
||||
- [ ] ماژول ایمپورت اکسل تعرفهها
|
||||
- [x] تولید PDF: AWB، INVOICE، Label — اولیه پیاده شد با لوگوی استخراجشده از اکسل و چیدمان مطابق ساختار شیتها. LABEL برای پرینتر لیزری+A4 طراحی شد. نیاز به تطبیق نهایی با قالبهای اکسل (بعد از تکمیل فاز ۱)
|
||||
- [x] ماژول ایمپورت اکسل تعرفهها — command با قابلیتهای --clear و --dry-run پیاده شد
|
||||
- [ ] صفحه استعلام قیمت واقعی
|
||||
|
||||
---
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
22
04_Laravel/app/Http/Controllers/PricingPageController.php
Normal file
22
04_Laravel/app/Http/Controllers/PricingPageController.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
BIN
04_Laravel/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
142
04_Laravel/resources/views/pricing/index.blade.php
Normal file
142
04_Laravel/resources/views/pricing/index.blade.php
Normal 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
|
||||
@ -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');
|
||||
|
||||
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user