diff --git a/04_Laravel/app/Exports/ShippingRatesTemplateExport.php b/04_Laravel/app/Exports/ShippingRatesTemplateExport.php new file mode 100644 index 0000000..7517be9 --- /dev/null +++ b/04_Laravel/app/Exports/ShippingRatesTemplateExport.php @@ -0,0 +1,78 @@ +title; + } + + public function array(): array + { + $rows = [ + ['RATE TABLE — ' . strtoupper($this->title)], + ['IFNEX Logistics'], + ['Weight (kg)', 'Zone 1', 'Zone 2', 'Zone 3', 'Zone 4', 'Zone 5', 'Zone 6', 'Zone 7', 'Zone 8', 'Zone 9', 'Zone 10'], + [''], + ]; + + $weights = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5, 10, + 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15, 15.5, 16, 16.5, 17, 17.5, 18, 18.5, 19, 19.5, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]; + + foreach ($weights as $w) { + $rows[] = array_merge([$w], array_fill(0, 10, '')); + } + + return $rows; + } +} + +class DocEcoSheet implements FromArray, WithTitle +{ + public function title(): string + { + return 'DocEco'; + } + + public function array(): array + { + $rows = [ + ['Weight (kg)', 'Zone', 'Rate (AED)'], + ]; + + $weights = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 50]; + + foreach ($weights as $w) { + $rows[] = [$w, 1, '']; + $rows[] = [$w, 2, '']; + $rows[] = [$w, 3, '']; + } + + return $rows; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Pages/ImportRatesPage.php b/04_Laravel/app/Filament/Pages/ImportRatesPage.php index 9389993..53311c9 100644 --- a/04_Laravel/app/Filament/Pages/ImportRatesPage.php +++ b/04_Laravel/app/Filament/Pages/ImportRatesPage.php @@ -2,12 +2,12 @@ namespace App\Filament\Pages; +use App\Exports\ShippingRatesTemplateExport; 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\Forms\Form; use Filament\Notifications\Notification; use Filament\Pages\Page; use Maatwebsite\Excel\Facades\Excel; @@ -17,37 +17,38 @@ 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 $navigationLabel = 'ایمپورت نرخ‌ها'; + protected static ?int $navigationSort = 100; protected static string $view = 'filament.pages.import-rates'; - public ?array $data = []; + public ?array $formData = []; public function mount(): void { $this->form->fill(); } - public function getFormSchema(): array + public function form(Form $form): Form { - 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(), - ]), - ]; + return $form + ->schema([ + FileUpload::make('file') + ->label('فایل اکسل نرخ‌ها') + ->acceptedFileTypes([ + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel', + ]) + ->maxSize(5120) + ->required() + ->disk('local') + ->directory('imports'), + ]) + ->statePath('formData'); + } + + public function downloadTemplate(): void + { + (new ShippingRatesTemplateExport)->download('IFNEX_Rate_Template.xlsx'); } public function import(): void @@ -75,25 +76,16 @@ class ImportRatesPage extends Page implements HasForms } 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"; - } - } + Excel::import(new ShippingRatesImport, $filePath); Notification::make() - ->title('ایمپورت موفق') - ->body(implode(' | ', $stats)) + ->title('موفق') + ->body('نرخ‌ها با موفقیت ایمپورت شدند.') ->success() ->send(); $this->form->fill(); - } catch (\Throwable $e) { + } catch (\Exception $e) { Notification::make() ->title('خطا در ایمپورت') ->body($e->getMessage()) @@ -101,15 +93,4 @@ class ImportRatesPage extends Page implements HasForms ->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/ShipmentResource.php b/04_Laravel/app/Filament/Resources/ShipmentResource.php index e12109d..296fb88 100644 --- a/04_Laravel/app/Filament/Resources/ShipmentResource.php +++ b/04_Laravel/app/Filament/Resources/ShipmentResource.php @@ -479,6 +479,42 @@ class ShipmentResource extends Resource ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), + + Action::make('changeStatus') + ->label('تغییر وضعیت') + ->icon('heroicon-o-arrow-path') + ->color('warning') + ->form([ + Forms\Components\Select::make('new_status') + ->label('وضعیت جدید') + ->required() + ->options(collect(ShipmentStatus::cases())->mapWithKeys(fn ($case) => [ + $case->value => $case->label(), + ])), + Forms\Components\Textarea::make('reason') + ->label('دلیل تغییر') + ->rows(2) + ->maxLength(500), + ]) + ->action(function (array $data, \Illuminate\Support\Collection $records) { + foreach ($records as $shipment) { + \App\Models\ShipmentStatusHistory::create([ + 'shipment_id' => $shipment->id, + 'from_status' => $shipment->status?->value, + 'to_status' => $data['new_status'], + 'reason' => $data['reason'] ?? null, + 'changed_by' => auth()->id(), + ]); + + $shipment->update(['status' => $data['new_status']]); + } + + \Filament\Notifications\Notification::make() + ->title(count($records) . ' محموله بروزرسانی شد') + ->success() + ->send(); + }) + ->deselectAfterCompletion(), ]), ]); } diff --git a/04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/TrackingEventsRelationManager.php b/04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/TrackingEventsRelationManager.php index 7c04171..0215966 100644 --- a/04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/TrackingEventsRelationManager.php +++ b/04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/TrackingEventsRelationManager.php @@ -28,6 +28,14 @@ class TrackingEventsRelationManager extends RelationManager ->default(now()) ->label('Event Time'), Forms\Components\TextInput::make('location')->nullable()->label('Location'), + Forms\Components\Select::make('source') + ->options([ + 'manual' => 'دستی', + 'api' => 'API', + 'system' => 'سیستم', + ]) + ->default('manual') + ->label('منبع'), Forms\Components\TextInput::make('event_description')->required()->label('Event Description'), Forms\Components\Select::make('delivery_status') ->options([ diff --git a/04_Laravel/app/Models/Shipment.php b/04_Laravel/app/Models/Shipment.php index 44c6a81..d11e6b6 100644 --- a/04_Laravel/app/Models/Shipment.php +++ b/04_Laravel/app/Models/Shipment.php @@ -76,6 +76,11 @@ class Shipment extends Model return $this->hasMany(ShipmentTrackingEvent::class); } + public function statusHistories(): HasMany + { + return $this->hasMany(ShipmentStatusHistory::class)->orderByDesc('created_at'); + } + public function isParcel(): bool { return $this->type === \App\Enums\ShipmentType::Parcel; diff --git a/04_Laravel/app/Models/ShipmentStatusHistory.php b/04_Laravel/app/Models/ShipmentStatusHistory.php new file mode 100644 index 0000000..94d0ca0 --- /dev/null +++ b/04_Laravel/app/Models/ShipmentStatusHistory.php @@ -0,0 +1,32 @@ +changed_by) && auth()->check()) { + $model->changed_by = auth()->id(); + } + }); + } + + public function shipment(): BelongsTo + { + return $this->belongsTo(Shipment::class); + } + + public function changedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'changed_by'); + } +} \ No newline at end of file diff --git a/04_Laravel/database/migrations/2026_08_24_300001_create_shipment_status_histories_table.php b/04_Laravel/database/migrations/2026_08_24_300001_create_shipment_status_histories_table.php new file mode 100644 index 0000000..6042c11 --- /dev/null +++ b/04_Laravel/database/migrations/2026_08_24_300001_create_shipment_status_histories_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('shipment_id')->constrained()->cascadeOnDelete(); + $table->string('from_status', 30)->nullable(); + $table->string('to_status', 30); + $table->text('reason')->nullable(); + $table->foreignId('changed_by')->constrained('users')->cascadeOnDelete(); + $table->timestamps(); + + $table->index('shipment_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('shipment_status_histories'); + } +}; \ 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 index 8501bec..02510a4 100644 --- a/04_Laravel/resources/views/filament/pages/import-rates.blade.php +++ b/04_Laravel/resources/views/filament/pages/import-rates.blade.php @@ -1,3 +1,53 @@ + {{ $this->form }} + +
+ + + + + دانلود template + + + +
+
\ No newline at end of file diff --git a/04_Laravel/routes/web.php b/04_Laravel/routes/web.php index 9e58f8c..a1629d7 100644 --- a/04_Laravel/routes/web.php +++ b/04_Laravel/routes/web.php @@ -39,4 +39,7 @@ Route::get('/payment-result', function (Illuminate\Http\Request $request) { 'amount' => $amount, 'message' => $message, ]); -})->name('payment.result'); \ No newline at end of file +})->name('payment.result'); +Route::get('/download-rate-template', function () { + return (new \App\Exports\ShippingRatesTemplateExport)->download('IFNEX_Rate_Template.xlsx'); +})->middleware(['auth', 'panel:admin'])->name('download.rate-template'); \ No newline at end of file