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:
parent
3848f0b399
commit
b9b83ffecf
@ -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';
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
])
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
46
04_Laravel/app/Models/ShipmentChecklistTemplate.php
Normal file
46
04_Laravel/app/Models/ShipmentChecklistTemplate.php
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user