340 lines
16 KiB
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, 'بازدید حذف شد.');
|
|
}
|
|
}
|