- Add picqer/php-barcode-generator dependency

- Redesign AWB PDF with barcode, From/To, Value, Service, Type
- Redesign Invoice PDF with 9-row table, legal declaration, barcode
- Redesign Label PDF to A5 landscape with large barcode
- Add DOC/PARCEL condition for Invoice (only PARCEL has invoice)
- Fix ShipmentType enum names (DocNormal/DocEconomy)
- Add migration for shipment_items (row_number, unit_price)
- Add migration to make name nullable
- Update PdfService with base64 barcode embedding
- Update ShipmentPdfController with DOC type handling
- Add test_pdf_generation.php script

Phase 3.5 — PDF redesign complete"
This commit is contained in:
Kazem Alghasi 2026-08-29 06:40:08 +03:30
parent b5e136a76d
commit f12dff5338
12 changed files with 1428 additions and 408 deletions

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\ShipmentType;
use App\Models\Shipment; use App\Models\Shipment;
use App\Services\PdfService; use App\Services\PdfService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -11,49 +12,119 @@ class ShipmentPdfController extends Controller
{ {
public function __construct(protected PdfService $pdf) {} public function __construct(protected PdfService $pdf) {}
/**
* تولید AWB PDF (برای همه نوع محموله‌ها)
*/
public function awb(Shipment $shipment) public function awb(Shipment $shipment)
{ {
try { try {
Log::info('Generating AWB PDF', ['shipment_id' => $shipment->id]); Log::info('Generating AWB PDF', ['shipment_id' => $shipment->id]);
$content = $this->pdf->awb($shipment); $content = $this->pdf->awb($shipment);
Log::info('AWB PDF generated successfully');
Log::info('AWB PDF generated successfully', ['awb' => $shipment->awb_no]);
return response($content, 200, [ return response($content, 200, [
'Content-Type' => 'application/pdf', 'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="AWB-' . $shipment->awb_no . '.pdf"', 'Content-Disposition' => 'attachment; filename="AWB-' . $shipment->awb_no . '.pdf"',
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('AWB PDF generation failed', [ Log::error('AWB PDF generation failed', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
]); ]);
return response('PDF generation failed: ' . $e->getMessage(), 500); return response('PDF generation failed: ' . $e->getMessage(), 500);
} }
} }
/**
* تولید Invoice PDF فقط برای محموله‌های دارای کالا (PARCEL)
*
* محموله‌های DOC (DOC_NORMAL/DOC_ECONOMY) که کالا ندارن،
* فاکتور صادراتی ندارن و این متد براشون ارور برمی‌گردونه.
*/
public function invoice(Shipment $shipment) public function invoice(Shipment $shipment)
{ {
try { try {
// بارگذاری items برای بررسی
$shipment->loadMissing(['items']);
// بررسی نوع محموله
$isDocType = in_array($shipment->type, [
ShipmentType::DocNormal,
ShipmentType::DocEconomy,
]);
if ($isDocType && $shipment->items->isEmpty()) {
Log::info('Invoice PDF not available for DOC shipment without items', [
'shipment_id' => $shipment->id,
'type' => $shipment->type?->value,
]);
return response()->json([
'success' => false,
'message' => 'فاکتور فقط برای محموله‌های دارای کالا (PARCEL) صادر می‌شود.',
'hint' => 'محموله‌های DOC (مدارک) فاکتور صادراتی ندارند.',
], 400);
}
$content = $this->pdf->invoice($shipment); $content = $this->pdf->invoice($shipment);
Log::info('Invoice PDF generated successfully', [
'awb' => $shipment->awb_no,
'type' => $shipment->type?->value,
'items_count' => $shipment->items->count(),
]);
return response($content, 200, [ return response($content, 200, [
'Content-Type' => 'application/pdf', 'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="INVOICE-' . $shipment->awb_no . '.pdf"', 'Content-Disposition' => 'attachment; filename="INVOICE-' . $shipment->awb_no . '.pdf"',
]); ]);
} catch (\InvalidArgumentException $e) {
// خطای مربوط به نوع محموله
Log::info('Invoice PDF skipped', [
'shipment_id' => $shipment->id,
'reason' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => $e->getMessage(),
], 400);
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('Invoice PDF generation failed', ['error' => $e->getMessage()]); Log::error('Invoice PDF generation failed', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return response('PDF generation failed: ' . $e->getMessage(), 500); return response('PDF generation failed: ' . $e->getMessage(), 500);
} }
} }
/**
* تولید Label PDF اندازه استاندارد لیبل پستی (100x150mm)
*/
public function label(Shipment $shipment) public function label(Shipment $shipment)
{ {
try { try {
$content = $this->pdf->label($shipment); $content = $this->pdf->label($shipment);
Log::info('Label PDF generated successfully', ['awb' => $shipment->awb_no]);
return response($content, 200, [ return response($content, 200, [
'Content-Type' => 'application/pdf', 'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="LABEL-' . $shipment->awb_no . '.pdf"', 'Content-Disposition' => 'attachment; filename="LABEL-' . $shipment->awb_no . '.pdf"',
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('Label PDF generation failed', ['error' => $e->getMessage()]); Log::error('Label PDF generation failed', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return response('PDF generation failed: ' . $e->getMessage(), 500); return response('PDF generation failed: ' . $e->getMessage(), 500);
} }
} }

View File

@ -2,30 +2,21 @@
namespace App\Services; namespace App\Services;
use App\Enums\ShipmentType;
use App\Models\Shipment; use App\Models\Shipment;
use App\Models\ShipmentItem;
use Dompdf\Dompdf; use Dompdf\Dompdf;
use Dompdf\Options; use Dompdf\Options;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Picqer\Barcode\BarcodeGeneratorHTML; use Illuminate\Support\Facades\Log;
use Picqer\Barcode\BarcodeGeneratorPNG;
class PdfService class PdfService
{ {
/**
* تولید AWB PDF (برای همه نوع محموله‌ها)
*/
public function awb(Shipment $shipment): string public function awb(Shipment $shipment): string
{
$shipment->loadMissing(['fromCountry', 'toCountry', 'items', 'packages']);
$data = [
'shipment' => $shipment,
'shipper' => $this->formatAddress($shipment, 'sender'),
'receiver' => $this->formatAddress($shipment, 'receiver'),
'items' => $shipment->items ?? collect(),
'barcode' => $this->generateBarcode($shipment->awb_no),
];
return $this->generatePdf(view('pdfs.awb', $data)->render(), 'A4', 'portrait');
}
public function invoice(Shipment $shipment): string
{ {
$shipment->loadMissing(['fromCountry', 'toCountry', 'items']); $shipment->loadMissing(['fromCountry', 'toCountry', 'items']);
@ -33,65 +24,156 @@ class PdfService
'shipment' => $shipment, 'shipment' => $shipment,
'shipper' => $this->formatAddress($shipment, 'sender'), 'shipper' => $this->formatAddress($shipment, 'sender'),
'receiver' => $this->formatAddress($shipment, 'receiver'), 'receiver' => $this->formatAddress($shipment, 'receiver'),
'items' => $shipment->isParcel() ? ($shipment->items ?? collect()) : collect(), 'items' => $shipment->items ?? collect(),
'invoice_total_usd' => $shipment->invoice_total_usd, 'invoice_total_usd' => optional($shipment->items)->sum('total_usd') ?? 0,
'barcode' => $this->generateBarcode($shipment->awb_no), 'barcode_base64' => $this->generateBarcode($shipment->awb_no),
'service_label' => $this->getServiceLabel($shipment),
'type_label' => $this->getTypeLabel($shipment),
]; ];
return $this->generatePdf(view('pdfs.invoice', $data)->render(), 'A4', 'portrait'); $html = view('pdfs.awb', $data)->render();
return $this->generatePdf($html, 'A4', 'portrait');
} }
/**
* تولید Invoice PDF فقط برای PARCEL و DOC_NORMAL/DOC_ECONOMY
* (نه برای محموله‌های DOC خالص که کالا ندارن)
*/
public function invoice(Shipment $shipment): string
{
// اگه محموله DOC هست و آیتم نداره، invoice تولید نکن
if ($shipment->type === ShipmentType::DocNormal || $shipment->type === ShipmentType::DocEconomy) {
if ($shipment->items->isEmpty()) {
throw new \InvalidArgumentException(
'فاکتور فقط برای محموله‌های دارای کالا (PARCEL) صادر می‌شود. محموله‌های DOC بدون کالا فاکتور ندارند.'
);
}
}
$shipment->loadMissing(['fromCountry', 'toCountry', 'items']);
$data = [
'shipment' => $shipment,
'shipper' => $this->formatAddress($shipment, 'sender'),
'receiver' => $this->formatAddress($shipment, 'receiver'),
'items' => $shipment->items ?? collect(),
'invoice_total_usd' => optional($shipment->items)->sum('total_usd') ?? 0,
'barcode_base64' => $this->generateBarcode($shipment->awb_no),
];
$html = view('pdfs.invoice', $data)->render();
return $this->generatePdf($html, 'A4', 'portrait');
}
/**
* تولید Label PDF اندازه استاندارد لیبل پستی (100x150mm)
*/
public function label(Shipment $shipment): string public function label(Shipment $shipment): string
{ {
$shipment->loadMissing(['fromCountry', 'toCountry']); $shipment->loadMissing(['fromCountry', 'toCountry']);
$data = [ $data = [
'shipment' => $shipment, 'shipment' => $shipment,
'shipper' => $this->formatAddress($shipment, 'sender'), 'barcode_base64' => $this->generateBarcode($shipment->awb_no),
'receiver' => $this->formatAddress($shipment, 'receiver'), 'origin_iso' => $shipment->fromCountry?->iso_code ?? 'IR',
'barcode' => $this->generateBarcode($shipment->awb_no), 'dest_iso' => $shipment->toCountry?->iso_code ?? '',
]; ];
return $this->generatePdf(view('pdfs.label', $data)->render(), 'A4', 'portrait'); $html = view('pdfs.label', $data)->render();
// A5 افقی (Landscape) — استاندارد لیبل پستی: 210×148 mm
return $this->generatePdf($html, 'A5', 'landscape');
} }
private function generatePdf(string $html, string $paper, string $orientation): string /**
* تولید بارکد از AWB number base64 embed (مطمئن‌ترین روش)
*/
private function generateBarcode(string $awbNo): string
{
try {
$generator = new BarcodeGeneratorPNG();
$barcode = $generator->getBarcode($awbNo, $generator::TYPE_CODE_128, 2, 40);
// تبدیل به base64 و استفاده از data URI
return 'data:image/png;base64,' . base64_encode($barcode);
} catch (\Throwable $e) {
Log::error('Barcode generation failed', [
'awb' => $awbNo,
'error' => $e->getMessage(),
]);
return '';
}
}
/**
* برچسب Service برای AWB (Outbound/Inbound)
*/
private function getServiceLabel(Shipment $shipment): string
{
return match ($shipment->direction?->value) {
'export' => 'Outbound',
'import' => 'Inbound',
default => 'Outbound',
};
}
/**
* برچسب Type برای AWB (NON DOC / DOC)
*/
private function getTypeLabel(Shipment $shipment): string
{
return match ($shipment->type) {
ShipmentType::Parcel => 'NON DOC',
ShipmentType::DocNormal => 'DOC NORMAL',
ShipmentType::DocEconomy => 'DOC ECONOMY',
default => 'NON DOC',
};
}
/**
* تولید PDF از HTML
*/
private function generatePdf(string $html, array|string $paper, string $orientation): string
{ {
$options = new Options(); $options = new Options();
$options->set('isHtml5ParserEnabled', true); $options->set('isHtml5ParserEnabled', true);
$options->set('isRemoteEnabled', true); $options->set('isRemoteEnabled', true); // مهم: برای data URI
$options->set('defaultFont', 'DejaVu Sans'); $options->set('defaultFont', 'DejaVu Sans');
$options->set('dpi', 150);
$options->set('debugKeepTemp', false);
$options->set('debugCss', false);
$options->set('debugLayout', false);
$dompdf = new Dompdf($options); $dompdf = new Dompdf($options);
$dompdf->loadHtml($html); $dompdf->loadHtml($html);
if (is_array($paper)) {
$dompdf->setPaper($paper, $orientation); $dompdf->setPaper($paper, $orientation);
} else {
$dompdf->setPaper($paper, $orientation);
}
$dompdf->render(); $dompdf->render();
return $dompdf->output(); return $dompdf->output();
} }
/**
* فرمت‌بندی آدرس فرستنده/گیرنده
*/
private function formatAddress(Shipment $shipment, string $prefix): Collection private function formatAddress(Shipment $shipment, string $prefix): Collection
{ {
return collect([ return collect([
'name' => $shipment->{"{$prefix}_name"}, 'name' => $shipment->{$prefix . '_name'},
'company' => $shipment->{"{$prefix}_company"}, 'company' => $shipment->{$prefix . '_company'},
'phone' => $shipment->{"{$prefix}_phone"}, 'phone' => $shipment->{$prefix . '_phone'},
'email' => $shipment->{"{$prefix}_email"}, 'email' => $shipment->{$prefix . '_email'},
'address' => $shipment->{"{$prefix}_address"}, 'address' => $shipment->{$prefix . '_address'},
'city' => $shipment->{"{$prefix}_city"}, 'city' => $shipment->{$prefix . '_city'},
'state' => $shipment->{"{$prefix}_state"}, 'zip' => $shipment->{$prefix . '_zip'},
'zip' => $shipment->{"{$prefix}_zip"}, 'id_number' => $shipment->{$prefix . '_id_number'},
'id_number' => $shipment->{"{$prefix}_id_number"},
]); ]);
} }
private function generateBarcode(string $code): string
{
try {
$generator = new BarcodeGeneratorHTML();
return $generator->getBarcode($code, $generator::TYPE_CODE_128, 2, 60);
} catch (\Throwable $e) {
return '<div style="color:#999;font-size:10px;">Barcode unavailable</div>';
}
}
} }

View File

@ -8,6 +8,7 @@
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"barryvdh/laravel-dompdf": "*", "barryvdh/laravel-dompdf": "*",
"doctrine/dbal": "^4.4",
"filament/filament": "3.3.*", "filament/filament": "3.3.*",
"laravel/framework": "^11.0", "laravel/framework": "^11.0",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "a22b2cc91f8cab3cca421c6ab381c544", "content-hash": "318d369da85cca6a3cf11f3c2f6a55de",
"packages": [ "packages": [
{ {
"name": "anourvalar/eloquent-serialize", "name": "anourvalar/eloquent-serialize",

View File

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('shipment_items', function (Blueprint $table) {
// اضافه‌کردن row_number اگر وجود نداره
if (!Schema::hasColumn('shipment_items', 'row_number')) {
$table->unsignedTinyInteger('row_number')->default(1)->after('shipment_id');
}
// اضافه‌کردن unit_price اگر وجود نداره (متفاوت از unit_price_usd)
if (!Schema::hasColumn('shipment_items', 'unit_price')) {
$table->decimal('unit_price', 12, 2)->default(0)->after('quantity');
}
});
// کپی داده‌ها از unit_price_usd به unit_price اگر مقدار داره
\DB::statement('UPDATE shipment_items SET unit_price = unit_price_usd WHERE unit_price_usd IS NOT NULL AND unit_price = 0');
// کپی داده‌ها از name به description اگر description خالیه
\DB::statement('UPDATE shipment_items SET description = name WHERE (description IS NULL OR description = "") AND name IS NOT NULL');
}
public function down(): void
{
Schema::table('shipment_items', function (Blueprint $table) {
if (Schema::hasColumn('shipment_items', 'row_number')) {
$table->dropColumn('row_number');
}
if (Schema::hasColumn('shipment_items', 'unit_price')) {
$table->dropColumn('unit_price');
}
});
}
};

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('shipment_items', function (Blueprint $table) {
$table->string('name')->nullable()->change();
});
}
public function down(): void
{
Schema::table('shipment_items', function (Blueprint $table) {
$table->string('name')->nullable(false)->change();
});
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

View File

@ -1,200 +1,358 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" dir="ltr"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: DejaVu Sans, sans-serif; font-size: 11px; direction: ltr; text-align: left; color: #333; } body {
.container { width: 190mm; min-height: 277mm; padding: 10mm; margin: 0 auto; } font-family: DejaVu Sans, Arial, sans-serif;
font-size: 10px;
color: #000;
background: #fff;
}
.page {
width: 210mm;
min-height: 297mm;
padding: 8mm;
margin: 0 auto;
}
.header { display: flex; align-items: center; margin-bottom: 12px; border-bottom: 2px solid #f37021; padding-bottom: 8px; } /* ─── HEADER ─── */
.logo { width: 70px; height: auto; } .header {
.title-area { flex: 1; text-align: center; } display: flex;
.title { font-size: 20px; font-weight: bold; color: #f37021; } justify-content: space-between;
.subtitle { font-size: 12px; color: #666; } align-items: center;
.awb-number { font-size: 14px; font-weight: bold; color: #f37021; text-align: right; } border-bottom: 2px solid #f37021;
padding-bottom: 6px;
margin-bottom: 10px;
}
.logo-area {
display: flex;
align-items: center;
gap: 8px;
}
.logo {
width: 50px;
height: 50px;
}
.brand {
font-size: 18px;
font-weight: bold;
color: #f37021;
}
.header-title {
text-align: center;
flex: 1;
}
.header-title h1 {
font-size: 18px;
color: #000;
margin-bottom: 2px;
}
.header-tagline {
font-size: 9px;
color: #666;
font-style: italic;
}
.header-date {
font-size: 10px;
text-align: right;
}
.section { border: 1px solid #ccc; padding: 8px; margin-bottom: 10px; } /* ─── AWB BARCODE ─── */
.section-title { font-weight: bold; color: #f37021; margin-bottom: 6px; font-size: 12px; border-bottom: 1px solid #eee; padding-bottom: 3px; } .awb-barcode-area {
text-align: center;
margin-bottom: 10px;
padding: 8px;
border: 2px solid #000;
}
.awb-barcode-area img {
height: 50px;
}
.awb-no-display {
font-size: 16px;
font-weight: bold;
letter-spacing: 2px;
margin-top: 4px;
}
.two-col { display: flex; gap: 10px; margin-bottom: 10px; } /* ─── MAIN SECTIONS ─── */
.col { flex: 1; } .section-row {
display: flex;
gap: 6px;
margin-bottom: 8px;
}
.section {
flex: 1;
border: 1px solid #000;
padding: 6px;
}
.section-title {
background: #f37021;
color: #fff;
padding: 3px 6px;
font-weight: bold;
font-size: 11px;
margin: -6px -6px 6px -6px;
}
.row {
display: flex;
margin-bottom: 3px;
font-size: 9px;
}
.label {
width: 80px;
font-weight: bold;
color: #444;
}
.value {
flex: 1;
color: #000;
}
.from-to {
font-weight: bold;
font-size: 11px;
color: #f37021;
margin-bottom: 4px;
}
.row { margin-bottom: 3px; } /* ─── SHIPMENT & PAYMENT ─── */
.label { font-weight: bold; color: #555; display: inline-block; width: 110px; } .shipment-row {
.value { display: inline-block; } display: flex;
gap: 6px;
margin-bottom: 8px;
}
.shipment-section {
flex: 1;
border: 1px solid #000;
padding: 6px;
}
.shipment-section.payment {
background: #f9f9f9;
}
.payment-section { border: 1px solid #ccc; padding: 8px; margin-bottom: 10px; background-color: #f9f9f9; } /* ─── SHIPMENT DETAILS ─── */
.payment-title { font-weight: bold; color: #f37021; margin-bottom: 6px; font-size: 12px; } .shipment-details {
border: 1px solid #000;
padding: 6px;
margin-bottom: 8px;
}
.shipment-details table {
width: 100%;
border-collapse: collapse;
font-size: 9px;
}
.shipment-details th,
.shipment-details td {
border: 1px solid #ccc;
padding: 3px;
text-align: left;
}
.shipment-details th {
background: #eee;
font-weight: bold;
}
.total-row { font-weight: bold; color: #f37021; font-size: 12px; border-top: 1px solid #ddd; padding-top: 4px; margin-top: 4px; } /* ─── NOTES ─── */
.notes {
border: 1px solid #000;
padding: 6px;
margin-bottom: 8px;
font-size: 8px;
line-height: 1.5;
background: #fffde7;
}
.notes strong {
color: #f37021;
}
.disclaimer { background-color: #fafafa; border: 1px solid #eee; padding: 8px; margin-bottom: 10px; font-size: 8px; color: #666; line-height: 1.4; } /* ─── SIGNATURE ─── */
.signature-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.signature-box {
width: 45%;
text-align: center;
font-size: 9px;
}
.signature-line {
border-top: 1px solid #000;
margin-top: 25px;
padding-top: 3px;
}
.signature { margin-top: 15px; display: flex; justify-content: space-between; } /* ─── FOOTER ─── */
.signature-box { width: 45%; border-top: 1px solid #333; padding-top: 5px; text-align: center; font-size: 10px; } .footer {
border-top: 2px solid #f37021;
padding-top: 5px;
text-align: center;
font-size: 9px;
}
.footer-brand {
color: #f37021;
font-weight: bold;
}
.track-url {
font-size: 10px;
margin-top: 2px;
}
.footer { margin-top: 10px; text-align: center; font-size: 10px; color: #999; border-top: 1px solid #ddd; padding-top: 8px; } /* ─── UTILITY ─── */
.total-row {
table { width: 100%; border-collapse: collapse; } background: #f37021;
td { vertical-align: top; padding: 0; } color: #fff;
font-weight: bold;
}
.total-row .label,
.total-row .value {
color: #fff;
}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="page">
<!-- HEADER -->
<div class="header"> <div class="header">
<img src="{{ asset('logo.png') }}" class="logo" alt="IFNEX Logo"> <div class="logo-area">
<div class="title-area"> <img src="{{ public_path('logo.png') }}" class="logo" alt="IFNEX">
<div class="title">Shipment Waybill</div> <div class="brand">IFNEx</div>
<div class="subtitle">We Deliver Value</div> </div>
<div class="header-title">
<h1>Shipment Waybill</h1>
<div class="header-tagline">We Deliver Value</div>
</div>
<div class="header-date">
Date: {{ $shipment->created_at?->format('m/d/Y') }}
</div> </div>
<div class="awb-number">AWB No: {{ $shipment->awb_no }}</div>
</div> </div>
<table> <!-- AWB BARCODE -->
<tr> <div class="awb-barcode-area">
<td width="50%" style="padding-right: 5px;"> @if($barcode_base64)
<img src="{{ $barcode_base64 }}" alt="barcode">
@endif
<div class="awb-no-display">{{ $shipment->awb_no }}</div>
</div>
<!-- SHIPPER & RECEIVER -->
<div class="section-row">
<div class="section"> <div class="section">
<div class="section-title">SHIPPER</div> <div class="section-title">SHIPPER</div>
<div class="row"><span class="label">From:</span><span class="value">{{ $shipment->fromCountry?->name }}</span></div> <div class="from-to">From: {{ $shipper['city'] }}, {{ $shipment->fromCountry?->name }} ({{ $shipment->fromCountry?->iso_code }})</div>
<div class="row"><span class="label">Company Name:</span><span class="value">{{ $shipper['company'] }}</span></div> <div class="row"><span class="label">Company Name:</span><span class="value">{{ $shipper['company'] ?: '—' }}</span></div>
<div class="row"><span class="label">Contact Name:</span><span class="value">{{ $shipper['name'] }}</span></div> <div class="row"><span class="label">Contact Name:</span><span class="value">{{ $shipper['name'] }}</span></div>
<div class="row"><span class="label">Tel/Mob:</span><span class="value">{{ $shipper['phone'] }}</span></div> <div class="row"><span class="label">Tel / Mob:</span><span class="value">{{ $shipper['phone'] }}</span></div>
<div class="row"><span class="label">Email:</span><span class="value">{{ $shipper['email'] }}</span></div> <div class="row"><span class="label">Email:</span><span class="value">{{ $shipper['email'] ?: '—' }}</span></div>
<div class="row"><span class="label">Address:</span><span class="value">{{ $shipper['address'] }}</span></div> <div class="row"><span class="label">Address:</span><span class="value">{{ $shipper['address'] }}</span></div>
<div class="row"><span class="label">ID Number:</span><span class="value">{{ $shipper['id_number'] }}</span></div> <div class="row"><span class="label">ID Number:</span><span class="value">{{ $shipper['id_number'] ?: '—' }}</span></div>
<div class="row"><span class="label">Zip Code:</span><span class="value">{{ $shipper['zip'] }}</span></div> <div class="row"><span class="label">Zip Code:</span><span class="value">{{ $shipper['zip'] ?: '—' }}</span></div>
</div> </div>
</td>
<td width="50%" style="padding-left: 5px;">
<div class="section"> <div class="section">
<div class="section-title">RECEIVER</div> <div class="section-title">RECEIVER</div>
<div class="row"><span class="label">To:</span><span class="value">{{ $shipment->toCountry?->name }}</span></div> <div class="from-to">To: {{ $receiver['city'] }}, {{ $shipment->toCountry?->name }} ({{ $shipment->toCountry?->iso_code }})</div>
<div class="row"><span class="label">Company Name:</span><span class="value">{{ $receiver['company'] }}</span></div> <div class="row"><span class="label">Company Name:</span><span class="value">{{ $receiver['company'] ?: '—' }}</span></div>
<div class="row"><span class="label">Contact Name:</span><span class="value">{{ $receiver['name'] }}</span></div> <div class="row"><span class="label">Contact Name:</span><span class="value">{{ $receiver['name'] }}</span></div>
<div class="row"><span class="label">Tel/Mobile:</span><span class="value">{{ $receiver['phone'] }}</span></div> <div class="row"><span class="label">Tel / Mobile:</span><span class="value">{{ $receiver['phone'] }}</span></div>
<div class="row"><span class="label">Email:</span><span class="value">{{ $receiver['email'] }}</span></div> <div class="row"><span class="label">Email:</span><span class="value">{{ $receiver['email'] ?: '—' }}</span></div>
<div class="row"><span class="label">Address:</span><span class="value">{{ $receiver['address'] }}</span></div> <div class="row"><span class="label">Address:</span><span class="value">{{ $receiver['address'] }}</span></div>
<div class="row"><span class="label">City:</span><span class="value">{{ $receiver['city'] }}</span></div> <div class="row"><span class="label">City:</span><span class="value">{{ $receiver['city'] ?: '—' }}</span></div>
<div class="row"><span class="label">Zip Code:</span><span class="value">{{ $receiver['zip'] }}</span></div> <div class="row"><span class="label">Zip Code:</span><span class="value">{{ $receiver['zip'] ?: '—' }}</span></div>
</div>
</div> </div>
</td>
</tr>
</table>
<table> <!-- SHIPMENT & PAYMENT -->
<tr> <div class="shipment-row">
<td width="50%" style="padding-right: 5px;"> <div class="shipment-section">
<div class="section">
<div class="section-title">SHIPMENT</div> <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">Gross Weight:</span><span class="value">{{ $shipment->weight }} (Kg)</span></div>
<div class="row"><span class="label">Dimensions:</span><span class="value">{{ $shipment->dimensions }} cm</span></div> <div class="row"><span class="label">Dimensions:</span><span class="value">{{ $shipment->dimensions ?: '—' }} (cm)</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">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> <div class="row"><span class="label">Chargeable Weight:</span><span class="value">{{ $shipment->chargeable_weight ?: max($shipment->weight, $shipment->volumetric_weight) }} (Kg)</span></div>
<div class="row"><span class="label">Value:</span><span class="value">${{ number_format($shipment->declared_value ?? 0, 2) }} USD</span></div> <div class="row"><span class="label">Value:</span><span class="value">{{ $invoice_total_usd > 0 ? number_format($invoice_total_usd, 0) . ' (USD)' : '—' }}</span></div>
<div class="row"><span class="label">Service:</span><span class="value">{{ $shipment->type?->label() }}</span></div> <div class="row"><span class="label">Service:</span><span class="value">{{ $service_label }}</span></div>
<div class="row"><span class="label">Type:</span><span class="value">{{ $shipment->direction?->value === 'export' ? 'Export' : 'Import' }}</span></div> <div class="row"><span class="label">Type:</span><span class="value">{{ $type_label }}</span></div>
<div class="row"><span class="label">Content:</span><span class="value">{{ $shipment->content_description ?: 'General Goods' }}</span></div> <div class="row"><span class="label">Content:</span><span class="value">{{ $shipment->content_description ?: $shipment->reason_for_export ?: 'General Goods' }}</span></div>
</div>
@if($shipment->isParcel() && $items->count() > 0) <div class="shipment-section payment">
<div style="margin-top: 8px; border-top: 1px solid #eee; padding-top: 6px;"> <div class="section-title">PAYMENT</div>
<div class="section-title" style="margin-bottom: 4px;">Shipment Details</div> <div class="row"><span class="label">Shipping Price:</span><span class="value">{{ $shipment->shipping_price ? number_format($shipment->shipping_price, 2) : '—' }}</span></div>
<table style="width: 100%; font-size: 10px; border-collapse: collapse;"> <div class="row"><span class="label">Extra Service:</span><span class="value">{{ number_format($shipment->extra_service ?? 0) }}</span></div>
<div class="row"><span class="label">Domestic Pickup:</span><span class="value">{{ number_format($shipment->domestic_pickup ?? 0) }}</span></div>
<div class="row"><span class="label">Packing Cost:</span><span class="value">{{ number_format($shipment->packing_cost ?? 0) }}</span></div>
<div class="row"><span class="label">Domestic Delivery:</span><span class="value">{{ number_format($shipment->domestic_delivery ?? 0) }}</span></div>
<div class="row"><span class="label">Warehousing:</span><span class="value">{{ number_format($shipment->warehousing_cost ?? 0) }}</span></div>
<div class="row"><span class="label">Discount:</span><span class="value">{{ number_format($shipment->discount ?? 0) }}</span></div>
<div class="row total-row"><span class="label">Total Fee:</span><span class="value">{{ number_format($shipment->total_fee ?? 0) }} IRR</span></div>
<div class="row"><span class="label">Cash on Delivery:</span><span class="value">{{ $shipment->cod_amount ? number_format($shipment->cod_amount, 2) . ' AED' : '—' }}</span></div>
</div>
</div>
<!-- SHIPMENT DETAILS (Items) -->
@if($items->isNotEmpty())
<div class="shipment-details">
<div class="section-title" style="background: #f37021; color: #fff; padding: 3px 6px; margin: -6px -6px 6px -6px;">Shipment Details</div>
<table>
<thead> <thead>
<tr style="background-color: #f37021; color: #fff;"> <tr>
<th style="border: 1px solid #ddd; padding: 3px; text-align: left;">Row</th> <th>No</th>
<th style="border: 1px solid #ddd; padding: 3px; text-align: left;">Description</th> <th>Description</th>
<th style="border: 1px solid #ddd; padding: 3px; text-align: left;">HS Code</th> <th>H.S. Code</th>
<th style="border: 1px solid #ddd; padding: 3px; text-align: left;">Qty</th> <th>Qty</th>
<th style="border: 1px solid #ddd; padding: 3px; text-align: left;">Unit Price (USD)</th> <th>Unit Price (USD)</th>
<th style="border: 1px solid #ddd; padding: 3px; text-align: left;">Total (USD)</th> <th>Total (USD)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($items as $item) @foreach($items as $item)
<tr> <tr>
<td style="border: 1px solid #ddd; padding: 3px;">{{ $item->row_number }}</td> <td>{{ $item->row_number }}</td>
<td style="border: 1px solid #ddd; padding: 3px;">{{ $item->description }}</td> <td>{{ $item->description }}</td>
<td style="border: 1px solid #ddd; padding: 3px;">{{ $item->hs_code }}</td> <td>{{ $item->hs_code }}</td>
<td style="border: 1px solid #ddd; padding: 3px;">{{ $item->quantity }}</td> <td>{{ $item->quantity }}</td>
<td style="border: 1px solid #ddd; padding: 3px;">{{ number_format($item->unit_price, 2) }}</td> <td>{{ number_format($item->unit_price, 2) }}</td>
<td style="border: 1px solid #ddd; padding: 3px;">{{ number_format($item->total_usd, 2) }}</td> <td>{{ number_format($item->total_usd, 2) }}</td>
</tr> </tr>
@endforeach @endforeach
</tbody> <tr style="background: #eee; font-weight: bold;">
</table> <td colspan="5" style="text-align: right;">Total Invoice Amount in USD:</td>
</div> <td>{{ number_format($invoice_total_usd, 2) }}</td>
@endif
</div>
</td>
<td width="50%" style="padding-left: 5px;">
<div class="payment-section">
<div class="payment-title">PAYMENT</div>
<div class="row"><span class="label">Shipping Price:</span><span class="value">{{ number_format($shipment->shipping_price ?? 0, 2) }} IRR</span></div>
<div class="row"><span class="label">Extra Service:</span><span class="value">{{ number_format($shipment->extra_service ?? 0) }} IRR</span></div>
<div class="row"><span class="label">Domestic Pickup:</span><span class="value">{{ number_format($shipment->domestic_pickup ?? 0) }} IRR</span></div>
<div class="row"><span class="label">Packing Cost:</span><span class="value">{{ number_format($shipment->packing_cost ?? 0) }} IRR</span></div>
<div class="row"><span class="label">Domestic Delivery:</span><span class="value">{{ number_format($shipment->domestic_delivery ?? 0) }} IRR</span></div>
<div class="row"><span class="label">Warehousing Cost:</span><span class="value">{{ number_format($shipment->warehousing_cost ?? 0) }} IRR</span></div>
<div class="row"><span class="label">Discount:</span><span class="value">{{ number_format($shipment->discount ?? 0) }} IRR</span></div>
<div class="row total-row"><span class="label">Total Fee:</span><span class="value">{{ number_format($shipment->total_fee ?? 0) }} IRR</span></div>
<div class="row total-row"><span class="label">Cash on Delivery:</span><span class="value">{{ number_format($shipment->cod_amount ?? 0, 2) }} AED</span></div>
</div>
</td>
</tr> </tr>
</table>
<div class="disclaimer">
Please note that: Door-to-door Services is only applicable when the shipment is general cargo. For special cargo, dangerous goods, or items requiring special handling, additional charges may apply. The receiver is responsible for customs clearance and any associated fees. Claims must be filed within 30 days of delivery. Shipper declares that the contents are accurately described and properly packed for transport.
</div>
</div>
@if($shipment->relationLoaded('packages') && $shipment->packages->count() > 1)
<div class="section">
<div class="section-title">PACKAGES ({{ $shipment->packages->count() }})</div>
<table style="width:100%; font-size:10px; border-collapse:collapse; margin-top:6px;">
<thead>
<tr style="background-color:#f37021; color:#fff;">
<th style="border:1px solid #ddd; padding:3px;">#</th>
<th style="border:1px solid #ddd; padding:3px;">Weight (kg)</th>
<th style="border:1px solid #ddd; padding:3px;">Vol. Wt (kg)</th>
<th style="border:1px solid #ddd; padding:3px;">Chg. Wt (kg)</th>
<th style="border:1px solid #ddd; padding:3px;">Dimensions</th>
<th style="border:1px solid #ddd; padding:3px;">Value (USD)</th>
<th style="border:1px solid #ddd; padding:3px;">Content</th>
</tr>
</thead>
<tbody>
@foreach($shipment->packages as $pkg)
<tr>
<td style="border:1px solid #ddd; padding:3px; text-align:center;">{{ $pkg->package_no }}</td>
<td style="border:1px solid #ddd; padding:3px;">{{ number_format($pkg->weight, 2) }}</td>
<td style="border:1px solid #ddd; padding:3px;">{{ number_format($pkg->volumetric_weight, 2) }}</td>
<td style="border:1px solid #ddd; padding:3px;">{{ number_format($pkg->chargeable_weight, 2) }}</td>
<td style="border:1px solid #ddd; padding:3px;">{{ $pkg->dimensions }}</td>
<td style="border:1px solid #ddd; padding:3px;">{{ number_format($pkg->declared_value, 2) }}</td>
<td style="border:1px solid #ddd; padding:3px;">{{ $pkg->content_description }}</td>
</tr>
@endforeach
</tbody> </tbody>
</table> </table>
</div> </div>
@endif @endif
<div class="signature"> <!-- NOTES -->
<div class="notes">
<strong>Please note that:</strong><br>
Door-to-door Services is only applicable when the shipment is general cargo and does not require any special approvals or regulations in origin &amp; destination.<br>
Only Consignee is aware of the importing rules in the destination and consignee is the person who is in contact with the shipper.<br>
The Courier Company cannot guarantee the clearance since All shipments are subjected to customs approval at Origin and Destination.
</div>
<!-- SIGNATURE -->
<div class="signature-row">
<div class="signature-box"> <div class="signature-box">
Shipper Name & Signature<br> Shipper Name &amp; Signature:
Date: ....../....../........ <div class="signature-line"></div>
</div> </div>
<div class="signature-box"> <div class="signature-box">
Track Your Shipment at:<br> Date:
http://ifnex.net <div class="signature-line">...../...../..........</div>
</div> </div>
</div> </div>
<!-- FOOTER -->
<div class="footer"> <div class="footer">
IFNEX Logistics We Deliver Value | Generated on {{ now()->format('Y-m-d H:i') }} <div class="footer-brand">IFNEx Logistics We Deliver Value</div>
<div class="track-url">Track Your Shipment at: http://ifnex.net</div>
<div style="margin-top: 3px; font-size: 8px; color: #999;">
AWB: {{ $shipment->awb_no }} | Generated: {{ now()->format('Y-m-d H:i') }}
</div>
</div> </div>
</div> </div>
</body> </body>

View File

@ -1,164 +1,362 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" dir="ltr"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: DejaVu Sans, sans-serif; font-size: 11px; direction: ltr; text-align: left; color: #333; } body {
.container { width: 190mm; min-height: 277mm; padding: 10mm; margin: 0 auto; } font-family: DejaVu Sans, Arial, sans-serif;
font-size: 10px;
color: #000;
background: #fff;
}
.page {
width: 210mm;
min-height: 297mm;
padding: 10mm;
margin: 0 auto;
}
.header { text-align: center; margin-bottom: 12px; border-bottom: 2px solid #f37021; padding-bottom: 8px; position: relative; } /* ─── HEADER ─── */
.header h1 { color: #f37021; font-size: 24px; font-weight: bold; } .header {
.header .meta { display: flex; justify-content: space-between; font-size: 10px; color: #666; margin-top: 5px; } text-align: center;
margin-bottom: 12px;
border-bottom: 2px solid #f37021;
padding-bottom: 8px;
}
.header h1 {
font-size: 22px;
color: #f37021;
margin-bottom: 4px;
letter-spacing: 2px;
}
.header-meta {
display: flex;
justify-content: space-between;
font-size: 10px;
color: #666;
}
.header-meta .awb-box {
background: #f9f9f9;
padding: 2px 8px;
border: 1px dashed #f37021;
font-weight: bold;
color: #000;
}
.section { border: 1px solid #ccc; padding: 8px; margin-bottom: 10px; } /* ─── SECTIONS ─── */
.section-title { font-weight: bold; color: #f37021; margin-bottom: 6px; font-size: 12px; border-bottom: 1px solid #eee; padding-bottom: 3px; } .two-col {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.section {
flex: 1;
border: 1px solid #000;
padding: 6px;
}
.section-title {
background: #f37021;
color: #fff;
padding: 3px 6px;
font-weight: bold;
font-size: 11px;
margin: -6px -6px 6px -6px;
}
.row {
display: flex;
margin-bottom: 3px;
font-size: 9px;
}
.label {
width: 110px;
font-weight: bold;
color: #444;
}
.value {
flex: 1;
color: #000;
}
.two-col { display: flex; gap: 10px; margin-bottom: 10px; } /* ─── CONTENT ─── */
.col { flex: 1; } .content-section {
border: 1px solid #000;
padding: 6px;
margin-bottom: 10px;
}
.content-section .section-title {
background: #f37021;
color: #fff;
}
.content-desc {
font-size: 10px;
margin-bottom: 6px;
padding: 4px;
background: #f9f9f9;
}
.row { margin-bottom: 3px; } /* ─── ITEMS TABLE ─── */
.label { font-weight: bold; color: #555; display: inline-block; width: 110px; } table {
.value { display: inline-block; } width: 100%;
border-collapse: collapse;
font-size: 9px;
}
th, td {
border: 1px solid #ccc;
padding: 4px;
text-align: left;
}
th {
background: #f37021;
color: #fff;
font-weight: bold;
text-align: center;
}
td.text-center { text-align: center; }
td.text-right { text-align: right; }
.total-row {
background: #f9f9f9;
font-weight: bold;
}
table { width: 100%; border-collapse: collapse; margin-top: 10px; } /* ─── WEIGHT & DIMENSION ─── */
th, td { border: 1px solid #ddd; padding: 5px; text-align: left; font-size: 10px; } .weight-section {
th { background-color: #f5f5f5; font-weight: bold; color: #333; } border: 1px solid #000;
padding: 6px;
margin-bottom: 10px;
display: flex;
gap: 8px;
}
.weight-box {
flex: 1;
text-align: center;
padding: 6px;
border: 1px solid #ccc;
}
.weight-label {
font-size: 8px;
color: #666;
margin-bottom: 2px;
}
.weight-value {
font-size: 12px;
font-weight: bold;
color: #f37021;
}
.content-box { border: 1px solid #ccc; padding: 8px; margin-bottom: 10px; } /* ─── REASON FOR EXPORT ─── */
.content-title { font-weight: bold; color: #f37021; margin-bottom: 6px; font-size: 12px; } .reason-section {
border: 1px solid #000;
padding: 6px;
margin-bottom: 10px;
}
.reason-section .section-title {
background: #f37021;
color: #fff;
}
.reason-text {
font-size: 10px;
font-style: italic;
color: #333;
}
.info-row { display: flex; justify-content: space-between; margin-bottom: 10px; } /* ─── DECLARATION ─── */
.info-box { flex: 1; border: 1px solid #ccc; padding: 8px; } .declaration {
border: 1px solid #000;
padding: 8px;
margin-bottom: 10px;
font-size: 9px;
line-height: 1.6;
}
.declaration strong {
color: #f37021;
}
.signature-area {
margin-top: 8px;
display: flex;
justify-content: space-between;
}
.signature-box {
width: 45%;
text-align: center;
font-size: 9px;
}
.signature-line {
border-top: 1px solid #000;
margin-top: 25px;
padding-top: 3px;
}
.declaration { border: 1px solid #ddd; padding: 10px; margin-bottom: 10px; font-size: 10px; color: #555; text-align: center; line-height: 1.5; } /* ─── BOTTOM BARCODE ─── */
.signature-line { margin-top: 15px; display: flex; justify-content: space-between; } .bottom-barcode {
.signature-box { width: 45%; border-top: 1px solid #333; padding-top: 5px; text-align: center; font-size: 10px; } text-align: center;
margin-top: 10px;
padding: 6px;
border-top: 1px solid #ddd;
}
.bottom-barcode img {
height: 35px;
}
.bottom-info {
display: flex;
justify-content: space-between;
font-size: 9px;
color: #666;
margin-top: 6px;
padding: 0 6px;
}
.label-section { border: 2px solid #000; padding: 10px; margin-bottom: 10px; } /* ─── FOOTER ─── */
.label-title { font-weight: bold; font-size: 12px; margin-bottom: 6px; } .footer {
.label-row { display: flex; justify-content: space-between; margin-bottom: 5px; } margin-top: 10px;
.barcode { text-align: center; margin-top: 8px; } border-top: 2px solid #f37021;
padding-top: 5px;
.footer { margin-top: 10px; text-align: center; font-size: 10px; color: #999; border-top: 1px solid #ddd; padding-top: 8px; } text-align: center;
font-size: 9px;
.total-row { font-weight: bold; color: #f37021; font-size: 12px; border-top: 1px solid #ddd; padding-top: 4px; margin-top: 4px; text-align: right; } color: #999;
}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="page">
<!-- HEADER -->
<div class="header"> <div class="header">
<h1>INVOICE</h1> <h1>INVOICE</h1>
<div class="meta"> <div class="header-meta">
<span>DATE: {{ $shipment->created_at?->format('Y-m-d') }}</span> <span>DATE: {{ $shipment->created_at?->format('m/d/Y') }}</span>
<span>INVOICE NO: {{ $shipment->awb_no }}</span> <span class="awb-box">INVOICE NO: {{ $shipment->awb_no }}</span>
</div> </div>
</div> </div>
<!-- SHIPPER & CONSIGNEE -->
<div class="two-col"> <div class="two-col">
<div class="col 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 Name:</span><span class="value">{{ $shipper['company'] }}</span></div>
<div class="row"><span class="label">Contact Person:</span><span class="value">{{ $shipper['name'] }}</span></div>
<div class="row"><span class="label">Address:</span><span class="value">{{ $shipper['address'] }}</span></div>
<div class="row"><span class="label">Phone:</span><span class="value">{{ $shipper['phone'] }}</span></div>
<div class="row"><span class="label">Email:</span><span class="value">{{ $shipper['email'] }}</span></div>
</div>
<div class="col section">
<div class="section-title">CONSIGNEE</div>
<div class="row"><span class="label">Name:</span><span class="value">{{ $receiver['name'] }}</span></div>
<div class="row"><span class="label">Company Name:</span><span class="value">{{ $receiver['company'] }}</span></div>
<div class="row"><span class="label">Contact Person:</span><span class="value">{{ $receiver['name'] }}</span></div>
<div class="row"><span class="label">Address:</span><span class="value">{{ $receiver['address'] }}</span></div>
<div class="row"><span class="label">Zip Code & City:</span><span class="value">{{ $receiver['zip'] }} {{ $receiver['city'] }}</span></div>
<div class="row"><span class="label">Phone:</span><span class="value">{{ $receiver['phone'] }}</span></div>
<div class="row"><span class="label">Email:</span><span class="value">{{ $receiver['email'] }}</span></div>
</div>
</div>
<div class="content-box">
<div class="content-title">CONTENT</div>
<p>{{ $shipment->content_description ?: 'General Goods' }}</p>
</div>
@if($shipment->isParcel() && $items->count() > 0)
<div class="section"> <div class="section">
<div class="section-title">SHIPMENT ITEMS</div> <div class="section-title">SHIPPER</div>
<div class="row"><span class="label">SHIPPER:</span><span class="value">{{ $shipper['city'] }}, {{ $shipment->fromCountry?->name }} ({{ $shipment->fromCountry?->iso_code }})</span></div>
<div class="row"><span class="label">COMPANY NAME:</span><span class="value">{{ $shipper['company'] ?: '—' }}</span></div>
<div class="row"><span class="label">CONTACT PERSON:</span><span class="value">{{ $shipper['name'] }}</span></div>
<div class="row"><span class="label">ADDRESS:</span><span class="value">{{ $shipper['address'] }}</span></div>
<div class="row"><span class="label">PHONE:</span><span class="value">{{ $shipper['phone'] }}</span></div>
<div class="row"><span class="label">EMAIL:</span><span class="value">{{ $shipper['email'] ?: '—' }}</span></div>
</div>
<div class="section">
<div class="section-title">CONSIGNEE</div>
<div class="row"><span class="label">CONSIGNEE:</span><span class="value">{{ $receiver['city'] }}, {{ $shipment->toCountry?->name }} ({{ $shipment->toCountry?->iso_code }})</span></div>
<div class="row"><span class="label">COMPANY NAME:</span><span class="value">{{ $receiver['company'] ?: '—' }}</span></div>
<div class="row"><span class="label">CONTACT PERSON:</span><span class="value">{{ $receiver['name'] }}</span></div>
<div class="row"><span class="label">ADDRESS:</span><span class="value">{{ $receiver['address'] }}</span></div>
<div class="row"><span class="label">Zip Code &amp; City:</span><span class="value">{{ $receiver['zip'] }} {{ $receiver['city'] }}</span></div>
<div class="row"><span class="label">PHONE:</span><span class="value">{{ $receiver['phone'] }}</span></div>
<div class="row"><span class="label">EMAIL:</span><span class="value">{{ $receiver['email'] ?: '—' }}</span></div>
</div>
</div>
<!-- CONTENT -->
<div class="content-section">
<div class="section-title">Content</div>
<div class="content-desc">{{ $shipment->content_description ?: 'General Goods' }}</div>
<table> <table>
<thead> <thead>
<tr> <tr>
<th style="width: 40px;">NO</th> <th style="width: 30px">NO</th>
<th>DESCRIPTION</th> <th>DESCRIPTION</th>
<th style="width: 100px;">H.S. CODE</th> <th style="width: 90px">H.S. CODE</th>
<th style="width: 70px;">QUANTITY</th> <th style="width: 60px">QUANTITY</th>
<th style="width: 90px;">UNIT PRICE (USD)</th> <th style="width: 80px">UNIT PRICE</th>
<th style="width: 100px;">TOTAL IN USD</th> <th style="width: 90px">TOTAL IN USD</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($items as $item) @forelse($items as $item)
<tr> <tr>
<td style="text-align: center;">{{ $item->row_number }}</td> <td class="text-center">{{ $item->row_number }}</td>
<td>{{ $item->description }}</td> <td>{{ $item->description }}</td>
<td style="text-align: center;">{{ $item->hs_code }}</td> <td class="text-center">{{ $item->hs_code }}</td>
<td style="text-align: center;">{{ $item->quantity }}</td> <td class="text-center">{{ $item->quantity }}</td>
<td style="text-align: center;">{{ number_format($item->unit_price, 2) }}</td> <td class="text-right">{{ number_format($item->unit_price, 2) }}</td>
<td style="text-align: center;">{{ number_format($item->total_usd, 2) }}</td> <td class="text-right">{{ number_format($item->total_usd, 2) }}</td>
</tr>
@empty
<tr><td colspan="6" class="text-center">No items</td></tr>
@endforelse
{{-- Fill empty rows to match reference (9 rows) --}}
@for($i = $items->count() + 1; $i <= 9; $i++)
<tr>
<td class="text-center">{{ $i }}</td>
<td>&nbsp;</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
@endfor
<tr class="total-row">
<td colspan="5" class="text-right">TOTAL INVOICE AMOUNT IN USD</td>
<td class="text-right">{{ number_format($invoice_total_usd, 2) }}</td>
</tr> </tr>
@endforeach
</tbody> </tbody>
</table> </table>
<div class="total-row">
TOTAL INVOICE AMOUNT IN USD: {{ number_format($invoice_total_usd, 2) }}
</div> </div>
</div>
@endif
<div class="info-row"> <!-- WEIGHT & DIMENSION -->
<div class="info-box"> <div class="weight-section">
<div class="row"><span class="label">WEIGHT:</span><span class="value">{{ $shipment->weight }} KG</span></div> <div class="weight-box">
<div class="row"><span class="label">DIMENSION:</span><span class="value">{{ $shipment->dimensions }} CM</span></div> <div class="weight-label">Gross Weight</div>
<div class="weight-value">{{ $shipment->weight }} KG</div>
</div> </div>
<div class="info-box"> <div class="weight-box">
<div class="row"><span class="label">REASON FOR EXPORT:</span><span class="value">{{ $shipment->reason_for_export ?: 'N/A' }}</span></div> <div class="weight-label">Volumetric Weight</div>
<div class="weight-value">{{ $shipment->volumetric_weight }} KG</div>
</div>
<div class="weight-box">
<div class="weight-label">W * L * H (cm)</div>
<div class="weight-value">{{ $shipment->dimensions ?: '—' }}</div>
</div> </div>
</div> </div>
<!-- REASON FOR EXPORT -->
<div class="reason-section">
<div class="section-title">REASON FOR EXPORT</div>
<div class="reason-text">
{{ strtoupper($shipment->reason_for_export ?: 'ITEM BEING SENT AS A SAMPLE, NOT FOR SALE') }}
</div>
</div>
<!-- DECLARATION -->
<div class="declaration"> <div class="declaration">
I HEREBY STATE THAT THE ABOVE INFORMATION IS TRUE AND I, THE UNDERSIGNED, AM LIABLE FOR ANY CONSEQUENCES THAT MAY ARISE DUE TO NON-DISCLOSURE OF FACTS <strong>I HEREBY STATE THAT THE ABOVE INFORMATION IS TRUE AND CORRECT TO THE BEST OF MY KNOWLEDGE AND I, THE UNDERSIGNED, AM LIABLE FOR ANY CONSEQUENCES THAT MAY ARISE DUE TO NON-DISCLOSURE OF FACTS.</strong>
</div> <div class="signature-area">
<div class="signature-line">
<div class="signature-box"> <div class="signature-box">
NAME - SIGNATURE NAME - SIGNATURE
<div class="signature-line"></div>
</div> </div>
<div class="signature-box"> <div class="signature-box">
NAME - SIGNATURE DATE
<div class="signature-line">...../...../..........</div>
</div>
</div> </div>
</div> </div>
<div class="label-section"> <!-- BOTTOM BARCODE & INFO -->
<div class="label-title">LABEL</div> <div class="bottom-barcode">
<div class="label-row"> @if($barcode_base64)
<span><strong>AWB:</strong> {{ $shipment->awb_no }}</span> <img src="{{ $barcode_base64 }}" alt="barcode">
<span><strong>Weight:</strong> {{ $shipment->weight }} KG</span> @endif
</div> <div class="bottom-info">
<div class="label-row"> <span><strong>{{ $shipment->awb_no }}</strong></span>
<span><strong>Gross Weight:</strong> {{ $shipment->weight }} KG</span> <span>{{ $shipment->weight }} KG</span>
<span><strong>Volumetric Weight:</strong> {{ $shipment->volumetric_weight }} KG</span> <span>{{ $shipment->created_at?->format('m/d/Y') }}</span>
</div> <span>{{ $shipment->fromCountry?->name }} {{ $shipment->toCountry?->name }}</span>
<div class="label-row">
<span><strong>W*L*H:</strong> {{ $shipment->dimensions }}</span>
</div>
<div class="barcode">
{!! $barcode !!}
</div> </div>
</div> </div>
<!-- FOOTER -->
<div class="footer"> <div class="footer">
IFNEX Logistics We Deliver Value | Generated on {{ now()->format('Y-m-d H:i') }} IFNEX Logistics We Deliver Value | AWB: {{ $shipment->awb_no }} | Generated: {{ now()->format('Y-m-d H:i') }}
</div> </div>
</div> </div>
</body> </body>

View File

@ -1,103 +1,248 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" dir="ltr"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: DejaVu Sans, sans-serif; font-size: 12px; direction: ltr; text-align: left; color: #333; } body {
.page { width: 190mm; min-height: 277mm; padding: 10mm; margin: 0 auto; } font-family: DejaVu Sans, Arial, sans-serif;
color: #000;
.label-box { background: #fff;
border: 3px solid #000; }
padding: 15px; /* A5 افقی: 210x148 mm — استفاده از min-height نه height */
.label-page {
width: 200mm;
padding: 4mm;
margin: 0 auto; margin: 0 auto;
} }
.header { display: flex; align-items: center; border-bottom: 2px solid #f37021; padding-bottom: 10px; margin-bottom: 15px; } /* ─── HEADER ─── */
.logo { width: 70px; height: auto; } .header {
.company-name { font-size: 18px; font-weight: bold; color: #f37021; margin-left: 10px; } display: flex;
justify-content: space-between;
.awb-badge { align-items: center;
background-color: #f37021; border-bottom: 3px solid #f37021;
color: #fff; padding-bottom: 2mm;
font-size: 22px; margin-bottom: 3mm;
}
.brand-name {
font-size: 16px;
font-weight: bold; font-weight: bold;
text-align: center; color: #f37021;
padding: 8px; }
margin-bottom: 15px; .brand-tagline {
letter-spacing: 3px; font-size: 8px;
color: #666;
}
.header-date {
font-size: 10px;
color: #666;
} }
.barcode { text-align: center; margin-bottom: 15px; } /* ─── MAIN: 2 ستون ─── */
.main {
display: flex;
gap: 4mm;
}
.grid { display: flex; gap: 15px; margin-bottom: 15px; } /* ─── ستون چپ: بارکد ─── */
.col { flex: 1; } .barcode-col {
width: 45%;
border: 2px solid #000;
padding: 3mm;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.barcode-col img {
max-width: 100%;
height: 25mm;
width: auto;
display: block;
margin: 0 auto;
}
.awb-label {
font-size: 8px;
color: #666;
margin-top: 2mm;
text-transform: uppercase;
}
.awb-display {
font-size: 16px;
font-weight: bold;
letter-spacing: 2px;
margin-top: 1mm;
font-family: 'Courier New', monospace;
}
.section-title { font-weight: bold; color: #f37021; font-size: 12px; margin-bottom: 8px; text-transform: uppercase; border-bottom: 1px solid #eee; padding-bottom: 3px; } /* ─── ستون راست: اطلاعات ─── */
.info-col {
flex: 1;
display: flex;
flex-direction: column;
gap: 2mm;
}
.row { margin-bottom: 4px; } /* ─── مسیر کشورها ─── */
.label { font-weight: bold; color: #555; display: inline-block; width: 100px; } .route {
.value { display: inline-block; } border: 2px solid #000;
padding: 2mm;
display: flex;
justify-content: space-between;
align-items: center;
background: #f9f9f9;
}
.country-box {
text-align: center;
flex: 1;
}
.country-label {
font-size: 7px;
color: #666;
text-transform: uppercase;
}
.country-name {
font-size: 11px;
font-weight: bold;
}
.country-iso {
font-size: 20px;
font-weight: bold;
color: #f37021;
}
.arrow {
font-size: 20px;
color: #f37021;
padding: 0 2mm;
}
.shipment-info { background-color: #f9f9f9; padding: 10px; border: 1px solid #ddd; margin-bottom: 15px; } /* ─── جدول وزن ─── */
.shipment-grid { display: flex; gap: 15px; } .weights {
.shipment-item { flex: 1; } display: flex;
border: 2px solid #000;
}
.weight-cell {
flex: 1;
border-right: 1px solid #000;
padding: 2mm;
text-align: center;
}
.weight-cell:last-child {
border-right: none;
}
.weight-label {
font-size: 7px;
color: #666;
text-transform: uppercase;
}
.weight-value {
font-size: 12px;
font-weight: bold;
margin-top: 1mm;
}
.footer { text-align: center; font-size: 10px; color: #666; border-top: 1px solid #ddd; padding-top: 10px; } /* ─── ZIP ─── */
.zip {
border: 2px solid #000;
padding: 2mm;
display: flex;
justify-content: space-between;
align-items: center;
background: #fffde7;
}
.zip-label {
font-size: 8px;
color: #666;
text-transform: uppercase;
}
.zip-value {
font-size: 16px;
font-weight: bold;
font-family: 'Courier New', monospace;
letter-spacing: 2px;
}
/* ─── FOOTER ─── */
.footer {
border-top: 2px solid #f37021;
padding-top: 2mm;
margin-top: 3mm;
font-size: 8px;
display: flex;
justify-content: space-between;
color: #666;
}
</style> </style>
</head> </head>
<body> <body>
<div class="page"> <div class="label-page">
<div class="label-box"> <!-- HEADER -->
<div class="header"> <div class="header">
<img src="{{ asset('logo.png') }}" class="logo" alt="IFNEX Logo"> <div>
<div class="company-name">IFNEX LOGISTICS</div> <div class="brand-name">IFNEx LOGISTICS</div>
<div class="brand-tagline">We Deliver Value</div>
</div>
<div class="header-date">Date: {{ $shipment->created_at?->format('m/d/Y') }}</div>
</div> </div>
<div class="awb-badge"> <!-- MAIN -->
{{ $shipment->awb_no }} <div class="main">
<!-- BARCODE -->
<div class="barcode-col">
@if($barcode_base64)
<img src="{{ $barcode_base64 }}" alt="barcode">
@endif
<div class="awb-label">AWB Number</div>
<div class="awb-display">{{ $shipment->awb_no }}</div>
</div> </div>
<div class="barcode"> <!-- INFO -->
{!! $barcode !!} <div class="info-col">
<!-- ROUTE -->
<div class="route">
<div class="country-box">
<div class="country-label">ORIGIN</div>
<div class="country-name">{{ $shipment->fromCountry?->name ?? '—' }}</div>
<div class="country-iso">{{ $origin_iso }}</div>
</div> </div>
<div class="arrow"></div>
<div class="grid"> <div class="country-box">
<div class="col"> <div class="country-label">DESTINATION</div>
<div class="section-title">SHIPPER</div> <div class="country-name">{{ $shipment->toCountry?->name ?? '—' }}</div>
<div class="row"><span class="label">Name:</span><span class="value">{{ $shipper['name'] }}</span></div> <div class="country-iso">{{ $dest_iso }}</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> </div>
<div class="shipment-info"> <!-- WEIGHTS -->
<div class="section-title">SHIPMENT DETAILS</div> <div class="weights">
<div class="shipment-grid"> <div class="weight-cell">
<div class="shipment-item"> <div class="weight-label">Gross Weight</div>
<div class="row"><span class="label">Gross Weight:</span><span class="value">{{ $shipment->weight }} KG</span></div> <div class="weight-value">{{ $shipment->weight }} kg</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">Dimensions:</span><span class="value">{{ $shipment->dimensions }} CM</span></div>
</div> </div>
<div class="shipment-item"> <div class="weight-cell">
<div class="row"><span class="label">From:</span><span class="value">{{ $shipment->fromCountry?->name }}</span></div> <div class="weight-label">Volumetric</div>
<div class="row"><span class="label">To:</span><span class="value">{{ $shipment->toCountry?->name }}</span></div> <div class="weight-value">{{ $shipment->volumetric_weight }} kg</div>
<div class="row"><span class="label">Service:</span><span class="value">{{ $shipment->type?->label() }}</span></div> </div>
<div class="weight-cell">
<div class="weight-label">W*L*H</div>
<div class="weight-value">{{ $shipment->dimensions ?: '—' }} cm</div>
</div>
</div>
<!-- ZIP -->
<div class="zip">
<span class="zip-label">DEST ZIP</span>
<span class="zip-value">{{ $shipment->receiver_zip ?: '—' }}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- FOOTER -->
<div class="footer"> <div class="footer">
IFNEX Logistics We Deliver Value <span>IFNEx Logistics We Deliver Value</span>
</div> <span><strong>{{ $shipment->awb_no }}</strong></span>
</div> </div>
</div> </div>
</body> </body>

View File

@ -0,0 +1,157 @@
<?php
/**
* اسکریپت تست تولید PDF برای IFNEX
*
* این اسکریپت یه محموله نمونه می‌سازه و سه تا PDF (AWB, Invoice, Label) تولید می‌کنه
* و اون‌ها رو توی پوشه storage/app/pdf-test/ ذخیره می‌کنه.
*
* استفاده:
* php test_pdf_generation.php
*
* پس از اجرا، فایل‌های تولیدشده رو بررسی کنید:
* storage/app/pdf-test/AWB-test.pdf
* storage/app/pdf-test/INVOICE-test.pdf
* storage/app/pdf-test/LABEL-test.pdf
*/
require __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
use App\Models\Shipment;
use App\Models\ShipmentItem;
use App\Models\Country;
use App\Services\PdfService;
use Illuminate\Support\Facades\File;
echo "═══════════════════════════════════════════\n";
echo "🧪 IFNEX PDF Generation Test\n";
echo "═══════════════════════════════════════════\n\n";
// ─── ۱. بررسی نصب بودن کتابخانه بارکد ───
echo "1⃣ Checking barcode library...\n";
if (class_exists(\Picqer\Barcode\BarcodeGeneratorPNG::class)) {
echo " ✅ picqer/php-barcode-generator is installed\n\n";
} else {
echo " ❌ picqer/php-barcode-generator is NOT installed\n";
echo " Run: composer require picqer/php-barcode-generator\n\n";
exit(1);
}
// ─── ۱.۵. تست تولید بارکد ───
echo "1.5️⃣ Testing barcode generation...\n";
try {
$generator = new \Picqer\Barcode\BarcodeGeneratorPNG();
$barcode = $generator->getBarcode('TEST123', $generator::TYPE_CODE_128, 3, 80);
$b64 = 'data:image/png;base64,' . base64_encode($barcode);
echo " ✅ Barcode generated, length: " . strlen($b64) . " chars\n";
echo " Preview: " . substr($b64, 0, 50) . "...\n\n";
} catch (\Throwable $e) {
echo " ❌ Barcode generation failed: " . $e->getMessage() . "\n\n";
}
// ─── ۲. ایجاد پوشه تست ───
$testDir = storage_path('app/pdf-test');
if (!File::exists($testDir)) {
File::makeDirectory($testDir, 0755, true);
echo "2⃣ Created test directory: {$testDir}\n\n";
} else {
echo "2⃣ Test directory exists: {$testDir}\n\n";
}
// ─── ۳. پیدا کردن یه محموله نمونه ───
echo "3⃣ Finding sample shipment...\n";
$shipment = Shipment::with(['fromCountry', 'toCountry', 'items'])->latest()->first();
if (!$shipment) {
echo " ❌ No shipment found in database. Please create one first.\n";
exit(1);
}
echo " ✅ Found shipment: {$shipment->awb_no}\n";
echo " - Type: {$shipment->type?->value}\n";
echo " - Direction: {$shipment->direction?->value}\n";
echo " - From: {$shipment->fromCountry?->name}\n";
echo " - To: {$shipment->toCountry?->name}\n";
echo " - Items: {$shipment->items->count()}\n\n";
// ─── ۴. اگر آیتم نداره، یه آیتم تستی اضافه کن ───
if ($shipment->items->isEmpty()) {
echo " ⚠️ Shipment has no items. Adding test item...\n";
ShipmentItem::create([
'shipment_id' => $shipment->id,
'row_number' => 1,
'description' => 'Electronics PCB Board',
'hs_code' => '8542390001',
'quantity' => 104,
'unit_price' => 1.10,
'total_usd' => 114.40,
]);
$shipment->load('items');
echo " ✅ Added test item\n\n";
}
// ─── ۵. تولید AWB PDF ───
echo "4⃣ Generating AWB PDF...\n";
try {
$pdfService = app(PdfService::class);
$awbContent = $pdfService->awb($shipment);
$awbPath = $testDir . '/AWB-' . $shipment->awb_no . '.pdf';
File::put($awbPath, $awbContent);
echo " ✅ AWB PDF saved: {$awbPath}\n";
echo " Size: " . number_format(strlen($awbContent) / 1024, 2) . " KB\n\n";
} catch (\Throwable $e) {
echo " ❌ AWB PDF failed: " . $e->getMessage() . "\n\n";
}
// ─── ۶. تولید Invoice PDF ───
echo "5⃣ Generating Invoice PDF...\n";
try {
$invoiceContent = $pdfService->invoice($shipment);
$invoicePath = $testDir . '/INVOICE-' . $shipment->awb_no . '.pdf';
File::put($invoicePath, $invoiceContent);
echo " ✅ Invoice PDF saved: {$invoicePath}\n";
echo " Size: " . number_format(strlen($invoiceContent) / 1024, 2) . " KB\n\n";
} catch (\InvalidArgumentException $e) {
echo " ⚠️ Invoice skipped: " . $e->getMessage() . "\n";
echo " (این طبیعی است اگر محموله DOC است)\n\n";
} catch (\Throwable $e) {
echo " ❌ Invoice PDF failed: " . $e->getMessage() . "\n\n";
}
// ─── ۷. تولید Label PDF ───
echo "6⃣ Generating Label PDF...\n";
try {
$labelContent = $pdfService->label($shipment);
$labelPath = $testDir . '/LABEL-' . $shipment->awb_no . '.pdf';
File::put($labelPath, $labelContent);
echo " ✅ Label PDF saved: {$labelPath}\n";
echo " Size: " . number_format(strlen($labelContent) / 1024, 2) . " KB\n\n";
} catch (\Throwable $e) {
echo " ❌ Label PDF failed: " . $e->getMessage() . "\n\n";
}
// ─── ۸. تست محموله DOC (بدون فاکتور) ───
echo "7⃣ Testing DOC shipment (should skip invoice)...\n";
$docShipment = Shipment::where('type', 'DOC_NORMAL')->first();
if ($docShipment) {
try {
$docShipment->load('items');
if ($docShipment->items->isEmpty()) {
$pdfService->invoice($docShipment);
echo " ❌ ERROR: Should have thrown exception for DOC shipment!\n\n";
} else {
echo " DOC shipment has items, invoice would work\n\n";
}
} catch (\InvalidArgumentException $e) {
echo " ✅ Correctly skipped invoice for DOC: " . $e->getMessage() . "\n\n";
}
} else {
echo " No DOC shipment found for testing (skipped)\n\n";
}
echo "═══════════════════════════════════════════\n";
echo "✅ Test completed!\n";
echo "═══════════════════════════════════════════\n\n";
echo "📁 Check the generated PDFs at:\n";
echo " {$testDir}\n\n";

View File

@ -0,0 +1,145 @@
<?php
/**
* اسکریپت تست تولید PDF برای IFNEX
*
* این اسکریپت یه محموله نمونه می‌سازه و سه تا PDF (AWB, Invoice, Label) تولید می‌کنه
* و اون‌ها رو توی پوشه storage/app/pdf-test/ ذخیره می‌کنه.
*
* استفاده:
* php test_pdf_generation.php
*
* پس از اجرا، فایل‌های تولیدشده رو بررسی کنید:
* storage/app/pdf-test/AWB-test.pdf
* storage/app/pdf-test/INVOICE-test.pdf
* storage/app/pdf-test/LABEL-test.pdf
*/
require __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
use App\Models\Shipment;
use App\Models\ShipmentItem;
use App\Models\Country;
use App\Services\PdfService;
use Illuminate\Support\Facades\File;
echo "═══════════════════════════════════════════\n";
echo "🧪 IFNEX PDF Generation Test\n";
echo "═══════════════════════════════════════════\n\n";
// ─── ۱. بررسی نصب بودن کتابخانه بارکد ───
echo "1⃣ Checking barcode library...\n";
if (class_exists(\Picqer\Barcode\BarcodeGeneratorPNG::class)) {
echo " ✅ picqer/php-barcode-generator is installed\n\n";
} else {
echo " ❌ picqer/php-barcode-generator is NOT installed\n";
echo " Run: composer require picqer/php-barcode-generator\n\n";
exit(1);
}
// ─── ۲. ایجاد پوشه تست ───
$testDir = storage_path('app/pdf-test');
if (!File::exists($testDir)) {
File::makeDirectory($testDir, 0755, true);
echo "2⃣ Created test directory: {$testDir}\n\n";
} else {
echo "2⃣ Test directory exists: {$testDir}\n\n";
}
// ─── ۳. پیدا کردن یه محموله نمونه ───
echo "3⃣ Finding sample shipment...\n";
$shipment = Shipment::with(['fromCountry', 'toCountry', 'items'])->latest()->first();
if (!$shipment) {
echo " ❌ No shipment found in database. Please create one first.\n";
exit(1);
}
echo " ✅ Found shipment: {$shipment->awb_no}\n";
echo " - Type: {$shipment->type?->value}\n";
echo " - Direction: {$shipment->direction?->value}\n";
echo " - From: {$shipment->fromCountry?->name}\n";
echo " - To: {$shipment->toCountry?->name}\n";
echo " - Items: {$shipment->items->count()}\n\n";
// ─── ۴. اگر آیتم نداره، یه آیتم تستی اضافه کن ───
if ($shipment->items->isEmpty()) {
echo " ⚠️ Shipment has no items. Adding test item...\n";
ShipmentItem::create([
'shipment_id' => $shipment->id,
'row_number' => 1,
'description' => 'Electronics PCB Board',
'hs_code' => '8542390001',
'quantity' => 104,
'unit_price' => 1.10,
'total_usd' => 114.40,
]);
$shipment->load('items');
echo " ✅ Added test item\n\n";
}
// ─── ۵. تولید AWB PDF ───
echo "4⃣ Generating AWB PDF...\n";
try {
$pdfService = app(PdfService::class);
$awbContent = $pdfService->awb($shipment);
$awbPath = $testDir . '/AWB-' . $shipment->awb_no . '.pdf';
File::put($awbPath, $awbContent);
echo " ✅ AWB PDF saved: {$awbPath}\n";
echo " Size: " . number_format(strlen($awbContent) / 1024, 2) . " KB\n\n";
} catch (\Throwable $e) {
echo " ❌ AWB PDF failed: " . $e->getMessage() . "\n\n";
}
// ─── ۶. تولید Invoice PDF ───
echo "5⃣ Generating Invoice PDF...\n";
try {
$invoiceContent = $pdfService->invoice($shipment);
$invoicePath = $testDir . '/INVOICE-' . $shipment->awb_no . '.pdf';
File::put($invoicePath, $invoiceContent);
echo " ✅ Invoice PDF saved: {$invoicePath}\n";
echo " Size: " . number_format(strlen($invoiceContent) / 1024, 2) . " KB\n\n";
} catch (\InvalidArgumentException $e) {
echo " ⚠️ Invoice skipped: " . $e->getMessage() . "\n";
echo " (این طبیعی است اگر محموله DOC است)\n\n";
} catch (\Throwable $e) {
echo " ❌ Invoice PDF failed: " . $e->getMessage() . "\n\n";
}
// ─── ۷. تولید Label PDF ───
echo "6⃣ Generating Label PDF...\n";
try {
$labelContent = $pdfService->label($shipment);
$labelPath = $testDir . '/LABEL-' . $shipment->awb_no . '.pdf';
File::put($labelPath, $labelContent);
echo " ✅ Label PDF saved: {$labelPath}\n";
echo " Size: " . number_format(strlen($labelContent) / 1024, 2) . " KB\n\n";
} catch (\Throwable $e) {
echo " ❌ Label PDF failed: " . $e->getMessage() . "\n\n";
}
// ─── ۸. تست محموله DOC (بدون فاکتور) ───
echo "7⃣ Testing DOC shipment (should skip invoice)...\n";
$docShipment = Shipment::where('type', 'DOC_NORMAL')->first();
if ($docShipment) {
try {
$docShipment->load('items');
if ($docShipment->items->isEmpty()) {
$pdfService->invoice($docShipment);
echo " ❌ ERROR: Should have thrown exception for DOC shipment!\n\n";
} else {
echo " DOC shipment has items, invoice would work\n\n";
}
} catch (\InvalidArgumentException $e) {
echo " ✅ Correctly skipped invoice for DOC: " . $e->getMessage() . "\n\n";
}
} else {
echo " No DOC shipment found for testing (skipped)\n\n";
}
echo "═══════════════════════════════════════════\n";
echo "✅ Test completed!\n";
echo "═══════════════════════════════════════════\n\n";
echo "📁 Check the generated PDFs at:\n";
echo " {$testDir}\n\n";