diff --git a/04_Laravel/app/Http/Controllers/ShipmentPdfController.php b/04_Laravel/app/Http/Controllers/ShipmentPdfController.php index bd3d5b8..dcf9d6f 100644 --- a/04_Laravel/app/Http/Controllers/ShipmentPdfController.php +++ b/04_Laravel/app/Http/Controllers/ShipmentPdfController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Enums\ShipmentType; use App\Models\Shipment; use App\Services\PdfService; use Illuminate\Http\Request; @@ -11,49 +12,119 @@ class ShipmentPdfController extends Controller { public function __construct(protected PdfService $pdf) {} + /** + * تولید AWB PDF (برای همه نوع محمولهها) + */ public function awb(Shipment $shipment) { try { Log::info('Generating AWB PDF', ['shipment_id' => $shipment->id]); + $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, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'attachment; filename="AWB-' . $shipment->awb_no . '.pdf"', ]); } catch (\Throwable $e) { Log::error('AWB PDF generation failed', [ + 'shipment_id' => $shipment->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); + return response('PDF generation failed: ' . $e->getMessage(), 500); } } + /** + * تولید Invoice PDF — فقط برای محمولههای دارای کالا (PARCEL) + * + * محمولههای DOC (DOC_NORMAL/DOC_ECONOMY) که کالا ندارن، + * فاکتور صادراتی ندارن و این متد براشون ارور برمیگردونه. + */ public function invoice(Shipment $shipment) { 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); + + Log::info('Invoice PDF generated successfully', [ + 'awb' => $shipment->awb_no, + 'type' => $shipment->type?->value, + 'items_count' => $shipment->items->count(), + ]); + return response($content, 200, [ 'Content-Type' => 'application/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) { - 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); } } + /** + * تولید Label PDF — اندازه استاندارد لیبل پستی (100x150mm) + */ public function label(Shipment $shipment) { try { $content = $this->pdf->label($shipment); + + Log::info('Label PDF generated successfully', ['awb' => $shipment->awb_no]); + return response($content, 200, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'attachment; filename="LABEL-' . $shipment->awb_no . '.pdf"', ]); } 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); } } diff --git a/04_Laravel/app/Services/PdfService.php b/04_Laravel/app/Services/PdfService.php index 3b6d57c..8320d53 100644 --- a/04_Laravel/app/Services/PdfService.php +++ b/04_Laravel/app/Services/PdfService.php @@ -2,30 +2,21 @@ namespace App\Services; +use App\Enums\ShipmentType; use App\Models\Shipment; +use App\Models\ShipmentItem; use Dompdf\Dompdf; use Dompdf\Options; use Illuminate\Support\Collection; -use Picqer\Barcode\BarcodeGeneratorHTML; +use Illuminate\Support\Facades\Log; +use Picqer\Barcode\BarcodeGeneratorPNG; class PdfService { + /** + * تولید AWB PDF (برای همه نوع محمولهها) + */ 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']); @@ -33,65 +24,156 @@ class PdfService 'shipment' => $shipment, 'shipper' => $this->formatAddress($shipment, 'sender'), 'receiver' => $this->formatAddress($shipment, 'receiver'), - 'items' => $shipment->isParcel() ? ($shipment->items ?? collect()) : collect(), - 'invoice_total_usd' => $shipment->invoice_total_usd, - 'barcode' => $this->generateBarcode($shipment->awb_no), + 'items' => $shipment->items ?? collect(), + 'invoice_total_usd' => optional($shipment->items)->sum('total_usd') ?? 0, + '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 { $shipment->loadMissing(['fromCountry', 'toCountry']); $data = [ 'shipment' => $shipment, - 'shipper' => $this->formatAddress($shipment, 'sender'), - 'receiver' => $this->formatAddress($shipment, 'receiver'), - 'barcode' => $this->generateBarcode($shipment->awb_no), + 'barcode_base64' => $this->generateBarcode($shipment->awb_no), + 'origin_iso' => $shipment->fromCountry?->iso_code ?? 'IR', + '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->set('isHtml5ParserEnabled', true); - $options->set('isRemoteEnabled', true); + $options->set('isRemoteEnabled', true); // مهم: برای data URI $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->loadHtml($html); - $dompdf->setPaper($paper, $orientation); + + if (is_array($paper)) { + $dompdf->setPaper($paper, $orientation); + } else { + $dompdf->setPaper($paper, $orientation); + } + $dompdf->render(); return $dompdf->output(); } + /** + * فرمتبندی آدرس فرستنده/گیرنده + */ private function formatAddress(Shipment $shipment, string $prefix): Collection { return collect([ - 'name' => $shipment->{"{$prefix}_name"}, - 'company' => $shipment->{"{$prefix}_company"}, - 'phone' => $shipment->{"{$prefix}_phone"}, - 'email' => $shipment->{"{$prefix}_email"}, - 'address' => $shipment->{"{$prefix}_address"}, - 'city' => $shipment->{"{$prefix}_city"}, - 'state' => $shipment->{"{$prefix}_state"}, - 'zip' => $shipment->{"{$prefix}_zip"}, - 'id_number' => $shipment->{"{$prefix}_id_number"}, + 'name' => $shipment->{$prefix . '_name'}, + 'company' => $shipment->{$prefix . '_company'}, + 'phone' => $shipment->{$prefix . '_phone'}, + 'email' => $shipment->{$prefix . '_email'}, + 'address' => $shipment->{$prefix . '_address'}, + 'city' => $shipment->{$prefix . '_city'}, + 'zip' => $shipment->{$prefix . '_zip'}, + '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 '
-
+ |
-
-
- SHIPPER
- From:{{ $shipment->fromCountry?->name }}
- Company Name:{{ $shipper['company'] }}
- Contact Name:{{ $shipper['name'] }}
- Tel/Mob:{{ $shipper['phone'] }}
- Email:{{ $shipper['email'] }}
- Address:{{ $shipper['address'] }}
- ID Number:{{ $shipper['id_number'] }}
- Zip Code:{{ $shipper['zip'] }}
- |
-
-
-
- RECEIVER
- To:{{ $shipment->toCountry?->name }}
- Company Name:{{ $receiver['company'] }}
- Contact Name:{{ $receiver['name'] }}
- Tel/Mobile:{{ $receiver['phone'] }}
- Email:{{ $receiver['email'] }}
- Address:{{ $receiver['address'] }}
- City:{{ $receiver['city'] }}
- Zip Code:{{ $receiver['zip'] }}
- |
-
|
-
-
- SHIPMENT
- Gross Weight:{{ $shipment->weight }} Kg
- Dimensions:{{ $shipment->dimensions }} cm
- Volumetric Weight:{{ $shipment->volumetric_weight }} Kg
- Chargeable Weight:{{ $shipment->chargeable_weight }} Kg
- Value:${{ number_format($shipment->declared_value ?? 0, 2) }} USD
- Service:{{ $shipment->type?->label() }}
- Type:{{ $shipment->direction?->value === 'export' ? 'Export' : 'Import' }}
- Content:{{ $shipment->content_description ?: 'General Goods' }}
-
- @if($shipment->isParcel() && $items->count() > 0)
-
-
- @endif
- Shipment Details
-
|
-
-
-
- PAYMENT
- Shipping Price:{{ number_format($shipment->shipping_price ?? 0, 2) }} IRR
- Extra Service:{{ number_format($shipment->extra_service ?? 0) }} IRR
- Domestic Pickup:{{ number_format($shipment->domestic_pickup ?? 0) }} IRR
- Packing Cost:{{ number_format($shipment->packing_cost ?? 0) }} IRR
- Domestic Delivery:{{ number_format($shipment->domestic_delivery ?? 0) }} IRR
- Warehousing Cost:{{ number_format($shipment->warehousing_cost ?? 0) }} IRR
- Discount:{{ number_format($shipment->discount ?? 0) }} IRR
- Total Fee:{{ number_format($shipment->total_fee ?? 0) }} IRR
- Cash on Delivery:{{ number_format($shipment->cod_amount ?? 0, 2) }} AED
- |
-
| # | -Weight (kg) | -Vol. Wt (kg) | -Chg. Wt (kg) | -Dimensions | -Value (USD) | -Content | +||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| No | +Description | +H.S. Code | +Qty | +Unit Price (USD) | +Total (USD) | |||||||
| {{ $pkg->package_no }} | -{{ number_format($pkg->weight, 2) }} | -{{ number_format($pkg->volumetric_weight, 2) }} | -{{ number_format($pkg->chargeable_weight, 2) }} | -{{ $pkg->dimensions }} | -{{ number_format($pkg->declared_value, 2) }} | -{{ $pkg->content_description }} | +{{ $item->row_number }} | +{{ $item->description }} | +{{ $item->hs_code }} | +{{ $item->quantity }} | +{{ number_format($item->unit_price, 2) }} | +{{ number_format($item->total_usd, 2) }} |
| Total Invoice Amount in USD: | +{{ number_format($invoice_total_usd, 2) }} | +|||||||||||
{{ $shipment->content_description ?: 'General Goods' }}
-| NO | +NO | DESCRIPTION | -H.S. CODE | -QUANTITY | -UNIT PRICE (USD) | -TOTAL IN USD | +H.S. CODE | +QUANTITY | +UNIT PRICE | +TOTAL IN USD |
|---|---|---|---|---|---|---|---|---|---|---|
| {{ $item->row_number }} | +{{ $item->row_number }} | {{ $item->description }} | -{{ $item->hs_code }} | -{{ $item->quantity }} | -{{ number_format($item->unit_price, 2) }} | -{{ number_format($item->total_usd, 2) }} | +{{ $item->hs_code }} | +{{ $item->quantity }} | +{{ number_format($item->unit_price, 2) }} | +{{ number_format($item->total_usd, 2) }} | +
| No items | ||||||||||
| {{ $i }} | ++ | + | + | + | + | |||||
| TOTAL INVOICE AMOUNT IN USD | +{{ number_format($invoice_total_usd, 2) }} | |||||||||
-