feat(admin): implement currency management and rate import system
Introduce a new currency management module including a dedicated resource, database migration, and a custom Filament page for importing exchange rates. Additionally, enhance the ShipmentResource table with improved column visibility, sorting, and route display, and integrate shipment update notifications.
This commit is contained in:
parent
a973c7ed5c
commit
e9a0c731c2
115
04_Laravel/app/Filament/Pages/ImportRatesPage.php
Normal file
115
04_Laravel/app/Filament/Pages/ImportRatesPage.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Imports\ShippingRatesImport;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class ImportRatesPage extends Page implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-arrow-up-tray';
|
||||
protected static ?string $navigationLabel = 'آپلود نرخها';
|
||||
protected static ?string $navigationGroup = 'تنظیمات';
|
||||
protected static ?int $navigationSort = 10;
|
||||
protected static string $view = 'filament.pages.import-rates';
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Section::make('فایل اکسل نرخها')
|
||||
->description('فایل اکسل حاوی شیتهای Export Rate, Import Rate و DocEco را آپلود کنید.')
|
||||
->schema([
|
||||
FileUpload::make('file')
|
||||
->label('فایل اکسل')
|
||||
->acceptedFileTypes([
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-excel',
|
||||
])
|
||||
->maxSize(5120)
|
||||
->required()
|
||||
->disk('local')
|
||||
->directory('imports')
|
||||
->preserveFilenames(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public function import(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
|
||||
if (empty($data['file'])) {
|
||||
Notification::make()
|
||||
->title('خطا')
|
||||
->body('لطفاً فایل اکسل را انتخاب کنید.')
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$filePath = storage_path('app/' . $data['file']);
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
Notification::make()
|
||||
->title('خطا')
|
||||
->body('فایل پیدا نشد.')
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$import = new ShippingRatesImport();
|
||||
Excel::import($import, $filePath);
|
||||
|
||||
$stats = [];
|
||||
foreach ($import->sheets() as $name => $sheet) {
|
||||
if (method_exists($sheet, 'getStats')) {
|
||||
$s = $sheet->getStats();
|
||||
$stats[] = "{$name}: {$s['imported']} imported, {$s['skipped']} skipped";
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('ایمپورت موفق')
|
||||
->body(implode(' | ', $stats))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$this->form->fill();
|
||||
} catch (\Throwable $e) {
|
||||
Notification::make()
|
||||
->title('خطا در ایمپورت')
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('import')
|
||||
->label('شروع ایمپورت')
|
||||
->icon('heroicon-o-arrow-up-tray')
|
||||
->color('success')
|
||||
->action('import'),
|
||||
];
|
||||
}
|
||||
}
|
||||
112
04_Laravel/app/Filament/Resources/CurrencyResource.php
Normal file
112
04_Laravel/app/Filament/Resources/CurrencyResource.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\CurrencyResource\Pages;
|
||||
use App\Models\Currency;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CurrencyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Currency::class;
|
||||
protected static ?string $navigationIcon = 'heroicon-o-banknotes';
|
||||
protected static ?string $navigationLabel = 'ارزها';
|
||||
protected static ?string $navigationGroup = 'تنظیمات';
|
||||
protected static ?int $navigationSort = 11;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make('اطلاعات ارز')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('code')
|
||||
->required()
|
||||
->maxLength(10)
|
||||
->unique(ignoreRecord: true)
|
||||
->label('کد ارز (مثل AED, USD, EUR)'),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(100)
|
||||
->label('نام ارز'),
|
||||
Forms\Components\TextInput::make('symbol')
|
||||
->maxLength(10)
|
||||
->nullable()
|
||||
->label('نماد (مثل $, €, د.إ)'),
|
||||
])->columns(3),
|
||||
|
||||
Forms\Components\Section::make('نرخ تبدیل')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('rate_to_rial')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(0)
|
||||
->suffix('ریال')
|
||||
->label('نرخ نسبت به ریال'),
|
||||
Forms\Components\TextInput::make('rate_to_aed')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(0)
|
||||
->suffix('درهم')
|
||||
->label('نرخ نسبت به درهم'),
|
||||
Forms\Components\Toggle::make('is_active')
|
||||
->default(true)
|
||||
->label('فعال'),
|
||||
])->columns(3),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('code')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->badge()
|
||||
->label('کد'),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->label('نام'),
|
||||
Tables\Columns\TextColumn::make('symbol')
|
||||
->label('نماد'),
|
||||
Tables\Columns\TextColumn::make('rate_to_rial')
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state) => number_format((float) $state, 0))
|
||||
->label('نرخ به ریال'),
|
||||
Tables\Columns\TextColumn::make('rate_to_aed')
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state) => number_format((float) $state, 4))
|
||||
->label('نرخ به درهم'),
|
||||
Tables\Columns\IconColumn::make('is_active')
|
||||
->boolean()
|
||||
->label('فعال'),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\TernaryFilter::make('is_active')
|
||||
->label('وضعیت'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListCurrencies::route('/'),
|
||||
'create' => Pages\CreateCurrency::route('/create'),
|
||||
'edit' => Pages\EditCurrency::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CurrencyResource\Pages;
|
||||
|
||||
use App\Filament\Resources\CurrencyResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateCurrency extends CreateRecord
|
||||
{
|
||||
protected static string $resource = CurrencyResource::class;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CurrencyResource\Pages;
|
||||
|
||||
use App\Filament\Resources\CurrencyResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCurrency extends EditRecord
|
||||
{
|
||||
protected static string $resource = CurrencyResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CurrencyResource\Pages;
|
||||
|
||||
use App\Filament\Resources\CurrencyResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCurrencies extends ListRecords
|
||||
{
|
||||
protected static string $resource = CurrencyResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use App\Notifications\ShipmentUpdatedNotification;
|
||||
|
||||
class ShipmentResource extends Resource
|
||||
{
|
||||
@ -99,9 +100,6 @@ class ShipmentResource extends Resource
|
||||
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')
|
||||
@ -284,43 +282,63 @@ 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('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'),
|
||||
->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'),
|
||||
->searchable()->label('Status'),
|
||||
Tables\Columns\TextColumn::make('sender_name')
|
||||
->searchable()
|
||||
->toggleable()
|
||||
->label('Sender'),
|
||||
->searchable()->toggleable()->label('Sender'),
|
||||
Tables\Columns\TextColumn::make('receiver_name')
|
||||
->searchable()->toggleable()->label('Receiver'),
|
||||
Tables\Columns\TextColumn::make('fromCountry.name')
|
||||
->label('From')
|
||||
->searchable()
|
||||
->toggleable()
|
||||
->label('Receiver'),
|
||||
Tables\Columns\TextColumn::make('total_fee')
|
||||
->money('IRR')
|
||||
->sortable()
|
||||
->label('Total Fee'),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('toCountry.name')
|
||||
->label('To')
|
||||
->searchable()
|
||||
->toggleable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('route')
|
||||
->label('Route')
|
||||
->state(fn ($record) => (
|
||||
($record->fromCountry?->name ?? '?') .
|
||||
' → ' .
|
||||
($record->toCountry?->name ?? '?')
|
||||
))
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
Tables\Columns\TextColumn::make('sender_phone')
|
||||
->label('Sender Phone')
|
||||
->searchable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
Tables\Columns\TextColumn::make('receiver_phone')
|
||||
->label('Receiver Phone')
|
||||
->searchable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
Tables\Columns\TextColumn::make('cod_amount')
|
||||
->label('COD (AED)')
|
||||
->money('AED')
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
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')
|
||||
@ -387,7 +405,7 @@ class ShipmentResource extends Resource
|
||||
$s->awb_no,
|
||||
$s->direction?->value ?? '',
|
||||
$s->type?->value ?? '',
|
||||
$s->status?->value ?? '',
|
||||
$s->status?->label() ?? '',
|
||||
$s->fromCountry?->name ?? '',
|
||||
$s->toCountry?->name ?? '',
|
||||
$s->weight,
|
||||
@ -418,7 +436,13 @@ class ShipmentResource extends Resource
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function afterSave(\Filament\Resources\Pages\Page $page, \Illuminate\Database\Eloquent\Model $record): void
|
||||
{
|
||||
if ($page instanceof \Filament\Resources\Pages\EditRecord) {
|
||||
ShipmentUpdatedNotification::notify($record);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
21
04_Laravel/app/Models/Currency.php
Normal file
21
04_Laravel/app/Models/Currency.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Currency extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'code', 'name', 'symbol', 'rate_to_rial', 'rate_to_aed', 'is_active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'rate_to_rial' => 'decimal:4',
|
||||
'rate_to_aed' => 'decimal:6',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
45
04_Laravel/app/Notifications/ShipmentUpdatedNotification.php
Normal file
45
04_Laravel/app/Notifications/ShipmentUpdatedNotification.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Shipment;
|
||||
use Filament\Notifications\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class ShipmentUpdatedNotification
|
||||
{
|
||||
public static function notify(Shipment $shipment): void
|
||||
{
|
||||
$changes = $shipment->getChanges();
|
||||
$ignored = ['updated_at'];
|
||||
|
||||
$changedFields = array_filter(
|
||||
array_keys($changes),
|
||||
fn ($key) => !in_array($key, $ignored)
|
||||
);
|
||||
|
||||
if (empty($changedFields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fieldList = implode('، ', $changedFields);
|
||||
$label = count($changedFields) === 1 ? $changedFields[0] : count($changedFields) . ' فیلد';
|
||||
|
||||
Notification::make()
|
||||
->title('محموله ویرایش شد')
|
||||
->body("AWB {$shipment->awb_no} — {$label} تغییر کرد: {$fieldList}")
|
||||
->info()
|
||||
->icon('heroicon-o-pencil-square')
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->label('مشاهده')
|
||||
->url(
|
||||
\Filament\Facades\Filament::getPanel()->getResourceUrl(Shipment::class) . '/' . $shipment->id,
|
||||
shouldOpenInNewTab: false
|
||||
),
|
||||
])
|
||||
->sendToDatabase(\App\Models\User::all());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
<?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('currencies', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code', 10)->unique();
|
||||
$table->string('name', 100);
|
||||
$table->string('symbol', 10)->nullable();
|
||||
$table->decimal('rate_to_rial', 20, 4)->default(0);
|
||||
$table->decimal('rate_to_aed', 20, 6)->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('currencies');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
<x-filament-panels::page>
|
||||
{{ $this->form }}
|
||||
</x-filament-panels::page>
|
||||
Loading…
Reference in New Issue
Block a user