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) {
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),
})
->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,
];
}

View File

@ -12,55 +12,65 @@ 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([
return $form
->schema([
Forms\Components\Section::make('اطلاعات تراکنش')
->schema([
Forms\Components\Select::make('wallet_id')
->relationship('wallet', 'id')
->required()
->searchable()
->preload()
->required()
->label('کیف پول'),
Forms\Components\TextInput::make('amount')
->numeric()
->suffix('ریال')
->required()
->label('مبلغ'),
Forms\Components\Select::make('type')
->options(TransactionType::class)
->options(collect(TransactionType::cases())->mapWithKeys(
fn($case) => [$case->value => $case->label()]
))
->required()
->label('نوع'),
->label('نوع تراکنش'),
Forms\Components\TextInput::make('amount')
->required()
->numeric()
->prefix('ریال')
->label('مبلغ'),
Forms\Components\Select::make('status')
->options(TransactionStatus::class)
->options(collect(TransactionStatus::cases())->mapWithKeys(
fn($case) => [$case->value => $case->label()]
))
->default(TransactionStatus::PENDING->value)
->required()
->label('وضعیت'),
Forms\Components\Select::make('gateway')
->options(PaymentGateway::class)
->label('درگاه'),
->options(collect(PaymentGateway::cases())->mapWithKeys(
fn($case) => [$case->value => $case->label()]
))
->label('درگاه پرداخت'),
Forms\Components\TextInput::make('gateway_reference_id')
->label('کد پیگیری'),
->label('کد پیگیری درگاه'),
Forms\Components\Textarea::make('description')
->label('توضیحات')
->maxLength(1000),
->columnSpanFull(),
Forms\Components\Textarea::make('admin_notes')
->label('یادداشت ادمین')
->maxLength(2000),
])
->columns(2),
->columnSpanFull(),
])->columns(2),
]);
}
@ -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')
Action::make('exportCsv')
->label('خروجی CSV')
->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(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(),
];
}
}