ultimatepos/app/Modules/Maintenance/Http/Controllers/Api/V1/InspectionVisitController.php

340 lines
16 KiB
PHP

<?php
namespace App\Modules\Maintenance\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Modules\Maintenance\Models\ChecklistResponse;
use App\Modules\Maintenance\Models\Equipment;
use App\Modules\Maintenance\Models\InspectionVisit;
use App\Modules\Maintenance\Models\InspectionVisitItem;
use App\Modules\Maintenance\Models\MaintenanceApproval;
use App\Modules\Maintenance\Models\ProductionLine;
use App\Modules\Maintenance\Models\PartReplacement;
use App\Modules\Maintenance\Traits\ResolvesBusiness;
use Modules\Project\Entities\Project;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class InspectionVisitController extends Controller
{
use ResolvesBusiness;
public function index(Request $request)
{
$business = $this->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 = \App\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:pjt_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, 'بازدید حذف شد.');
}
}