feat(shipment): add multi-package support and AWB integration

Introduce a new shipment packages system to allow handling shipments
consisting of multiple individual packages. This includes a new
ShipmentPackage model, a dedicated database table, and a repeater
field in the Filament ShipmentResource for managing package details
such as weight, dimensions, and declared value.

Additionally, update the AWB PDF generation service and template to
include a detailed breakdown of all associated packages when multiple
items are present.
This commit is contained in:
Kazem Alghasi 2026-08-24 01:47:45 +03:30
parent e9a0c731c2
commit 157ce1bdd4
6 changed files with 147 additions and 2 deletions

View File

@ -102,6 +102,52 @@ class ShipmentResource extends Resource
->label('Content Description'),
])->columns(4),
Forms\Components\Section::make('Packages')
->description('برای محموله‌های چند بسته‌ای، اطلاعات هر بسته را وارد کنید.')
->schema([
Forms\Components\Repeater::make('packages')
->relationship()
->schema([
Forms\Components\TextInput::make('package_no')
->required()
->numeric()
->minValue(1)
->maxValue(99)
->label('#'),
Forms\Components\TextInput::make('weight')
->numeric()
->nullable()
->suffix('kg')
->label('Weight'),
Forms\Components\TextInput::make('volumetric_weight')
->numeric()
->nullable()
->suffix('kg')
->label('Volumetric Wt'),
Forms\Components\TextInput::make('chargeable_weight')
->numeric()
->nullable()
->suffix('kg')
->label('Chargeable Wt'),
Forms\Components\TextInput::make('dimensions')
->nullable()
->label('WxLxH (cm)'),
Forms\Components\TextInput::make('declared_value')
->numeric()
->nullable()
->prefix('USD')
->label('Value'),
Forms\Components\TextInput::make('content_description')
->nullable()
->label('Content'),
])
->columns(4)
->defaultItems(1)
->reorderable()
->label('بسته‌ها'),
])->collapsible(),
Forms\Components\Section::make('Financial Info')
->schema([
Forms\Components\TextInput::make('shipping_price')

View File

@ -90,4 +90,9 @@ class Shipment extends Model
{
return $this->items->sum('total_usd');
}
public function packages(): HasMany
{
return $this->hasMany(ShipmentPackage::class)->orderBy('package_no');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ShipmentPackage extends Model
{
protected $fillable = [
'shipment_id', 'package_no', 'weight', 'volumetric_weight',
'chargeable_weight', 'dimensions', 'declared_value', 'content_description',
];
protected function casts(): array
{
return [
'weight' => 'decimal:2',
'volumetric_weight' => 'decimal:2',
'chargeable_weight' => 'decimal:2',
'declared_value' => 'decimal:2',
];
}
public function shipment(): BelongsTo
{
return $this->belongsTo(Shipment::class);
}
}

View File

@ -12,7 +12,7 @@ class PdfService
{
public function awb(Shipment $shipment): string
{
$shipment->loadMissing(['fromCountry', 'toCountry', 'items']);
$shipment->loadMissing(['fromCountry', 'toCountry', 'items', 'packages']);
$data = [
'shipment' => $shipment,

View File

@ -0,0 +1,31 @@
<?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::create('shipment_packages', function (Blueprint $table) {
$table->id();
$table->foreignId('shipment_id')->constrained()->cascadeOnDelete();
$table->unsignedSmallInteger('package_no')->default(1);
$table->decimal('weight', 10, 2)->nullable();
$table->decimal('volumetric_weight', 10, 2)->nullable();
$table->decimal('chargeable_weight', 10, 2)->nullable();
$table->string('dimensions', 50)->nullable();
$table->decimal('declared_value', 12, 2)->nullable()->unsigned();
$table->string('content_description')->nullable();
$table->timestamps();
$table->unique(['shipment_id', 'package_no']);
});
}
public function down(): void
{
Schema::dropIfExists('shipment_packages');
}
};

View File

@ -148,6 +148,40 @@
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>
</table>
</div>
@endif
<div class="signature">
<div class="signature-box">
Shipper Name & Signature<br>