resolveBusiness($request); if (! $business) { return $this->jsonError('کسب‌وکار یافت نشد.', 404); } $query = InspectionVisit::where('business_id', $business->id) ->with([ 'equipment:id,code,name', 'productionLine:id,name,code,project_id', 'productionLine.project:id,name', 'project:id,name', 'inspector:id,name', 'serviceProvider:id,name', ]) ->orderByDesc('visit_date'); if ($equipmentId = $request->get('equipment_id')) { $query->where('equipment_id', $equipmentId); } if ($projectId = $request->get('project_id')) { $query->where(function ($q) use ($projectId) { $q->where('project_id', $projectId) ->orWhereHas('productionLine', fn ($lq) => $lq->where('project_id', $projectId)) ->orWhereHas('equipment.productionLine', fn ($lq) => $lq->where('project_id', $projectId)); }); } if ($lineId = $request->get('production_line_id')) { $query->where('production_line_id', $lineId); } $paginator = $query->paginate((int) $request->get('per_page', 15)); return $this->jsonSuccess([ 'items' => $paginator->items(), 'pagination' => [ 'total' => $paginator->total(), 'per_page' => $paginator->perPage(), 'current_page' => $paginator->currentPage(), 'last_page' => $paginator->lastPage(), ], ]); } public function show(Request $request, InspectionVisit $inspectionVisit) { if (! $this->ensureBusinessOwnership($request, $inspectionVisit->business_id)) { return $this->jsonError('دسترسی ندارید.', 403); } return $this->jsonSuccess($this->formatVisitDetail($inspectionVisit)); } private function formatVisitDetail(InspectionVisit $visit): array { $visit->load([ 'equipment:id,code,name,manufacturer,model,serial_number,location,production_line_id,health_percentage', 'equipment.productionLine:id,name,code,location,project_id', 'equipment.productionLine.project:id,name', 'project:id,name', 'productionLine:id,name,code,location,project_id', 'productionLine.project:id,name', 'inspector:id,name,email', 'serviceProvider:id,name,provider_type,capability', 'items.equipmentPart:id,part_name,part_code,part_type,is_critical', 'approval.approver:id,name,email', 'checklistResponse.template:id,name,description', 'checklistResponse.template.items.equipmentPart:id,part_name,part_code', 'checklistResponse.completedByUser:id,name', ]); $relatedReplacements = collect(); $relatedServices = collect(); if ($visit->equipment_id) { $visitDate = $visit->visit_date?->toDateString(); $relatedReplacements = PartReplacement::where('business_id', $visit->business_id) ->where('equipment_id', $visit->equipment_id) ->when($visitDate, function ($q) use ($visitDate) { $q->whereBetween('replaced_at', [ \Carbon\Carbon::parse($visitDate)->subDays(30)->toDateString(), \Carbon\Carbon::parse($visitDate)->addDays(30)->toDateString(), ]); }) ->with(['equipmentPart:id,part_name,part_code', 'sparePart:id,name,code', 'performer:id,name']) ->orderByDesc('replaced_at') ->get(); $relatedServices = \Modules\Maintenance\Models\ServiceRecord::where('business_id', $visit->business_id) ->where('equipment_id', $visit->equipment_id) ->when($visitDate, function ($q) use ($visitDate) { $q->whereBetween('started_at', [ \Carbon\Carbon::parse($visitDate)->subDays(30)->startOfDay(), \Carbon\Carbon::parse($visitDate)->addDays(30)->endOfDay(), ]); }) ->with(['performer:id,name', 'repairProvider:id,name']) ->orderByDesc('started_at') ->limit(10) ->get(); } $checklistItems = $this->buildChecklistItems($visit); $partsNeedingReplacement = $visit->items->where('needs_replacement', true)->values(); $data = $visit->toArray(); $data['report_number'] = 'INV-'.str_pad((string) $visit->id, 6, '0', STR_PAD_LEFT); $data['checklist_items'] = $checklistItems; $data['related_replacements'] = $relatedReplacements; $data['related_services'] = $relatedServices; $data['parts_needing_replacement'] = $partsNeedingReplacement; $data['stats'] = [ 'parts_inspected' => $visit->items->count(), 'parts_needing_replacement' => $partsNeedingReplacement->count(), 'parts_critical_condition' => $visit->items->whereIn('condition', ['poor', 'critical'])->count(), 'checklist_total' => count($checklistItems), 'checklist_passed' => collect($checklistItems)->where('status', 'pass')->count(), 'checklist_failed' => collect($checklistItems)->where('status', 'fail')->count(), 'checklist_warning' => collect($checklistItems)->where('status', 'warning')->count(), 'checklist_completion' => count($checklistItems) ? round((collect($checklistItems)->whereNotNull('answer')->where('answer', '!=', '')->count() / count($checklistItems)) * 100) : null, 'replacements_near_visit' => $relatedReplacements->count(), 'services_near_visit' => $relatedServices->count(), 'has_approval' => (bool) $visit->approval_id, ]; return $data; } private function buildChecklistItems(InspectionVisit $visit): array { if (! $visit->checklistResponse?->template) { return []; } $responses = $visit->checklistResponse->responses ?? []; $items = []; foreach ($visit->checklistResponse->template->items->sortBy('sort_order') as $item) { $answer = $responses[$item->id] ?? $responses[(string) $item->id] ?? null; $items[] = [ 'id' => $item->id, 'label' => $item->label, 'item_type' => $item->item_type, 'is_required' => $item->is_required, 'equipment_part' => $item->equipmentPart, 'answer' => $answer, 'status' => $this->checklistItemStatus($item->item_type, $answer), ]; } return $items; } private function checklistItemStatus(?string $itemType, mixed $answer): string { if ($answer === null || $answer === '') { return 'pending'; } $v = (string) $answer; return match ($itemType) { 'pass_fail' => $v === 'fail' ? 'fail' : ($v === 'pass' ? 'pass' : 'neutral'), 'yes_no' => $v === 'no' ? 'warning' : ($v === 'yes' ? 'pass' : 'neutral'), default => 'neutral', }; } public function store(Request $request) { $business = $this->resolveBusiness($request); if (! $business) { return $this->jsonError('کسب‌وکار یافت نشد.', 404); } $data = $request->validate([ 'project_id' => ['nullable', 'exists:projects,id'], 'equipment_id' => ['nullable', 'exists:maintenance_equipment,id'], 'production_line_id' => ['nullable', 'exists:maintenance_production_lines,id'], 'visit_date' => ['required', 'date'], 'health_percentage' => ['nullable', 'numeric', 'min:0', 'max:100'], 'working_hours_at_visit' => ['nullable', 'numeric', 'min:0'], 'visit_type' => ['sometimes', 'string', 'in:routine,periodic,emergency,commissioning,project_audit'], 'inspector_id' => ['nullable', 'exists:users,id'], 'service_provider_id' => ['nullable', 'exists:maintenance_service_providers,id'], 'summary' => ['nullable', 'string'], 'attachments' => ['nullable', 'array'], 'status' => ['sometimes', 'string', 'in:draft,completed,approved'], 'items' => ['nullable', 'array'], 'items.*.equipment_part_id' => ['nullable', 'exists:maintenance_equipment_parts,id'], 'items.*.part_label' => ['required_with:items', 'string', 'max:255'], 'items.*.health_percentage' => ['nullable', 'numeric', 'min:0', 'max:100'], 'items.*.condition' => ['nullable', 'string', 'in:good,fair,poor,critical'], 'items.*.notes' => ['nullable', 'string'], 'items.*.photos' => ['nullable', 'array'], 'items.*.needs_replacement' => ['sometimes', 'boolean'], 'items.*.expected_replacement_date' => ['nullable', 'date'], 'approval' => ['nullable', 'array'], 'approval.approval_type' => ['required_with:approval', 'string', 'in:signature_canvas,signature_photo,stamp_photo'], 'approval.signature_data' => ['nullable', 'string'], 'approval.photo_path' => ['nullable', 'string'], 'approval.approver_name' => ['nullable', 'string', 'max:255'], 'approval.approver_role' => ['nullable', 'string', 'max:100'], 'approval.notes' => ['nullable', 'string'], 'checklist_template_id' => ['nullable', 'exists:maintenance_checklist_templates,id'], 'checklist_responses' => ['nullable', 'array'], ]); if (empty($data['equipment_id']) && empty($data['project_id'])) { return $this->jsonError('پروژه یا تجهیز باید مشخص شود.', 422); } $equipment = null; if (! empty($data['equipment_id'])) { $equipment = Equipment::find($data['equipment_id']); if (! $equipment || $equipment->business_id !== $business->id) { return $this->jsonError('تجهیز یافت نشد.', 404); } } if (! empty($data['project_id'])) { $project = Project::where('business_id', $business->id)->find($data['project_id']); if (! $project) { return $this->jsonError('پروژه یافت نشد.', 404); } } if (! empty($data['production_line_id'])) { $line = ProductionLine::where('business_id', $business->id)->find($data['production_line_id']); if (! $line) { return $this->jsonError('خط تولید یافت نشد.', 404); } if (! empty($data['project_id']) && $line->project_id && $line->project_id != $data['project_id']) { return $this->jsonError('خط تولید به این پروژه تعلق ندارد.', 422); } $data['project_id'] = $data['project_id'] ?? $line->project_id; } $visit = DB::transaction(function () use ($business, $data, $request, $equipment) { $approvalId = null; if (! empty($data['approval'])) { $approval = MaintenanceApproval::create([ 'business_id' => $business->id, 'approvable_type' => (new InspectionVisit)->getMorphClass(), 'approvable_id' => 0, 'approval_type' => $data['approval']['approval_type'], 'signature_data' => $data['approval']['signature_data'] ?? null, 'photo_path' => $data['approval']['photo_path'] ?? null, 'approver_id' => $request->user()?->id, 'approver_name' => $data['approval']['approver_name'] ?? $request->user()?->name, 'approver_role' => $data['approval']['approver_role'] ?? null, 'approved_at' => now(), 'notes' => $data['approval']['notes'] ?? null, ]); $approvalId = $approval->id; } $visit = InspectionVisit::create([ 'business_id' => $business->id, 'project_id' => $data['project_id'] ?? null, 'equipment_id' => $equipment?->id, 'production_line_id' => $data['production_line_id'] ?? $equipment?->production_line_id, 'visit_date' => $data['visit_date'], 'health_percentage' => $data['health_percentage'] ?? null, 'working_hours_at_visit' => $data['working_hours_at_visit'] ?? $equipment?->working_hours, 'visit_type' => $data['visit_type'] ?? ($equipment ? 'routine' : 'project_audit'), 'inspector_id' => $data['inspector_id'] ?? $request->user()?->id, 'service_provider_id' => $data['service_provider_id'] ?? null, 'summary' => $data['summary'] ?? null, 'attachments' => $data['attachments'] ?? null, 'approval_id' => $approvalId, 'status' => $data['status'] ?? 'completed', ]); if ($approvalId) { MaintenanceApproval::where('id', $approvalId)->update(['approvable_id' => $visit->id]); } if (! empty($data['checklist_template_id']) && ! empty($data['checklist_responses'])) { $response = ChecklistResponse::create([ 'business_id' => $business->id, 'template_id' => $data['checklist_template_id'], 'equipment_id' => $equipment?->id, 'responses' => $data['checklist_responses'], 'completed_by' => $request->user()?->id, 'completed_at' => now(), ]); $visit->update(['checklist_response_id' => $response->id]); } foreach ($data['items'] ?? [] as $item) { InspectionVisitItem::create(array_merge($item, ['inspection_visit_id' => $visit->id])); } if ($equipment && isset($data['health_percentage'])) { $equipment->update(['health_percentage' => $data['health_percentage']]); } return $visit; }); return $this->jsonSuccess($this->formatVisitDetail($visit->fresh()), 'بازدید ثبت شد.', 201); } public function destroy(Request $request, InspectionVisit $inspectionVisit) { if (! $this->ensureBusinessOwnership($request, $inspectionVisit->business_id)) { return $this->jsonError('دسترسی ندارید.', 403); } $inspectionVisit->items()->delete(); $inspectionVisit->delete(); return $this->jsonSuccess(null, 'بازدید حذف شد.'); } }