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.
247 lines
7.7 KiB
PHP
247 lines
7.7 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Enums\ReviewState;
|
||
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;
|
||
|
||
class ShipmentReviewService
|
||
{
|
||
/**
|
||
* درخواست اصلاح سفارش توسط کارمند.
|
||
*
|
||
* این عملیات یک Review Revision جدید ایجاد میکند،
|
||
* چون یک submission جدید برای بررسی ثبت شده است.
|
||
*/
|
||
public function requestChanges(
|
||
Shipment $shipment,
|
||
User $user,
|
||
string $reason,
|
||
?string $notes = null
|
||
): Shipment {
|
||
return DB::transaction(function () use ($shipment, $user, $reason, $notes) {
|
||
|
||
$shipment = Shipment::query()
|
||
->lockForUpdate()
|
||
->findOrFail($shipment->id);
|
||
|
||
$this->assertPendingReview(
|
||
$shipment,
|
||
'این سفارش در وضعیت قابل درخواست اصلاح نیست.'
|
||
);
|
||
|
||
$revisionNo = $this->nextReviewRevision($shipment);
|
||
|
||
$shipment->update([
|
||
'review_state' => ReviewState::ChangesRequested,
|
||
]);
|
||
|
||
ShipmentReview::create([
|
||
'shipment_id' => $shipment->id,
|
||
'revision_no' => $revisionNo,
|
||
'decision' => ReviewState::ChangesRequested->value,
|
||
'reason' => $reason,
|
||
'notes' => $notes,
|
||
'reviewed_by' => $user->id,
|
||
'reviewed_at' => now(),
|
||
]);
|
||
|
||
return $shipment->fresh();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* تأیید سفارش توسط کارمند.
|
||
*
|
||
* تأیید، Review جدید ایجاد نمیکند.
|
||
* آخرین revision با وضعیت pending را finalize میکند.
|
||
*/
|
||
public function approve(
|
||
Shipment $shipment,
|
||
User $user,
|
||
?string $notes = null
|
||
): Shipment {
|
||
return DB::transaction(function () use ($shipment, $user, $notes) {
|
||
|
||
$shipment = Shipment::query()
|
||
->lockForUpdate()
|
||
->findOrFail($shipment->id);
|
||
|
||
$this->assertPendingReview(
|
||
$shipment,
|
||
'این سفارش در وضعیت قابل تأیید نیست.'
|
||
);
|
||
|
||
$review = $this->getPendingReview($shipment);
|
||
|
||
$oldStatus = $shipment->status;
|
||
|
||
$shipment->update([
|
||
'status' => ShipmentStatus::Approved,
|
||
'review_state' => ReviewState::Approved,
|
||
]);
|
||
|
||
ShipmentStatusHistory::create([
|
||
'shipment_id' => $shipment->id,
|
||
'from_status' => $oldStatus->value,
|
||
'to_status' => ShipmentStatus::Approved->value,
|
||
'reason' => $notes ?: 'تأیید توسط کارمند',
|
||
'changed_by' => $user->id,
|
||
]);
|
||
|
||
$review->update([
|
||
'decision' => ReviewState::Approved->value,
|
||
'reason' => null,
|
||
'notes' => $notes,
|
||
'reviewed_by' => $user->id,
|
||
'reviewed_at' => now(),
|
||
]);
|
||
|
||
// خودکارسازی چکلیست از قالبهای فعال (بعد از تأیید سفارش)
|
||
$this->instantiateChecklist($shipment);
|
||
|
||
return $shipment->fresh();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* رد سفارش توسط کارمند.
|
||
*
|
||
* رد، Review جدید ایجاد نمیکند.
|
||
* آخرین revision با وضعیت pending را finalize میکند.
|
||
*/
|
||
public function reject(
|
||
Shipment $shipment,
|
||
User $user,
|
||
string $reason
|
||
): Shipment {
|
||
return DB::transaction(function () use ($shipment, $user, $reason) {
|
||
|
||
$shipment = Shipment::query()
|
||
->lockForUpdate()
|
||
->findOrFail($shipment->id);
|
||
|
||
$this->assertPendingReview(
|
||
$shipment,
|
||
'این سفارش در وضعیت قابل رد نیست.'
|
||
);
|
||
|
||
$review = $this->getPendingReview($shipment);
|
||
|
||
$oldStatus = $shipment->status;
|
||
|
||
$shipment->update([
|
||
'status' => ShipmentStatus::Cancelled,
|
||
'review_state' => ReviewState::Rejected,
|
||
]);
|
||
|
||
ShipmentStatusHistory::create([
|
||
'shipment_id' => $shipment->id,
|
||
'from_status' => $oldStatus->value,
|
||
'to_status' => ShipmentStatus::Cancelled->value,
|
||
'reason' => 'رد شده: ' . $reason,
|
||
'changed_by' => $user->id,
|
||
]);
|
||
|
||
$review->update([
|
||
'decision' => ReviewState::Rejected->value,
|
||
'reason' => $reason,
|
||
'notes' => null,
|
||
'reviewed_by' => $user->id,
|
||
'reviewed_at' => now(),
|
||
]);
|
||
|
||
return $shipment->fresh();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* بررسی وضعیت فعلی Shipment برای عملیات Review.
|
||
*/
|
||
private function assertPendingReview(
|
||
Shipment $shipment,
|
||
string $message
|
||
): void {
|
||
if (
|
||
$shipment->status !== ShipmentStatus::PendingApproval ||
|
||
$shipment->review_state !== ReviewState::Pending
|
||
) {
|
||
throw new RuntimeException($message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* پیدا کردن Review فعلی که منتظر تصمیم کارمند است.
|
||
*
|
||
* بعد از resubmit باید دقیقاً یک revision با decision=pending
|
||
* وجود داشته باشد.
|
||
*/
|
||
private function getPendingReview(Shipment $shipment): ShipmentReview
|
||
{
|
||
$review = $shipment->reviews()
|
||
->where('decision', ReviewState::Pending->value)
|
||
->orderByDesc('revision_no')
|
||
->first();
|
||
|
||
if (!$review) {
|
||
throw new RuntimeException(
|
||
'برای این سفارش هیچ Review در وضعیت pending پیدا نشد.'
|
||
);
|
||
}
|
||
|
||
return $review;
|
||
}
|
||
|
||
/**
|
||
* تعیین شماره revision بعدی.
|
||
*
|
||
* هر resubmission یک revision جدید ایجاد میکند.
|
||
*/
|
||
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);
|
||
}
|
||
} |