[ 'label' => 'Engineering Drawing', 'column' => 'pdf_engineering', 'folder' => 'storage/documents/001_Drawings/001_Engineering', ], 'spool' => [ 'label' => 'Spool Drawing', 'column' => 'pdf_spool_iso', 'folder' => 'storage/documents/001_Drawings/002_Iso', ], 'as_build' => [ 'label' => 'As-Build Drawing', 'column' => 'pdf_as_build', 'folder' => 'storage/documents/001_Drawings/003_As_Build(ID)', ], ]; public function drawingTypes(): JsonResponse { $types = collect(self::DRAWING_TYPES)->map(function (array $config, string $key) { return [ 'value' => $key, 'label' => $config['label'], ]; })->values(); return response()->json([ 'success' => true, 'data' => $types, ]); } public function drawings(Request $request): JsonResponse { $type = $request->query('type', 'engineering'); $config = self::DRAWING_TYPES[$type] ?? null; if ($config === null) { return response()->json([ 'success' => false, 'message' => 'Unsupported drawing type', ], 422); } $column = $config['column']; $query = DocumentRevision::query() ->whereNotNull('drawing_no') ->where('drawing_no', '!=', ''); if ($column) { $query->whereNotNull($column)->where($column, '!=', ''); } $drawings = $query ->select('drawing_no') ->distinct() ->orderBy('drawing_no') ->get() ->map(fn ($row) => [ 'value' => $row->drawing_no, 'label' => $row->drawing_no, ]); if ($drawings->isEmpty() && isset($config['folder'])) { $drawings = collect($this->scanDrawingFolder($config['folder'])) ->map(fn (string $name) => [ 'value' => $name, 'label' => $name, ]); } return response()->json([ 'success' => true, 'data' => $drawings, ]); } public function files(Request $request): JsonResponse { $type = $request->query('type'); $drawing = $request->query('drawing'); if (!$type || !$drawing) { return response()->json([ 'success' => false, 'message' => 'Drawing type and drawing number are required', ], 422); } $config = self::DRAWING_TYPES[$type] ?? null; if ($config === null) { return response()->json([ 'success' => false, 'message' => 'Unsupported drawing type', ], 422); } $column = $config['column']; $files = collect(); if ($column) { $files = $this->buildFileListFromColumn($drawing, $column); } if ($files->isEmpty() && $type === 'as_build') { $fallbackColumns = ['working_documentation', 'axonometry']; foreach ($fallbackColumns as $fallbackColumn) { $files = $this->buildFileListFromColumn($drawing, $fallbackColumn); if ($files->isNotEmpty()) { break; } } } if ($files->isEmpty() && isset($config['folder'])) { $publicFolder = public_path($config['folder']); if (is_dir($publicFolder)) { $pattern = $publicFolder . '/' . $drawing . '*.pdf'; $matches = glob($pattern) ?: []; $files = collect($matches)->map(function (string $filePath) { $relativePath = $this->relativePublicPath($filePath); $filename = basename($filePath); return [ 'id' => md5($filePath), 'revision' => null, 'label' => $filename, 'url' => $relativePath ? url($relativePath) : null, 'path' => $relativePath, 'is_latest' => false, 'exists' => true, 'updated_at' => Carbon::createFromTimestamp(filemtime($filePath))->toDateTimeString(), ]; })->sortByDesc('updated_at')->values(); if ($files->isNotEmpty()) { $files = $files->map(function (array $item, int $index) { $item['is_latest'] = $index === 0; return $item; }); } } } return response()->json([ 'success' => true, 'data' => $files->values(), ]); } public function updateDocumentStage(Request $request): JsonResponse { $validated = $request->validate([ 'drawing' => ['required', 'string'], 'stage' => ['required', Rule::in(['pdf_spool_iso', 'pdf_as_build'])], 'pdf_path' => ['required', 'string'], 'revision_id' => ['nullable', 'integer'], ]); $pdfPath = trim($validated['pdf_path']); if ($pdfPath === '') { return response()->json([ 'success' => false, 'message' => 'PDF path cannot be empty.', ], 422); } $query = DocumentRevision::query() ->where('drawing_no', $validated['drawing']); if (!empty($validated['revision_id'])) { $query->where('id', $validated['revision_id']); } $updatedRows = $query->update([ $validated['stage'] => $pdfPath, ]); if ($updatedRows === 0) { return response()->json([ 'success' => false, 'message' => 'No matching document revisions found for update.', ], 404); } return response()->json([ 'success' => true, 'updated_rows' => $updatedRows, 'stage' => $validated['stage'], 'pdf_path' => $pdfPath, ]); } private function buildFileListFromColumn(string $drawing, string $column): \Illuminate\Support\Collection { $records = DocumentRevision::query() ->where('drawing_no', $drawing) ->whereNotNull($column) ->where($column, '!=', '') ->orderByDesc('publish_date') ->orderByDesc('updated_at') ->get(['id', 'drawing_no', 'revision_no', 'publish_date', 'updated_at', $column]); if ($records->isEmpty()) { return collect(); } $latestId = optional($records->first())->id; return $records->map(function (DocumentRevision $revision) use ($latestId, $column) { $path = $revision->{$column}; if (!$path) { return null; } $fullPath = public_path($path); $exists = $fullPath ? file_exists($fullPath) : false; $timestamp = $revision->publish_date ? Carbon::parse($revision->publish_date)->startOfDay() : ($revision->updated_at instanceof Carbon ? $revision->updated_at : Carbon::parse($revision->updated_at)); return [ 'id' => $revision->id, 'revision' => $revision->revision_no, 'label' => $this->formatLabel($revision->revision_no, $path), 'url' => $path ? url($path) : null, 'path' => $path, 'is_latest' => $revision->id === $latestId, 'exists' => $exists, 'updated_at' => $timestamp?->toDateTimeString(), ]; })->filter()->values(); } public function weldData(Request $request): JsonResponse { $drawing = trim((string) $request->query('drawing', '')); if ($drawing === '') { return response()->json([ 'success' => false, 'message' => 'Drawing number is required', ], 422); } $logs = WeldLog::query() ->where('iso_number', $drawing) ->select([ 'spool_number', 'spool_number2', 'no_of_the_joint_as_per_as_built_survey', 'type_of_joint', 'welder_1', 'welder_2', 'real_welder_1', 'real_welder_2', ]) ->orderBy('spool_number') ->get(); if ($logs->isEmpty()) { return response()->json([ 'success' => true, 'data' => [ 'assignments' => [], ], ]); } $assignmentMap = []; foreach ($logs as $log) { $spool = $this->sanitizeValue($log->spool_number) ?? $this->sanitizeValue($log->spool_number2); $joint = $this->sanitizeValue($log->no_of_the_joint_as_per_as_built_survey); $jointType = $this->sanitizeValue($log->type_of_joint); if (!$spool && !$joint) { continue; } $key = $spool . '|' . $joint; if (!isset($assignmentMap[$key])) { $assignmentMap[$key] = [ 'spool' => $spool, 'joint' => $joint, 'joint_type' => $jointType ? strtoupper($jointType) : null, 'welders' => [], ]; } elseif ($jointType && empty($assignmentMap[$key]['joint_type'])) { $assignmentMap[$key]['joint_type'] = strtoupper($jointType); } $welders = [ $this->sanitizeValue($log->real_welder_1), $this->sanitizeValue($log->real_welder_2), $this->sanitizeValue($log->welder_1), $this->sanitizeValue($log->welder_2), ]; foreach ($welders as $welder) { if ($welder) { $assignmentMap[$key]['welders'][$welder] = $welder; } } } $assignments = collect($assignmentMap) ->map(function (array $assignment) { $assignment['welders'] = array_values($assignment['welders']); return $assignment; }) ->values(); return response()->json([ 'success' => true, 'data' => [ 'assignments' => $assignments, ], ]); } private function formatLabel(?string $revision, ?string $path): string { $filename = $path ? basename($path) : null; if ($revision && $filename) { return sprintf('Rev %s — %s', $revision, $filename); } if ($revision) { return sprintf('Rev %s', $revision); } return $filename ?? 'Unknown PDF'; } private function relativePublicPath(string $absolutePath): ?string { $publicPath = public_path(); if (Str::startsWith($absolutePath, $publicPath)) { return ltrim(str_replace($publicPath, '', $absolutePath), '/'); } return null; } private function sanitizeValue($value): ?string { if ($value === null) { return null; } $trimmed = trim((string) $value); return $trimmed === '' ? null : $trimmed; } /** * @return string[] */ private function scanDrawingFolder(string $relativeFolder): array { $basePath = public_path($relativeFolder); if (!is_dir($basePath)) { return []; } $files = glob($basePath . DIRECTORY_SEPARATOR . '*.pdf') ?: []; if (empty($files)) { return []; } $names = collect($files) ->map(fn (string $path) => pathinfo($path, PATHINFO_FILENAME)) ->filter() ->unique() ->sort() ->values() ->all(); return $names; } /** * Get IC Log members for a drawing * Note: Currently drawing_number field is empty in database, so we return all members * TODO: When drawing_number is populated, filter by drawing_number */ public function getICLogMembers(Request $request): JsonResponse { $request->validate(['drawing' => 'required|string']); $drawingNumber = $request->drawing; // First try to get by drawing_number (when it's populated) $members = IncomingControl::where('drawing_number', $drawingNumber) ->whereNotNull('description_ru') ->where('description_ru', '!=', '') ->distinct('description_ru') ->pluck('description_ru') ->filter() ->values() ->toArray(); // If no results found by drawing_number, return all members // (This is a fallback since drawing_number field is currently empty) if (empty($members)) { $members = IncomingControl::whereNotNull('description_ru') ->where('description_ru', '!=', '') ->distinct('description_ru') ->pluck('description_ru') ->filter() ->values() ->toArray(); } return response()->json([ 'success' => true, 'data' => $members, 'filtered_by_drawing' => !empty($members) && IncomingControl::where('drawing_number', $drawingNumber)->exists() ]); } /** * Get IC Log member details */ public function getICLogMemberDetails(Request $request): JsonResponse { $request->validate(['member_description' => 'required|string']); $memberDescription = $request->member_description; $details = IncomingControl::where('description_ru', $memberDescription) ->select( 'material', 'odmm_1 as diameter', // Outer diameter in mm 'dn_1 as nps', // Nominal pipe size 'thicknessmm_1 as thickness' // Thickness in mm ) ->first(); // Get the first matching record return response()->json(['success' => true, 'data' => $details]); } /** * Check existing weldmap data for a drawing */ public function checkExistingWeldmapData(Request $request): JsonResponse { $request->validate(['drawing' => 'required|string']); $drawingNumber = $request->drawing; $existingJoints = WeldLog::where('iso_number', $drawingNumber) ->select( 'no_of_the_joint_as_per_as_built_survey as joint_no', 'joint_type', 'welder_1', 'welder_2', 'spool_number_1 as spool_no_1', 'spool_number_2 as spool_no_2', 'pose_number_1 as pose_no_1', 'pose_number_2 as pose_no_2', 'member_1', 'member_2', 'material_1', 'material_2', 'dia_1', 'dia_2', 'nps_1', 'nps_2', 'thickness_1', 'thickness_2', 'welding_date' ) ->get(); return response()->json([ 'success' => true, 'has_data' => $existingJoints->isNotEmpty(), 'joints' => $existingJoints ]); } /** * Get qualified welders based on material, welding method, and expiry dates * Phase 3.1: API Endpoints for Joint Data * Phase 6: Welder Qualification Filtering */ public function getQualifiedWelders(Request $request): JsonResponse { $request->validate([ 'drawing_number' => 'required|string', 'material' => 'nullable|string', 'welding_method' => 'nullable|string', 'joint_no' => 'nullable|string', 'skip_qualification' => 'nullable|boolean', // Skip qualification check if true ]); try { $drawingNumber = $request->drawing_number; $jointNo = $request->joint_no; $skipQualification = $request->boolean('skip_qualification', false); Log::debug('Phase 6: getQualifiedWelders called', [ 'drawing_number' => $drawingNumber, 'joint_no' => $jointNo, 'skip_qualification' => $skipQualification ]); // If skip_qualification is true, return all welders from weld_logs (legacy behavior) if ($skipQualification) { return $this->getWeldersFromWeldLogs($drawingNumber, $jointNo); } // Get joint data from weld_logs to determine qualification criteria $jointData = null; if (!empty($jointNo)) { $jointData = WeldLog::where('iso_number', $drawingNumber) ->where('no_of_the_joint_as_per_as_built_survey', $jointNo) ->first(); } // If no joint data found, try to get any record for this drawing if (!$jointData) { $jointData = WeldLog::where('iso_number', $drawingNumber)->first(); } // If still no data, return empty list with message if (!$jointData) { Log::debug('Phase 6: No joint data found for qualification', [ 'drawing_number' => $drawingNumber, 'joint_no' => $jointNo ]); return response()->json([ 'success' => true, 'data' => [], 'count' => 0, 'message' => 'No joint data found for qualification filtering' ]); } // Extract qualification criteria from joint data $criteria = $this->extractQualificationCriteria($jointData); Log::debug('Phase 6: Qualification criteria extracted', $criteria); // Get qualified welders from WelderTest table $qualifiedWelders = $this->filterWeldersByQualification($criteria, $drawingNumber); return response()->json([ 'success' => true, 'data' => $qualifiedWelders, 'count' => count($qualifiedWelders), 'criteria' => $criteria, 'message' => 'Welders filtered by qualification' ]); } catch (\Exception $e) { Log::error('Phase 6: getQualifiedWelders error', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return response()->json([ 'success' => false, 'message' => 'Failed to get qualified welders', 'error' => $e->getMessage() ], 500); } } /** * Get welders from weld_logs without qualification filtering (legacy behavior) */ private function getWeldersFromWeldLogs(string $drawingNumber, ?string $jointNo): JsonResponse { $query = WeldLog::where('iso_number', $drawingNumber) ->whereNotNull('welder_1') ->where('welder_1', '!=', ''); if (!empty($jointNo)) { $query->where('no_of_the_joint_as_per_as_built_survey', $jointNo); } $welders1 = $query->pluck('welder_1')->unique()->filter()->values(); $query2 = WeldLog::where('iso_number', $drawingNumber) ->whereNotNull('welder_2') ->where('welder_2', '!=', ''); if (!empty($jointNo)) { $query2->where('no_of_the_joint_as_per_as_built_survey', $jointNo); } $welders2 = $query2->pluck('welder_2')->unique()->filter()->values(); $allWelders = $welders1->merge($welders2)->unique()->sort()->values(); return response()->json([ 'success' => true, 'data' => $allWelders->toArray(), 'count' => $allWelders->count(), 'message' => 'Welders from weld_logs (no qualification filtering)' ]); } /** * Extract qualification criteria from joint data */ private function extractQualificationCriteria(WeldLog $jointData): array { return [ 'material_group_1' => $jointData->ru_material_group_1 ?? null, 'material_group_2' => $jointData->ru_material_group_2 ?? null, 'welding_method' => $jointData->welding_method ?? null, 'welding_date' => $jointData->welding_date ?? now()->toDateString(), 'outside_diameter' => $jointData->outside_diameter_1 ?? null, 'wall_thickness' => $jointData->wall_thickness_1 ?? null, 'iso_number' => $jointData->iso_number ?? null, ]; } /** * Filter welders by qualification criteria using WelderTest table * Based on existing welder-assignment/welder_1.php logic */ private function filterWeldersByQualification(array $criteria, string $drawingNumber): array { $logKey = 'PHASE6_WELDER_FILTER_' . uniqid(); Log::debug("[$logKey] Starting welder qualification filtering", $criteria); $qualifiedWelders = []; $today = Carbon::now(); $welderQualificationExpiredDays = (int) setting("WelderQualitificationStatusExpiredDate", 180); // Build material combinations for qualification check $qualifiedCombinations = $this->buildMaterialCombinations( $criteria['material_group_1'], $criteria['material_group_2'] ); Log::debug("[$logKey] Material combinations", [ 'combinations' => $qualifiedCombinations ]); // If no material info, skip qualification and return welders from weld_logs if (empty($qualifiedCombinations)) { Log::debug("[$logKey] No material combinations, falling back to weld_logs welders"); return $this->getWeldersFromWeldLogsArray($drawingNumber); } // Build base query with WelderTest $query = WelderTest::leftJoin('naks_welders', function($join) { $join->on('naks_welders.naks_certificate_no', '=', 'welder_tests.naks_no'); }); // Filter by material combinations $query->where(function ($q) use ($qualifiedCombinations) { foreach ($qualifiedCombinations as $combo) { $q->orWhere(function($subQ) use ($combo) { $subQ->where("material_group_1", "like", "%{$combo['mat1']}%") ->where("material_group_2", "like", "%{$combo['mat2']}%"); }); } }); // Filter by diameter if available if (!empty($criteria['outside_diameter']) && is_numeric($criteria['outside_diameter'])) { $diameter = (float) $criteria['outside_diameter']; $query->where("dia_min", "<=", $diameter) ->where(function($q) use ($diameter) { $q->where("dia_max", ">=", $diameter) ->orWhereNull("dia_max") ->orWhere("dia_max", 0); }); } // Filter by thickness if available if (!empty($criteria['wall_thickness']) && is_numeric($criteria['wall_thickness'])) { $thickness = (float) $criteria['wall_thickness']; $query->where("thk_min", "<=", $thickness) ->where(function($q) use ($thickness) { $q->where("thk_max", ">=", $thickness) ->orWhereNull("thk_max") ->orWhere("thk_max", 0); }); } // Filter by welding method if available if (!empty($criteria['welding_method'])) { $weldingMethod = $criteria['welding_method']; $weldingMethodParts = explode(",", $weldingMethod); $reverseWeldingMethod = implode(",", array_reverse($weldingMethodParts)); $query->where(function($q) use ($weldingMethod, $reverseWeldingMethod) { $q->where("welding_method", $weldingMethod) ->orWhere("welding_method", $reverseWeldingMethod); }); } // Filter by approval date (must be approved before welding date) if (!empty($criteria['welding_date'])) { $query->whereDate("approval_date", "<=", $criteria['welding_date']); } // Select and group results $results = $query->select( 'welder_tests.naks_id', 'welder_tests.naks_validity', DB::raw('MIN(naks_welders.period_of_validity) as period_of_validity') ) ->groupBy('welder_tests.naks_id', 'welder_tests.naks_validity') ->get(); Log::debug("[$logKey] Query results", [ 'count' => $results->count() ]); // Process results and add status indicators foreach ($results as $welder) { $welderDisplay = $welder->naks_id; // Check NAKS validity if (!empty($welder->naks_validity)) { try { $naksValidity = Carbon::parse($welder->naks_validity); $remainingDays = $today->diffInDays($naksValidity, false); if ($remainingDays < 10 && $remainingDays >= 0) { $welderDisplay = "❗" . $welderDisplay; // Expiring soon } elseif ($remainingDays < 0) { $welderDisplay = "❌" . $welderDisplay; // Expired continue; // Skip expired welders } } catch (\Exception $e) { // Invalid date format, skip validity check } } // Check if welder is already assigned to this ISO $isAssigned = WeldLog::where('iso_number', $drawingNumber) ->where(function($q) use ($welder) { $q->where('welder_1', 'like', "%{$welder->naks_id}%") ->orWhere('real_welder_1', 'like', "%{$welder->naks_id}%"); }) ->exists(); if ($isAssigned) { $welderDisplay = "🟡" . $welderDisplay; // Already assigned } // Check period of validity if (!empty($welder->period_of_validity) && !empty($criteria['welding_date'])) { try { $weldingDate = Carbon::parse($criteria['welding_date']); $periodOfValidity = Carbon::parse($welder->period_of_validity); $expiredDate = $periodOfValidity->copy()->addDays($welderQualificationExpiredDays); if ($weldingDate->greaterThan($expiredDate)) { $welderDisplay = "❌📅" . $welderDisplay; // Welding date expired continue; // Skip } } catch (\Exception $e) { // Invalid date format, skip check } } $qualifiedWelders[] = $welderDisplay; } Log::debug("[$logKey] Qualified welders", [ 'count' => count($qualifiedWelders), 'welders' => $qualifiedWelders ]); return array_values(array_unique($qualifiedWelders)); } /** * Build material combinations for qualification check */ private function buildMaterialCombinations(?string $mat1, ?string $mat2): array { if (empty($mat1) && empty($mat2)) { return []; } $mat1 = $mat1 ?? ''; $mat2 = $mat2 ?? $mat1; $isSameMaterial = ($mat1 === $mat2); $materialCombination = $isSameMaterial ? $mat1 : "{$mat1}+{$mat2}"; $qualifiedCombinations = []; // Check MaterialGroupMap for qualified materials $exactMatch = MaterialGroupMap::where("material_name", $materialCombination)->first(); if ($exactMatch && !empty($exactMatch->qualified_materials)) { $qualifiedMaterialsList = explode(",", $exactMatch->qualified_materials); foreach ($qualifiedMaterialsList as $combo) { $combo = trim($combo); $parts = explode("+", $combo); if (count($parts) == 2) { $qualifiedCombinations[] = ['mat1' => trim($parts[0]), 'mat2' => trim($parts[1])]; } elseif (count($parts) == 1) { $qualifiedCombinations[] = ['mat1' => trim($parts[0]), 'mat2' => trim($parts[0])]; } } } // Always include original combination if ($isSameMaterial) { $qualifiedCombinations[] = ['mat1' => $mat1, 'mat2' => $mat2]; } else { $qualifiedCombinations[] = ['mat1' => $mat1, 'mat2' => $mat2]; $qualifiedCombinations[] = ['mat1' => $mat2, 'mat2' => $mat1]; // Reverse } return $qualifiedCombinations; } /** * Get welders from weld_logs as array (helper for fallback) */ private function getWeldersFromWeldLogsArray(string $drawingNumber): array { $welders1 = WeldLog::where('iso_number', $drawingNumber) ->whereNotNull('welder_1') ->where('welder_1', '!=', '') ->pluck('welder_1') ->unique() ->filter() ->values(); $welders2 = WeldLog::where('iso_number', $drawingNumber) ->whereNotNull('welder_2') ->where('welder_2', '!=', '') ->pluck('welder_2') ->unique() ->filter() ->values(); return $welders1->merge($welders2)->unique()->sort()->values()->toArray(); } /** * Get type of weld options from joint_types table * Phase 3: Type of Weld dropdown options */ public function getTypeOfWeldOptions(Request $request): JsonResponse { try { $options = \DB::table('joint_types') ->select('short_name_en') ->whereNotNull('short_name_en') ->where('short_name_en', '!=', '') ->orderBy('short_name_en') ->distinct() ->pluck('short_name_en') ->filter() ->values() ->toArray(); // Fallback: If no options found, use default types if (empty($options)) { $options = ['BW', 'FW', 'SW', 'TW', 'BJ', 'FJ', 'TH', 'CJ']; } return response()->json([ 'success' => true, 'data' => $options ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Failed to get type of weld options', 'error' => $e->getMessage() ], 500); } } }