format('ym'); $last = FieldMission::where('business_id', $business->id) ->where('mission_number', 'like', $prefix.'%') ->orderByDesc('id') ->value('mission_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 getOrCreateForSource(Business $business, string $sourceType, ?int $sourceId, ?User $user = null): FieldMission { if ($sourceType === 'standalone') { throw new InvalidArgumentException('برای مأموریت مستقل از createMission استفاده کنید.'); } $existing = FieldMission::where('business_id', $business->id) ->where('source_type', $sourceType) ->where('source_id', $sourceId) ->whereNotIn('status', ['closed', 'cancelled']) ->first(); if ($existing) { return $existing; } $meta = $this->resolveSourceMeta($business->id, $sourceType, $sourceId); return $this->createMission($business, array_merge($meta, [ 'source_type' => $sourceType, 'source_id' => $sourceId, ]), $user); } public function createMission(Business $business, array $data, ?User $user = null): FieldMission { return DB::transaction(function () use ($business, $data, $user) { $mission = FieldMission::create([ 'business_id' => $business->id, 'mission_number' => $data['mission_number'] ?? $this->generateNumber($business), 'mission_type' => $data['mission_type'] ?? 'repair', 'source_type' => $data['source_type'] ?? 'standalone', 'source_id' => $data['source_id'] ?? null, 'title' => $data['title'] ?? 'مأموریت میدانی', 'description' => $data['description'] ?? null, 'equipment_id' => $data['equipment_id'] ?? null, 'destination_location_id' => $data['destination_location_id'] ?? null, 'destination_site' => $data['destination_site'] ?? null, 'status' => $data['status'] ?? 'draft', 'planned_departure_at' => ! empty($data['planned_departure_at']) ? Carbon::parse($data['planned_departure_at']) : null, 'planned_return_at' => ! empty($data['planned_return_at']) ? Carbon::parse($data['planned_return_at']) : null, 'notes' => $data['notes'] ?? null, 'created_by' => $user?->id, ]); if (! empty($data['seed_crew_from_source'])) { $this->seedCrewFromSource($mission); } return $mission->fresh(['crewMembers.user', 'items', 'expenses']); }); } public function seedCrewFromSource(FieldMission $mission): void { if (! $mission->source_id || $mission->source_type === 'standalone') { return; } if ($mission->crewMembers()->exists()) { return; } if ($mission->source_type === 'work_order') { $wo = WorkOrder::find($mission->source_id); if ($wo?->assigned_technician_id) { $this->addCrewMember($mission, [ 'user_id' => $wo->assigned_technician_id, 'is_lead' => true, 'role' => 'lead', ]); } return; } if ($mission->source_type === 'inspection_visit') { $visit = InspectionVisit::find($mission->source_id); if ($visit?->inspector_id) { $this->addCrewMember($mission, [ 'user_id' => $visit->inspector_id, 'is_lead' => true, 'role' => 'lead', ]); } return; } if ($mission->source_type === 'overhaul') { $overhaul = Overhaul::find($mission->source_id); if ($overhaul?->supervisor_id) { $this->addCrewMember($mission, [ 'user_id' => $overhaul->supervisor_id, 'is_lead' => true, 'role' => 'supervisor', ]); } $team = $overhaul->assigned_team ?? []; if (is_array($team)) { foreach ($team as $member) { $userId = is_array($member) ? ($member['user_id'] ?? $member['id'] ?? null) : $member; if ($userId) { $this->addCrewMember($mission, [ 'user_id' => (int) $userId, 'role' => 'technician', ]); } } } } } public function addCrewMember(FieldMission $mission, array $data): MissionCrewMember { if (! empty($data['is_lead'])) { $mission->crewMembers()->update(['is_lead' => false]); } $member = MissionCrewMember::create([ 'mission_id' => $mission->id, 'user_id' => $data['user_id'] ?? null, 'person_name' => $data['person_name'] ?? null, 'role' => $data['role'] ?? 'technician', 'is_lead' => ! empty($data['is_lead']), 'labor_hours' => $data['labor_hours'] ?? 0, 'hourly_rate' => $data['hourly_rate'] ?? 0, 'per_diem' => $data['per_diem'] ?? 0, 'notes' => $data['notes'] ?? null, ]); $member->recalculateLaborCost(); $member->save(); if ($member->is_lead && $member->user_id) { $mission->update(['lead_user_id' => $member->user_id]); } $this->recalculateTotals($mission); return $member->fresh('user'); } public function updateCrewMember(MissionCrewMember $member, array $data): MissionCrewMember { if (! empty($data['is_lead'])) { $member->mission->crewMembers()->where('id', '!=', $member->id)->update(['is_lead' => false]); } $member->update($data); $member->recalculateLaborCost(); $member->save(); if ($member->is_lead && $member->user_id) { $member->mission->update(['lead_user_id' => $member->user_id]); } $this->recalculateTotals($member->mission); return $member->fresh('user'); } public function removeCrewMember(MissionCrewMember $member): void { $mission = $member->mission; $member->delete(); $this->recalculateTotals($mission); } public function addItem(FieldMission $mission, array $data): MissionItem { $itemType = $data['item_type'] ?? 'spare_part'; if ($itemType === 'spare_part' && ! empty($data['spare_part_id'])) { $part = SparePart::where('business_id', $mission->business_id)->findOrFail($data['spare_part_id']); $data['product_id'] = $part->product_id; $data['variation_id'] = $data['variation_id'] ?? $this->inventoryService->resolveVariationId($part); $data['part_name'] = $data['part_name'] ?? $part->name; $data['part_number'] = $data['part_number'] ?? $part->part_number; $data['unit'] = $data['unit'] ?? $part->unit; $data['location_id'] = $data['location_id'] ?? $this->inventoryService->resolveLocationId($part, $mission->business_id); } if ($itemType === 'tool' && ! empty($data['tool_id'])) { $tool = MaintenanceTool::where('business_id', $mission->business_id)->findOrFail($data['tool_id']); $data['part_name'] = $tool->name; $data['quantity_issued'] = 1; } return MissionItem::create([ 'mission_id' => $mission->id, 'item_type' => $itemType, 'spare_part_id' => $data['spare_part_id'] ?? null, 'product_id' => $data['product_id'] ?? null, 'variation_id' => $data['variation_id'] ?? null, 'tool_id' => $data['tool_id'] ?? null, 'part_name' => $data['part_name'] ?? null, 'part_number' => $data['part_number'] ?? null, 'location_id' => $data['location_id'] ?? $mission->destination_location_id ?? $this->stockService->resolveDestinationLocationId($mission->business_id), 'quantity_issued' => $data['quantity_issued'] ?? 1, 'unit' => $data['unit'] ?? null, 'notes' => $data['notes'] ?? null, 'status' => 'planned', ]); } public function removeItem(MissionItem $item): void { if ($item->stock_deducted) { throw new InvalidArgumentException('این قلم از انبار خارج شده و قابل حذف نیست.'); } $item->delete(); } public function dispatch(FieldMission $mission, ?User $user = null): FieldMission { if (! in_array($mission->status, ['draft', 'planned'], true)) { throw new InvalidArgumentException('وضعیت مأموریت برای اعزام مناسب نیست.'); } return DB::transaction(function () use ($mission, $user) { foreach ($mission->items()->where('status', 'planned')->get() as $item) { $this->issueItemStock($mission, $item, $user); } $mission->update([ 'status' => 'dispatched', 'actual_departure_at' => now(), ]); return $mission->fresh(['crewMembers.user', 'items', 'expenses']); }); } protected function issueItemStock(FieldMission $mission, MissionItem $item, ?User $user): void { if ($item->stock_deducted || $item->quantity_issued <= 0) { return; } if ($item->item_type === 'tool' && $item->tool_id) { $tool = MaintenanceTool::findOrFail($item->tool_id); $leadId = $mission->lead_user_id ?? $user?->id ?? auth()->id(); $this->toolCribService->checkout( $mission->business, $tool, (int) $leadId, $mission->source_type === 'work_order' ? $mission->source_id : null, $mission->equipment_id, $mission->planned_return_at?->toDateTimeString(), 'مأموریت '.$mission->mission_number, $user ); $item->update(['stock_deducted' => true, 'status' => 'issued']); return; } if ($item->product_id && $item->variation_id && $item->location_id) { $product = Product::where('business_id', $mission->business_id)->find($item->product_id); if ($product && (int) $product->enable_stock === 1) { $this->productUtil->decreaseProductQuantity( $item->product_id, $item->variation_id, $item->location_id, (float) $item->quantity_issued ); } $item->update(['stock_deducted' => true, 'status' => 'issued']); } } public function recordReturns(FieldMission $mission, array $lines): FieldMission { return DB::transaction(function () use ($mission, $lines) { foreach ($lines as $line) { $item = MissionItem::where('mission_id', $mission->id)->findOrFail($line['item_id']); $returned = (float) ($line['quantity_returned'] ?? 0); $lost = (float) ($line['quantity_lost'] ?? 0); $maxReturn = (float) $item->quantity_issued - (float) $item->quantity_lost; if ($returned + $lost > $maxReturn) { throw new InvalidArgumentException('مقدار برگشت/ضایعات بیش از حد مجاز است: '.$item->displayName()); } $deltaReturn = $returned - (float) $item->quantity_returned; if ($deltaReturn > 0 && $item->stock_deducted && $item->product_id && $item->variation_id && $item->location_id) { $this->productUtil->updateProductQuantity( $item->location_id, $item->product_id, $item->variation_id, $deltaReturn, 0, null, false ); } $item->update([ 'quantity_returned' => $returned, 'quantity_lost' => $lost, 'status' => $returned + $lost >= (float) $item->quantity_issued ? 'closed' : 'partially_returned', ]); } $mission->update(['status' => 'returning']); return $mission->fresh(['items']); }); } public function returnTools(FieldMission $mission, ?User $user = null): void { foreach ($mission->items()->where('item_type', 'tool')->where('stock_deducted', true)->get() as $item) { if (! $item->tool_id) { continue; } $tool = MaintenanceTool::find($item->tool_id); if ($tool && $tool->status === 'checked_out') { $this->toolCribService->returnTool($mission->business, $tool, 'برگشت مأموریت '.$mission->mission_number, $user); } } } public function closeMission(FieldMission $mission, ?User $user = null): FieldMission { return DB::transaction(function () use ($mission, $user) { $this->returnTools($mission, $user); $this->recalculateTotals($mission); $mission->update([ 'status' => 'closed', 'actual_return_at' => now(), ]); $this->syncSourceCost($mission); return $mission->fresh(['crewMembers.user', 'items', 'expenses']); }); } public function addExpense(FieldMission $mission, array $data): MissionExpense { $expense = MissionExpense::create([ 'mission_id' => $mission->id, 'crew_member_id' => $data['crew_member_id'] ?? null, 'expense_type' => $data['expense_type'] ?? 'other', 'description' => $data['description'] ?? null, 'amount' => (int) ($data['amount'] ?? 0), 'expense_date' => ! empty($data['expense_date']) ? Carbon::parse($data['expense_date']) : null, 'receipt_ref' => $data['receipt_ref'] ?? null, 'vendor_name' => $data['vendor_name'] ?? null, 'paid_by' => $data['paid_by'] ?? 'company', 'notes' => $data['notes'] ?? null, ]); $this->recalculateTotals($mission); return $expense; } public function removeExpense(MissionExpense $expense): void { $mission = $expense->mission; $expense->delete(); $this->recalculateTotals($mission); } public function recalculateTotals(FieldMission $mission): void { $mission->load(['crewMembers', 'expenses']); $labor = 0; foreach ($mission->crewMembers as $member) { $member->recalculateLaborCost(); $member->save(); $labor += (int) $member->labor_cost + (int) $member->per_diem; } $expenses = (int) $mission->expenses->sum('amount'); $mission->update([ 'total_labor_cost' => $labor, 'total_expense_cost' => $expenses, 'total_cost' => $labor + $expenses, ]); } protected function syncSourceCost(FieldMission $mission): void { if (! $mission->source_id) { return; } if ($mission->source_type === 'work_order') { WorkOrder::where('id', $mission->source_id)->update(['cost' => $mission->total_cost]); } elseif ($mission->source_type === 'overhaul') { Overhaul::where('id', $mission->source_id)->update(['actual_cost' => $mission->total_cost]); } } protected function resolveSourceMeta(int $businessId, string $sourceType, int $sourceId): array { return match ($sourceType) { 'overhaul' => $this->metaFromOverhaul($businessId, $sourceId), 'rebuild' => $this->metaFromRebuild($businessId, $sourceId), 'work_order' => $this->metaFromWorkOrder($businessId, $sourceId), 'inspection_visit' => $this->metaFromInspection($businessId, $sourceId), default => ['title' => 'مأموریت میدانی', 'mission_type' => 'other'], }; } protected function metaFromOverhaul(int $businessId, int $id): array { $o = Overhaul::where('business_id', $businessId)->findOrFail($id); return [ 'title' => 'اعزام — '.$o->title, 'mission_type' => 'overhaul', 'equipment_id' => $o->equipment_id, 'description' => $o->description, 'seed_crew_from_source' => true, ]; } protected function metaFromRebuild(int $businessId, int $id): array { $r = RebuildProject::where('business_id', $businessId)->findOrFail($id); return [ 'title' => 'اعزام — '.$r->title, 'mission_type' => 'commissioning', 'description' => $r->description, ]; } protected function metaFromWorkOrder(int $businessId, int $id): array { $wo = WorkOrder::where('business_id', $businessId)->findOrFail($id); return [ 'title' => 'اعزام — '.$wo->work_order_number, 'mission_type' => 'repair', 'equipment_id' => $wo->equipment_id, 'description' => $wo->description, 'seed_crew_from_source' => true, ]; } protected function metaFromInspection(int $businessId, int $id): array { $v = InspectionVisit::where('business_id', $businessId)->findOrFail($id); return [ 'title' => 'بازدید — '.($v->equipment?->name ?? '#'.$id), 'mission_type' => 'inspection', 'equipment_id' => $v->equipment_id, 'seed_crew_from_source' => true, ]; } /** * @return array */ public function getPanelData(Business $business, string $sourceType, int $sourceId): array { $mission = FieldMission::where('business_id', $business->id) ->where('source_type', $sourceType) ->where('source_id', $sourceId) ->whereNotIn('status', ['closed', 'cancelled']) ->with(['crewMembers.user', 'items.sparePart', 'items.tool', 'expenses.crewMember']) ->first(); return [ 'mission' => $mission, 'source_type' => $sourceType, 'source_id' => $sourceId, 'status_labels' => $this->statusLabels(), 'mission_type_labels' => $this->missionTypeLabels(), 'role_labels' => $this->roleLabels(), 'expense_type_labels' => $this->expenseTypeLabels(), 'locations' => $this->stockService->locationsForBusiness($business->id), ]; } public function statusLabels(): array { return [ 'draft' => __('maintenance::lang.fm_status_draft'), 'planned' => __('maintenance::lang.fm_status_planned'), 'dispatched' => __('maintenance::lang.fm_status_dispatched'), 'on_site' => __('maintenance::lang.fm_status_on_site'), 'returning' => __('maintenance::lang.fm_status_returning'), 'closed' => __('maintenance::lang.fm_status_closed'), 'cancelled' => __('maintenance::lang.fm_status_cancelled'), ]; } public function missionTypeLabels(): array { return [ 'commissioning' => __('maintenance::lang.fm_type_commissioning'), 'overhaul' => __('maintenance::lang.fm_type_overhaul'), 'repair' => __('maintenance::lang.fm_type_repair'), 'inspection' => __('maintenance::lang.fm_type_inspection'), 'preventive' => __('maintenance::lang.fm_type_preventive'), 'other' => __('maintenance::lang.fm_type_other'), ]; } public function roleLabels(): array { return [ 'lead' => __('maintenance::lang.fm_role_lead'), 'supervisor' => __('maintenance::lang.fm_role_supervisor'), 'technician' => __('maintenance::lang.fm_role_technician'), 'driver' => __('maintenance::lang.fm_role_driver'), 'other' => __('maintenance::lang.fm_role_other'), ]; } public function expenseTypeLabels(): array { return [ 'transport' => __('maintenance::lang.fm_exp_transport'), 'travel' => __('maintenance::lang.fm_exp_travel'), 'accommodation' => __('maintenance::lang.fm_exp_accommodation'), 'per_diem' => __('maintenance::lang.fm_exp_per_diem'), 'labor' => __('maintenance::lang.fm_exp_labor'), 'material' => __('maintenance::lang.fm_exp_material'), 'other' => __('maintenance::lang.fm_exp_other'), ]; } }