diff --git a/04_Laravel/app/Filament/Pages/ImportRatesPage.php b/04_Laravel/app/Filament/Pages/ImportRatesPage.php new file mode 100644 index 0000000..9389993 --- /dev/null +++ b/04_Laravel/app/Filament/Pages/ImportRatesPage.php @@ -0,0 +1,115 @@ +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'), + ]; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Resources/CurrencyResource.php b/04_Laravel/app/Filament/Resources/CurrencyResource.php new file mode 100644 index 0000000..deef07e --- /dev/null +++ b/04_Laravel/app/Filament/Resources/CurrencyResource.php @@ -0,0 +1,112 @@ +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'), + ]; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Resources/CurrencyResource/Pages/CreateCurrency.php b/04_Laravel/app/Filament/Resources/CurrencyResource/Pages/CreateCurrency.php new file mode 100644 index 0000000..2dfb5c1 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/CurrencyResource/Pages/CreateCurrency.php @@ -0,0 +1,12 @@ +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 [ diff --git a/04_Laravel/app/Models/Currency.php b/04_Laravel/app/Models/Currency.php new file mode 100644 index 0000000..a39c8cf --- /dev/null +++ b/04_Laravel/app/Models/Currency.php @@ -0,0 +1,21 @@ + 'decimal:4', + 'rate_to_aed' => 'decimal:6', + 'is_active' => 'boolean', + ]; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Notifications/ShipmentUpdatedNotification.php b/04_Laravel/app/Notifications/ShipmentUpdatedNotification.php new file mode 100644 index 0000000..488c830 --- /dev/null +++ b/04_Laravel/app/Notifications/ShipmentUpdatedNotification.php @@ -0,0 +1,45 @@ +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()); + } +} \ No newline at end of file diff --git a/04_Laravel/database/migrations/2026_08_24_100001_create_currencies_table.php b/04_Laravel/database/migrations/2026_08_24_100001_create_currencies_table.php new file mode 100644 index 0000000..9ff45e0 --- /dev/null +++ b/04_Laravel/database/migrations/2026_08_24_100001_create_currencies_table.php @@ -0,0 +1,27 @@ +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'); + } +}; \ No newline at end of file diff --git a/04_Laravel/resources/views/filament/pages/import-rates.blade.php b/04_Laravel/resources/views/filament/pages/import-rates.blade.php new file mode 100644 index 0000000..8501bec --- /dev/null +++ b/04_Laravel/resources/views/filament/pages/import-rates.blade.php @@ -0,0 +1,3 @@ + + {{ $this->form }} + \ No newline at end of file