diff --git a/04_Laravel/app/Filament/Resources/ShipmentChecklistResource.php b/04_Laravel/app/Filament/Resources/ShipmentChecklistResource.php index b738a0d..2abdd1f 100644 --- a/04_Laravel/app/Filament/Resources/ShipmentChecklistResource.php +++ b/04_Laravel/app/Filament/Resources/ShipmentChecklistResource.php @@ -4,58 +4,58 @@ namespace App\Filament\Resources; use App\Filament\Resources\ShipmentChecklistResource\Pages; use App\Models\ShipmentChecklist; -use App\Enums\ShipmentStatus; use Filament\Forms; use Filament\Forms\Form; +use Filament\Notifications\Notification; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; +use Filament\Tables\Actions\Action; +/** + * چک‌لیست سفارشات — نمای گزارش (reporting). + * instance‌ها هنگام approve سفارش از قالب‌ها خودکار ساخته می‌شوند. + */ class ShipmentChecklistResource extends Resource { protected static ?string $model = ShipmentChecklist::class; protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-check'; protected static ?string $navigationGroup = 'عملیات'; protected static ?string $navigationLabel = 'چک‌لیست سفارشات'; - protected static ?string $modelLabel = 'چک‌لیست سفارش'; + protected static ?string $modelLabel = 'آیتم چک‌لیست'; protected static ?string $pluralModelLabel = 'چک‌لیست سفارشات'; protected static ?int $navigationSort = 2; public static function form(Form $form): Form { return $form->schema([ - Forms\Components\Section::make('اطلاعات سفارش') - ->schema([ - Forms\Components\Select::make('shipment_id') - ->label('سفارش') - ->relationship('shipment', 'awb_no') - ->searchable() - ->required() - ->getOptionLabelFromRecordUsing(fn ($record) => "{$record->awb_no} - {$record->status->label()}"), - Forms\Components\Select::make('user_id') - ->label('کارمند') - ->relationship('user', 'name') - ->searchable() - ->required(), - ])->columns(2), + Forms\Components\Section::make('اطلاعات آیتم')->schema([ + Forms\Components\TextInput::make('item_title') + ->label('عنوان آیتم') + ->disabled(), - Forms\Components\Section::make('اطلاعات چک‌لیست') - ->schema([ - Forms\Components\Select::make('status') - ->label('وضعیت فعلی') - ->options(collect(ShipmentStatus::cases())->mapWithKeys(fn ($status) => [$status->value => $status->label()])->toArray()) - ->required(), - Forms\Components\Toggle::make('is_completed') - ->label('تکمیل شده') - ->default(false), - Forms\Components\Textarea::make('notes') - ->label('یادداشت‌ها') - ->rows(3), - Forms\Components\DateTimePicker::make('completed_at') - ->label('زمان تکمیل') - ->disabled(), - ]), + Forms\Components\Select::make('shipment_id') + ->label('سفارش') + ->relationship('shipment', 'awb_no') + ->disabled(), + + Forms\Components\Toggle::make('is_required') + ->label('الزامی') + ->disabled(), + + Forms\Components\Toggle::make('is_completed') + ->label('تکمیل شده') + ->disabled(), + + Forms\Components\Textarea::make('notes') + ->label('یادداشت‌ها') + ->rows(3) + ->columnSpanFull(), + + Forms\Components\TextInput::make('attachment') + ->label('پیوست (مسیر فایل)') + ->maxLength(255), + ])->columns(2), ]); } @@ -63,116 +63,85 @@ class ShipmentChecklistResource extends Resource { return $table ->columns([ - Tables\Columns\TextColumn::make('id') - ->label('شناسه') - ->sortable(), Tables\Columns\TextColumn::make('shipment.awb_no') ->label('شماره سفارش') ->searchable() ->sortable(), - Tables\Columns\TextColumn::make('user.name') - ->label('کارمند') + + Tables\Columns\TextColumn::make('item_title') + ->label('آیتم') ->searchable() - ->sortable(), - Tables\Columns\TextColumn::make('status') - ->label('وضعیت') - ->formatStateUsing(fn ($state) => match($state) { - 'pending_approval' => 'در انتظار تأیید', - 'approved' => 'تأیید شده', - 'pending_payment' => 'در انتظار پرداخت', - 'paid' => 'پرداخت شده', - 'processed' => 'در حال پردازش', - 'picked_up' => 'جمع‌آوری شده', - 'in_transit' => 'در حال ارسال', - 'out_for_delivery' => 'آماده تحویل', - 'delivered' => 'تحویل شده', - 'cancelled' => 'لغو شده', - default => $state, - }) - ->badge() - ->color(fn ($state) => match($state) { - 'pending_approval' => 'warning', - 'approved' => 'info', - 'pending_payment' => 'warning', - 'paid' => 'success', - 'processed' => 'info', - 'picked_up' => 'info', - 'in_transit' => 'primary', - 'out_for_delivery' => 'success', - 'delivered' => 'success', - 'cancelled' => 'danger', - default => 'gray', - }), + ->weight('bold'), + + Tables\Columns\IconColumn::make('is_required') + ->label('الزامی') + ->boolean() + ->trueIcon('heroicon-o-exclamation-circle') + ->falseIcon('heroicon-o-minus-circle') + ->trueColor('danger') + ->falseColor('gray'), + Tables\Columns\IconColumn::make('is_completed') ->label('تکمیل') - ->boolean(), + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-x-circle') + ->trueColor('success') + ->falseColor('danger'), + + Tables\Columns\TextColumn::make('completedBy.name') + ->label('انجام‌دهنده') + ->toggleable(), + + Tables\Columns\TextColumn::make('completed_at') + ->label('تاریخ تکمیل') + ->dateTime('Y/m/d H:i') + ->sortable() + ->toggleable(), + Tables\Columns\TextColumn::make('notes') ->label('یادداشت') - ->limit(50) - ->wrap(), - Tables\Columns\TextColumn::make('created_at') - ->label('تاریخ ایجاد') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - Tables\Columns\TextColumn::make('completed_at') - ->label('زمان تکمیل') - ->dateTime() - ->sortable(), + ->limit(40) + ->toggleable(), ]) + ->defaultSort('is_completed', 'asc') // ناقص‌ها اول ->filters([ - Tables\Filters\SelectFilter::make('is_completed') - ->label('وضعیت تکمیل') - ->options([ - 'completed' => 'تکمیل شده', - 'pending' => 'در انتظار', - ]) - ->query(function ($query, $filter) { - $state = is_array($filter->getState()) ? ($filter->getState()['value'] ?? null) : $filter->getState(); - match ($state) { - 'completed' => $query->where('is_completed', true), - 'pending' => $query->where('is_completed', false), - default => null, - }; - }), - Tables\Filters\SelectFilter::make('status') - ->label('وضعیت سفارش') - ->options(collect(ShipmentStatus::cases())->mapWithKeys(fn ($status) => [$status->value => $status->label()])->toArray()), - Tables\Filters\SelectFilter::make('user_id') - ->label('کارمند') - ->relationship('user', 'name') - ->searchable(), + Tables\Filters\TernaryFilter::make('is_completed') + ->label('تکمیل') + ->placeholder('همه') + ->trueLabel('تکمیل‌شده') + ->falseLabel('ناقص'), + + Tables\Filters\TernaryFilter::make('is_required') + ->label('الزامی') + ->placeholder('همه') + ->trueLabel('الزامی') + ->falseLabel('اختیاری'), ]) ->actions([ - Tables\Actions\EditAction::make(), - Tables\Actions\Action::make('complete') + Action::make('complete') ->label('تکمیل') ->icon('heroicon-o-check-circle') ->color('success') - ->requiresConfirmation() - ->modalHeading('تأیید تکمیل') - ->modalDescription('آیا از تکمیل این چک‌لیست اطمینان دارید؟') - ->action(fn ($record) => $record->complete()) - ->visible(fn ($record) => !$record->is_completed), - Tables\Actions\Action::make('add_note') - ->label('افزودن یادداشت') - ->icon('heroicon-o-chat-bubble-bottom-center-text') - ->color('info') + ->visible(fn ($record) => !$record->is_completed) ->form([ Forms\Components\Textarea::make('note') - ->label('یادداشت جدید') - ->required(), + ->label('یادداشت (اختیاری)') + ->rows(2), ]) - ->action(function ($record, array $data): void { - $existingNotes = $record->notes ? $record->notes . "\n\n" : ''; - $record->update([ - 'notes' => $existingNotes . "[" . now()->format('Y-m-d H:i') . "] " . $data['note'], - ]); + ->action(function ($record, array $data) { + $record->complete(auth()->id(), $data['note'] ?? null); + Notification::make()->title('آیتم تکمیل شد')->success()->send(); }), + + Tables\Actions\ViewAction::make(), + Tables\Actions\EditAction::make() + ->visible(fn () => auth()->user()?->hasRole('super_admin')), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->visible(fn () => auth()->user()?->hasRole('super_admin')), ]), ]); } @@ -186,13 +155,43 @@ class ShipmentChecklistResource extends Resource { return [ 'index' => Pages\ListShipmentChecklists::route('/'), - 'create' => Pages\CreateShipmentChecklist::route('/create'), + 'view' => Pages\ViewShipmentChecklist::route('/{record}'), 'edit' => Pages\EditShipmentChecklist::route('/{record}/edit'), ]; } + public static function canViewAny(): bool + { + return auth()->user()?->hasAnyRole(['super_admin', 'admin', 'staff']) ?? false; + } + + public static function canCreate(): bool + { + // instance‌ها خودکار از قالب ساخته می‌شوند + return false; + } + + public static function canEdit($record): bool + { + return auth()->user()?->hasRole('super_admin') ?? false; + } + + public static function canDelete($record): bool + { + return auth()->user()?->hasRole('super_admin') ?? false; + } + + /** + * badge: تعداد آیتم‌های الزامیِ ناقص (نشانگر سفارش‌های مشکل‌دار). + */ public static function getNavigationBadge(): ?string { - return static::getModel()::where('is_completed', false)->count(); + $count = static::getModel()::where('is_completed', false)->where('is_required', true)->count(); + return $count > 0 ? (string) $count : null; } -} + + public static function getNavigationBadgeColor(): ?string + { + return 'danger'; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Resources/ShipmentChecklistResource/Pages/ViewShipmentChecklist.php b/04_Laravel/app/Filament/Resources/ShipmentChecklistResource/Pages/ViewShipmentChecklist.php new file mode 100644 index 0000000..4a3a7f3 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/ShipmentChecklistResource/Pages/ViewShipmentChecklist.php @@ -0,0 +1,11 @@ +schema([ + Forms\Components\Section::make('اطلاعات قالب')->schema([ + Forms\Components\TextInput::make('item_title') + ->label('عنوان آیتم/مرحله') + ->required() + ->maxLength(255) + ->placeholder('مثلاً: تماس با گیرنده، آماده‌سازی بار، ارسال به انبار'), + + Forms\Components\Toggle::make('is_required') + ->label('الزامی') + ->default(true) + ->helperText('اگر الزامی باشد، ناقص ماندنش در گزارش «سفارش‌های مشکل‌دار» ظاهر می‌شود.'), + + Forms\Components\TextInput::make('sort_order') + ->label('ترتیب نمایش') + ->numeric() + ->default(0) + ->minValue(0), + + Forms\Components\Select::make('direction') + ->label('اعمال به جهت') + ->options([ + 'both' => 'هر دو (صادرات و واردات)', + 'export' => 'فقط صادرات', + 'import' => 'فقط واردات', + ]) + ->default('both') + ->required(), + + Forms\Components\Toggle::make('is_active') + ->label('فعال') + ->default(true), + ])->columns(2), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('item_title') + ->label('عنوان') + ->searchable() + ->weight('bold'), + + Tables\Columns\IconColumn::make('is_required') + ->label('الزامی') + ->boolean() + ->trueIcon('heroicon-o-exclamation-circle') + ->falseIcon('heroicon-o-minus-circle') + ->trueColor('danger') + ->falseColor('gray'), + + Tables\Columns\TextColumn::make('sort_order') + ->label('ترتیب') + ->sortable(), + + Tables\Columns\TextColumn::make('direction') + ->label('جهت') + ->badge() + ->formatStateUsing(fn (string $state) => match ($state) { + 'both' => 'هر دو', + 'export' => 'صادرات', + 'import' => 'واردات', + default => $state, + }) + ->color(fn (string $state) => match ($state) { + 'both' => 'info', + 'export' => 'warning', + 'import' => 'success', + default => 'gray', + }), + + Tables\Columns\IconColumn::make('is_active') + ->label('فعال') + ->boolean(), + ]) + ->defaultSort('sort_order', 'asc') + ->filters([ + Tables\Filters\SelectFilter::make('direction') + ->options([ + 'both' => 'هر دو', + 'export' => 'صادرات', + 'import' => 'واردات', + ]), + Tables\Filters\TernaryFilter::make('is_active')->label('فعال'), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return []; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListShipmentChecklistTemplates::route('/'), + 'create' => Pages\CreateShipmentChecklistTemplate::route('/create'), + 'edit' => Pages\EditShipmentChecklistTemplate::route('/{record}/edit'), + ]; + } + + public static function canViewAny(): bool + { + return auth()->user()?->hasAnyRole(['super_admin', 'admin', 'staff']) ?? false; + } + + public static function canCreate(): bool + { + return auth()->user()?->hasRole('super_admin') ?? false; + } + + public static function canEdit($record): bool + { + return auth()->user()?->hasRole('super_admin') ?? false; + } + + public static function canDelete($record): bool + { + return auth()->user()?->hasRole('super_admin') ?? false; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Resources/ShipmentChecklistTemplateResource/Pages/CreateShipmentChecklistTemplate.php b/04_Laravel/app/Filament/Resources/ShipmentChecklistTemplateResource/Pages/CreateShipmentChecklistTemplate.php new file mode 100644 index 0000000..0bf437c --- /dev/null +++ b/04_Laravel/app/Filament/Resources/ShipmentChecklistTemplateResource/Pages/CreateShipmentChecklistTemplate.php @@ -0,0 +1,17 @@ +id(); + return $data; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Resources/ShipmentChecklistTemplateResource/Pages/EditShipmentChecklistTemplate.php b/04_Laravel/app/Filament/Resources/ShipmentChecklistTemplateResource/Pages/EditShipmentChecklistTemplate.php new file mode 100644 index 0000000..8e05d3f --- /dev/null +++ b/04_Laravel/app/Filament/Resources/ShipmentChecklistTemplateResource/Pages/EditShipmentChecklistTemplate.php @@ -0,0 +1,19 @@ +schema([ - Forms\Components\TextInput::make('status') + Forms\Components\TextInput::make('item_title') + ->label('عنوان آیتم/مرحله') ->required() - ->label('عنوان مرحله/آیتم'), - Forms\Components\Toggle::make('is_completed') - ->label('تکمیل شده') - ->default(false), + ->maxLength(255), + + Forms\Components\Toggle::make('is_required') + ->label('الزامی') + ->default(true), + Forms\Components\Textarea::make('notes') - ->rows(3) - ->label('یادداشت'), - Forms\Components\DateTimePicker::make('completed_at') - ->label('تاریخ/ساعت تکمیل'), + ->label('یادداشت') + ->rows(3), + + Forms\Components\TextInput::make('attachment') + ->label('پیوست (مسیر فایل)') + ->maxLength(255), ]); } @@ -37,7 +44,19 @@ class ChecklistsRelationManager extends RelationManager { return $table ->columns([ - Tables\Columns\TextColumn::make('status')->label('مرحله')->searchable()->weight('bold'), + Tables\Columns\TextColumn::make('item_title') + ->label('آیتم') + ->searchable() + ->weight('bold'), + + Tables\Columns\IconColumn::make('is_required') + ->label('الزامی') + ->boolean() + ->trueIcon('heroicon-o-exclamation-circle') + ->falseIcon('heroicon-o-minus-circle') + ->trueColor('danger') + ->falseColor('gray'), + Tables\Columns\IconColumn::make('is_completed') ->label('وضعیت') ->boolean() @@ -45,15 +64,49 @@ class ChecklistsRelationManager extends RelationManager ->falseIcon('heroicon-o-x-circle') ->trueColor('success') ->falseColor('danger'), - Tables\Columns\TextColumn::make('user.name')->label('انجام‌دهنده')->toggleable(), - Tables\Columns\TextColumn::make('completed_at')->label('تاریخ تکمیل')->dateTime('Y/m/d H:i')->toggleable(), - Tables\Columns\TextColumn::make('notes')->label('یادداشت')->limit(40)->toggleable(), + + Tables\Columns\TextColumn::make('completedBy.name') + ->label('انجام‌دهنده') + ->toggleable(), + + Tables\Columns\TextColumn::make('completed_at') + ->label('تاریخ تکمیل') + ->dateTime('Y/m/d H:i') + ->toggleable(), + + Tables\Columns\TextColumn::make('notes') + ->label('یادداشت') + ->limit(40) + ->toggleable(), + ]) + ->defaultSort('sort_order', 'asc') + ->filters([ + Tables\Filters\TernaryFilter::make('is_completed')->label('تکمیل'), + Tables\Filters\TernaryFilter::make('is_required')->label('الزامی'), ]) - ->defaultSort('created_at', 'desc') ->headerActions([ - Tables\Actions\CreateAction::make(), + Tables\Actions\CreateAction::make() + ->mutateFormDataUsing(function (array $data): array { + $data['completed_by'] = null; + return $data; + }), ]) ->actions([ + Action::make('complete') + ->label('تکمیل') + ->icon('heroicon-o-check-circle') + ->color('success') + ->visible(fn ($record) => !$record->is_completed) + ->form([ + Forms\Components\Textarea::make('note') + ->label('یادداشت (اختیاری)') + ->rows(2), + ]) + ->action(function ($record, array $data) { + $record->complete(auth()->id(), $data['note'] ?? null); + Notification::make()->title('آیتم تکمیل شد')->success()->send(); + }), + Tables\Actions\EditAction::make(), Tables\Actions\DeleteAction::make(), ]) diff --git a/04_Laravel/app/Models/ShipmentChecklist.php b/04_Laravel/app/Models/ShipmentChecklist.php index 49171a1..695af9e 100644 --- a/04_Laravel/app/Models/ShipmentChecklist.php +++ b/04_Laravel/app/Models/ShipmentChecklist.php @@ -4,6 +4,8 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Facades\Auth; class ShipmentChecklist extends Model { @@ -11,37 +13,72 @@ class ShipmentChecklist extends Model protected $fillable = [ 'shipment_id', - 'user_id', - 'status', + 'user_id', // (deprecated) نگه‌داری‌شده برای backward-compat + 'status', // (deprecated) — از item_title استفاده شود + 'item_title', + 'is_required', + 'sort_order', 'is_completed', + 'completed_by', 'notes', + 'attachment', 'completed_at', ]; protected $casts = [ + 'is_required' => 'boolean', 'is_completed' => 'boolean', + 'sort_order' => 'integer', 'completed_at' => 'datetime', ]; // ─── Relationships ─────────────────────────────── - - public function shipment() + + public function shipment(): BelongsTo { return $this->belongsTo(Shipment::class); } - public function user() + public function user(): BelongsTo { - return $this->belongsTo(User::class); + return $this->belongsTo(User::class, 'user_id'); } - // ─── Helper Methods ────────────────────────────── - - public function complete(): void + public function completedBy(): BelongsTo { - $this->update([ - 'is_completed' => true, - 'completed_at' => now(), - ]); + return $this->belongsTo(User::class, 'completed_by'); } -} + + // ─── Scopes ─────────────────────────────────────── + + public function scopeIncomplete($query) + { + return $query->where('is_completed', false); + } + + public function scopeRequired($query) + { + return $query->where('is_required', true); + } + + // ─── Helpers ───────────────────────────────────── + + /** + * تکمیل یک آیتم چک‌لیست توسط کارمند. + */ + public function complete(?int $userId = null, ?string $note = null): void + { + $data = [ + 'is_completed' => true, + 'completed_by' => $userId ?? Auth::id(), + 'completed_at' => now(), + ]; + + if ($note) { + $existing = $this->notes ? $this->notes . "\n\n" : ''; + $data['notes'] = $existing . '[' . now()->format('Y-m-d H:i') . '] ' . $note; + } + + $this->update($data); + } +} \ No newline at end of file diff --git a/04_Laravel/app/Models/ShipmentChecklistTemplate.php b/04_Laravel/app/Models/ShipmentChecklistTemplate.php new file mode 100644 index 0000000..3fff6b8 --- /dev/null +++ b/04_Laravel/app/Models/ShipmentChecklistTemplate.php @@ -0,0 +1,46 @@ + 'boolean', + 'is_active' => 'boolean', + 'sort_order' => 'integer', + ]; + + public function createdBy(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + // ─── Scopes ─────────────────────────────────────── + + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + /** + * قالب‌هایی که به جهت مشخص (export/import/both) اعمال می‌شوند. + */ + public function scopeForDirection($query, string $direction) + { + return $query->where(function ($q) use ($direction) { + $q->where('direction', 'both')->orWhere('direction', $direction); + }); + } +} \ No newline at end of file diff --git a/04_Laravel/app/Services/ShipmentReviewService.php b/04_Laravel/app/Services/ShipmentReviewService.php index 5376639..aceaf11 100644 --- a/04_Laravel/app/Services/ShipmentReviewService.php +++ b/04_Laravel/app/Services/ShipmentReviewService.php @@ -7,6 +7,7 @@ use App\Enums\ShipmentStatus; use App\Models\Shipment; use App\Models\ShipmentReview; use App\Models\ShipmentStatusHistory; +use App\Models\ShipmentChecklistTemplate; use App\Models\User; use Illuminate\Support\Facades\DB; use RuntimeException; @@ -95,7 +96,7 @@ class ShipmentReviewService 'changed_by' => $user->id, ]); - $review->update([ + $review->update([ 'decision' => ReviewState::Approved->value, 'reason' => null, 'notes' => $notes, @@ -103,6 +104,9 @@ class ShipmentReviewService 'reviewed_at' => now(), ]); + // خودکارسازی چک‌لیست از قالب‌های فعال (بعد از تأیید سفارش) + $this->instantiateChecklist($shipment); + return $shipment->fresh(); }); } @@ -200,8 +204,44 @@ class ShipmentReviewService * * هر resubmission یک revision جدید ایجاد می‌کند. */ - private function nextReviewRevision(Shipment $shipment): int + private function nextReviewRevision(Shipment $shipment): int { return ((int) $shipment->reviews()->max('revision_no')) + 1; } + + /** + * ساخت instance‌های چک‌لیست از قالب‌های فعال متناسب با جهت سفارش. + * فقط اگه هنوز چک‌لیستی برای این سفارش ساخته نشده باشد. + */ + private function instantiateChecklist(Shipment $shipment): void + { + // جلوگیری از تکرار اگه قبلاً ساخته شده + if ($shipment->checklists()->exists()) { + return; + } + + $direction = $shipment->direction->value; // 'export' | 'import' + + $templates = ShipmentChecklistTemplate::query() + ->where('is_active', true) + ->where(function ($q) use ($direction) { + $q->where('direction', 'both')->orWhere('direction', $direction); + }) + ->orderBy('sort_order') + ->get(); + + if ($templates->isEmpty()) { + return; // قالب فعالی نیست — چک‌لیست ساخته نمی‌شه + } + + $rows = $templates->map(fn ($t) => [ + 'shipment_id' => $shipment->id, + 'item_title' => $t->item_title, + 'is_required' => $t->is_required, + 'sort_order' => $t->sort_order, + 'is_completed' => false, + ])->toArray(); + + $shipment->checklists()->createMany($rows); + } } \ No newline at end of file diff --git a/04_Laravel/database/migrations/2026_09_30_200001_create_shipment_checklist_templates_table.php b/04_Laravel/database/migrations/2026_09_30_200001_create_shipment_checklist_templates_table.php new file mode 100644 index 0000000..9388af2 --- /dev/null +++ b/04_Laravel/database/migrations/2026_09_30_200001_create_shipment_checklist_templates_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('item_title')->comment('عنوان آیتم/مرحله'); + $table->boolean('is_required')->default(true)->comment('الزامی یا اختیاری'); + $table->integer('sort_order')->default(0); + $table->enum('direction', ['export', 'import', 'both'])->default('both') + ->comment('به کدام جهت سفارش اعمال شود'); + $table->boolean('is_active')->default(true); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + $table->index(['is_active', 'direction'], 'sct_active_dir_idx'); + }); + } + + public function down(): void + { + Schema::dropIfExists('shipment_checklist_templates'); + } +}; \ No newline at end of file diff --git a/04_Laravel/database/migrations/2026_09_30_200002_extend_shipment_checklists_table.php b/04_Laravel/database/migrations/2026_09_30_200002_extend_shipment_checklists_table.php new file mode 100644 index 0000000..2156cc1 --- /dev/null +++ b/04_Laravel/database/migrations/2026_09_30_200002_extend_shipment_checklists_table.php @@ -0,0 +1,46 @@ +string('item_title')->nullable() + ->after('status') + ->comment('عنوان آیتم (snapshot از قالب)'); + + $table->boolean('is_required')->default(true)->after('item_title'); + $table->integer('sort_order')->default(0)->after('is_required'); + + $table->foreignId('completed_by')->nullable() + ->after('completed_at') + ->constrained('users') + ->nullOnDelete() + ->comment('کارمندی که این آیتم را تکمیل کرده'); + + $table->string('attachment')->nullable() + ->after('notes') + ->comment('مسیر فایل پیوست'); + + $table->index(['shipment_id', 'is_completed'], 'sc_shipment_completed_idx'); + $table->index(['is_completed', 'is_required'], 'sc_incomplete_required_idx'); + }); + } + + public function down(): void + { + Schema::table('shipment_checklists', function (Blueprint $table) { + $table->dropIndex('sc_shipment_completed_idx'); + $table->dropIndex('sc_incomplete_required_idx'); + $table->dropForeign(['completed_by']); + $table->dropColumn(['item_title', 'is_required', 'sort_order', 'completed_by', 'attachment']); + }); + } +}; \ No newline at end of file