ifnex/04_Laravel/app/Filament/Resources/ShipmentResource.php
Kazem Alghasi b051ce4496 feat(shipment): expand schema and enhance PDF generation
Extend the shipment database schema to support additional tracking fields,
state information, and financial data. This update also transitions
shipment documentation from RTL/Persian to LTR/English layouts and
integrates barcode generation for AWB and labels.

- feat(db): add migration for missing shipment fields (state, cod_amount,
  declared_value, etc.)
- feat(shipment): update Shipment model with new casts and helper methods
- feat(ui): refactor Filament resource to use ShipmentStatus enum and
  add new input fields
- feat(pdf): implement barcode generation using picqer/php-barcode-generator
- feat(pdf): redesign AWB, Invoice, and Label templates for LTR/English
  support
- refactor(enum): simplify ShipmentStatus enum and update labels/colors
- chore(deps): add picqer/php-barcode-generator dependency
2026-08-23 22:31:58 +03:30

439 lines
20 KiB
PHP

<?php
namespace App\Filament\Resources;
use App\Enums\ShipmentStatus;
use App\Filament\Resources\ShipmentResource\Pages;
use App\Filament\Resources\ShipmentResource\RelationManagers;
use App\Models\Shipment;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Tables\Actions\Action;
class ShipmentResource extends Resource
{
protected static ?string $model = Shipment::class;
protected static ?string $navigationIcon = 'heroicon-o-truck';
protected static ?string $navigationLabel = 'مرسولات';
protected static ?string $navigationGroup = 'عملیات';
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make('Route & Status')
->schema([
Forms\Components\TextInput::make('awb_no')
->required()
->unique(ignoreRecord: true)
->label('AWB No.'),
Forms\Components\Select::make('direction')
->options([
'export' => 'Export',
'import' => 'Import'
])
->required(),
Forms\Components\Select::make('type')
->options([
'DOC_NORMAL' => 'Doc Normal',
'DOC_ECONOMY' => 'Doc Economy',
'PARCEL' => 'Parcel',
])
->required()
->live(),
Forms\Components\Select::make('status')
->options(collect(ShipmentStatus::cases())->mapWithKeys(fn ($case) => [
$case->value => $case->label(),
]))
->default('processed')
->required(),
Forms\Components\TextInput::make('reason_for_export')
->nullable()
->label('Reason for Export'),
Forms\Components\Select::make('from_country_id')
->relationship('fromCountry', 'name')
->searchable()
->preload()
->label('From Country'),
Forms\Components\Select::make('to_country_id')
->relationship('toCountry', 'name')
->searchable()
->preload()
->label('To Country'),
Forms\Components\TextInput::make('forwarder')
->nullable()
->label('Forwarder'),
])->columns(3),
Forms\Components\Section::make('Weight & Dimensions')
->schema([
Forms\Components\TextInput::make('weight')
->numeric()
->nullable()
->suffix('kg')
->label('Weight'),
Forms\Components\TextInput::make('volumetric_weight')
->numeric()
->nullable()
->suffix('kg')
->label('Volumetric Weight'),
Forms\Components\TextInput::make('chargeable_weight')
->numeric()
->nullable()
->suffix('kg')
->label('Chargeable Weight'),
Forms\Components\TextInput::make('dimensions')
->nullable()
->label('Dimensions (WxLxH)'),
Forms\Components\TextInput::make('declared_value')
->numeric()
->nullable()
->prefix('USD')
->label('Declared Value'),
Forms\Components\TextInput::make('content_description')
->nullable()
->label('Content Description'),
Forms\Components\TextInput::make('reason_for_export')
->nullable()
->label('Reason for Export'),
])->columns(4),
Forms\Components\Section::make('Financial Info')
->schema([
Forms\Components\TextInput::make('shipping_price')
->numeric()
->nullable()
->prefix('درهم')
->label('Shipping Price'),
Forms\Components\TextInput::make('extra_service')
->numeric()
->default(0)
->prefix('درهم')
->label('Extra Service'),
Forms\Components\TextInput::make('packing_cost')
->numeric()
->default(0)
->prefix('ریال')
->label('Packing Cost'),
Forms\Components\TextInput::make('domestic_pickup')
->numeric()
->default(0)
->prefix('ریال')
->label('Domestic Pickup'),
Forms\Components\TextInput::make('domestic_delivery')
->numeric()
->default(0)
->prefix('ریال')
->label('Domestic Delivery'),
Forms\Components\TextInput::make('warehousing_cost')
->numeric()
->default(0)
->prefix('ریال')
->label('Warehousing Cost'),
Forms\Components\TextInput::make('vat_amount')
->numeric()
->default(0)
->prefix('ریال')
->label('VAT Amount'),
Forms\Components\TextInput::make('discount')
->numeric()
->default(0)
->prefix('ریال')
->label('Discount'),
Forms\Components\TextInput::make('total_fee')
->numeric()
->nullable()
->prefix('ریال')
->label('Total Fee'),
Forms\Components\TextInput::make('net_dirham')
->numeric()
->nullable()
->prefix('درهم')
->label('Net Dirham'),
Forms\Components\TextInput::make('net_rial')
->numeric()
->nullable()
->prefix('ریال')
->label('Net Rial'),
Forms\Components\TextInput::make('cod_amount')
->numeric()
->default(0)
->prefix('AED')
->label('Cash on Delivery'),
])->columns(4),
Forms\Components\Section::make('Sender Info')
->schema([
Forms\Components\TextInput::make('sender_name')
->nullable()
->label('Name'),
Forms\Components\TextInput::make('sender_company')
->nullable()
->label('Company'),
Forms\Components\TextInput::make('sender_phone')
->tel()
->nullable()
->label('Phone'),
Forms\Components\TextInput::make('sender_email')
->email()
->nullable()
->label('Email'),
Forms\Components\TextInput::make('sender_address')
->nullable()
->label('Address'),
Forms\Components\TextInput::make('sender_city')
->nullable()
->label('City'),
Forms\Components\TextInput::make('sender_state')
->nullable()
->label('State/Province'),
Forms\Components\TextInput::make('sender_zip')
->nullable()
->label('ZIP'),
Forms\Components\TextInput::make('sender_id_number')
->nullable()
->label('ID Number'),
])->columns(4),
Forms\Components\Section::make('Receiver Info')
->schema([
Forms\Components\TextInput::make('receiver_name')
->nullable()
->label('Name'),
Forms\Components\TextInput::make('receiver_company')
->nullable()
->label('Company'),
Forms\Components\TextInput::make('receiver_phone')
->tel()
->nullable()
->label('Phone'),
Forms\Components\TextInput::make('receiver_email')
->email()
->nullable()
->label('Email'),
Forms\Components\TextInput::make('receiver_address')
->nullable()
->label('Address'),
Forms\Components\TextInput::make('receiver_city')
->nullable()
->label('City'),
Forms\Components\TextInput::make('receiver_state')
->nullable()
->label('State/Province'),
Forms\Components\TextInput::make('receiver_zip')
->nullable()
->label('ZIP'),
Forms\Components\TextInput::make('receiver_id_number')
->nullable()
->label('ID Number'),
])->columns(4),
Forms\Components\Section::make('Customs Items')
->schema([
Forms\Components\Repeater::make('items')
->relationship()
->schema([
Forms\Components\TextInput::make('row_number')
->required()
->numeric()
->minValue(1)
->maxValue(9)
->label('Row'),
Forms\Components\TextInput::make('description')
->required()
->columnSpan(2)
->label('Description'),
Forms\Components\TextInput::make('hs_code')
->required()
->label('HS Code'),
Forms\Components\TextInput::make('quantity')
->required()
->numeric()
->label('Qty'),
Forms\Components\TextInput::make('unit_price')
->required()
->numeric()
->prefix('USD')
->label('Unit Price'),
Forms\Components\TextInput::make('total_usd')
->required()
->numeric()
->prefix('USD')
->label('Total'),
])
->columns(6)
->maxItems(9)
->defaultItems(1)
->reorderable()
->label('Items'),
]),
Forms\Components\Section::make('Customer Notes')->schema([
Forms\Components\Textarea::make('customer_notes')->nullable()->rows(3)->label('Customer Notes'),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('awb_no')
->searchable()
->label('AWB No.')
->sortable(),
Tables\Columns\TextColumn::make('direction')
->badge()
->color(fn (\App\Enums\ShipmentDirection $state): string => match ($state->value) {
'import' => 'success',
'export' => 'info',
default => 'gray',
})
->label('Direction'),
Tables\Columns\TextColumn::make('type')
->badge()
->label('Type'),
Tables\Columns\TextColumn::make('status')
->badge()
->color(fn (ShipmentStatus $state): string => $state->color())
->formatStateUsing(fn (ShipmentStatus $state): string => $state->label())
->searchable()
->label('Status'),
Tables\Columns\TextColumn::make('sender_name')
->searchable()
->toggleable()
->label('Sender'),
Tables\Columns\TextColumn::make('receiver_name')
->searchable()
->toggleable()
->label('Receiver'),
Tables\Columns\TextColumn::make('total_fee')
->money('IRR')
->sortable()
->label('Total Fee'),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\SelectFilter::make('status')
->options(collect(ShipmentStatus::cases())->mapWithKeys(fn ($case) => [
$case->value => $case->label(),
])),
Tables\Filters\SelectFilter::make('type')
->options([
'DOC_NORMAL' => 'Doc Normal',
'DOC_ECONOMY' => 'Doc Economy',
'PARCEL' => 'Parcel',
]),
Tables\Filters\SelectFilter::make('direction')
->options([
'export' => 'Export',
'import' => 'Import'
]),
])
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
])
->headerActions([
Action::make('exportCsv')
->label('خروجی CSV')
->icon('heroicon-o-arrow-down-tray')
->color('success')
->action(function () {
return response()->streamDownload(function () {
$csv = fopen('php://output', 'w');
// BOM برای پشتیبانی از فارسی در Excel
fwrite($csv, "\xEF\xBB\xBF");
// Header
fputcsv($csv, [
'AWB No',
'Direction',
'Type',
'Status',
'From Country',
'To Country',
'Weight (kg)',
'Chargeable Weight (kg)',
'Shipping Price (AED)',
'Extra Service (AED)',
'Packing Cost (IRR)',
'Total Fee (IRR)',
'Net Rial (IRR)',
'Sender Name',
'Sender Phone',
'Receiver Name',
'Receiver Phone',
'Created At'
]);
// Data
Shipment::query()
->with(['fromCountry', 'toCountry'])
->orderBy('created_at', 'desc')
->chunk(500, function ($shipments) use ($csv) {
foreach ($shipments as $s) {
fputcsv($csv, [
$s->awb_no,
$s->direction?->value ?? '',
$s->type?->value ?? '',
$s->status?->value ?? '',
$s->fromCountry?->name ?? '',
$s->toCountry?->name ?? '',
$s->weight,
$s->chargeable_weight,
$s->shipping_price,
$s->extra_service,
$s->packing_cost,
$s->total_fee,
$s->net_rial,
$s->sender_name,
$s->sender_phone,
$s->receiver_name,
$s->receiver_phone,
$s->created_at?->format('Y-m-d H:i'),
]);
}
});
fclose($csv);
}, 'shipments_' . now()->format('Y-m-d_H-i') . '.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
RelationManagers\CarrierMappingsRelationManager::class,
RelationManagers\TrackingEventsRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListShipments::route('/'),
'create' => Pages\CreateShipment::route('/create'),
'view' => Pages\ViewShipment::route('/{record}'),
'edit' => Pages\EditShipment::route('/{record}/edit'),
];
}
}