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, }; } }