feat(admin): enhance shipment and wallet transaction resources

- Refactor ShipmentResource to improve form schema formatting and structure
- Update WalletTransactionResource with improved navigation icons and Persian labels
- Implement dynamic transaction type options using enum cases
- Add dedicated Create and Edit pages for WalletTransactionResource to support full CRUD operations
This commit is contained in:
Kazem Alghasi 2026-08-10 00:25:15 +03:30
parent 810004aab1
commit bc52e94dc3
4 changed files with 425 additions and 155 deletions

View File

@ -3,14 +3,14 @@
namespace App\Filament\Resources;
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 App\Exports\ShipmentsExport;
use Filament\Tables\Actions\ExportAction;
use Filament\Tables\Actions\Action;
class ShipmentResource extends Resource
{
@ -33,7 +33,10 @@ class ShipmentResource extends Resource
->unique(ignoreRecord: true)
->label('AWB No.'),
Forms\Components\Select::make('direction')
->options(['export' => 'Export', 'import' => 'Import'])
->options([
'export' => 'Export',
'import' => 'Import'
])
->required(),
Forms\Components\Select::make('type')
->options([
@ -54,8 +57,12 @@ class ShipmentResource extends Resource
])
->default('processed')
->required(),
Forms\Components\TextInput::make('reason_for_export')->nullable()->label('Reason for Export'),
Forms\Components\TextInput::make('forwarder')->nullable()->label('Forwarder'),
Forms\Components\TextInput::make('reason_for_export')
->nullable()
->label('Reason for Export'),
Forms\Components\TextInput::make('forwarder')
->nullable()
->label('Forwarder'),
Forms\Components\Select::make('from_country_id')
->relationship('fromCountry', 'name')
->searchable()
@ -70,49 +77,143 @@ class ShipmentResource extends Resource
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('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)'),
])->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('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'),
])->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_zip')->nullable()->label('ZIP'),
Forms\Components\TextInput::make('sender_id_number')->nullable()->label('ID Number'),
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_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_zip')->nullable()->label('ZIP'),
Forms\Components\TextInput::make('receiver_id_number')->nullable()->label('ID Number'),
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_zip')
->nullable()
->label('ZIP'),
Forms\Components\TextInput::make('receiver_id_number')
->nullable()
->label('ID Number'),
])->columns(4),
Forms\Components\Section::make('Customs Items')
@ -161,18 +262,41 @@ class ShipmentResource extends Resource
{
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()->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),
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()
->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')
@ -192,23 +316,83 @@ class ShipmentResource extends Resource
'PARCEL' => 'Parcel',
]),
Tables\Filters\SelectFilter::make('direction')
->options(['export' => 'Export', 'import' => 'Import']),
->options([
'export' => 'Export',
'import' => 'Import'
]),
])
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
])
->headerActions([
ExportAction::make()
->label('خروجی Excel')
Action::make('exportCsv')
->label('خروجی CSV')
->icon('heroicon-o-arrow-down-tray')
->color('success')
->export(fn ($query) => new ShipmentsExport([
'status' => request()->input('filters.status'),
'type' => request()->input('filters.type'),
'direction' => request()->input('filters.direction'),
]))
->fileName('shipments_' . now()->format('Y-m-d_H-i')),
->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([
@ -220,8 +404,8 @@ class ShipmentResource extends Resource
public static function getRelations(): array
{
return [
ShipmentResource\RelationManagers\CarrierMappingsRelationManager::class,
ShipmentResource\RelationManagers\TrackingEventsRelationManager::class,
RelationManagers\CarrierMappingsRelationManager::class,
RelationManagers\TrackingEventsRelationManager::class,
];
}
@ -234,4 +418,4 @@ class ShipmentResource extends Resource
'edit' => Pages\EditShipment::route('/{record}/edit'),
];
}
}
}

View File

