İlk temizlik tamamlandı bir önceki projeden

This commit is contained in:
Ümit Tunç
2026-04-28 21:14:25 +03:00
commit f80443aec0
10000 changed files with 959965 additions and 0 deletions
@@ -0,0 +1,829 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Annotation;
use App\Models\DocumentRevision;
use App\Models\WeldLog;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Schema;
class AnnotationController extends Controller
{
public function store(Request $request): JsonResponse
{
$request->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 = '<img src="' . $tempImagePath . '" style="width: 100%; height: auto;" />';
$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);
}
}
}
@@ -0,0 +1,401 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
/**
* @group Read Only Endpoints
*
* APIs for retrieving read-only data from various system modules.
*/
class ApiReadOnlyController extends Controller
{
public function __construct()
{
// Auth handled at route level with 'auth:sanctum,web' to support both token and session auth
}
private function renderLegacyScript($scriptPath, Request $request)
{
try {
// Merge request parameters into global $_GET for legacy script compatibility
if (!empty($request->all())) {
$_GET = array_merge($_GET, $request->all());
}
// Define path to the view
$fullPath = resource_path("views/admin-ajax/{$scriptPath}");
if (!file_exists($fullPath)) {
return response()->json(['error' => 'Module not found: ' . $scriptPath], 404);
}
// Capture output
ob_start();
include($fullPath);
$output = ob_get_clean();
// Attempt to decode JSON to ensure it's valid, otherwise return raw string if appropriate or wrap it
$json = json_decode($output);
if (json_last_error() === JSON_ERROR_NONE) {
return response()->json($json);
}
// If not JSON, it might be an error or HTML. For this API, we expect JSON.
// We'll return it as a 'raw_output' field if it's not valid JSON
return response()->json(['raw_output' => $output]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
public function ndtRequest(Request $request)
{
return $this->renderLegacyScript('request-ndt.blade.php', $request);
}
public function ndtCalculation(Request $request)
{
return $this->renderLegacyScript('ndt-calculation.blade.php', $request);
}
/**
* Spool Area Release
*
* Get spool area release data with pagination and filtering.
*
* @queryParam take int Number of items to take (pagination). Default: 100. Example: 10
* @queryParam skip int Number of items to skip (pagination). Default: 0. Example: 0
* @queryParam filter_iso_number string Filter by ISO Number. Example: 10-BFW-RS1-0001-AH06HS-HC
* @queryParam filter_spool_number string Filter by Spool Number. Example: SPL-0004
* @queryParam filter_line_number string Filter by Line Number. Example: LN-001
* @queryParam filter_painting_cycle string Filter by Painting Cycle. Example: C1
* @queryParam filter_spool_status string Filter by Spool Status. Example: Completed
* @queryParam module string Specific module filter (e.g., 'spool-release-paint'). Example: spool-release-paint
*
* @response {
* "summary": {"Completed": 10, "Waiting": 5},
* "esummary": {"Completed": 2},
* "datagrid": [
* {
* "id": 1,
* "iso_number": "ISO-001",
* "spool_number": "SPL-001",
* "spool_status": "Completed",
* "project": "PROJECT A"
* }
* ]
* }
*/
public function spoolAreaRelease(Request $request)
{
$limit = $request->get('take', 100);
$offset = $request->get('skip', 0);
$isFiltered = $request->has('filter_iso_number') || $request->has('filter_spool_number');
$module = $request->get('module');
// Spool status order mapping
$statusOrder = [
'Waiting' => 1,
'Manufacturing' => 2,
'QC' => 3,
'Paint' => 4,
'Completed' => 5,
];
$caseSql = "CASE spool_status ";
foreach ($statusOrder as $status => $order) {
$caseSql .= "WHEN '$status' THEN $order ";
}
$caseSql .= "ELSE 99 END";
// Subquery for min status order
$minStatusSub = apply_welded_filter(db("weld_logs"))
->select(
'iso_number',
'spool_number',
\DB::raw("MIN(CASE WHEN type_of_joint = 'S' THEN {$caseSql} END) as min_status_order")
)
->groupBy('iso_number', 'spool_number');
// Base query
$query = apply_welded_filter(db("weld_logs"))
->leftJoinSub($minStatusSub, 'min_status_order_tbl', function($join) {
$join->on('weld_logs.iso_number', '=', 'min_status_order_tbl.iso_number')
->on('weld_logs.spool_number', '=', 'min_status_order_tbl.spool_number');
});
// Apply filters
if ($request->has('filter_iso_number')) {
$query->where("weld_logs.iso_number", "like", "%" . $request->get("filter_iso_number") . "%");
}
if ($request->has('filter_spool_number')) {
$query->where("weld_logs.spool_number", "like", "%" . $request->get("filter_spool_number") . "%");
}
if ($request->has('filter_line_number')) {
$query->where("weld_logs.line_number", "like", "%" . $request->get("filter_line_number") . "%");
}
if ($request->has('filter_painting_cycle')) {
$query->where("weld_logs.painting_cycle", "like", "%" . $request->get("filter_painting_cycle") . "%");
}
if ($request->has('filter_spool_status')) {
$query->where("weld_logs.spool_status", $request->get("filter_spool_status"));
}
if ($module === "spool-release-paint") {
$query->whereNotNull("painting_cycle");
}
// Field selection - always group by ISO and Spool to get single row per spool
$query->select(
'weld_logs.id',
'weld_logs.iso_number',
'weld_logs.spool_number',
\DB::raw("GROUP_CONCAT(DISTINCT no_of_the_joint_as_per_as_built_survey ORDER BY CAST(no_of_the_joint_as_per_as_built_survey AS UNSIGNED) SEPARATOR ', ') AS joint_no"),
\DB::raw("GROUP_CONCAT(DISTINCT type_of_joint SEPARATOR '/') as type_of_joint"),
'project',
'iso_rev',
'painting_cycle',
'spool_status',
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN clearance_no END) as clearance_no"),
\DB::raw("MIN(CASE WHEN type_of_joint = 'S' THEN clearance_date END) as min_clearance_date"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN clearance_date END) as clearance_date"),
\DB::raw("COUNT(DISTINCT CASE WHEN type_of_joint = 'S' THEN COALESCE(clearance_date, 'NULL_VALUE') END) as clearance_date_count"),
\DB::raw("MIN(CASE WHEN type_of_joint = 'S' THEN ndt_release_date END) as min_ndt_release_date"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN ndt_release_date END) as ndt_release_date"),
\DB::raw("COUNT(DISTINCT CASE WHEN type_of_joint = 'S' THEN COALESCE(ndt_release_date, 'NULL_VALUE') END) as ndt_release_date_count"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN register_paint_no END) as register_paint_no"),
\DB::raw("MIN(CASE WHEN type_of_joint = 'S' THEN register_date END) as min_register_date"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN register_date END) as register_date"),
\DB::raw("COUNT(DISTINCT CASE WHEN type_of_joint = 'S' THEN COALESCE(register_date, 'NULL_VALUE') END) as register_date_count"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN ndt_release_no END) as ndt_release_no"),
\DB::raw("MIN(CASE WHEN type_of_joint = 'S' THEN paint_release_date END) as min_paint_release_date"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN paint_release_date END) as paint_release_date"),
\DB::raw("COUNT(DISTINCT CASE WHEN type_of_joint = 'S' THEN COALESCE(paint_release_date, 'NULL_VALUE') END) as paint_release_date_count"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN paint_release_no END) as paint_release_no"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN spool_zone END) as spool_zone"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN spool_ndt_check END) as spool_ndt_check"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN spool_paint_check END) as spool_paint_check"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN spool_ndt_check_no END) as spool_ndt_check_no"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN ndt_release_check END) as ndt_release_check"),
\DB::raw("MIN(CASE WHEN type_of_joint = 'S' THEN spool_ndt_check_date END) as min_spool_ndt_check_date"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN spool_ndt_check_date END) as spool_ndt_check_date"),
\DB::raw("COUNT(DISTINCT CASE WHEN type_of_joint = 'S' THEN COALESCE(spool_ndt_check_date, 'NULL_VALUE') END) as spool_ndt_check_date_count"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN paint_release_check END) as paint_release_check"),
\DB::raw("MAX(CASE WHEN type_of_joint = 'S' THEN spool_remarks END) as spool_remarks"),
'min_status_order_tbl.min_status_order'
)
->groupBy("weld_logs.iso_number", "weld_logs.spool_number");
// Get total results before pagination for summaries
$allResults = $query->get();
// Summary calculations
$summary = [];
$esummary = [];
$users = usersArray();
$documentRevisions = [];
$revisionQuery = db("document_revisions")->select('drawing_no', 'revision_no')->get();
foreach ($revisionQuery as $revision) {
$documentRevisions[$revision->drawing_no] = $revision->revision_no;
}
$allSpoolStatusesQuery = db("weld_logs")
->select('spool_status', 'iso_number', 'spool_number')->where("type_of_joint", "S")->get();
$allSpoolStatuses = [];
foreach ($allSpoolStatusesQuery as $statusQuery) {
$allSpoolStatuses[$statusQuery->iso_number][$statusQuery->spool_number][] = $statusQuery->spool_status;
}
$orderToStatus = array_flip($statusOrder);
foreach ($allResults as $q) {
// Processing logic from Blade
if (strpos($q->type_of_joint, 'S') !== false && isset($q->min_status_order) && $q->min_status_order >= 1 && $q->min_status_order <= 5) {
$q->spool_status = $orderToStatus[$q->min_status_order];
}
$q->revision_no = $documentRevisions[$q->iso_number] ?? null;
$q->project = strtoupper($q->project);
if ($q->spool_ndt_check != "") {
foreach (['spool_ndt_check', 'spool_paint_check', 'ndt_release_check', 'paint_release_check'] as $field) {
if (isset($users[$q->$field])) {
$q->$field = "✒️" . $users[$q->$field]->name;
}
}
}
if (isset($q->spool_status)) {
$types = explode('/', $q->type_of_joint);
$hasS = in_array('S', $types) || in_array('F/S', $types);
$hasF = in_array('F', $types);
if ($hasS) {
if (!isset($summary[$q->spool_status])) $summary[$q->spool_status] = 0;
$summary[$q->spool_status]++;
}
if ($hasF) {
if (!isset($esummary[$q->spool_status])) $esummary[$q->spool_status] = 0;
$esummary[$q->spool_status]++;
}
}
$spoolStatuses = $allSpoolStatuses[$q->iso_number][$q->spool_number] ?? [];
$q->spool_status = getLowestSpoolStatus($spoolStatuses);
$q->latest_status = getLatestSpoolStatus($spoolStatuses);
$dateWarnings = [
'ndt_release_date_count' => ['min_ndt_release_date', 'ndt_release_date'],
'clearance_date_count' => ['min_clearance_date', 'clearance_date'],
'register_date_count' => ['min_register_date', 'register_date'],
'paint_release_date_count' => ['min_paint_release_date', 'paint_release_date'],
'spool_ndt_check_date_count' => ['min_spool_ndt_check_date', 'spool_ndt_check_date'],
];
foreach ($dateWarnings as $countField => $dateFields) {
if ($q->$countField > 1 || $q->{$dateFields[0]} != $q->{$dateFields[1]}) {
$q->spool_status .= "⚠️";
break;
}
}
}
// Apply skip/take to datagrid
$datagrid = array_slice($allResults->toArray(), $offset, $limit);
return response()->json([
'summary' => $summary,
'esummary' => $esummary,
'datagrid' => $datagrid,
], 200, [], JSON_UNESCAPED_UNICODE);
}
public function rfiDashboard(Request $request)
{
return $this->renderLegacyScript('rfi-dashboard.blade.php', $request);
}
public function welderQualificationStatus(Request $request)
{
return $this->renderLegacyScript('welder-qualification-status.php', $request);
}
public function punchList(Request $request)
{
return $this->renderLegacyScript('punch-summary/punch-list.blade.php', $request);
}
public function naksWelders(Request $request)
{
// "Naks (Welder, Consu, Equipment)" - Checking specific files usually in admin-ajax
// Assuming welder-qualification-status covers welders.
// Need to check for Consumables and Equipment if separate files exist.
return $this->renderLegacyScript('welder-qualification-status.php', $request);
}
public function testPackagePivot(Request $request) {
return $this->renderLegacyScript('test-package/pivot.blade.php', $request);
}
public function testPackageStatusPivot(Request $request) {
return $this->renderLegacyScript('test-package/status-pivot.blade.php', $request);
}
public function testPackageSummary(Request $request) {
return $this->renderLegacyScript('test-package/summary.blade.php', $request);
}
public function testPackageSummary2(Request $request) {
return $this->renderLegacyScript('test-package/summary2.blade.php', $request);
}
/**
* Spool Release Detail
*
* Get detailed spool release information with filtering options.
*
* @queryParam filter_iso_number string Filter by ISO Number. Example: 10-BFW-RS1-0001-AH06HS-HC
* @queryParam filter_spool_number string Filter by Spool Number. Example: SPL-0004
*/
public function spoolReleaseDetail(Request $request) {
return $this->renderLegacyScript('spool-release-detail.blade.php', $request);
}
/**
* Repair Table Statistics
*
* Get repair table statistics including defect types by welder, NPS, and material.
*
* @queryParam d1 string Start date (YYYY-MM-DD). Example: 2024-01-01
* @queryParam d2 string End date (YYYY-MM-DD). Example: 2024-03-31
*/
public function repairTable(Request $request)
{
return $this->renderLegacyScript('repair-table.blade.php', $request);
}
public function welderBasedRepairRatioDistribution(Request $request)
{
return $this->renderLegacyScript('repair-table/welder-based-repair-ratio-distribution.blade.php', $request);
}
public function defectTypeByWelder(Request $request)
{
return $this->renderLegacyScript('repair-table/defect-type-by-welder.blade.php', $request);
}
public function welderQualificationTable(Request $request)
{
return $this->renderLegacyScript('welder-qualification-table.blade.php', $request);
}
public function rfiTasks(Request $request)
{
return $this->renderLegacyScript('rfi-tasks.blade.php', $request);
}
public function rfiStatus(Request $request)
{
$settingValue = setting('rfi-status');
$data = json_decode($settingValue, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$data = $settingValue;
}
return response()->json([
'status' => 'success',
'data' => $data
]);
}
/**
* NDT Release List
*
* Get list of NDT ready spools grouped by ISO and spool number.
*/
public function ndtReleaseList(Request $request)
{
return $this->renderLegacyScript('ndt-list.blade.php', $request);
}
/**
* Paint Release List
*
* Get list of paint released spools with paint release details.
*/
public function paintReleaseList(Request $request)
{
return $this->renderLegacyScript('ndt-paint-list.blade.php', $request);
}
// Placeholder for other modules as they are identified
}
@@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
/**
* @group Authentication
*
* API endpoints for user authentication and token management.
*/
class AuthController extends Controller
{
/**
* Login (Get Token)
*
* Authenticates a user via email and password, returning a Bearer Token.
* @unauthenticated
*
* @bodyParam email string required User's email address. Example: admin@example.com
* @bodyParam password string required User's password. Example: password
*/
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
return response()->json([
'status' => 'error',
'message' => 'Invalid credentials'
], 401);
}
// Create Token (Device name is optional)
$token = $user->createToken($request->device_name ?? 'api-client')->plainTextToken;
return response()->json([
'status' => 'success',
'message' => 'Login successful',
'data' => [
'user' => $user,
'access_token' => $token,
'token_type' => 'Bearer',
]
]);
}
/**
* Get User Profile
*
* Returns the authenticated user's information.
*/
public function me(Request $request)
{
return response()->json([
'status' => 'success',
'data' => [
'user' => $request->user()
]
]);
}
/**
* Logout
*
* Revokes the user's current access token.
*/
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'status' => 'success',
'message' => 'Token revoked'
]);
}
}
@@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class AutocompleteTypeController extends Controller
{
/**
* Mobile API wrapper for AdminController@autocompleteType.
*
* Web uses "/{hash}/autocomplete_type/{type}". Mobile cannot know {hash},
* so we expose a safe, whitelisted endpoint under "/api/autocomplete-type/{type}".
*/
public function handle(Request $request, string $type)
{
if (!auth()->check() && !$request->user()) {
return response()->json(['message' => 'Unauthenticated.'], 401);
}
// Security: whitelist the only types needed by mobile screens.
$allowedTypes = [
'wps_no2',
'real_welder_1',
'real_welder_2',
];
if (!in_array($type, $allowedTypes, true)) {
return response()->json([
'status' => 'error',
'message' => 'Unsupported autocomplete type.',
], 400);
}
// Permission: treat this as weldmap read (options lookup).
if (!isAuth('weldmap', 'read')) {
return response()->json([
'status' => 'error',
'message' => 'Unauthorized action.',
'required_permission' => 'weldmap:read',
], 403);
}
// Provide $rowData to legacy autocomplete scripts.
$rowData = (object) $request->all();
// Support "*" mapping convention used in AdminController.
$type = str_replace('*', '/', $type);
$file = base_path("app/Http/Controllers/AutoCompleteType/{$type}.php");
if (!file_exists($file)) {
return response()->json([
'status' => 'error',
'message' => 'Autocomplete script not found.',
], 404);
}
$response = [];
include $file;
return response()->json($response);
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\JsonResponse;
class DashboardController extends Controller
{
/**
* Get dashboard summary data extracted from the cached blade file.
*
* @return JsonResponse
*/
public function summary(): JsonResponse
{
try {
// Get the cached dashboard content from the 'documents' disk
$cachedContent = Storage::disk('local')->get('cache/dashboard.blade.php');
if (!$cachedContent) {
return response()->json([
'status' => 'error',
'message' => 'Cached dashboard content not found.'
], 404);
}
// Regex pattern to extract icon, value, and label
// Matches block-content, img src, font-size-h3 (value), and font-size-sm (label)
$pattern = '/<div class="block-content[^>]*>.*?<img src="([^"]+)"[^>]*>.*?<div class="font-size-h3[^>]*>(.*?)<\/div>\s*<div class="font-size-sm[^>]*>(.*?)<\/div>/s';
preg_match_all($pattern, $cachedContent, $matches, PREG_SET_ORDER);
$data = [];
$baseUrl = url('/');
foreach ($matches as $match) {
$iconPath = $match[1];
$value = trim(strip_tags($match[2]));
$label = trim(strip_tags($match[3]));
// Clean up excessive whitespace and newlines
$value = preg_replace('/\s+/', ' ', $value);
$label = preg_replace('/\s+/', ' ', $label);
// Make the icon URL absolute
$iconUrl = str_starts_with($iconPath, 'http') ? $iconPath : rtrim($baseUrl, '/') . '/' . ltrim($iconPath, '/');
if ($label) {
$data[] = [
'label' => $label,
'value' => $value,
'icon' => $iconUrl
];
}
}
return response()->json([
'status' => 'success',
'data' => $data
]);
} catch (\Exception $e) {
return response()->json([
'status' => 'error',
'message' => 'An error occurred while extracting dashboard data: ' . $e->getMessage()
], 500);
}
}
}
@@ -0,0 +1,119 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\DocumentHistory;
use Illuminate\Http\Request;
/**
* @group Document Management
*
* APIs for managing documents and their history.
*/
class DocumentHistoryController extends Controller
{
/**
* Get Document History
*
* Retrieve the history of a document based on file name or table reference.
*
* @bodyParam table string The name of the table (e.g., document_revisions). Example: document_revisions
* @bodyParam column string The name of the column (e.g., pdf_spool_iso). Example: pdf_spool_iso
* @bodyParam row_id integer The ID of the row. Example: 3021
* @bodyParam file_name string The name of the file to search for. Example: MST-80-AH01PR-FE31.803.pdf
*
* @response {
* "id": 345,
* "user_name": "Linga Samiii",
* "action": "Archive",
* "file_name": "MST-80-AH01PR-FE31.803.pdf",
* "file_size": "474.4 KB",
* "moved_from": "001_Drawings",
* "moved_to": "Archive/001_Drawings",
* "action_date": "2025-07-19 16:11:00",
* "file_path": "storage/documents/...",
* "notes": "File replaced with new version..."
* }
*/
public function index(Request $request)
{
$table = $request->input('table');
$column = $request->input('column');
$row_id = $request->input('row_id');
$file_name = $request->input('file_name');
$query = DocumentHistory::query();
if ($file_name) {
// File name'e göre arama
$base_name = str_replace(".pdf", "", $file_name);
// Sadece tam dosya adlarını veya '_' ile ayrılmış arşiv adlarını (örn: _2025_08_30...) eşleştir
$query->where(function ($q) use ($base_name, $file_name) {
$q->where('file_name', $file_name)
->orWhere('file_name', 'LIKE', $base_name . '_%.pdf');
});
} elseif ($table && $row_id) {
// Table ve row_id'ye göre dosyayı bulup arama yap
try {
$row = \Illuminate\Support\Facades\DB::table($table)->find($row_id);
if ($row) {
$filenames = [];
if ($column && isset($row->$column)) {
// Belirli bir kolon varsa
$value = $row->$column;
if ($value && is_string($value)) {
$filenames[] = basename($value);
}
} else {
// Kolon belirtilmemişse, olası dosya kolonlarını tara
foreach ((array) $row as $key => $value) {
if ($value && is_string($value) && preg_match('/\.(pdf|xls|xlsx|doc|docx|jpg|png|jpeg)$/i', $value)) {
$filenames[] = basename($value);
}
}
}
if (!empty($filenames)) {
$query->whereIn('file_name', $filenames);
} else {
// Dosya bulunamadıysa boş dön
return response()->json([]);
}
} else {
return response()->json([]);
}
} catch (\Exception $e) {
// Tablo hatası vs.
return response()->json([]);
}
} else {
// Hiçbir geçerli filtre yoksa boş döndür
return response()->json([]);
}
$documentHistory = $query->orderBy('action_date', 'DESC')
->get()
->map(function ($item) {
return [
'id' => $item->id,
'user_name' => $item->user_name,
'action' => $item->action,
'file_name' => $item->file_name,
'file_size' => $item->file_size,
'moved_from' => $item->moved_from,
'moved_to' => $item->moved_to,
'action_date' => $item->action_date,
'file_path' => $item->file_path,
'notes' => $item->notes
];
});
return response()->json($documentHistory);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,434 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
/**
* @group Endpoints
*
* API endpoints for serving admin-ajax blade content.
* These endpoints provide access to backend blade files that return JSON or HTML responses.
*/
class EndpointController extends Controller
{
/**
* List Available Endpoints
*
* Returns a list of all available admin-ajax endpoints that can be accessed via the API.
* Each endpoint corresponds to a blade file in the admin-ajax directory.
*
* The response includes all endpoints with their names, URLs, HTTP methods, response types, and categories.
*
* @responseFile storage/app/scribe/endpoints-response.json
*/
public function index()
{
$endpoints = $this->getAvailableEndpoints();
return response()->json([
'status' => 'success',
'data' => [
'endpoints' => $endpoints,
'total' => count($endpoints)
]
]);
}
/**
* Execute Endpoint
*
* Executes the specified admin-ajax endpoint and returns the response.
* The response type is automatically detected:
* - If the output is valid JSON, returns JSON response
* - Otherwise, returns HTML response
*
* @urlParam endpoint string required The endpoint name (e.g., users, ndt-calculation). Example: users
*
* @queryParam _format string Optional. Force response format: 'json' or 'html'. Example: json
*
* @response 200 scenario="JSON Response" {
* "status": "success",
* "data": [{"id": 1, "name": "John Doe"}],
* "format": "json"
* }
*
* @response 200 scenario="HTML Response" {
* "status": "success",
* "html": "<div>Content here</div>",
* "format": "html"
* }
*
* @response 404 {
* "status": "error",
* "message": "Endpoint not found"
* }
*/
public function handle(Request $request, string $endpoint)
{
// Sanitize endpoint name
$endpoint = $this->sanitizeEndpoint($endpoint);
// Check if endpoint exists
if (!$this->endpointExists($endpoint)) {
return response()->json([
'status' => 'error',
'message' => 'Endpoint not found',
'endpoint' => $endpoint
], 404);
}
// SECURITY: Only allow readonly endpoints via API
if (!$this->isReadonlyEndpoint($endpoint)) {
return response()->json([
'status' => 'error',
'message' => 'This endpoint is not available via API. Only readonly endpoints are accessible.',
'endpoint' => $endpoint
], 403);
}
try {
// Render the blade view and capture output
$output = $this->renderEndpoint($endpoint, $request);
// Detect response format
$format = $request->input('_format');
if ($format === 'html') {
return $this->htmlResponse($output);
}
if ($format === 'json' || $this->isJson($output)) {
return $this->jsonResponse($output);
}
return $this->htmlResponse($output);
} catch (\Exception $e) {
return response()->json([
'status' => 'error',
'message' => $e->getMessage(),
'endpoint' => $endpoint
], 500);
}
}
/**
* Get Endpoint Info
*
* Returns detailed information about a specific endpoint.
*
* @urlParam endpoint string required The endpoint name. Example: users
*
* @response 200 {
* "status": "success",
* "data": {
* "name": "users",
* "url": "/api/endpoints/users",
* "methods": ["GET", "POST"],
* "exists": true,
* "api_accessible": true,
* "path": "resources/views/admin-ajax/users.blade.php"
* }
* }
*/
public function info(Request $request, string $endpoint)
{
$endpoint = $this->sanitizeEndpoint($endpoint);
$exists = $this->endpointExists($endpoint);
$isReadonly = $exists ? $this->isReadonlyEndpoint($endpoint) : false;
return response()->json([
'status' => 'success',
'data' => [
'name' => $endpoint,
'url' => "/api/endpoints/{$endpoint}",
'methods' => ['GET', 'POST'],
'exists' => $exists,
'api_accessible' => $isReadonly,
'path' => $exists ? "resources/views/admin-ajax/{$endpoint}.blade.php" : null,
'note' => !$isReadonly && $exists ? 'This endpoint is not marked as @api-readonly and cannot be accessed via API' : null
]
]);
}
/**
* Sanitize endpoint name to prevent directory traversal
*/
protected function sanitizeEndpoint(string $endpoint): string
{
// Remove any path traversal attempts
$endpoint = str_replace(['..', '/', '\\'], '', $endpoint);
// Convert to lowercase and replace spaces with dashes
$endpoint = Str::slug($endpoint);
return $endpoint;
}
/**
* Check if endpoint blade file exists
*/
protected function endpointExists(string $endpoint): bool
{
return View::exists("admin-ajax.{$endpoint}");
}
/**
* Check if endpoint is marked as readonly (safe for API access)
*
* Endpoints must have the annotation {{-- @api-readonly --}} at the beginning
* to be accessible via the API. This prevents write/delete operations from
* being exposed externally.
*/
protected function isReadonlyEndpoint(string $endpoint): bool
{
$path = resource_path("views/admin-ajax/{$endpoint}.blade.php");
if (!File::exists($path)) {
return false;
}
$content = File::get($path);
// Check for @api-readonly annotation
// Supports: {{-- @api-readonly --}} or <?php // @api-readonly
return Str::contains($content, '@api-readonly');
}
/**
* Get the file path for an endpoint
*/
protected function getEndpointPath(string $endpoint): ?string
{
// Check main directory
$path = resource_path("views/admin-ajax/{$endpoint}.blade.php");
if (File::exists($path)) {
return $path;
}
// Check subdirectories
$parts = explode('/', $endpoint);
if (count($parts) > 1) {
$subPath = resource_path("views/admin-ajax/" . implode('/', $parts) . ".blade.php");
if (File::exists($subPath)) {
return $subPath;
}
}
return null;
}
/**
* Render the endpoint blade file
*/
protected function renderEndpoint(string $endpoint, Request $request): string
{
// Start output buffering to capture any echo/print statements
ob_start();
try {
// Render the view with request data
$view = View::make("admin-ajax.{$endpoint}", [
'request' => $request
]);
// Get rendered content
$rendered = $view->render();
// Get any buffered output (from echo statements in blade)
$buffered = ob_get_clean();
// Return buffered output if exists, otherwise rendered content
return !empty($buffered) ? $buffered : $rendered;
} catch (\Exception $e) {
ob_end_clean();
throw $e;
}
}
/**
* Check if string is valid JSON
*/
protected function isJson(string $string): bool
{
if (empty($string)) {
return false;
}
$string = trim($string);
// Quick check for JSON structure
if (!in_array($string[0], ['{', '['])) {
return false;
}
json_decode($string);
return json_last_error() === JSON_ERROR_NONE;
}
/**
* Return JSON response
*/
protected function jsonResponse(string $output)
{
$data = json_decode($output, true);
// If already has status wrapper, return as-is
if (is_array($data) && isset($data['status'])) {
return response()->json($data);
}
return response()->json([
'status' => 'success',
'data' => $data,
'format' => 'json'
]);
}
/**
* Return HTML response
*/
protected function htmlResponse(string $output)
{
return response()->json([
'status' => 'success',
'html' => $output,
'format' => 'html'
]);
}
/**
* Get all available endpoints from admin-ajax directory
* Only returns endpoints marked with @api-readonly annotation
*/
public function getAvailableEndpoints(): array
{
$endpoints = [];
$path = resource_path('views/admin-ajax');
if (!File::isDirectory($path)) {
return $endpoints;
}
// Scan all blade files recursively
$this->scanEndpointsRecursive($path, '', $endpoints);
// Sort alphabetically
usort($endpoints, fn($a, $b) => strcmp($a['name'], $b['name']));
return $endpoints;
}
/**
* Recursively scan directory for readonly endpoints
*/
protected function scanEndpointsRecursive(string $basePath, string $prefix, array &$endpoints): void
{
$files = File::files($basePath);
foreach ($files as $file) {
$filename = $file->getFilename();
// Only process .blade.php files
if (!Str::endsWith($filename, '.blade.php')) {
continue;
}
// Extract endpoint name
$name = str_replace('.blade.php', '', $filename);
$fullName = $prefix ? "{$prefix}/{$name}" : $name;
// Check file content for @api-readonly annotation
$content = File::get($file->getPathname());
// SECURITY: Only include endpoints marked as readonly
if (!Str::contains($content, '@api-readonly')) {
continue;
}
$endpoints[] = [
'name' => $fullName,
'url' => "/api/endpoints/{$fullName}",
'methods' => ['GET', 'POST']
];
}
// Scan subdirectories
$directories = File::directories($basePath);
foreach ($directories as $dir) {
$dirName = basename($dir);
$newPrefix = $prefix ? "{$prefix}/{$dirName}" : $dirName;
$this->scanEndpointsRecursive($dir, $newPrefix, $endpoints);
}
}
/**
* Get endpoints grouped by category (for documentation)
*/
public function getEndpointsByCategory(): array
{
$endpoints = $this->getAvailableEndpoints();
$categories = [];
foreach ($endpoints as $endpoint) {
$name = $endpoint['name'];
// Determine category based on prefix
$category = $this->categorizeEndpoint($name);
if (!isset($categories[$category])) {
$categories[$category] = [];
}
$categories[$category][] = $endpoint;
}
ksort($categories);
return $categories;
}
/**
* Categorize endpoint based on name prefix
*/
protected function categorizeEndpoint(string $name): string
{
$prefixes = [
'ndt-' => 'NDT Operations',
'welder-' => 'Welder Management',
'weld' => 'Welding',
'register-' => 'Register Creator',
'test-package' => 'Test Packages',
'tp-' => 'Test Packages',
'document-' => 'Document Management',
'report-' => 'Reports',
'summary' => 'Summaries',
'excel-' => 'Excel Operations',
'pdf' => 'PDF Operations',
'user' => 'User Management',
'permission' => 'Permissions',
'content-' => 'Content Management',
'spool-' => 'Spool Operations',
'material-' => 'Materials',
'paint-' => 'Paint Operations',
'repair-' => 'Repair Operations',
'rfi-' => 'RFI Operations',
'punch-' => 'Punch Lists',
'mto' => 'MTO Operations',
];
foreach ($prefixes as $prefix => $category) {
if (Str::startsWith($name, $prefix)) {
return $category;
}
}
return 'General';
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Types;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
class MobileTypeController extends Controller
{
public function index()
{
$types = Types::where('is_mobile', true)->where('enabled', true)->orderBy("s", "ASC")->get();
$result = [];
foreach ($types as $type) {
if (!isAuth($type->slug, 'read')) {
continue;
}
$bladePath = resource_path("views/admin/type/{$type->slug}.blade.php");
$tableName = null;
if (File::exists($bladePath)) {
$content = File::get($bladePath);
if (preg_match('/\$tableName\s*=\s*["\']([^"\']+)["\']\s*;/', $content, $matches)) {
$tableName = $matches[1];
}
}
// Get saved mobile columns from settings
$mobileColumnsJson = setting("type_{$type->id}_mobile_columns", false, '');
$columns = [];
if ($mobileColumnsJson) {
$decoded = json_decode($mobileColumnsJson, true);
if (is_array($decoded)) {
$columns = $decoded;
}
}
$result[] = [
'title' => $type->title,
'slug' => $type->slug,
'table_name' => $tableName,
'order' => $type->s,
'parent' => $type->kid,
'icon' => asset("assets/icons/{$type->icon}.png"),
'columns' => $columns,
];
}
return response()->json($result);
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\NaksSyncService;
use Illuminate\Support\Facades\Log;
class NaksSyncController extends Controller
{
/**
* Handle upcoming sync trigger from another project.
* This endpoint is called by other projects when they update a record.
*/
public function triggerSync(Request $request, NaksSyncService $syncService)
{
$validated = $request->validate([
'module' => 'required|string',
'source_project' => 'required|string'
]);
$module = $validated['module'];
$source = $validated['source_project'];
Log::info("Received NAKS Sync Trigger from [$source] for module [$module]");
// Prevent infinite loops if logic isn't careful, but NaksSyncService checks timestamps.
// We will queue the actual sync to perform it in background
// We use 'dispatch' helper or creating a Job closure
// We can run the sync service directly here if we want immediate feedback, but it might take time.
// Best practice is to queue it.
// Let's create a closure job or reuse existing mechanism.
// Since we don't have a dedicated "RunSyncJob", we'll do it inline with fast execution or
// dispatch a closure.
dispatch(function () use ($syncService, $module) {
try {
// Sync specific module from all projects (or finding the specific source, but the service supports 'all' or filtered)
// The service reads from 'api/project-app-urls' list.
// We should sync 'all' projects to be safe, or we could optimize later.
$syncService->sync($module);
} catch (\Exception $e) {
Log::error("Error in Queued NAKS Sync: " . $e->getMessage());
}
});
return response()->json([
'message' => 'Sync triggered successfully',
'status' => 'queued'
]);
}
}
@@ -0,0 +1,914 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\DocumentRevision;
use App\Models\WeldLog;
use App\Models\WelderTest;
use App\Models\MaterialGroupMap;
use App\Models\IncomingControl;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class PdfEditorLookupController extends Controller
{
private const DRAWING_TYPES = [
'engineering' => [
'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);
}
}
}
+200
View File
@@ -0,0 +1,200 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Spool;
use App\Models\Element;
use App\Models\Joint;
use App\Models\Welder;
use App\Models\WelderAssignment;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class PdfmController extends Controller
{
// Spools CRUD
public function getSpools(): JsonResponse
{
$spools = Spool::orderBy('spool_no')->get();
return response()->json([
'success' => true,
'data' => $spools
]);
}
public function createSpool(Request $request): JsonResponse
{
$request->validate([
'spool_no' => 'required|string|unique:spools',
'description' => 'nullable|string',
'material' => 'nullable|string',
'diameter' => 'nullable|string',
'thickness' => 'nullable|string',
'length' => 'nullable|string',
'classification' => 'nullable|string'
]);
$spool = Spool::create($request->all());
return response()->json([
'success' => true,
'data' => $spool,
'message' => 'Spool created successfully!'
], 201);
}
// Elements CRUD
public function getElements(): JsonResponse
{
$elements = Element::orderBy('element_code')->get();
return response()->json([
'success' => true,
'data' => $elements
]);
}
public function createElement(Request $request): JsonResponse
{
$request->validate([
'element_code' => 'required|string|unique:elements',
'element_name' => 'required|string',
'element_type' => 'required|string',
'material' => 'nullable|string',
'diameter' => 'nullable|string',
'thickness' => 'nullable|string',
'classification' => 'nullable|string',
'weight' => 'nullable|numeric'
]);
$element = Element::create($request->all());
return response()->json([
'success' => true,
'data' => $element,
'message' => 'Element created successfully!'
], 201);
}
// Joints CRUD
public function getJoints(): JsonResponse
{
$joints = Joint::with(['spool', 'elementFrom', 'elementTo', 'welderAssignments.welder'])
->orderBy('joint_no')
->get();
return response()->json([
'success' => true,
'data' => $joints
]);
}
public function createJoint(Request $request): JsonResponse
{
$request->validate([
'joint_no' => 'required|string|unique:joints',
'spool_no' => 'required|string|exists:spools,spool_no',
'element_from' => 'nullable|string|exists:elements,element_code',
'element_to' => 'nullable|string|exists:elements,element_code',
'joint_type' => 'nullable|string',
'welding_type' => 'nullable|string',
'classification' => 'nullable|string'
]);
$joint = Joint::create($request->all());
return response()->json([
'success' => true,
'data' => $joint,
'message' => 'Joint created successfully!'
], 201);
}
// Welders CRUD
public function getWelders(): JsonResponse
{
$welders = Welder::orderBy('welder_name')->get();
return response()->json([
'success' => true,
'data' => $welders
]);
}
public function createWelder(Request $request): JsonResponse
{
$request->validate([
'welder_code' => 'required|string|unique:welders',
'name' => 'required|string',
'certification' => 'nullable|string',
'specialization' => 'nullable|string'
]);
$welder = Welder::create($request->all());
return response()->json([
'success' => true,
'data' => $welder,
'message' => 'Welder created successfully!'
], 201);
}
// Welder Assignments
public function assignWelder(Request $request): JsonResponse
{
$request->validate([
'joint_no' => 'required|string|exists:joints,joint_no',
'welder_code' => 'required|string|exists:welders,welder_code',
'assigned_date' => 'nullable|date'
]);
$assignment = WelderAssignment::create($request->all());
return response()->json([
'success' => true,
'data' => $assignment,
'message' => 'Welder assigned successfully!'
], 201);
}
// Auto Generate Spool Number
public function generateSpoolNumber(): JsonResponse
{
$lastSpool = Spool::orderBy('id', 'desc')->first();
$nextNumber = $lastSpool ? (int)substr($lastSpool->spool_no, 3) + 1 : 1;
$spoolNumber = 'SP-' . str_pad($nextNumber, 3, '0', STR_PAD_LEFT);
// Auto-create the spool in database so it persists
$spool = Spool::create([
'spool_no' => $spoolNumber,
'description' => 'Generated from PDF Editor'
]);
return response()->json([
'success' => true,
'data' => ['spool_no' => $spoolNumber, 'id' => $spool->id]
]);
}
// Auto Generate Joint Number
public function generateJointNumber(Request $request): JsonResponse
{
// Make spool_no optional with nullable validation
$validated = $request->validate(['spool_no' => 'nullable|string']);
$spoolNo = $validated['spool_no'] ?? 'SP-001'; // Default spool if not provided
$lastJoint = Joint::where('spool_no', $spoolNo)
->orderBy('id', 'desc')
->first();
$nextNumber = $lastJoint ? (int)substr($lastJoint->joint_no, 3) + 1 : 1;
$jointNumber = 'J-' . str_pad($nextNumber, 3, '0', STR_PAD_LEFT);
// Auto-create the joint in database so it persists
$joint = Joint::create([
'joint_no' => $jointNumber,
'spool_no' => $spoolNo,
'joint_type' => 'Auto-generated',
'description' => 'Generated from PDF Editor'
]);
return response()->json([
'success' => true,
'data' => ['joint_no' => $jointNumber, 'id' => $joint->id]
]);
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
/**
* @group Project Settings
*
* APIs for project-related settings and configurations.
*/
class ProjectAppUrlsController extends Controller
{
/**
* Get Project App URLs
*
* Returns a list of configured project names and their corresponding application URLs.
* This endpoint is public and requires no authentication.
*
* @response 200 {
* "status": "success",
* "data": [
* {
* "project_name": "208 - Dzerzhinsk",
* "url": "https://208.qms.stellarcons.com/admin"
* },
* {
* "project_name": "102 - VIKSA OMK",
* "url": "https://viksaqms.stellarcons.com/"
* }
* ]
* }
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
// "project-app-urls" ayarını getir
$settings = setting('project-app-urls');
// Eğer string gelirse decode et, zaten array/object ise olduğu gibi kullan
$data = is_string($settings) ? json_decode($settings, true) : $settings;
// Veri yoksa boş array döndür
if (is_null($data)) {
$data = [];
}
return response()->json([
'status' => 'success',
'data' => $data
]);
}
}
@@ -0,0 +1,110 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Endroid\QrCode\QrCode as EndroidQrCode;
use Endroid\QrCode\Writer\PngWriter;
class QRCodeController extends Controller
{
public function generate(Request $request): JsonResponse
{
try {
$request->validate([
'shape_id' => 'required|string',
]);
$shapeId = $request->get('shape_id');
// QR kod içeriği - shape ID ile URL oluştur
$qrContent = url('/api/pdf-editor/annotations/' . $shapeId);
// QR kod PNG olarak üret (Endroid QR Code v6)
$qrCode = new EndroidQrCode(
data: $qrContent,
size: 200,
margin: 10
);
$writer = new PngWriter();
$result = $writer->write($qrCode);
$qrCodeData = $result->getString();
// QR kod dosyasını kaydet
$fileName = 'qr_' . $shapeId . '_' . time() . '.png';
// Klasörü oluştur
$qrDir = storage_path('app/public/qr-codes');
if (!file_exists($qrDir)) {
mkdir($qrDir, 0755, true);
}
// QR kod dosyasını kaydet
$filePath = $qrDir . '/' . $fileName;
file_put_contents($filePath, $qrCodeData);
// Public klasörüne de kopyala
$publicQrDir = public_path('storage/qr-codes');
if (!file_exists($publicQrDir)) {
mkdir($publicQrDir, 0755, true);
}
$publicFilePath = $publicQrDir . '/' . $fileName;
file_put_contents($publicFilePath, $qrCodeData);
return response()->json([
'success' => true,
'data' => [
'shape_id' => $shapeId,
'qr_content' => $qrContent,
'qr_code_url' => url('storage/qr-codes/' . $fileName)
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage(),
'data' => null
], 500);
}
}
public function generateForAnnotation($shapeId): JsonResponse
{
try {
// QR kod içeriği
$qrContent = url('/api/annotations/' . $shapeId);
// QR kod PNG olarak üret (Endroid QR Code v6)
$qrCode = new EndroidQrCode(
data: $qrContent,
size: 150,
margin: 5
);
$writer = new PngWriter();
$result = $writer->write($qrCode);
$qrCodeData = $result->getString();
// Base64 encode for frontend
$base64QrCode = 'data:image/png;base64,' . base64_encode($qrCodeData);
return response()->json([
'success' => true,
'data' => [
'shape_id' => $shapeId,
'qr_code' => $base64QrCode,
'qr_content' => $qrContent
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage(),
'data' => null
], 500);
}
}
}
@@ -0,0 +1,102 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class SettingController extends Controller
{
/**
* Get a specific setting value for mobile.
*
* @param Request $request
* @param string $key
* @return \Illuminate\Http\JsonResponse
*/
public function get(Request $request, $key)
{
$allowedKeys = [
'unit_kgmetc',
'akt_status',
];
if (!in_array($key, $allowedKeys)) {
return response()->json([
'status' => 'error',
'message' => 'Unauthorized or invalid key'
], 404);
}
$value = setting($key);
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE && (is_array($decoded) || is_object($decoded))) {
$data = $decoded;
} else {
$data = $value;
}
return response()->json([
'status' => 'success',
'key' => $key,
'data' => $data
]);
}
/**
* Get all common settings.
*
* @return \Illuminate\Http\JsonResponse
*/
public function common()
{
$keys = [
'project_name',
'project_name_ru',
'project_number',
'company_code',
'row_count',
'summary_mails',
'register_creator_file_name_pattern',
'register_creator_start_row',
'register_creator_path',
'register_creator_column_based',
'hand_over_zone_column',
'tp_register_creator_path',
'main_subcontractor',
'cron_job_batch_process_count',
'cron_job_period',
'repair_table_target_rate',
'ndt_limit',
'ndt_wdi',
'repair_limit',
'repair_wdi',
'WelderQualitificationStatusExpiredDate',
'WelderQualitificationStatusRemaningDays',
'ndt_date_validation_active',
'header_color',
'DevExpress_Theme',
'pdf_template_path',
'handover_control_count'
];
$data = [];
foreach ($keys as $key) {
$value = setting($key);
// Attempt to decode JSON if it looks like an array/object
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE && (is_array($decoded) || is_object($decoded))) {
$data[$key] = $decoded;
} else {
$data[$key] = $value;
}
}
return response()->json([
'status' => 'success',
'data' => $data
]);
}
}
@@ -0,0 +1,273 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
/**
* @group Spool Release Workflow
*
* API endpoints for managing spool release workflow steps and rollbacks.
*/
class SpoolReleaseWorkflowController extends Controller
{
/**
* Advance Spool Release Workflow
*
* Advance the status of a spool through the quality control and painting workflow.
*
* @authenticated
*
* @bodyParam action string required The action to perform (paint, ndt_release, ndt_paint_release, spool_release). Example: paint
* @bodyParam iso_number string Filter by ISO Number (for single operation). Example: 10-BFW-RS1-0001
* @bodyParam spool_number string Filter by Spool Number (for single operation). Example: SPL-001
* @bodyParam iso_spool_pairs string Comma-separated ISO|SPOOL pairs for bulk operations. Example: ISO-001|SPL-001,ISO-001|SPL-002
*
* @response {
* "status": "success",
* "message": "1 row affected",
* "count": 1
* }
*/
public function advance(Request $request)
{
$action = $request->input('action');
$isoNumber = $request->input('iso_number');
$spoolNumber = $request->input('spool_number');
$isoSpoolPairs = $request->input('iso_spool_pairs');
if (!$isoSpoolPairs && (!$isoNumber || !$spoolNumber)) {
return response()->json(['status' => 'error', 'message' => 'Missing ISO and Spool information']);
}
// Prepare pairs array
$pairs = [];
if ($isoSpoolPairs) {
$rawPairs = explode(",", $isoSpoolPairs);
foreach ($rawPairs as $p) {
$parts = explode("|", trim($p));
if (count($parts) == 2) {
$pairs[] = ['iso' => trim($parts[0]), 'spool' => trim($parts[1])];
}
}
} else {
$pairs[] = ['iso' => trim($isoNumber), 'spool' => trim($spoolNumber)];
}
if (empty($pairs)) {
return response()->json(['status' => 'error', 'message' => 'No valid ISO|Spool pairs found']);
}
// Reconstruct pairs string for side effects if needed
$reconstructedPairs = implode(',', array_map(fn($p) => "{$p['iso']}|{$p['spool']}", $pairs));
// Mock $_POST for legacy view calls (side effects)
$_POST['iso_spool_pairs'] = $reconstructedPairs;
$_POST['iso_number'] = implode(',', array_column($pairs, 'iso'));
$_POST['spool_number'] = implode(',', array_column($pairs, 'spool'));
// Ensure legacy u() function works
if ($request->user()) {
\Session::put('cached_user', $request->user());
\Session::put('uid', $request->user()->id);
}
$userId = $request->user() ? $request->user()->id : 0;
$totalAffected = 0;
try {
DB::beginTransaction();
if ($action == 'paint') {
$pattern = setting("paint_release_number_pattern");
$checkNo = str_replace("{number}", get_counter($pattern), $pattern);
foreach ($pairs as $pair) {
$affected = db("weld_logs")
->where("iso_number", $pair['iso'])
->where("spool_number", $pair['spool'])
->whereNotIn("spool_status", ['Completed'])
->whereIn("spool_status", ['Paint', 'Spool Release', 'NDT Release', 'QC', 'Manufacturing', 'Weld Complete'])
->update([
"spool_paint_check" => $userId,
"paint_release_no" => $checkNo,
"paint_release_date" => simdi(),
"spool_status" => "Completed"
]);
$totalAffected += $affected;
if ($affected > 0) {
try {
ob_start();
view('admin.type.spool-release.paint-follow-up-updater', [
'isoNumber' => $pair['iso'],
'spoolNumber' => $pair['spool'],
])->render();
ob_end_clean();
} catch (\Exception $e) {
ob_end_clean();
\Log::warning("Paint follow up updater error: " . $e->getMessage());
}
}
}
} elseif ($action == 'ndt_release') {
$pattern = setting("ndt_release_number_pattern");
$newReleaseNo = str_replace("{number}", get_counter($pattern), $pattern);
foreach ($pairs as $pair) {
$affected = db("weld_logs")
->where("iso_number", $pair['iso'])
->where("spool_number", $pair['spool'])
->whereNotNull("spool_ndt_check_no")
->whereNull("ndt_release_no")
->update([
"ndt_release_no" => $newReleaseNo,
"ndt_release_check" => $userId,
"ndt_release_date" => Carbon::now(),
'spool_status' => "Paint",
]);
$totalAffected += $affected;
}
} elseif ($action == 'ndt_paint_release') {
$pattern = setting("paint_release_number_pattern");
foreach ($pairs as $pair) {
$newRequestNo = str_replace("{number}", get_counter($pattern), $pattern);
$checkPaintingCycle = db("weld_logs")
->where('iso_number', $pair['iso'])
->where('spool_number', $pair['spool'])
->select("painting_cycle")
->first();
$spoolStatus = (!empty($checkPaintingCycle) && !empty($checkPaintingCycle->painting_cycle)) ? "Paint" : "Completed";
$affected = db("weld_logs")
->where('iso_number', $pair['iso'])
->where('spool_number', $pair['spool'])
->whereNotNull("spool_ndt_check_no")
->whereNull("ndt_release_date")
->update([
"ndt_release_no" => $newRequestNo,
"ndt_release_check" => $userId,
"ndt_release_date" => Carbon::now(),
'spool_status' => $spoolStatus,
]);
$totalAffected += $affected;
if ($affected > 0) {
try {
ob_start();
view('admin.type.spool-release.paint-follow-up-updater', [
'isoNumber' => $pair['iso'],
'spoolNumber' => $pair['spool'],
])->render();
ob_end_clean();
} catch (\Exception $e) {
ob_end_clean();
\Log::warning("Paint follow up updater error: " . $e->getMessage());
}
}
}
} elseif ($action == 'spool_release' || $action == 'checklist_ok') {
$pattern = setting("spool_release_check_number_pattern");
$checkNo = str_replace("{number}", get_counter($pattern), $pattern);
foreach ($pairs as $pair) {
$affected = db("weld_logs")
->where("iso_number", $pair['iso'])
->where("spool_number", $pair['spool'])
->whereIn("spool_status", ["Spool Release", "Manufacturing", "Weld Complete"])
->update([
"spool_ndt_check" => $userId,
"spool_ndt_check_no" => $checkNo,
"spool_ndt_check_date" => simdi(),
"spool_status" => "NDT Release"
]);
$totalAffected += $affected;
}
// Side effects
ob_start();
view('admin.type.spool-release.spool-document-generate')->render();
view('cron.spool-status-changer', ['iso_spool_pairs' => $reconstructedPairs])->render();
ob_end_clean();
} else {
DB::rollBack();
return response()->json(['status' => 'error', 'message' => 'Invalid action']);
}
dispatchCacheBladeViews();
DB::commit();
return response()->json([
'status' => 'success',
'message' => $totalAffected . ' row(s) affected',
'count' => $totalAffected
]);
} catch (\Exception $e) {
DB::rollBack();
\Log::error("SpoolReleaseWorkflow error: " . $e->getMessage());
return response()->json([
'status' => 'error',
'message' => 'Internal server error: ' . $e->getMessage()
], 500);
}
}
/**
* Rollback Spool Release
*
* Reverts a spool release action using the legacy rollback system.
*
* @authenticated
*
* @bodyParam id integer required The ID of the spool or record to rollback. Example: 123
* @bodyParam iso_number string required The ISO number to rollback. Example: 10-BFW-RS1-0001
* @bodyParam spool_number string required The Spool number to rollback. Example: SPL-001
*
* @response {
* "status": "success",
* "message": "Rollback applied"
* }
* @response {
* "status": "error",
* "message": "Missing parameters for rollback"
* }
*/
public function rollback(Request $request)
{
$id = $request->input('id');
$isoNumber = $request->input('iso_number');
$spoolNumber = $request->input('spool_number');
if (!$id || !$isoNumber || !$spoolNumber) {
return response()->json(['status' => 'error', 'message' => 'Missing parameters for rollback']);
}
// Ensure u() acts like authenticated API user inside rollback
$_GET['rollback'] = $id;
$_GET['iso_number'] = $isoNumber;
$_GET['spool_number'] = $spoolNumber;
ob_start();
include resource_path('views/admin/type/spool-release/rollback.blade.php');
$output = ob_get_clean();
// Rollback outputs bilgi() which sets session flash and might render a Pjax wrapper
// But the rollback was successful if it gets to the end. Let's just return success JSON
return response()->json([
'status' => 'success',
'message' => 'Rollback applied'
]);
}
}
@@ -0,0 +1,656 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Models\WeldLog;
use App\Models\Handover;
use App\Models\WeldingEquipment;
/**
* @group Stats
*
* İstatistikler ve Hesaplamalar ile ilgili endpointler.
*/
class StatsController extends Controller
{
/**
* NDT Calculation (General)
*
* Genel NDT hesaplama istatistiklerini getirir.
* Bu endpoint varsayılan 'ndt-calculation' blade cache sonucunu döndürür.
*
* @response 200 {
* "status": "success",
* "type": "general",
* "content": "..."
* }
*/
public function ndtCalculation()
{
// Varsayılan hesaplama (cacheBladeLoad('ndt-calculation'))
// cacheBladeLoad echo yaptığı için çıktıyı yakalıyoruz.
ob_start();
cacheBladeLoad('ndt-calculation');
$content = ob_get_clean();
// Eğer content bir JSON string ise decode et
$decoded = json_decode($content);
if (json_last_error() === JSON_ERROR_NONE) {
$content = $decoded;
}
return response()->json([
'status' => 'success',
'type' => 'general',
'content' => $content
]);
}
/**
* NDT Calculation (Test Packs)
*
* Test Paketleri için NDT hesaplama istatistiklerini getirir.
* 'ndtCalculationTestPack' ayarını döndürür.
*
* @response 200 {
* "status": "success",
* "type": "test_packs",
* "content": "..."
* }
*/
public function ndtCalculationTestPacks()
{
$content = setting('ndtCalculationTestPack');
// Eğer content bir JSON string ise decode et
$decoded = json_decode($content);
if (json_last_error() === JSON_ERROR_NONE) {
$content = $decoded;
}
return response()->json([
'status' => 'success',
'type' => 'test_packs',
'content' => $content
]);
}
/**
* NDT Calculation (Backlogs)
*
* Backloglar için NDT hesaplama istatistiklerini getirir.
* 'ndtCalculationBacklogs' ayarını döndürür.
*
* @queryParam type string Filtreleme türü (opsiyonel). Example: backlogs
*
* @response 200 {
* "status": "success",
* "type": "backlogs",
* "content": "..."
* }
*/
public function ndtCalculationBacklogs()
{
request()->merge(['type' => 'backlogs']);
$_GET['type'] = 'backlogs';
ob_start();
cacheBladeLoad('ndt-calculation');
$content = ob_get_clean();
// Eğer content bir JSON string ise decode et
$decoded = json_decode($content);
if (json_last_error() === JSON_ERROR_NONE) {
$content = $decoded;
}
return response()->json([
'status' => 'success',
'type' => 'backlogs',
'content' => $content
]);
}
/**
* Dashboard Overview Stats
*
* Get aggregated statistics for project overview dashboard.
* This endpoint is protected by a static hash.
* Includes:
* - WDI Monthly (Shop/Field/Total)
* - Spool Progress
* - NDT/Repair Backlog
* - Hand-over Status
* - Welding Equipment Status
*
* @header X-Api-Hash string required The static API hash for authentication. Example: your-secret-hash-key
*
* @response 200 {
* "status": "success",
* "data": {
* "wdi_monthly": [...],
* "spool_progress": [...],
* "ndt_backlog": [...],
* "handover_status": [...],
* "welding_equipment": [...]
* }
* }
*/
public function dashboardStats(\Illuminate\Http\Request $request)
{
$year = $request->input('year', date('Y'));
// 1. WDI Monthly Data (Global Aggregate) - Calendar Year
$wdiMonthly = WeldLog::selectRaw("
DATE_FORMAT(welding_date, '%Y-%m') as month,
MONTH(welding_date) as month_num,
SUM(CASE WHEN type_of_joint = 'S' THEN nps_1 ELSE 0 END) as shop,
SUM(CASE WHEN type_of_joint != 'S' OR type_of_joint IS NULL THEN nps_1 ELSE 0 END) as field,
SUM(nps_1) as total
")
->whereNotNull('welding_date')
// ->whereYear('welding_date', $year) // Removed to allow client-side filtering
->groupBy('month', 'month_num')
->orderBy('month', 'asc')
->get();
// 1b. WDI Monthly Data (By Project)
$wdiByProjectRaw = WeldLog::selectRaw("
project,
DATE_FORMAT(welding_date, '%Y-%m') as month,
MONTH(welding_date) as month_num,
SUM(CASE WHEN type_of_joint = 'S' THEN nps_1 ELSE 0 END) as shop,
SUM(CASE WHEN type_of_joint != 'S' OR type_of_joint IS NULL THEN nps_1 ELSE 0 END) as field,
SUM(nps_1) as total
")
->whereNotNull('welding_date')
->whereNotNull('project')
->where('project', '!=', '')
->whereYear('welding_date', $year) // Keep this for now or remove if needed, but it's unused by dashboard
->groupBy('project', 'month', 'month_num')
->get();
$wdiByProject = [];
foreach($wdiByProjectRaw as $row) {
// Use month_num (1-12) as key for the frontend table columns which expect 1..12
// For the new 'wdiByProject' which is custom for the table, we simply need the integer month key
// because the table logic iterates 1 through 12.
$wdiByProject[$row->project][$row->month_num] = [
'shop' => (float)$row->shop,
'field' => (float)$row->field,
'total' => (float)$row->total,
'month_str' => $row->month // Useful for debugging or generic logic
];
}
// 2. Spool Progress (Time-independent)
// Logic: Group by iso_number + spool_number (Shop only), find effective status based on priority logic.
// Priority: Waiting < Manufacturing < QC < Paint < Completed
$statusCase = "CASE spool_status
WHEN 'Waiting' THEN 1
WHEN 'Manufacturing' THEN 2
WHEN 'QC' THEN 3
WHEN 'Paint' THEN 4
WHEN 'Completed' THEN 5
ELSE 99 END";
$spoolStats = DB::table('weld_logs')
->selectRaw("status_val, COUNT(*) as count")
->fromSub(function($query) use ($statusCase) {
$query->select('iso_number', 'spool_number')
->selectRaw("MIN($statusCase) as status_order")
->selectRaw("
CASE MIN($statusCase)
WHEN 1 THEN 'Waiting'
WHEN 2 THEN 'Manufacturing'
WHEN 3 THEN 'QC'
WHEN 4 THEN 'Paint'
WHEN 5 THEN 'Completed'
ELSE 'Other'
END as status_val
")
->from('weld_logs')
->where('type_of_joint', 'S') // Shop joints drive spool status
->whereNotNull('spool_number')
->where('spool_number', '!=', '')
->whereNotNull('welding_date')
->groupBy('iso_number', 'spool_number');
}, 'sub')
->groupBy('status_val')
->get();
// Transform to array format for frontend
$spoolProgress = [];
foreach ($spoolStats as $row) {
$spoolProgress[] = [
'status' => $row->status_val,
'count' => (int)$row->count
];
}
// 3. NDT/Repair Backlog
// Using existing setting logic from ndt-backlog.blade.php
$ndtData = json_decode(setting("ndtCalculation"), true);
$ndtBacklogStats = [];
if (is_array($ndtData)) {
foreach($ndtData as $d) {
foreach($d as $field => $value) {
if(strpos($field, "backlog") !== false) {
$key = strtoupper(str_replace("_backlog", "", $field));
if(!isset($ndtBacklogStats[$key])) {
$ndtBacklogStats[$key] = 0;
}
if($value > 0) {
$ndtBacklogStats[$key] += $value;
}
}
}
}
}
// Add dynamic limits from Settings
$ndtBacklogStats['ndt_limit'] = setting('ndt_limit');
$ndtBacklogStats['ndt_wdi'] = setting('ndt_wdi');
$ndtBacklogStats['repair_limit'] = setting('repair_limit');
$ndtBacklogStats['repair_wdi'] = setting('repair_wdi');
// 4. Hand-over Status
// Statuses: Dynamically fetched from id_status
$handoverProgress = Handover::selectRaw('id_status, COUNT(*) as count')
->whereNotNull('id_status')
->where('id_status', '!=', '')
->groupBy('id_status')
->pluck('count', 'id_status')
->toArray();
// 5. Welding Equipment Status
// Statuses: Working, Repair, Out of use
$equipmentStats = WeldingEquipment::selectRaw('working_status, COUNT(*) as count')
->whereNotNull('working_status')
->groupBy('working_status')
->get()
->pluck('count', 'working_status');
$targetEquipmentStatuses = ['Working', 'Repair', 'Out of use'];
$equipmentProgress = [];
foreach ($targetEquipmentStatuses as $status) {
$equipmentProgress[$status] = $equipmentStats[$status] ?? 0;
}
// Add others
$otherEquipmentCount = 0;
foreach ($equipmentStats as $status => $count) {
if (!in_array($status, $targetEquipmentStatuses)) {
$otherEquipmentCount += $count;
}
}
if ($otherEquipmentCount > 0) {
$equipmentProgress['Other'] = $otherEquipmentCount;
}
// 6. Welding Status (Welder Distribution by Site)
// Logic: Group by project (Site).
// CS = distinct real_welder_1 & real_welder_2 where ru_material_group_1 matches 'M01'
// SS/AS = distinct real_welder_1 & real_welder_2 where ru_material_group_1 does NOT match 'M01'
$weldingStatusData = DB::table(function ($query) {
$query->select('project', 'type_of_joint', 'ru_material_group_1', 'real_welder_1 as welder', 'welding_date')
->from('weld_logs')
->whereNotNull('real_welder_1')
->where('real_welder_1', '!=', '')
->whereNotNull('welding_date')
->unionAll(
DB::table('weld_logs')
->select('project', 'type_of_joint', 'ru_material_group_2', 'real_welder_1 as welder', 'welding_date')
->from('weld_logs')
->whereNotNull('real_welder_2')
->where('real_welder_2', '!=', '')
->whereNotNull('welding_date')
);
}, 'u')
->select('project')
->selectRaw("YEAR(welding_date) as year")
->selectRaw("MONTH(welding_date) as month")
->selectRaw("COUNT(DISTINCT CASE WHEN type_of_joint = 'S' THEN welder END) as shop")
->selectRaw("COUNT(DISTINCT CASE WHEN type_of_joint != 'S' OR type_of_joint IS NULL THEN welder END) as field")
->selectRaw("COUNT(DISTINCT CASE WHEN (ru_material_group_1 LIKE '%M01%' OR ru_material_group_1 LIKE '%М01%') THEN welder END) as cs")
->selectRaw("COUNT(DISTINCT CASE WHEN (ru_material_group_1 NOT LIKE '%M01%' AND ru_material_group_1 NOT LIKE '%М01%') OR ru_material_group_1 IS NULL THEN welder END) as ss")
->selectRaw("COUNT(DISTINCT welder) as actual_total")
->whereNotNull('project')
->where('project', '!=', '')
->groupBy('project', 'year', 'month')
->get();
$weldingStatus = [];
foreach($weldingStatusData as $row) {
$weldingStatus[] = [
'site' => $row->project,
'year' => (int)$row->year,
'month' => (int)$row->month,
'shop' => (int)$row->shop,
'field' => (int)$row->field,
'cs' => (int)$row->cs,
'ss' => (int)$row->ss,
'total' => (int)$row->actual_total
];
}
// 7. Welding Status Overall Summary (No Site Breakdown)
// Count unique welders across ALL projects for Shop and Field
// 7. Welding Status Overall Summary (No Site Breakdown)
// Count unique welders across ALL projects for Shop and Field
$weldingSummaryRaw = DB::table(function ($query) {
$query->select('type_of_joint', 'real_welder_1 as welder')
->from('weld_logs')
->whereNotNull('real_welder_1')
->where('real_welder_1', '!=', '')
->whereNotNull('welding_date')
->unionAll(
DB::table('weld_logs')
->select('type_of_joint', 'real_welder_2 as welder')
->from('weld_logs')
->whereNotNull('real_welder_2')
->where('real_welder_2', '!=', '')
->whereNotNull('welding_date')
);
}, 'u')
->selectRaw("COUNT(DISTINCT CASE WHEN type_of_joint = 'S' THEN welder END) as shop")
->selectRaw("COUNT(DISTINCT CASE WHEN type_of_joint != 'S' OR type_of_joint IS NULL THEN welder END) as field")
->selectRaw("COUNT(DISTINCT welder) as actual_total")
->first();
$weldingStatusSummary = [
'shop' => (int)$weldingSummaryRaw->shop,
'field' => (int)$weldingSummaryRaw->field,
'total' => (int)$weldingSummaryRaw->actual_total
];
// 8. Welding Status Monthly (Shop/Field/Total per Month)
// Groups distinct welders by month and project
// 8. Welding Status Monthly (Shop/Field/Total per Month)
// Groups distinct welders by month (Global Site Scope)
$weldingStatusMonthly = DB::table(function ($query) use ($year) {
$query->select('project', 'welding_date', 'type_of_joint', 'real_welder_1 as welder')
->from('weld_logs')
->whereNotNull('real_welder_1')
->where('real_welder_1', '!=', '')
->whereNotNull('welding_date')
->unionAll(
DB::table('weld_logs')
->select('project', 'welding_date', 'type_of_joint', 'real_welder_2 as welder')
->from('weld_logs')
->whereNotNull('real_welder_2')
->where('real_welder_2', '!=', '')
->whereNotNull('welding_date')
);
}, 'u')
->selectRaw("
DATE_FORMAT(welding_date, '%Y-%m') as month_key,
MONTH(welding_date) as month,
YEAR(welding_date) as year,
COUNT(DISTINCT CASE WHEN type_of_joint = 'S' THEN welder END) as shop,
COUNT(DISTINCT CASE WHEN type_of_joint != 'S' OR type_of_joint IS NULL THEN welder END) as field,
COUNT(DISTINCT welder) as actual_total
")
->groupBy('month_key', 'month', 'year')
->orderBy('month_key', 'asc')
->get()
->map(function($item) {
return [
'project' => 'Total', // Value is now aggregated for the whole site
'month_key' => $item->month_key,
'month' => (int)$item->month,
'year' => (int)$item->year,
'shop' => (int)$item->shop,
'field' => (int)$item->field,
'total' => (int)$item->actual_total
];
});
// 9. NDT Summary (RT & UT)
// New calculation logic mirrored from summary-nocache.blade.php
$ndtSummary = [];
// RT Logic
$rtTotal = DB::table('radiographic_tests')->whereNotNull('rt_result')->count();
$rtWDI = DB::table('radiographic_tests')->whereNotNull('rt_result')->sum('nps_1');
$rtAccepted = DB::table('radiographic_tests')->where('rt_result', 'Accept / Годен')->count();
$rtRejected = DB::table('radiographic_tests')->whereNotNull('rt_result')->whereNotIn('rt_result', ['', 'Accept / Годен'])->count();
// RT Repair Rate logic: (ShopRepair + FieldRepair) / (ShopTotal + FieldTotal)
$rtShopTotal = DB::table('radiographic_tests')->where('type_of_joint', 'S')->whereNotIn('rt_result', [''])->count();
$rtShopRepair = DB::table('radiographic_tests')->where('type_of_joint', 'S')->whereNotIn('rt_result', ['', 'Accept / Годен'])->count();
$rtFieldTotal = DB::table('radiographic_tests')->where('type_of_joint', 'F')->whereNotIn('rt_result', [''])->count();
$rtFieldRepair = DB::table('radiographic_tests')->where('type_of_joint', 'F')->whereNotIn('rt_result', ['', 'Accept / Годен'])->count();
$rtRepairRate = ($rtShopTotal + $rtFieldTotal) > 0
? round(($rtShopRepair + $rtFieldRepair) * 100 / ($rtShopTotal + $rtFieldTotal), 2)
: 0;
$ndtSummary['rt'] = [
'total' => $rtTotal,
'wdi' => (float)$rtWDI,
'accepted' => $rtAccepted,
'rejected' => $rtRejected,
'repair_rate' => $rtRepairRate
];
// UT Logic
$utTotal = DB::table('ultrasonic_tests')->whereNotNull('ut_result')->count();
$utWDI = DB::table('ultrasonic_tests')->whereNotNull('ut_result')->sum('nps_1');
$utAccepted = DB::table('ultrasonic_tests')->where('ut_result', 'Accept / Годен')->count();
$utRejected = DB::table('ultrasonic_tests')->whereNotNull('ut_result')->where('ut_result', '!=', 'Accept / Годен')->count();
// UT Repair Rate
$utShopTotal = DB::table('ultrasonic_tests')->where('type_of_joint', 'S')->whereNotIn('ut_result', [''])->count();
$utShopRepair = DB::table('ultrasonic_tests')->where('type_of_joint', 'S')->whereNotIn('ut_result', ['', 'Accept / Годен'])->count();
$utFieldTotal = DB::table('ultrasonic_tests')->where('type_of_joint', 'F')->whereNotIn('ut_result', [''])->count();
$utFieldRepair = DB::table('ultrasonic_tests')->where('type_of_joint', 'F')->whereNotIn('ut_result', ['', 'Accept / Годен'])->count();
$utRepairRate = ($utShopTotal + $utFieldTotal) > 0
? round(($utShopRepair + $utFieldRepair) * 100 / ($utShopTotal + $utFieldTotal), 2)
: 0;
$ndtSummary['ut'] = [
'total' => $utTotal,
'wdi' => (float)$utWDI,
'accepted' => $utAccepted,
'rejected' => $utRejected,
'repair_rate' => $utRepairRate
];
// 10. WDI Statistics (CS vs SS/AS) by Project
// CS: Material Group contains M01
// SS: Material Group does not contain M01
$wdiStatsData = DB::table('weld_logs')
->select('project')
->selectRaw("YEAR(welding_date) as year")
->selectRaw("MONTH(welding_date) as month")
->selectRaw("SUM(CASE WHEN (ru_material_group_1 LIKE '%M01%' OR ru_material_group_1 LIKE '%М01%') THEN nps_1 ELSE 0 END) as cs_wdi")
->selectRaw("SUM(CASE WHEN (ru_material_group_1 NOT LIKE '%M01%' AND ru_material_group_1 NOT LIKE '%М01%') OR ru_material_group_1 IS NULL THEN nps_1 ELSE 0 END) as ss_wdi")
->whereNotNull('welding_date')
->where('project', '!=', '')
->whereNotNull('project')
->groupBy('project', 'year', 'month')
->get();
$wdiStats = [];
foreach($wdiStatsData as $row) {
$wdiStats[] = [
'project' => $row->project,
'year' => (int)$row->year,
'month' => (int)$row->month,
'cs' => (float)$row->cs_wdi,
'ss' => (float)$row->ss_wdi,
'total' => (float)$row->cs_wdi + (float)$row->ss_wdi
];
}
// 11. Weld Backlog (Prefab vs Field) by Project
// Backlog: Items with no welding date, grouped by year/month from fit_up_date for frontend filtering
$weldBacklogData = DB::table('weld_logs')
->select('project')
->selectRaw("SUM(CASE WHEN type_of_joint = 'S' THEN nps_1 ELSE 0 END) as prefab_backlog")
->selectRaw("SUM(CASE WHEN type_of_joint != 'S' OR type_of_joint IS NULL THEN nps_1 ELSE 0 END) as field_backlog")
->selectRaw("SUM(nps_1) as total_backlog")
->whereNull('welding_date')
->where('project', '!=', '')
->whereNotNull('project')
->groupBy('project')
->get();
$weldBacklog = [];
foreach($weldBacklogData as $row) {
$weldBacklog[] = [
'project' => $row->project,
'prefab' => (float)$row->prefab_backlog,
'field' => (float)$row->field_backlog,
'total' => (float)$row->total_backlog
];
}
// 12. Welder Current (from naks_welders table)
// Total unique welders at each site, categorized by CS/SS
// CS: material_1 contains М01 or M01
// SS: material_1 does NOT contain М01/M01
// No date filter - shows total registered welders per site
$welderCurrentData = DB::table('welder_tests')
->selectRaw("COUNT(DISTINCT CASE WHEN (material_group_1 LIKE '%M01%' OR material_group_1 LIKE '%М01%') THEN naks_id END) as cs")
->selectRaw("COUNT(DISTINCT CASE WHEN (material_group_1 NOT LIKE '%M01%' AND material_group_1 NOT LIKE '%М01%') OR material_group_1 IS NULL THEN naks_id END) as ss")
->selectRaw("COUNT(DISTINCT naks_id) as total")
->where('status', 'Active')
->get();
$welderCurrent = [];
foreach($welderCurrentData as $row) {
$welderCurrent[] = [
'project' => '',
'cs' => (int)$row->cs,
'ss' => (int)$row->ss,
'total' => (int)$row->total
];
}
return response()->json([
'status' => 'success',
'data' => [
'wdi_monthly' => $wdiMonthly,
'spool_progress' => $spoolProgress,
'ndt_backlog' => $ndtBacklogStats,
'handover_status' => $handoverProgress,
'welding_equipment' => $equipmentProgress,
'welding_status' => $weldingStatus,
'welding_status_monthly' => $weldingStatusMonthly,
'welding_status_summary' => $weldingStatusSummary,
'ndt_summary' => $ndtSummary,
'wdi_by_project' => $wdiByProject,
'wdi_stats' => $wdiStats,
'weld_backlog' => $weldBacklog,
'repair_stats_monthly' => DB::table('weld_logs')
->selectRaw("
DATE_FORMAT(welding_date, '%Y-%m') as month_key,
YEAR(welding_date) as year,
MONTH(welding_date) as month,
SUM(CASE WHEN type_of_joint = 'S' AND rt_result IS NOT NULL AND rt_result != '' THEN 1 ELSE 0 END) as rt_shop_total,
SUM(CASE WHEN type_of_joint = 'S' AND rt_result NOT IN ('', 'Accept / Годен') THEN 1 ELSE 0 END) as rt_shop_repair,
SUM(CASE WHEN type_of_joint = 'F' AND rt_result IS NOT NULL AND rt_result != '' THEN 1 ELSE 0 END) as rt_field_total,
SUM(CASE WHEN type_of_joint = 'F' AND rt_result NOT IN ('', 'Accept / Годен') THEN 1 ELSE 0 END) as rt_field_repair,
SUM(CASE WHEN type_of_joint = 'S' AND ut_result IS NOT NULL AND ut_result != '' THEN 1 ELSE 0 END) as ut_shop_total,
SUM(CASE WHEN type_of_joint = 'S' AND ut_result NOT IN ('', 'Accept / Годен') THEN 1 ELSE 0 END) as ut_shop_repair,
SUM(CASE WHEN type_of_joint = 'F' AND ut_result IS NOT NULL AND ut_result != '' THEN 1 ELSE 0 END) as ut_field_total,
SUM(CASE WHEN type_of_joint = 'F' AND ut_result NOT IN ('', 'Accept / Годен') THEN 1 ELSE 0 END) as ut_field_repair
")
->whereNotNull('welding_date')
->groupBy('month_key', 'year', 'month')
->get(),
'test_packages' => DB::table('test_packages')
->leftJoinSub(function ($query) {
$query->from('weld_logs')
->select(
'test_package_no',
DB::raw('MAX(project) as project'),
DB::raw('MAX(welding_date) as last_weld_date')
)
->whereNotNull('test_package_no')
->where('test_package_no', '!=', '')
->whereNotNull('welding_date')
->groupBy('test_package_no');
}, 'projects', 'projects.test_package_no', '=', 'test_packages.test_package_number')
->selectRaw("
COALESCE(projects.project, 'Unknown') as project,
count(test_packages.id) as total_tp,
SUM(CASE WHEN walkdown_date IS NOT NULL THEN 1 ELSE 0 END) as line_check,
SUM(COALESCE(a_punch_point_open, 0)) as punch_a,
SUM(CASE WHEN test_date IS NOT NULL THEN 1 ELSE 0 END) as test,
SUM(CASE WHEN reinstatement_date IS NOT NULL THEN 1 ELSE 0 END) as reinstatement
")
->groupBy(
DB::raw("COALESCE(projects.project, 'Unknown')")
)
->get(),
'welder_current' => $welderCurrent,
]
]);
}
/**
* Welding Status By Project
*
* Get summary data for "Sahaya Göre Kaynak Durumu" (Welding Status by Site).
* Breakdown of WDI (NPS-1) by project, grouped into SHOP and FIELD.
*
* @response 200 {
* "status": "success",
* "data": [
* {
* "project": "COOLER TOWER",
* "shop": 82,
* "field": 122,
* "total": 204,
* "percentage": 93
* },
* ...
* ]
* }
*/
public function weldingByProject()
{
$weldingData = DB::table('weld_logs')
->select('project')
->selectRaw("SUM(CASE WHEN type_of_joint = 'S' AND welding_date IS NOT NULL THEN nps_1 ELSE 0 END) as shop")
->selectRaw("SUM(CASE WHEN (type_of_joint != 'S' OR type_of_joint IS NULL) AND welding_date IS NOT NULL THEN nps_1 ELSE 0 END) as field")
->selectRaw("SUM(CASE WHEN welding_date IS NOT NULL THEN nps_1 ELSE 0 END) as total_welded")
->selectRaw("SUM(nps_1) as total_scope")
->whereNotNull('project')
->where('project', '!=', '')
->groupBy('project')
->get();
$result = $weldingData->map(function ($row) {
$totalWelded = (float)$row->total_welded;
$totalScope = (float)$row->total_scope;
$percentage = $totalScope > 0 ? round(($totalWelded / $totalScope) * 100) : 0;
return [
'project' => $row->project,
'shop' => (float)$row->shop,
'field' => (float)$row->field,
'total' => $totalWelded,
'percentage' => (int)$percentage
];
});
return response()->json([
'status' => 'success',
'data' => $result
]);
}
}
@@ -0,0 +1,327 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\SurfaceAreaConstant;
use App\Models\PaintFollowUp;
use Illuminate\Support\Facades\Schema;
use DB;
class SurfaceAreaController extends Controller
{
/**
* Get distinct element types for a given standard
*/
public function getElements(Request $request)
{
$standard = $request->get('standard', 'ASME');
$elements = SurfaceAreaConstant::where('standard_type', $standard)
->where('is_active', true)
->select('element_type_en', 'element_type_ru')
->distinct()
->orderBy('element_type_en')
->get();
return response()->json($elements);
}
/**
* Get diameter options for a given standard and element
*/
public function getDiameters(Request $request)
{
$standard = $request->get('standard', 'ASME');
$element = $request->get('element');
// Element can be either EN or RU, so we need to check both
$diameters = SurfaceAreaConstant::where('standard_type', $standard)
->where(function($query) use ($element) {
$query->where('element_type_en', $element)
->orWhere('element_type_ru', $element);
})
->where('is_active', true)
->select('diameter_inch', 'diameter_mm', 'surface_area_m2', 'manufacturing_standard', 'element_type_en', 'element_type_ru', 'general_name')
->orderBy('diameter_mm')
->get();
return response()->json($diameters);
}
/**
* Get MTO total for a given iso_drawings
*/
public function getMtoTotal(Request $request)
{
$isoDrawings = $request->get('iso_drawings');
if (!$isoDrawings) {
return response()->json(['error' => 'iso_drawings parameter is required'], 400);
}
// MTO tablosundan pipe verilerini hesapla
// Formula: SUM(quantity * PI * (odmm_1/2000)^2)
$mtoTotal = DB::table('m_t_o_s')
->where('description_en', 'like', '%pipe%')
->where('line', $isoDrawings)
->selectRaw('SUM(quantity * POWER(odmm_1/2000, 2) * PI()) AS total')
->first()->total;
return response()->json([
'success' => true,
'iso_drawings' => $isoDrawings,
'mto_total' => $mtoTotal ?? 0
]);
}
/**
* Get saved surface area items for a construction paint log
*/
public function getSavedItems(Request $request)
{
$constructionPaintLogId = $request->get('construction_paint_log_id');
if (!$constructionPaintLogId) {
return response()->json(['error' => 'construction_paint_log_id parameter is required'], 400);
}
$paintLog = DB::table('construction_paint_logs')
->where('id', $constructionPaintLogId)
->select('surface_area_items', 'fittings_m2', 'pipe_m2', 'total_m2')
->first();
if (!$paintLog) {
return response()->json(['error' => 'Construction Paint Log not found'], 404);
}
$items = [];
if ($paintLog->surface_area_items) {
$items = json_decode($paintLog->surface_area_items, true) ?: [];
}
return response()->json([
'success' => true,
'items' => $items,
'fittings_m2' => $paintLog->fittings_m2,
'pipe_m2' => $paintLog->pipe_m2,
'total_m2' => $paintLog->total_m2
]);
}
/**
* Save surface area calculation to construction_paint_logs and paint_follow_ups
*/
public function save(Request $request)
{
$constructionPaintLogId = $request->get('construction_paint_log_id');
$fittingsTotal = $request->get('fittings_total');
$pipeTotal = $request->get('pipe_total');
$grandTotal = $request->get('grand_total');
$cuttingMm = $request->get('cutting_mm');
$items = json_decode($request->get('items', '[]'), true);
$dn1 = $request->get('dn_1');
$dn2 = $request->get('dn_2');
$linePipeMm1 = $request->get('line_pipe_mm_1');
$linePipeMm2 = $request->get('line_pipe_mm_2');
$totalLineMm = $request->get('total_line_mm');
// Calculate total_line_mm if not provided (sum of line_pipe_mm_1 and line_pipe_mm_2)
if ($totalLineMm === null || $totalLineMm === '') {
$linePipeMm1Val = (float)($linePipeMm1 ?? 0);
$linePipeMm2Val = (float)($linePipeMm2 ?? 0);
$totalLineMm = $linePipeMm1Val + $linePipeMm2Val;
}
// Get construction paint log record
$paintLog = DB::table('construction_paint_logs')->find($constructionPaintLogId);
if (!$paintLog) {
return response()->json(['error' => 'Construction Paint Log not found'], 404);
}
// Process items to calculate fitting quantities and areas
$elbowPcs = 0;
$elbow45Pcs = 0;
$elbowM2 = 0;
$teePcs = 0;
$teeM2 = 0;
$reductionsPcs = 0;
$reductionsM2 = 0;
$flangePcs = 0;
$flangeM2 = 0;
$ventPcs = 0;
$ventM2 = 0;
$capPcs = 0;
$capM2 = 0;
foreach ($items as $item) {
$elementEn = $item['element_en'] ?? '';
$elementRu = $item['element_ru'] ?? '';
$quantity = (int)($item['quantity'] ?? 0);
$total = (float)($item['total'] ?? 0);
// Check both EN and RU element names (use EN first, fallback to RU)
$elementName = trim($elementEn ?: $elementRu);
$elementNameLower = strtolower($elementName);
// Elbow 90° 1.5D
if (stripos($elementNameLower, 'elbow') !== false &&
(stripos($elementNameLower, '90') !== false || stripos($elementName, '90°') !== false)) {
$elbowPcs += $quantity;
$elbowM2 += $total;
}
// Elbow 45° 1.5D
elseif (stripos($elementNameLower, 'elbow') !== false &&
(stripos($elementNameLower, '45') !== false || stripos($elementName, '45°') !== false)) {
$elbow45Pcs += $quantity;
$elbowM2 += $total;
}
// Reductions (Concentric, Eccentric, any reducer)
elseif (stripos($elementNameLower, 'reducer') !== false ||
stripos($elementNameLower, 'reduction') !== false) {
$reductionsPcs += $quantity;
$reductionsM2 += $total;
}
// Slip On Flange (Class 150)
elseif (stripos($elementNameLower, 'slip on flange') !== false ||
(stripos($elementNameLower, 'slip') !== false && stripos($elementNameLower, 'flange') !== false)) {
$flangePcs += $quantity;
$flangeM2 += $total;
}
// Weldneck Flange (Class 150)
elseif (stripos($elementNameLower, 'weldneck flange') !== false ||
(stripos($elementNameLower, 'weldneck') !== false && stripos($elementNameLower, 'flange') !== false)) {
$flangePcs += $quantity;
$flangeM2 += $total;
}
// Tee (Equal Tee, Reducing Tee, Cross Tee - all types)
elseif (stripos($elementNameLower, 'tee') !== false ||
stripos($elementNameLower, 'cross') !== false) {
$teePcs += $quantity;
$teeM2 += $total;
}
// Nipple/Olet (replaces Vent)
elseif (stripos($elementNameLower, 'nipple') !== false ||
stripos($elementNameLower, 'olet') !== false ||
stripos($elementNameLower, 'nipel') !== false ||
stripos($elementNameLower, 'nippel') !== false) {
$ventPcs += $quantity;
$ventM2 += $total;
}
// Cap
elseif (stripos($elementNameLower, 'cap') !== false) {
$capPcs += $quantity;
$capM2 += $total;
}
}
// Prepare update data
$updateData = [
'fittings_m2' => $fittingsTotal,
'pipe_m2' => $pipeTotal,
'total_m2' => $grandTotal,
'cutting_mm' => $cuttingMm,
'surface_area_items' => json_encode($items),
'elbow_pcs' => $elbowPcs,
'elbow_45_pcs' => $elbow45Pcs,
'elbow_m2' => round($elbowM2, 2),
'tee_pcs' => $teePcs,
'tee_m2' => round($teeM2, 2),
'reductions_pcs' => $reductionsPcs,
'reductions_m2' => round($reductionsM2, 2),
'flange_pcs' => $flangePcs,
'flange_m2' => round($flangeM2, 2),
'vent_pcs' => $ventPcs,
'vent_m2' => round($ventM2, 2),
'cap_pcs' => $capPcs,
'cap_m2' => round($capM2, 2),
'updated_at' => now()
];
// Add pipe dimensions if provided (only if not empty and not zero)
if ($dn1 !== null && $dn1 !== '') {
$updateData['dn_1'] = $dn1;
}
if ($dn2 !== null && $dn2 !== '') {
$updateData['dn_2'] = $dn2;
}
// Check if columns exist before updating line_pipe_mm fields
$hasLinePipeMm1 = Schema::hasColumn('construction_paint_logs', 'line_pipe_mm_1');
$hasLinePipeMm2 = Schema::hasColumn('construction_paint_logs', 'line_pipe_mm_2');
$hasTotalLineMm = Schema::hasColumn('construction_paint_logs', 'total_line_mm');
if ($hasLinePipeMm1 && $linePipeMm1 !== null && $linePipeMm1 !== '' && $linePipeMm1 != 0) {
$updateData['line_pipe_mm_1'] = (int)$linePipeMm1;
}
if ($hasLinePipeMm2 && $linePipeMm2 !== null && $linePipeMm2 !== '' && $linePipeMm2 != 0) {
$updateData['line_pipe_mm_2'] = (int)$linePipeMm2;
}
// Add total_line_mm (sum of line_pipe_mm_1 and line_pipe_mm_2)
// Only update if total is greater than 0 and column exists
if ($hasTotalLineMm && $totalLineMm !== null && $totalLineMm > 0) {
$updateData['total_line_mm'] = (float)$totalLineMm;
}
// Update construction paint log with surface area values and fitting details
DB::table('construction_paint_logs')
->where('id', $constructionPaintLogId)
->update($updateData);
// Find or create paint follow up record based on spool/line
$paintFollowUp = PaintFollowUp::where('spool_no_joint_no', $paintLog->spool)
->where('line', $paintLog->line)
->first();
$paintFollowUpAction = '';
$paintFollowUpId = null;
if ($paintFollowUp) {
$paintFollowUp->update([
'volume_1' => $grandTotal,
'volume_2' => $grandTotal,
'volume_3' => $grandTotal,
'total_volume' => $grandTotal * 3,
]);
$paintFollowUpAction = 'updated';
$paintFollowUpId = $paintFollowUp->id;
} else {
$newRecord = PaintFollowUp::create([
'line' => $paintLog->line,
'iso_number' => $paintLog->iso_drawings,
'spool_no_joint_no' => $paintLog->spool,
'volume_1' => $grandTotal,
'volume_2' => $grandTotal,
'volume_3' => $grandTotal,
'total_volume' => $grandTotal * 3,
]);
$paintFollowUpAction = 'created';
$paintFollowUpId = $newRecord->id;
}
return response()->json([
'success' => true,
'message' => 'Surface area saved successfully',
'construction_paint_log' => [
'id' => $constructionPaintLogId,
'spool' => $paintLog->spool,
'line' => $paintLog->line,
'fittings_m2' => $fittingsTotal,
'pipe_m2' => $pipeTotal,
'total_m2' => $grandTotal
],
'paint_follow_up' => [
'action' => $paintFollowUpAction,
'id' => $paintFollowUpId,
'spool_no_joint_no' => $paintLog->spool,
'line' => $paintLog->line,
'volume_1' => $grandTotal,
'volume_2' => $grandTotal,
'volume_3' => $grandTotal,
'total_volume' => $grandTotal * 3
]
]);
}
}
@@ -0,0 +1,176 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class WorkPermitHierarchyController extends Controller
{
/**
* Get Work Permit Hierarchy
*
* Returns a structured hiearchy of work permit projects, companies, and personnel.
*/
public function index(Request $request)
{
try {
// Get all column names from both tables for placeholders
$workPermitColumns = Schema::getColumnListing('work_permit_documents');
$subcontractorColumns = Schema::getColumnListing('subcontractors');
$query = DB::table("work_permit_documents")
->select([
'work_permit_documents.id',
'work_permit_documents.name_surname',
'work_permit_documents.company',
'work_permit_documents.certificates_number',
'work_permit_documents.duty',
'work_permit_documents.document_number',
'work_permit_documents.attorney',
'work_permit_documents.attorney_date',
'work_permit_documents.zone as project',
'work_permit_documents.sign_order as personnel_sign_order',
'subcontractors.company_name_ru',
'subcontractors.company_name_en',
'subcontractors.job_description',
'subcontractors.company_code',
'subcontractors.operation_type',
'subcontractors.city',
'subcontractors.address',
'subcontractors.logo',
'subcontractors.sign_order as company_sign_order'
])
->join("subcontractors", "work_permit_documents.company", "subcontractors.company_name_ru")
->orderBy('subcontractors.operation_type')
->orderBy('work_permit_documents.sign_order');
// Apply project filter if provided
if ($request->has('project') && !empty($request->project)) {
$query->where('work_permit_documents.zone', 'LIKE', '%' . $request->project . '%');
}
$workPermitUsers = $query->get();
$projectMapping = [];
// Parse comma-separated projects and organize
foreach ($workPermitUsers as $user) {
$projectParts = explode(',', $user->project);
foreach ($projectParts as $singleProject) {
$singleProject = trim($singleProject);
if (empty($singleProject)) continue;
if ($request->has('project') && !empty($request->project)) {
if (stripos($singleProject, $request->project) === false) {
continue;
}
}
if (!isset($projectMapping[$singleProject])) {
$projectMapping[$singleProject] = [];
}
// Avoid duplicates in mapping
$found = false;
foreach ($projectMapping[$singleProject] as $existingUser) {
if ($existingUser->name_surname === $user->name_surname &&
$existingUser->operation_type === $user->operation_type &&
$existingUser->personnel_sign_order === $user->personnel_sign_order) {
$found = true;
break;
}
}
if (!$found) {
// Enhance user object with placeholders
$user->placeholders = $this->generatePlaceholders($user, $workPermitColumns, $subcontractorColumns);
$projectMapping[$singleProject][] = $user;
}
}
}
// Group into hiearchy: Project -> Operation Type -> Companies -> Users
$response = [];
foreach ($projectMapping as $projectName => $users) {
$operationGroups = collect($users)->groupBy('operation_type');
$projectData = [
'project_name' => $projectName,
'operation_types' => []
];
foreach ($operationGroups as $operationType => $operationGroup) {
$companies = [];
$byCompany = collect($operationGroup)->groupBy('company_name_ru');
foreach ($byCompany as $companyName => $companyUsers) {
$companies[] = [
'company_name' => $companyName,
'company_sign_order' => $companyUsers->first()->company_sign_order,
'users' => $companyUsers->sortBy('personnel_sign_order')->values()->all()
];
}
$projectData['operation_types'][] = [
'type' => $operationType,
'companies' => $companies
];
}
$response[] = $projectData;
}
// Sort alphabetical by project name
usort($response, function($a, $b) {
return strcmp($a['project_name'], $b['project_name']);
});
return response()->json([
'status' => 'success',
'data' => $response
]);
} catch (\Exception $e) {
return response()->json([
'status' => 'error',
'message' => $e->getMessage()
], 500);
}
}
/**
* Generate placeholders for a user
*/
private function generatePlaceholders($user, $wpColumns, $sbColumns)
{
$job = $user->job_description ?? '';
$fso = $user->company_sign_order ?? '';
$pso = $user->personnel_sign_order ?? '';
$prefix = "{$job}{$fso}_{$pso}_";
$placeholders = [
'work_permit' => [],
'subcontractor' => [],
'common' => [
'name_surname' => "{" . $prefix . "name_surname}",
'company_name' => "{" . $prefix . "company_name}",
'certificate_no' => "{" . $prefix . "certificate_no}",
'duty' => "{" . $prefix . "duty}",
]
];
foreach ($wpColumns as $col) {
$placeholders['work_permit'][$col] = "{" . $prefix . $col . "}";
}
foreach ($sbColumns as $col) {
$placeholders['subcontractor'][$col] = "{" . $prefix . "sub_" . $col . "}";
}
return $placeholders;
}
}