ultimatepos/Modules/Maintenance/Services/RepairDispatchService.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,
};
}
}