validate([ 'annotations' => 'required|array', 'annotations.*.shape_id' => 'required|string', 'annotations.*.spool_no' => 'nullable|string', 'annotations.*.joint_type' => 'nullable|string', 'annotations.*.welder_id' => 'nullable|string', 'annotations.*.classification' => 'nullable|string', 'annotations.*.page_number' => 'required|integer', 'annotations.*.x_coordinate' => 'required|numeric', 'annotations.*.y_coordinate' => 'required|numeric', 'annotations.*.width' => 'required|numeric', 'annotations.*.height' => 'required|numeric', 'annotations.*.shape_type' => 'required|in:rect,circle,triangle,spool,weld,shop_weld,leader_line,text,line', 'annotations.*.pdf_file_path' => 'required|string', 'annotations.*.created_by' => 'required|integer', 'annotations.*.original_start_point' => 'nullable|string', 'annotations.*.connected_shape_id' => 'nullable|string', ]); try { $annotations = []; foreach ($request->annotations as $annotationData) { // Check if annotation already exists $existingAnnotation = Annotation::where('shape_id', $annotationData['shape_id'])->first(); if ($existingAnnotation) { // Update existing annotation $existingAnnotation->update($annotationData); $annotations[] = $existingAnnotation; } else { // Create new annotation $annotations[] = Annotation::create($annotationData); } } return response()->json([ 'success' => true, 'data' => $annotations, 'message' => 'Annotations saved successfully!' ], 201); } catch (\Exception $e) { return response()->json([ 'success' => false, 'error' => $e->getMessage(), 'message' => 'Failed to save annotations' ], 500); } } public function index(Request $request): JsonResponse { $pdfFile = $request->get('pdf_file'); $annotations = Annotation::when($pdfFile, function ($query, $pdfFile) { return $query->where('pdf_file_path', $pdfFile); }) ->where('is_active', 1) ->orderBy('created_at', 'asc') ->get(); return response()->json([ 'success' => true, 'data' => $annotations ]); } public function show(Annotation $annotation): JsonResponse { return response()->json([ 'success' => true, 'data' => $annotation ]); } public function update(Request $request, Annotation $annotation): JsonResponse { $request->validate([ 'spool_no' => 'nullable|string', 'joint_type' => 'nullable|string', 'welder_id' => 'nullable|string', 'classification' => 'nullable|string', ]); $annotation->update($request->only(['spool_no', 'joint_type', 'welder_id', 'classification'])); return response()->json([ 'success' => true, 'data' => $annotation ]); } public function destroy(Annotation $annotation): JsonResponse { $annotation->delete(); return response()->json([ 'success' => true, 'message' => 'Annotation deleted successfully' ]); } public function deleteMultiple(Request $request): JsonResponse { $request->validate([ 'shape_ids' => 'required|array', 'shape_ids.*' => 'required|string', 'pdf_file' => 'required|string', 'page_number' => 'required|integer' ]); try { $deletedCount = Annotation::whereIn('shape_id', $request->shape_ids) ->where('pdf_file_path', $request->pdf_file) ->where('page_number', $request->page_number) ->delete(); return response()->json([ 'success' => true, 'deleted_count' => $deletedCount, 'message' => "Successfully deleted {$deletedCount} annotations from page {$request->page_number}" ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'error' => $e->getMessage(), 'message' => 'Failed to delete annotations' ], 500); } } public function sync(Request $request): JsonResponse { $request->validate([ 'annotations' => 'required|array', 'pdf_file' => 'required|string', 'page_number' => 'required|integer' ]); try { $pdfFile = $request->pdf_file; $pageNumber = $request->page_number; $userId = $request->user_id ?? auth()->id() ?? 1; $lastVersion = Annotation::where('pdf_file_path', 'like', '%' . basename($pdfFile) . '%') ->where('page_number', $pageNumber) ->max('version'); $newVersion = ($lastVersion ?? 0) + 1; $saved = []; foreach ($request->annotations as $data) { // Use updateOrCreate to prevent duplicates - update existing annotation with same shape_id, pdf_file_path, page_number // Only create new version if shape_id doesn't exist in current version $annotation = Annotation::updateOrCreate( [ 'shape_id' => $data['shape_id'], 'pdf_file_path' => $pdfFile, 'page_number' => $pageNumber, 'version' => $newVersion, 'is_active' => 1 ], [ 'spool_no' => $data['spool_no'] ?? null, 'joint_type' => $data['joint_type'] ?? null, 'welder_id' => $data['welder_id'] ?? null, 'classification' => $data['classification'] ?? null, 'x_coordinate' => $data['x_coordinate'] ?? 0, 'y_coordinate' => $data['y_coordinate'] ?? 0, 'width' => $data['width'] ?? 0, 'height' => $data['height'] ?? 0, 'shape_type' => $data['shape_type'] ?? 'rect', 'qr_code_data' => $data['qr_code_data'] ?? null, 'created_by' => $data['created_by'] ?? $userId, 'original_start_point' => $data['original_start_point'] ?? null, 'connected_shape_id' => $data['connected_shape_id'] ?? null, 'stroke_color' => $data['stroke_color'] ?? '#000000', 'stroke_width' => $data['stroke_width'] ?? 2, ] ); $saved[] = $annotation; } return response()->json([ 'success' => true, 'message' => "Page synced successfully as version {$newVersion}", 'version' => $newVersion, 'data' => $saved ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'error' => $e->getMessage(), 'message' => 'Failed to sync annotations' ], 500); } } public function exportPDF(Request $request) { try { $request->validate([ 'pdf_file' => 'required|string', 'annotations' => 'required|array', 'canvas_image' => 'required|string', 'export_format' => 'required|string', // Optional parameters for saving to Document Revision 'save_to_revision' => 'nullable|boolean', 'stage' => 'nullable|in:pdf_spool_iso,pdf_as_build', 'drawing_no' => 'nullable|string', 'revision_id' => 'nullable|integer', ]); // Canvas image'i decode et $imageData = $request->canvas_image; // Remove data:image/png;base64, part $imageData = str_replace('data:image/png;base64,', '', $imageData); $imageData = str_replace(' ', '+', $imageData); $decodedImage = base64_decode($imageData); // Temporary image file oluştur $tempImagePath = storage_path('app/temp_export_' . time() . '.png'); file_put_contents($tempImagePath, $decodedImage); // mPDF ile PDF oluştur $mpdf = new \Mpdf\Mpdf([ 'mode' => 'utf-8', 'format' => 'A4-L', // Landscape orientation 'margin_left' => 0, 'margin_right' => 0, 'margin_top' => 0, 'margin_bottom' => 0 ]); // Image'i PDF'e ekle $html = ''; $mpdf->WriteHTML($html); // Generate filename $originalFilename = basename($request->pdf_file, '.pdf'); $filename = 'annotated_' . $originalFilename . '_' . time() . '.pdf'; // Temporary file'ı sil if (file_exists($tempImagePath)) { unlink($tempImagePath); } // Check if we should save to Document Revision $saveToRevision = $request->input('save_to_revision', false); if ($saveToRevision && $request->filled('stage') && $request->filled('drawing_no')) { $stage = $request->input('stage'); $drawingNo = $request->input('drawing_no'); $revisionId = $request->input('revision_id'); // Determine storage folder based on stage $folderMap = [ 'pdf_spool_iso' => 'storage/documents/001_Drawings/002_Iso', 'pdf_as_build' => 'storage/documents/001_Drawings/003_As_Build(ID)', ]; $folder = $folderMap[$stage] ?? 'storage/documents/001_Drawings/003_As_Build(ID)'; $storagePath = public_path($folder); // Ensure directory exists if (!is_dir($storagePath)) { mkdir($storagePath, 0755, true); } // Generate unique filename with drawing number $safeDrawingNo = preg_replace('/[^A-Za-z0-9\-\_\.]/', '_', $drawingNo); $savedFilename = $safeDrawingNo . '_annotated_' . date('Ymd_His') . '.pdf'; $fullSavePath = $storagePath . '/' . $savedFilename; // Save PDF to storage $pdfContent = $mpdf->Output('', 'S'); file_put_contents($fullSavePath, $pdfContent); \Log::info('PDF saved to storage', [ 'path' => $fullSavePath, 'stage' => $stage, 'drawing_no' => $drawingNo ]); // Update document_revisions table $relativePath = $folder . '/' . $savedFilename; $query = DocumentRevision::query() ->where('drawing_no', $drawingNo); if ($revisionId) { $query->where('id', $revisionId); } $updatedRows = $query->update([ $stage => $relativePath, ]); \Log::info('Document revision updated', [ 'updated_rows' => $updatedRows, 'stage' => $stage, 'pdf_path' => $relativePath ]); return response()->json([ 'success' => true, 'message' => 'PDF saved to ' . $stage . ' successfully', 'pdf_path' => $relativePath, 'updated_rows' => $updatedRows, 'filename' => $savedFilename, ]); } // Return as download (default behavior) return response($mpdf->Output($filename, 'S'), 200) ->header('Content-Type', 'application/pdf') ->header('Content-Disposition', 'attachment; filename="' . $filename . '"'); } catch (\Exception $e) { \Log::error('Export PDF failed', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return response()->json([ 'success' => false, 'error' => $e->getMessage(), 'message' => 'Failed to export PDF' ], 500); } } public function actionHistory(Request $request): JsonResponse { $pdfFile = $request->query('pdf_file'); $logs = Annotation::query() ->with(['updatedUser:id,name']) ->when($pdfFile, fn($q) => $q->where('pdf_file_path', 'like', "%{$pdfFile}%")) ->orderByDesc('updated_at') ->select('version', 'updated_by', 'updated_at', 'is_active') ->limit(200) ->get(); $grouped = $logs ->sortByDesc('updated_at') ->groupBy('version') ->map(fn($items) => $items->sortByDesc('updated_at')->first()); return response()->json([ 'success' => true, 'data' => $grouped->map(fn($log) => [ 'version' => $log->version, 'user' => $log->updatedUser->name ?? 'System', 'action' => $log->version == 1 ? 'New Record' : 'Updated', 'time' => $log->updated_at?->format('Y-m-d H:i:s'), ])->values(), ]); } public function versionAnnotations(Request $request): JsonResponse { $request->validate([ 'pdf_file' => 'required|string', 'page_number' => 'required|integer', 'version' => 'required|integer', ]); $pdfFile = $request->pdf_file; $page = (int) $request->page_number; $version = (int) $request->version; $items = Annotation::where('pdf_file_path', 'like', '%' . basename($pdfFile) . '%') ->where('page_number', $page) ->where('version', $version) ->orderBy('id', 'asc') ->get(); return response()->json([ 'success' => true, 'data' => $items, ]); } /** * Save joint data to Base Weldmap (weld_logs table) * Phase 3.2: Base Weldmap Integration */ public function saveJointData(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'drawing_number' => 'required|string', 'joint_data' => 'required|array', 'joint_data.*.joint_no' => 'nullable|string', 'joint_data.*.type_of_joint' => 'nullable|string|in:F,S', 'joint_data.*.type_of_welds' => 'nullable|string', 'joint_data.*.welding_date' => 'nullable|date', 'joint_data.*.spool_no1' => 'nullable|string', 'joint_data.*.spool_no2' => 'nullable|string', 'joint_data.*.pose_no1' => 'nullable|string', 'joint_data.*.pose_no2' => 'nullable|string', 'joint_data.*.member1' => 'nullable|string', 'joint_data.*.member2' => 'nullable|string', 'joint_data.*.material' => 'nullable|string', 'joint_data.*.dia1' => 'nullable|numeric', 'joint_data.*.dia2' => 'nullable|numeric', 'joint_data.*.nps1' => 'nullable|string', 'joint_data.*.nps2' => 'nullable|string', 'joint_data.*.thickness1' => 'nullable|numeric', 'joint_data.*.thickness2' => 'nullable|numeric', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $validator->errors() ], 422); } try { DB::beginTransaction(); $drawingNumber = $request->drawing_number; $jointDataArray = $request->joint_data; $savedJoints = []; foreach ($jointDataArray as $jointData) { // Map widget form data to weld_logs table structure $weldLogData = [ 'iso_number' => $drawingNumber, 'no_of_the_joint_as_per_as_built_survey' => $jointData['joint_no'] ?? null, 'type_of_joint' => $jointData['type_of_joint'] ?? null, 'type_of_welds' => $jointData['type_of_welds'] ?? null, 'welding_date' => $jointData['welding_date'] ?? null, ]; // Handle spool numbers - use spool_number_1/2 if exists, otherwise use spool_number/spool_number2 if (isset($jointData['spool_no1'])) { if (Schema::hasColumn('weld_logs', 'spool_number_1')) { $weldLogData['spool_number_1'] = $jointData['spool_no1']; } else { $weldLogData['spool_number'] = $jointData['spool_no1']; } } if (isset($jointData['spool_no2'])) { if (Schema::hasColumn('weld_logs', 'spool_number_2')) { $weldLogData['spool_number_2'] = $jointData['spool_no2']; } elseif (Schema::hasColumn('weld_logs', 'spool_number2')) { $weldLogData['spool_number2'] = $jointData['spool_no2']; } } // Handle pose numbers - use pose_number_1/2 if exists, otherwise use pose_no_1/2 if (isset($jointData['pose_no1'])) { if (Schema::hasColumn('weld_logs', 'pose_number_1')) { $weldLogData['pose_number_1'] = $jointData['pose_no1']; } else { $weldLogData['pose_no_1'] = $jointData['pose_no1']; } } if (isset($jointData['pose_no2'])) { if (Schema::hasColumn('weld_logs', 'pose_number_2')) { $weldLogData['pose_number_2'] = $jointData['pose_no2']; } else { $weldLogData['pose_no_2'] = $jointData['pose_no2']; } } // Handle members - use member_1/2 if exists, otherwise use member_no_1/2 if (isset($jointData['member1'])) { if (Schema::hasColumn('weld_logs', 'member_1')) { $weldLogData['member_1'] = $jointData['member1']; } else { $weldLogData['member_no_1'] = $jointData['member1']; } } if (isset($jointData['member2'])) { if (Schema::hasColumn('weld_logs', 'member_2')) { $weldLogData['member_2'] = $jointData['member2']; } else { $weldLogData['member_no_2'] = $jointData['member2']; } } // Handle material - use material_1/2 if exists, otherwise use material_no_1/2 if (isset($jointData['material'])) { if (Schema::hasColumn('weld_logs', 'material_1')) { $weldLogData['material_1'] = $jointData['material']; $weldLogData['material_2'] = $jointData['material']; } else { $weldLogData['material_no_1'] = $jointData['material']; $weldLogData['material_no_2'] = $jointData['material']; } } // Handle diameter - use dia_1/2 if exists, otherwise use outside_diameter_1/2 if (isset($jointData['dia1'])) { if (Schema::hasColumn('weld_logs', 'dia_1')) { $weldLogData['dia_1'] = $jointData['dia1']; } else { $weldLogData['outside_diameter_1'] = $jointData['dia1']; } } if (isset($jointData['dia2'])) { if (Schema::hasColumn('weld_logs', 'dia_2')) { $weldLogData['dia_2'] = $jointData['dia2']; } else { $weldLogData['outside_diameter_2'] = $jointData['dia2']; } } // Handle NPS - nps_1/2 should exist in both cases if (isset($jointData['nps1'])) { $weldLogData['nps_1'] = $jointData['nps1']; } if (isset($jointData['nps2'])) { $weldLogData['nps_2'] = $jointData['nps2']; } // Handle thickness - use thickness_1/2 if exists, otherwise use wall_thickness_1/2 if (isset($jointData['thickness1'])) { if (Schema::hasColumn('weld_logs', 'thickness_1')) { $weldLogData['thickness_1'] = $jointData['thickness1']; } else { $weldLogData['wall_thickness_1'] = $jointData['thickness1']; } } if (isset($jointData['thickness2'])) { if (Schema::hasColumn('weld_logs', 'thickness_2')) { $weldLogData['thickness_2'] = $jointData['thickness2']; } else { $weldLogData['wall_thickness_2'] = $jointData['thickness2']; } } // Use updateOrCreate to prevent duplicates // Match by iso_number and joint_no (if provided) // CRITICAL: Try multiple matching strategies to find existing records $matchConditions = [ 'iso_number' => $drawingNumber, ]; // CRITICAL: Try to find existing record using multiple strategies // Strategy 1: Match by joint_no (e.g., "53A") // Strategy 2: Match by draw_joint_no (e.g., "F53A") if joint_no doesn't match // Strategy 3: Match by type_of_joint + joint_no combination $existingRecord = null; if (!empty($jointData['joint_no'])) { $jointNo = trim($jointData['joint_no']); // Strategy 1: Try exact match with joint_no $existingRecord = WeldLog::where('iso_number', $drawingNumber) ->where('no_of_the_joint_as_per_as_built_survey', $jointNo) ->first(); // Strategy 2: If not found, try with draw_joint_no (Type + Joint No, e.g., "F53A") if (!$existingRecord && !empty($jointData['draw_joint_no'])) { $drawJointNo = trim($jointData['draw_joint_no']); $existingRecord = WeldLog::where('iso_number', $drawingNumber) ->where('no_of_the_joint_as_per_as_built_survey', $drawJointNo) ->first(); } // Strategy 3: If still not found, try with type_of_joint + joint_no if (!$existingRecord && !empty($jointData['type_of_joint'])) { $existingRecord = WeldLog::where('iso_number', $drawingNumber) ->where('type_of_joint', $jointData['type_of_joint']) ->where('no_of_the_joint_as_per_as_built_survey', $jointNo) ->first(); } // Set match conditions based on what we found if ($existingRecord) { // If we found an existing record, match by its ID to ensure update $matchConditions = [ 'id' => $existingRecord->id ]; } else { // If no existing record, match by iso_number + joint_no for new record $matchConditions['no_of_the_joint_as_per_as_built_survey'] = $jointNo; } } $weldLog = WeldLog::updateOrCreate( $matchConditions, $weldLogData ); $savedJoints[] = $weldLog; } DB::commit(); return response()->json([ 'success' => true, 'message' => 'Joint data saved successfully', 'data' => $savedJoints, 'count' => count($savedJoints) ]); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Failed to save joint data', 'error' => $e->getMessage() ], 500); } } /** * Get joint data by shape_id or joint_no * Phase 3.1: API Endpoints for Joint Data */ public function getJointData(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'drawing_number' => 'required|string', 'joint_no' => 'nullable|string', 'shape_id' => 'nullable|string', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $validator->errors() ], 422); } try { $drawingNumber = $request->drawing_number; $query = WeldLog::where('iso_number', $drawingNumber); if ($request->has('joint_no') && !empty($request->joint_no)) { $query->where('no_of_the_joint_as_per_as_built_survey', $request->joint_no); } // Dynamic column selection based on schema $selectColumns = [ 'id', 'no_of_the_joint_as_per_as_built_survey as joint_no', 'type_of_joint', 'type_of_welds', 'welding_date', ]; // Add spool numbers (check which columns exist) if (Schema::hasColumn('weld_logs', 'spool_number_1')) { $selectColumns[] = 'spool_number_1 as spool_no1'; } else { $selectColumns[] = 'spool_number as spool_no1'; } if (Schema::hasColumn('weld_logs', 'spool_number_2')) { $selectColumns[] = 'spool_number_2 as spool_no2'; } elseif (Schema::hasColumn('weld_logs', 'spool_number2')) { $selectColumns[] = 'spool_number2 as spool_no2'; } // Add pose numbers if (Schema::hasColumn('weld_logs', 'pose_number_1')) { $selectColumns[] = 'pose_number_1 as pose_no1'; } else { $selectColumns[] = 'pose_no_1 as pose_no1'; } if (Schema::hasColumn('weld_logs', 'pose_number_2')) { $selectColumns[] = 'pose_number_2 as pose_no2'; } else { $selectColumns[] = 'pose_no_2 as pose_no2'; } // Add members if (Schema::hasColumn('weld_logs', 'member_1')) { $selectColumns[] = 'member_1 as member1'; } else { $selectColumns[] = 'member_no_1 as member1'; } if (Schema::hasColumn('weld_logs', 'member_2')) { $selectColumns[] = 'member_2 as member2'; } else { $selectColumns[] = 'member_no_2 as member2'; } // Add material if (Schema::hasColumn('weld_logs', 'material_1')) { $selectColumns[] = 'material_1 as material'; } else { $selectColumns[] = 'material_no_1 as material'; } // Add diameter if (Schema::hasColumn('weld_logs', 'dia_1')) { $selectColumns[] = 'dia_1 as dia1'; } else { $selectColumns[] = 'outside_diameter_1 as dia1'; } if (Schema::hasColumn('weld_logs', 'dia_2')) { $selectColumns[] = 'dia_2 as dia2'; } else { $selectColumns[] = 'outside_diameter_2 as dia2'; } // Add NPS $selectColumns[] = 'nps_1 as nps1'; $selectColumns[] = 'nps_2 as nps2'; // Add thickness if (Schema::hasColumn('weld_logs', 'thickness_1')) { $selectColumns[] = 'thickness_1 as thickness1'; } else { $selectColumns[] = 'wall_thickness_1 as thickness1'; } if (Schema::hasColumn('weld_logs', 'thickness_2')) { $selectColumns[] = 'thickness_2 as thickness2'; } else { $selectColumns[] = 'wall_thickness_2 as thickness2'; } // Add welders $selectColumns[] = 'welder_1'; $selectColumns[] = 'welder_2'; $joints = $query->get($selectColumns); return response()->json([ 'success' => true, 'data' => $joints ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Failed to get joint data', 'error' => $e->getMessage() ], 500); } } /** * Update welder assignment in Weldlog * Phase 3.3: Weldlog Integration */ public function updateWelderAssignment(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'drawing_number' => 'required|string', 'joint_no' => 'required|string', 'welder_id1' => 'nullable|string', 'welder_id2' => 'nullable|string', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $validator->errors() ], 422); } try { DB::beginTransaction(); $weldLog = WeldLog::where('iso_number', $request->drawing_number) ->where('no_of_the_joint_as_per_as_built_survey', $request->joint_no) ->first(); if (!$weldLog) { return response()->json([ 'success' => false, 'message' => 'Joint not found' ], 404); } // Update welder assignments if ($request->has('welder_id1')) { $weldLog->welder_1 = $request->welder_id1; // Also update real_welder_1 if it exists if (Schema::hasColumn('weld_logs', 'real_welder_1')) { $weldLog->real_welder_1 = $request->welder_id1; } } if ($request->has('welder_id2')) { $weldLog->welder_2 = $request->welder_id2; // Also update real_welder_2 if it exists if (Schema::hasColumn('weld_logs', 'real_welder_2')) { $weldLog->real_welder_2 = $request->welder_id2; } } $weldLog->save(); DB::commit(); return response()->json([ 'success' => true, 'message' => 'Welder assignment updated successfully', 'data' => $weldLog ]); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Failed to update welder assignment', 'error' => $e->getMessage() ], 500); } } }