feat(shipment): implement checklist templating and automated instantiation

Introduce a template-based system for shipment checklists to automate
the creation of required inspection items upon shipment approval.

- Add `ShipmentChecklistTemplate` model and migration to define reusable
  checklist structures.
- Extend `ShipmentChecklist` schema to support item titles, requirement
  flags, sorting, and attachment paths.
- Implement `instantiateChecklist` in `ShipmentReviewService` to
  automatically generate checklist items from active templates based on
  shipment direction (import/export).
- Refactor `ShipmentChecklistResource` and `ChecklistsRelationManager`
  in Filament to support the new data structure and improved UI.
- Update `ShipmentChecklist` model with helper methods for completion
  tracking and improved relationship handling.
This commit is contained in:
Kazem Alghasi 2026-09-30 18:35:25 +03:30
parent 3848f0b399
commit b9b83ffecf
12 changed files with 636 additions and 154 deletions

View File

@ -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';
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\ShipmentChecklistResource\Pages;
use App\Filament\Resources\ShipmentChecklistResource;
use Filament\Resources\Pages\ViewRecord;
class ViewShipmentChecklist extends ViewRecord
{
protected static string $resource = ShipmentChecklistResource::class;
}

View File

@ -0,0 +1,160 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\ShipmentChecklistTemplateResource\Pages;
use App\Models\ShipmentChecklistTemplate;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
/**
* قالب‌های قابل تکرار چک‌لیست. فقط super_admin ویرایش می‌کند.
*/
class ShipmentChecklistTemplateResource extends Resource
{
protected static ?string $model = ShipmentChecklistTemplate::class;
protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-list';
protected static ?string $navigationGroup = 'تنظیمات';
protected static ?string $navigationLabel = 'قالب‌های چک‌لیست';
protected static ?string $modelLabel = 'قالب چک‌لیست';
protected static ?string $pluralModelLabel = 'قالب‌های چک‌لیست';
protected static ?int $navigationSort = 9;
public static function form(Form $form): Form
{
return $form->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;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Filament\Resources\ShipmentChecklistTemplateResource\Pages;
use App\Filament\Resources\ShipmentChecklistTemplateResource;
use Filament\Resources\Pages\CreateRecord;
class CreateShipmentChecklistTemplate extends CreateRecord
{
protected static string $resource = ShipmentChecklistTemplateResource::class;
protected function mutateFormDataBeforeCreate(array $data): array
{
$data['created_by'] = auth()->id();
return $data;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\ShipmentChecklistTemplateResource\Pages;
use App\Filament\Resources\ShipmentChecklistTemplateResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditShipmentChecklistTemplate extends EditRecord
{
protected static string $resource = ShipmentChecklistTemplateResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\ShipmentChecklistTemplateResource\Pages;
use App\Filament\Resources\ShipmentChecklistTemplateResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListShipmentChecklistTemplates extends ListRecords
{
protected static string $resource = ShipmentChecklistTemplateResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -4,9 +4,11 @@ namespace App\Filament\Resources\ShipmentResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Tables\Actions\Action;
class ChecklistsRelationManager extends RelationManager
{
@ -19,17 +21,22 @@ class ChecklistsRelationManager extends RelationManager
public function form(Form $form): Form
{
return $form->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(),
])

View File

@ -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);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ShipmentChecklistTemplate extends Model
{
protected $fillable = [
'item_title',
'is_required',
'sort_order',
'direction',
'is_active',
'created_by',
];
protected $casts = [
'is_required' => '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);
});
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* قالب‌های قابل تکرار چک‌لیست سفارش.
* مدیر قالب‌ها را یک‌بار تعریف می‌کند؛ هنگام approve سفارش، از قالب‌های فعال
* برای آن direction، یک instance روی shipment_checklists ساخته می‌شود.
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('shipment_checklist_templates', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* گسترش shipment_checklists با فیلدهای واقعی چک‌لیست.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('shipment_checklists', function (Blueprint $table) {
$table->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']);
});
}
};