249 lines
9.8 KiB
PHP
249 lines
9.8 KiB
PHP
<?php
|
|
|
|
namespace Modules\Maintenance\Services;
|
|
|
|
use App\Business;
|
|
use App\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use InvalidArgumentException;
|
|
use Modules\Maintenance\Models\MaterialLine;
|
|
use Modules\Maintenance\Models\MaterialList;
|
|
use Modules\Maintenance\Models\RepairDispatch;
|
|
use Modules\Maintenance\Models\RepairDispatchEvent;
|
|
|
|
class RepairDispatchService
|
|
{
|
|
public const STATUSES = [
|
|
'draft',
|
|
'sent',
|
|
'at_repair_site',
|
|
'in_repair',
|
|
'repair_done',
|
|
'returning',
|
|
'received',
|
|
'cancelled',
|
|
];
|
|
|
|
public const INTERNAL_FLOW = ['draft', 'sent', 'in_repair', 'repair_done', 'received'];
|
|
|
|
public const EXTERNAL_FLOW = ['draft', 'sent', 'at_repair_site', 'in_repair', 'repair_done', 'returning', 'received'];
|
|
|
|
public function generateNumber(Business $business): string
|
|
{
|
|
$prefix = 'RD-'.now()->format('ym');
|
|
$last = RepairDispatch::where('business_id', $business->id)
|
|
->where('dispatch_number', 'like', $prefix.'%')
|
|
->orderByDesc('id')
|
|
->value('dispatch_number');
|
|
|
|
$seq = 1;
|
|
if ($last && preg_match('/(\d+)$/', $last, $m)) {
|
|
$seq = (int) $m[1] + 1;
|
|
}
|
|
|
|
return $prefix.str_pad((string) $seq, 4, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
public function createDispatch(MaterialList $list, MaterialLine $line, array $data, ?User $user = null): RepairDispatch
|
|
{
|
|
if ($line->line_type !== 'repair') {
|
|
throw new InvalidArgumentException('فقط اقلام نوع «تعمیر» قابل ارسال هستند.');
|
|
}
|
|
|
|
if ($line->activeRepairDispatch()->exists()) {
|
|
throw new InvalidArgumentException('برای این قطعه یک مسیر تعمیر فعال وجود دارد.');
|
|
}
|
|
|
|
$routeType = $data['route_type'] ?? 'internal';
|
|
$this->validateRoute($routeType, $data);
|
|
|
|
return DB::transaction(function () use ($list, $line, $data, $routeType, $user) {
|
|
$business = Business::findOrFail($list->business_id);
|
|
|
|
$dispatch = RepairDispatch::create([
|
|
'business_id' => $list->business_id,
|
|
'material_list_id' => $list->id,
|
|
'material_line_id' => $line->id,
|
|
'dispatch_number' => $data['dispatch_number'] ?? $this->generateNumber($business),
|
|
'route_type' => $routeType,
|
|
'origin_location_id' => $data['origin_location_id'] ?? $list->destination_location_id,
|
|
'holding_entity_id' => $routeType === 'internal' ? ($data['holding_entity_id'] ?? null) : null,
|
|
'org_unit_id' => $routeType === 'internal' ? ($data['org_unit_id'] ?? null) : null,
|
|
'service_provider_id' => $routeType === 'external' ? ($data['service_provider_id'] ?? null) : null,
|
|
'external_party_name' => $routeType === 'external' ? ($data['external_party_name'] ?? null) : null,
|
|
'external_address' => $routeType === 'external' ? ($data['external_address'] ?? null) : null,
|
|
'quantity' => $data['quantity'] ?? $line->quantity_required,
|
|
'status' => 'draft',
|
|
'expected_return_at' => ! empty($data['expected_return_at']) ? Carbon::parse($data['expected_return_at']) : null,
|
|
'send_notes' => $data['send_notes'] ?? null,
|
|
'tracking_number_out' => $data['tracking_number_out'] ?? null,
|
|
'carrier' => $data['carrier'] ?? null,
|
|
'created_by' => $user?->id,
|
|
]);
|
|
|
|
$line->update(['status' => 'repair_planned']);
|
|
$this->logEvent($dispatch, 'draft', 'مسیر تعمیر ایجاد شد.', $user);
|
|
|
|
return $dispatch->fresh(['holdingEntity', 'orgUnit', 'serviceProvider', 'materialLine']);
|
|
});
|
|
}
|
|
|
|
public function advanceStatus(RepairDispatch $dispatch, ?string $notes = null, ?array $extra = [], ?User $user = null): RepairDispatch
|
|
{
|
|
$flow = $dispatch->route_type === 'internal' ? self::INTERNAL_FLOW : self::EXTERNAL_FLOW;
|
|
$currentIndex = array_search($dispatch->status, $flow, true);
|
|
|
|
if ($currentIndex === false || $currentIndex >= count($flow) - 1) {
|
|
throw new InvalidArgumentException('وضعیت فعلی قابل پیشبرد نیست.');
|
|
}
|
|
|
|
$nextStatus = $flow[$currentIndex + 1];
|
|
|
|
return $this->transitionTo($dispatch, $nextStatus, $notes, $extra, $user);
|
|
}
|
|
|
|
public function transitionTo(
|
|
RepairDispatch $dispatch,
|
|
string $status,
|
|
?string $notes = null,
|
|
?array $extra = [],
|
|
?User $user = null
|
|
): RepairDispatch {
|
|
if (! in_array($status, self::STATUSES, true)) {
|
|
throw new InvalidArgumentException('وضعیت نامعتبر است.');
|
|
}
|
|
|
|
return DB::transaction(function () use ($dispatch, $status, $notes, $extra, $user) {
|
|
$updates = ['status' => $status];
|
|
|
|
if ($status === 'sent') {
|
|
$updates['sent_at'] = now();
|
|
$updates['sent_by'] = $user?->id;
|
|
if (! empty($extra['tracking_number_out'])) {
|
|
$updates['tracking_number_out'] = $extra['tracking_number_out'];
|
|
}
|
|
}
|
|
|
|
if ($status === 'repair_done') {
|
|
$updates['repair_completed_at'] = now();
|
|
if (isset($extra['repair_report'])) {
|
|
$updates['repair_report'] = $extra['repair_report'];
|
|
}
|
|
if (isset($extra['repair_cost'])) {
|
|
$updates['repair_cost'] = (int) $extra['repair_cost'];
|
|
}
|
|
}
|
|
|
|
if ($status === 'returning') {
|
|
$updates['return_dispatched_at'] = now();
|
|
if (! empty($extra['tracking_number_return'])) {
|
|
$updates['tracking_number_return'] = $extra['tracking_number_return'];
|
|
}
|
|
}
|
|
|
|
if ($status === 'received') {
|
|
$updates['received_at'] = now();
|
|
$updates['received_by'] = $user?->id;
|
|
if (isset($extra['receive_notes'])) {
|
|
$updates['receive_notes'] = $extra['receive_notes'];
|
|
}
|
|
$dispatch->materialLine?->update([
|
|
'status' => 'fulfilled',
|
|
'quantity_fulfilled' => $dispatch->materialLine->quantity_required,
|
|
]);
|
|
}
|
|
|
|
if ($status === 'cancelled') {
|
|
$dispatch->materialLine?->update(['status' => 'planned']);
|
|
}
|
|
|
|
$dispatch->update($updates);
|
|
$this->logEvent($dispatch, $status, $notes, $user);
|
|
|
|
return $dispatch->fresh(['events.user', 'materialLine', 'holdingEntity', 'orgUnit', 'serviceProvider']);
|
|
});
|
|
}
|
|
|
|
public function cancel(RepairDispatch $dispatch, ?string $notes = null, ?User $user = null): RepairDispatch
|
|
{
|
|
if (in_array($dispatch->status, ['received', 'cancelled'], true)) {
|
|
throw new InvalidArgumentException('این ارسال قابل لغو نیست.');
|
|
}
|
|
|
|
return $this->transitionTo($dispatch, 'cancelled', $notes, [], $user);
|
|
}
|
|
|
|
protected function validateRoute(string $routeType, array $data): void
|
|
{
|
|
if ($routeType === 'internal') {
|
|
if (empty($data['holding_entity_id'])) {
|
|
throw new InvalidArgumentException('واحد هلدینگ (مقصد داخلی) الزامی است.');
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if ($routeType === 'external') {
|
|
if (empty($data['service_provider_id']) && empty($data['external_party_name'])) {
|
|
throw new InvalidArgumentException('ارائهدهنده خارجی یا نام طرف خارجی الزامی است.');
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function logEvent(RepairDispatch $dispatch, string $status, ?string $notes, ?User $user): void
|
|
{
|
|
RepairDispatchEvent::create([
|
|
'repair_dispatch_id' => $dispatch->id,
|
|
'status' => $status,
|
|
'notes' => $notes,
|
|
'user_id' => $user?->id,
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
|
|
public function statusLabels(): array
|
|
{
|
|
return [
|
|
'draft' => __('maintenance::lang.rd_status_draft'),
|
|
'sent' => __('maintenance::lang.rd_status_sent'),
|
|
'at_repair_site' => __('maintenance::lang.rd_status_at_repair_site'),
|
|
'in_repair' => __('maintenance::lang.rd_status_in_repair'),
|
|
'repair_done' => __('maintenance::lang.rd_status_repair_done'),
|
|
'returning' => __('maintenance::lang.rd_status_returning'),
|
|
'received' => __('maintenance::lang.rd_status_received'),
|
|
'cancelled' => __('maintenance::lang.rd_status_cancelled'),
|
|
];
|
|
}
|
|
|
|
public function routeTypeLabels(): array
|
|
{
|
|
return [
|
|
'internal' => __('maintenance::lang.rd_route_internal'),
|
|
'external' => __('maintenance::lang.rd_route_external'),
|
|
];
|
|
}
|
|
|
|
public function nextActionLabel(RepairDispatch $dispatch): ?string
|
|
{
|
|
$flow = $dispatch->route_type === 'internal' ? self::INTERNAL_FLOW : self::EXTERNAL_FLOW;
|
|
$idx = array_search($dispatch->status, $flow, true);
|
|
|
|
if ($idx === false || $idx >= count($flow) - 1) {
|
|
return null;
|
|
}
|
|
|
|
$next = $flow[$idx + 1];
|
|
|
|
return match ($next) {
|
|
'sent' => __('maintenance::lang.rd_action_send'),
|
|
'at_repair_site' => __('maintenance::lang.rd_action_at_site'),
|
|
'in_repair' => __('maintenance::lang.rd_action_start_repair'),
|
|
'repair_done' => __('maintenance::lang.rd_action_complete_repair'),
|
|
'returning' => __('maintenance::lang.rd_action_return_ship'),
|
|
'received' => __('maintenance::lang.rd_action_receive'),
|
|
default => null,
|
|
};
|
|
}
|
|
}
|