@ -12,56 +12,66 @@ use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use App\Exports\WalletTransactionsExport;
use Filament\Tables\Actions\ExportAction;
use Maatwebsite\Excel\Facades\Excel;
use Filament\Tables\Actions\Action;
class WalletTransactionResource extends Resource
{
protected static ?string $model = WalletTransaction::class;
protected static ?string $navigationIcon = 'heroicon-o-arrows-right-left';
protected static ?string $navigationLabel = 'تراکنش‌ها';
protected static ?string $navigationIcon = 'heroicon-o-banknotes';
protected static ?string $navigationLabel = 'تراکنش‌های کیف پول';
protected static ?string $navigationGroup = 'مالی';
protected static ?int $navigationSort = 2;
protected static ?string $modelLabel = 'تراکنش';
protected static ?string $pluralModelLabel = 'تراکنش‌ها';
public static function form(Form $form): Form
{
return $form->schema([
Forms\Components\Section::make('اطلاعات تراکنش')
->schema([
Forms\Components\Select::make('wallet_id')
->relationship('wallet', 'id')
->searchable()
->preload()
->required()
->label('کیف پول'),
Forms\Components\TextInput::make('amount')
->numeric()
->suffix('ریال')
->required()
->label('مبلغ'),
Forms\Components\Select::make('type')
->options(TransactionType::class)
->required()
->label('نوع'),
Forms\Components\Select::make('status')
->options(TransactionStatus::class)
->required()
->label('وضعیت'),
Forms\Components\Select::make('gateway')
->options(PaymentGateway::class)
->label('درگاه'),
Forms\Components\TextInput::make('gateway_reference_id')
->label('کد پیگیری'),
Forms\Components\Textarea::make('description')
->label('توضیحات')
->maxLength(1000),
Forms\Components\Textarea::make('admin_notes')
->label('یادداشت ادمین')
->maxLength(2000),
])
->columns(2),
]);
return $form
->schema([
Forms\Components\Section::make('اطلاعات تراکنش')
->schema([
Forms\Components\Select::make('wallet_id')
->relationship('wallet', 'id')
->required()
->searchable()
->preload()
->label('کیف پول'),
Forms\Components\Select::make('type')
->options(collect(TransactionType::cases())->mapWithKeys(
fn($case) => [$case->value => $case->label()]
))
->required()
->label('نوع تراکنش'),
Forms\Components\TextInput::make('amount')
->required()
->numeric()
->prefix('ریال')
->label('مبلغ'),
Forms\Components\Select::make('status')
->options(collect(TransactionStatus::cases())->mapWithKeys(
fn($case) => [$case->value => $case->label()]
))
->default(TransactionStatus::PENDING->value)
->required()
->label('وضعیت'),
Forms\Components\Select::make('gateway')
->options(collect(PaymentGateway::cases())->mapWithKeys(
fn($case) => [$case->value => $case->label()]
))
->label('درگاه پرداخت'),
Forms\Components\TextInput::make('gateway_reference_id')
->label('کد پیگیری درگاه'),
Forms\Components\Textarea::make('description')
->label('توضیحات')
->columnSpanFull(),
Forms\Components\Textarea::make('admin_notes')
->label('یادداشت ادمین')
->columnSpanFull(),
])->columns(2),
]);
}
public static function table(Table $table): Table
@ -70,96 +80,141 @@ class WalletTransactionResource extends Resource
->columns([
Tables\Columns\TextColumn::make('id')
->label('شناسه')
->sortable(),
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('wallet.user.name')
->label('کاربر')
->searchable(),
Tables\Columns\TextColumn::make('amount')
->label('مبلغ')
->money('IRR', divideBy: 1)
->color(fn ($record) => $record->amount >= 0 ? 'success' : 'danger')
->searchable()
->sortable(),
Tables\Columns\BadgeColumn::make('type')
->label('نوع')
->getStateUsing(fn ($record) => $record->type->label())
->colors([
'success' => TransactionType::DEPOSIT,
'danger' => TransactionType::WITHDRAWAL,
'warning' => TransactionType::ORDER_PAYMENT,
'info' => TransactionType::REFUND,
]),
Tables\Columns\BadgeColumn::make('status')
->label('وضعیت')
->getStateUsing(fn ($record) => $record->status->label())
->colors([
'success' => TransactionStatus::COMPLETED,
'warning' => TransactionStatus::PENDING,
'danger' => TransactionStatus::FAILED,
'info' => TransactionStatus::REFUNDED,
]),
Tables\Columns\BadgeColumn::make('gateway')
->label('درگاه')
->getStateUsing(fn ($record) => $record->gateway?->label() ?? '-')
->colors([
'primary' => PaymentGateway::ZARINPAL,
'success' => PaymentGateway::MANUAL,
'gray' => PaymentGateway::SYSTEM,
]),
Tables\Columns\TextColumn::make('type')
->badge()
->color(fn (TransactionType $state): string => $state->color())
->formatStateUsing(fn (TransactionType $state): string => $state->label())
->label('نوع'),
Tables\Columns\TextColumn::make('amount')
->money('IRR')
->sortable()
->label('مبلغ'),
Tables\Columns\TextColumn::make('status')
->badge()
->color(fn (TransactionStatus $state): string => $state->color())
->formatStateUsing(fn (TransactionStatus $state): string => $state->label())
->label('وضعیت'),
Tables\Columns\TextColumn::make('gateway')
->badge()
->color('info')
->label('درگاه'),
Tables\Columns\TextColumn::make('gateway_reference_id')
->label('کد پیگیری')
->searchable()
->toggleable(),
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('description')
->label('توضیحات')
->limit(50)
->toggleable(),
Tables\Columns\TextColumn::make('creator.name')
->label('ایجادکننده')
->toggleable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime('Y/m/d H:i', 'Asia/Tehran')
->label('تاریخ')
->sortable(),
->dateTime('Y/m/d H:i')
->sortable()
->label('تاریخ'),
])
->defaultSort('created_at', 'desc')
->filters([
Tables\Filters\SelectFilter::make('type')
->options(TransactionType::class)
->options(collect(TransactionType::cases())->mapWithKeys(
fn($case) => [$case->value => $case->label()]
))
->label('نوع تراکنش'),
Tables\Filters\SelectFilter::make('status')
->options(TransactionStatus::class)
->options(collect(TransactionStatus::cases())->mapWithKeys(
fn($case) => [$case->value => $case->label()]
))
->label('وضعیت'),
Tables\Filters\SelectFilter::make('gateway')
->options(PaymentGateway::class)
->options(collect(PaymentGateway::cases())->mapWithKeys(
fn($case) => [$case->value => $case->label()]
))
->label('درگاه'),
Tables\Filters\Filter::make('created_at')
->form([
Forms\Components\DatePicker::make('created_from')->label('از تاریخ'),
Forms\Components\DatePicker::make('created_until')->label('تا تاریخ'),
])
->query(function ($query, array $data) {
return $query
->when($data['created_from'], fn($q, $date) => $q->whereDate('created_at', '>=', $date))
->when($data['created_until'], fn($q, $date) => $q->whereDate('created_at', '<=', $date));
}),
])
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
])
->headerActions([
ExportAction::make()
->label('خروجی Excel')
->icon('heroicon-o-arrow-down-tray')
->color('success')
->export(fn ($query) => new WalletTransactionsExport([
'type' => request()->input('filters.type'),
'status' => request()->input('filters.status'),
'gateway' => request()->input('filters.gateway'),
]))
->fileName('transactions_' . now()->format('Y-m-d_H-i')),
Action::make('exportCsv')
->label('خروجی CSV')
->icon('heroicon-o-arrow-down-tray')
->color('success')
->action(function () {
return response()->streamDownload(function () {
$csv = fopen('php://output', 'w');
fwrite($csv, "\xEF\xBB\xBF");
fputcsv($csv, [
'شناسه', 'کاربر', 'ایمیل', 'نوع تراکنش', 'مبلغ (ریال)',
'وضعیت', 'درگاه', 'کد پیگیری', 'توضیحات', 'یادداشت ادمین', 'تاریخ'
]);
WalletTransaction::query()
->with(['wallet.user'])
->orderBy('created_at', 'desc')
->chunk(500, function ($transactions) use ($csv) {
foreach ($transactions as $t) {
fputcsv($csv, [
$t->id,
$t->wallet?->user?->name ?? '',
$t->wallet?->user?->email ?? '',
$t->type?->label() ?? '',
$t->amount,
$t->status?->label() ?? '',
$t->gateway?->label() ?? '',
$t->gateway_reference_id ?? '',
$t->description ?? '',
$t->admin_notes ?? '',
$t->created_at?->format('Y-m-d H:i:s'),
]);
}
});
fclose($csv);
}, 'wallet-transactions_' . now()->format('Y-m-d_H-i') . '.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListWalletTransactions::route('/'),
'create' => Pages\CreateWalletTransaction::route('/create'),
'view' => Pages\ViewWalletTransaction::route('/{record}'),
'edit' => Pages\EditWalletTransaction::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\WalletTransactionResource\Pages;
use App\Filament\Resources\WalletTransactionResource;
use Filament\Resources\Pages\CreateRecord;
class CreateWalletTransaction extends CreateRecord
{
protected static string $resource = WalletTransactionResource::class;
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\WalletTransactionResource\Pages;
use App\Filament\Resources\WalletTransactionResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditWalletTransaction extends EditRecord
{
protected static string $resource = WalletTransactionResource::class;
protected function getHeaderActions(): array
{
return [
Actions\ViewAction::make(),
Actions\DeleteAction::make(),
];
}
}