İlk temizlik tamamlandı bir önceki projeden
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user