Files
citrus-cms/app/Http/Controllers/Api/PdfEditorLookupController.php
T
2026-04-28 21:14:25 +03:00

915 lines
31 KiB
PHP

<?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);
}
}
}