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

1229 lines
47 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\QueryBuilderService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use App\Models\User;
use App\Jobs\ExecuteSaveTriggerJob;
/**
* @group Dynamic Resource Management
*
* API endpoints for dynamically managing database tables and rendering views based on Modules or direct Table names.
*/
class DynamicResourceController extends Controller
{
protected $queryBuilder;
public function __construct(QueryBuilderService $queryBuilder)
{
$this->queryBuilder = $queryBuilder;
}
/**
* Check User Permissions for a Module
*
* @urlParam module string required The Module Slug. Example: test-packages
* @urlParam permission string Optional. Specific permission to check (read, write, update, delete, full_control). If omitted, returns all permissions.
*/
public function checkAuth(Request $request, $module, $permission = null)
{
if ($permission) {
$mappedPermission = $permission;
if ($permission === 'update') {
$mappedPermission = 'write';
} elseif ($permission === 'delete') {
$mappedPermission = 'full_control';
}
$hasPermission = isAuth($module, $mappedPermission);
return response()->json([
'module' => $module,
'permission' => $permission,
'authorized' => $hasPermission
]);
}
// Return all permissions map
return response()->json([
'module' => $module,
'permissions' => [
'read' => isAuth($module, 'read'),
'write' => isAuth($module, 'write'),
'update' => isAuth($module, 'write'),
'delete' => isAuth($module, 'full_control'),
'full_control' => isAuth($module, 'full_control'),
]
]);
}
/**
* Handle Dynamic Request
*
* Performs the specified CRUD action or View rendering on the target table or module.
*
* This endpoint supports both direct table names (e.g., `users`) and Module Slugs (e.g., `radiographic-test`).
* The `table` and `action` parameters are **required** path parameters.
*
* @urlParam table string required The Module Slug or Table Name. Example: radiographic-test, users, weld-logs
* @urlParam action string required The operation to perform. Example: read, update, delete, view, summary, calculate, columns, values.
*
* @bodyParam filter json Optional. JSON array for filtering data (DevExtreme format). Example: [["id", ">", 10], "and", ["status", "=", "active"]]
* @bodyParam sort json Optional. Array of sorting configurations. Example: [{"selector": "id", "desc": true}]
* @bodyParam skip integer Optional. Number of records to skip for pagination. Example: 0
* @bodyParam take integer Optional. Number of records to take for pagination. Example: 20
* @bodyParam columns array Optional. Array of column names to retrieve. If null, all columns are returned. Example: ["id", "title", "status"]
* @bodyParam data object Data object for Create/Update operations. Example: {"name": "John Doe", "email": "john@example.com"}
* @bodyParam id integer ID of the record for Update/Delete operations. Example: 1
* @bodyParam file string Filename for the 'view' action (without .blade.php extension). Defaults to 'index'. Example: form
*
* @response 200 {
* "data": [{"id": 1, "name": "John"}],
* "totalCount": 1,
* "status": "success"
* }
* @response 403 {
* "error": "Unauthorized access to module"
* }
*/
public function handle(Request $request, $table, $action)
{
// Resolve Table Name & Check Permissions
$resolution = $this->resolveResource($table, $action);
if (isset($resolution['error'])) {
return response()->json(['error' => $resolution['error']], 403);
}
$realTable = $resolution['table'];
switch ($action) {
case 'read':
case 'list':
case 'get':
return $this->read($request, $realTable);
case 'create':
case 'store':
case 'insert':
return $this->create($request, $realTable);
case 'update':
case 'save':
return $this->update($request, $realTable);
case 'delete':
case 'remove':
return $this->delete($request, $realTable);
case 'view':
case 'render':
// For View rendering, we might still need the original slug if the view path relies on it
// But generally renderView checks both admin.{table} and admin.type.{table}
// If $table was a slug like 'test-packages', renderView looking for admin.type.test-packages is correct.
// If we pass $realTable (test_packages), it might fail if folder is named with dashes.
// So for renderView, we prefer the original input if it was a module, OR try both.
// Let's pass the original input $table to renderView as it handles path checking.
return $this->renderView($request, $table);
case 'summary':
// Only weld_logs supports summary (totalCount + sumNps1); same pattern as docs endpoints
if ($realTable === 'weld_logs') {
return app(\App\Http\Controllers\WeldLogController::class)->summary($request);
}
return response()->json(['error' => 'Summary not supported for this resource'], 400);
case 'calculate':
return $this->calculate($request, $realTable);
case 'columns':
return $this->getColumns($request, $realTable);
case 'values':
return $this->getValues($request, $realTable);
default:
return response()->json(['error' => 'Invalid action'], 400);
}
}
/**
* Resolve Module Slug to Table Name and Check Permissions
*/
protected function resolveResource($slugOrTable, $action)
{
// 1. Resolve the module slug using the helper (handles table names, slug guessing, blade parsing)
$moduleSlug = resolveModuleFromTable($slugOrTable);
// 2. Try to find the Module (Type) by the resolved slug
$module = DB::table('types')->where('slug', $moduleSlug)->first();
if ($module) {
// It is a Module
$permissionType = $this->getPermissionType($action);
// Check Module Permission
if (!isAuth($module->slug, $permissionType)) {
return ['error' => 'Unauthorized access to module'];
}
// Determine Table Name
// Priority:
// 1. Explicit 'target_table' request parameter (for overriding)
// 2. $tableName from blade file (like MobileTypeController)
// 3. Slug converted to snake_case
if (request()->has('target_table')) {
$realTable = request('target_table');
} else {
// Try to read $tableName from the blade file
$realTable = $this->resolveTableNameFromBlade($module->slug);
if (!$realTable) {
$realTable = Str::snake($module->slug);
if (!Schema::hasTable($realTable)) {
$pluralTable = Str::snake(Str::plural($module->slug));
if (Schema::hasTable($pluralTable)) {
$realTable = $pluralTable;
}
}
}
}
return ['table' => $realTable];
}
// 2. Not a Module (Direct Table Access)
// Check if user is Admin to allow direct table access bypassing module permissions
// Or generic auth check
if (auth()->check()) {
// If strict module mode is required, reject here.
// But for flexibility, we allow Admins or if explicit table access is needed.
// Warning: This bypasses 'types' table permissions!
return ['table' => $slugOrTable];
}
return ['error' => 'Unauthorized or Resource not found'];
}
protected function getPermissionType($action)
{
switch ($action) {
case 'read':
case 'list':
case 'get':
case 'summary':
case 'view':
case 'render':
case 'calculate':
case 'columns':
case 'values':
return 'read';
case 'create':
case 'store':
case 'insert':
return 'write'; // or 'create' if exists
case 'update':
case 'save':
return 'write'; // commonly 'write' covers update, or use 'modify'
case 'delete':
case 'remove':
return 'full_control'; // Delete usually requires higher privs
default:
return 'read';
}
}
/**
* Retrieve and Filter Data
*/
protected function read(Request $request, $table)
{
try {
// Check if table exists (optional logic)
$query = DB::table($table);
// Handle Columns Selection
$columns = $request->input('columns');
if (!empty($columns)) {
if (is_string($columns)) {
// Try JSON decode first
$decoded = json_decode($columns, true);
if (json_last_error() === JSON_ERROR_NONE) {
$columns = $decoded;
} else {
// Fallback to comma-separated string
$columns = array_map('trim', explode(',', $columns));
}
}
if (is_array($columns) && !empty($columns)) {
$query->select($columns);
}
}
// Apply Contractor Isolation
$query = $this->applyContractorIsolation($query, $table);
// Apply Filter, Sort, Pagination
$query = $this->queryBuilder->apply(
$query,
$request->input('filter'),
$request->input('sort'),
$request->input('skip'),
$request->input('take')
);
// 1. Filtered Count & Summary (Existing Logic)
// Clone the query to get count matches the filter but ignores skip/take
$filteredQuery = $query->clone();
// Remove pagination for filtered sum/count
$filteredQuery->orders = null;
$filteredQuery->limit = null;
$filteredQuery->offset = null;
$totalCount = $filteredQuery->count();
// Calculate Filtered Summary
$summary = [];
$sumConfig = [
'weldlog' => ['nps_1'],
'weld_logs' => ['nps_1'],
];
if (array_key_exists($table, $sumConfig)) {
$fieldsToSum = $sumConfig[$table];
// Filtered Summary
foreach ($fieldsToSum as $field) {
$summary[$field] = $filteredQuery->clone()->sum($field);
}
}
// 2. General (Unfiltered) Count & Summary
// Create a fresh query without any filters
$generalQuery = DB::table($table);
$generalTotalCount = $generalQuery->count();
$generalSummary = [];
if (array_key_exists($table, $sumConfig)) {
$fieldsToSum = $sumConfig[$table];
foreach ($fieldsToSum as $field) {
$generalSummary[$field] = $generalQuery->clone()->sum($field);
}
}
$data = $query->get();
// Resolve Module for blockGroup and documentColumns
$moduleSlug = resolveModuleFromTable($table);
$blockGroup = $this->resolveBlockGroupFromBlade($moduleSlug);
$documentColumns = $this->resolveDocumentColumnsFromBlade($moduleSlug);
// Translate column names using e2 function
$translatedColumns = [];
if (!empty($data) && count($data) > 0) {
$firstItem = (array) $data[0];
foreach (array_keys($firstItem) as $key) {
$translatedColumns[$key] = e2($key);
}
}
return response()->json([
'totalCount' => $totalCount, // Filtered Count
'summary' => $summary, // Filtered Sum
'generalTotalCount' => $generalTotalCount, // General Unfiltered Count
'generalSummary' => $generalSummary, // General Unfiltered Sum
'translatedColumns' => $translatedColumns,
'blockGroup' => $blockGroup,
'documentColumns' => $documentColumns,
'status' => 'success',
'data' => $data,
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Create New Record
*/
protected function create(Request $request, $table)
{
try {
$data = $request->input('data') ?? $request->input('values') ?? $request->all();
if (empty($data)) {
return response()->json(['error' => 'No data provided'], 400);
}
if (is_string($data)) {
$data = json_decode($data, true);
}
// Filter valid columns
$data = $this->filterData($table, $data);
// Validation can be added here based on the 'Fields' table
// Add created_at, updated_at, uid
$data['created_at'] = now();
$data['updated_at'] = now();
if (auth()->check()) {
$data['uid'] = auth()->id();
// Force contractor for subcontractors on create
$user = auth()->user();
if ($user->level >= 4 && !empty($user->subcontructer)) {
if (Schema::hasColumn($table, 'contractor')) {
$data['contractor'] = $user->subcontructer;
}
}
}
$id = DB::table($table)->insertGetId($data);
$newRecord = DB::table($table)->find($id);
$beforeData = null; // No previous data for inserts
// Prepare request data with key for the job
$requestData = $request->all();
$requestData['key'] = ['id' => $id];
// Trigger Save Trigger Job (like AdminController)
ExecuteSaveTriggerJob::dispatch(
$table,
$requestData,
$requestData['key'],
$data,
$newRecord,
"insert"
);
return response()->json([
'data' => $newRecord,
'id' => $id,
'status' => 'success',
'message' => 'Record created successfully'
], 201);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Update Existing Record
*/
protected function update(Request $request, $table)
{
\Log::info("DynamicResourceController@update: table=$table", ['request' => $request->all()]);
try {
// Handle both formats: {id: 123} or {key: {id: 123}} or {key: 123}
$id = $request->input('id');
if ($id === null) {
$key = $request->input('key');
if (is_array($key) && isset($key['id'])) {
$id = $key['id'];
} elseif ($key !== null) {
$id = $key;
}
}
$data = $request->input('data') ?? $request->input('values') ?? $request->all();
// DevExtreme sometimes sends only changed fields in 'values'
if ($id === null || $id === '') {
return response()->json(['error' => 'ID is required'], 400);
}
if (!is_array($data)) {
$data = json_decode($data, true);
}
// Filter valid columns
$data = $this->filterData($table, $data);
// NDT rule: no new request for joints with welding_date or existing RQ (weld_logs only)
if ($table === 'weld_logs') {
$validationError = $this->validateWeldLogsNdtRequestRule($id, $data);
if ($validationError) {
return response()->json(['error' => $validationError], 422);
}
}
$data['updated_at'] = now();
// Try to use Eloquent Model if exists (to trigger Observers)
$modelClass = $this->getModelClass($table);
if ($modelClass) {
$record = $modelClass::find($id);
if ($record && auth()->check()) {
$user = auth()->user();
$levelIndex = getLevelIndex($user->level);
if ($levelIndex !== null && $levelIndex >= 4 && !empty($user->subcontructer)) {
$company = $user->subcontructer;
$allowed = false;
if ($table === 'weld_logs') {
// weld_logs stores company in three columns
$allowed = ($record->contractor ?? '') === $company
|| ($record->ste_subcontractor ?? '') === $company
|| ($record->general_contractor ?? '') === $company;
} elseif ($table === 'r_f_i_s') {
// r_f_i_s stores company in contractor OR subcontractor
$allowed = ($record->contractor ?? '') === $company
|| ($record->subcontractor ?? '') === $company;
} else {
$allowed = !isset($record->contractor) || ($record->contractor ?? '') === $company;
}
if (!$allowed) {
return response()->json(['error' => 'Unauthorized to update this record (contractor mismatch)'], 403);
}
}
}
if ($record) {
$beforeData = clone $record; // Capture state before update
$record->update($data);
$updatedRecord = $record->fresh();
// Prepare request data with key for the job
$requestData = $request->all();
$requestData['key'] = ['id' => $id];
ExecuteSaveTriggerJob::dispatch(
$table,
$requestData,
$requestData['key'],
$request->input('values') ?? $request->input('data'),
$beforeData,
"update"
);
return response()->json([
'data' => $updatedRecord,
'status' => 'success',
'message' => 'Record updated successfully'
]);
}
}
$beforeData = DB::table($table)->find($id);
DB::table($table)->where('id', $id)->update($data);
$updatedRecord = DB::table($table)->find($id);
// Prepare request data with key for the job
$requestData = $request->all();
$requestData['key'] = ['id' => $id];
ExecuteSaveTriggerJob::dispatch(
$table,
$requestData,
$requestData['key'],
$request->input('values') ?? $request->input('data'),
$beforeData,
"update"
);
return response()->json([
'data' => $updatedRecord,
'status' => 'success',
'message' => 'Record updated successfully'
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Validate NDT request rule for weld_logs: no new request (first-time request_date) allowed
* when the joint has welding_date set or already has any request_no (RQ) set.
* Returns error message string if validation fails, null otherwise.
*/
protected function validateWeldLogsNdtRequestRule($id, array $data): ?string
{
// Bypass validation if the request is specifically for 'weldmap' updates
// from the mobile app NDT Request screen where new requests are intended.
$requestedTableOrSlug = request()->segment(2); // /api/{weldmap}/update
$targetTable = request()->input('target_table');
if ($requestedTableOrSlug === 'weldmap' || $targetTable === 'weldmap' || request()->route('table') === 'weldmap') {
return null;
}
// Failsafe bypass: allow if the request explicitly has NO request_no but has request_date
$isNdtRequestUpdate = false;
foreach (['vt', 'rt', 'ut', 'pt', 'mt', 'pmi', 'pwht', 'ht', 'ferrite'] as $t) {
if (isset($data["{$t}_request_date"])) {
$isNdtRequestUpdate = true;
break;
}
}
if ($isNdtRequestUpdate) {
return null;
}
$current = DB::table('weld_logs')->where('id', $id)->first();
if (!$current) {
return null; // record not found, let update fail elsewhere
}
$testTypes = ['vt', 'rt', 'ut', 'pt', 'mt', 'pmi', 'pwht', 'ht', 'ferrite'];
$rejectedDates = [null, '', '0000-00-00', '1970-01-01'];
$hasWeldingDate = isset($current->welding_date) && !in_array($current->welding_date, $rejectedDates, true)
&& trim((string) $current->welding_date) !== '';
$hasAnyRequestNo = false;
foreach ($testTypes as $type) {
$requestNoField = $type . '_request_no';
$val = $current->$requestNoField ?? null;
if ($val !== null && trim((string) $val) !== '') {
$hasAnyRequestNo = true;
break;
}
}
foreach ($testTypes as $type) {
$requestDateField = $type . '_request_date';
$requestNoField = $type . '_request_no';
if (empty($data[$requestDateField])) {
continue;
}
$newRequestDate = $data[$requestDateField];
if (in_array($newRequestDate, $rejectedDates, true) || trim((string) $newRequestDate) === '') {
continue;
}
$currentRequestNo = $current->$requestNoField ?? null;
$currentIsEmpty = $currentRequestNo === null || trim((string) $currentRequestNo) === '';
if (!$currentIsEmpty) {
continue; // already has request_no for this type, allow update
}
if ($hasWeldingDate || $hasAnyRequestNo) {
return 'New request entry is not allowed for this joint (welding date exists or request already exists). Only changes to existing requests are allowed.';
}
}
return null;
}
/**
* Resolve Model Class from Table Name
*/
protected function getModelClass($table)
{
$className = Str::studly(Str::singular($table));
$fullClass = "App\\Models\\$className";
if (class_exists($fullClass)) {
return $fullClass;
}
return null;
}
/**
* Delete Record
*/
protected function delete(Request $request, $table)
{
try {
$id = $request->input('id') ?? $request->input('key');
if ($id === null || $id === '') {
return response()->json(['error' => 'ID is required'], 400);
}
$query = DB::table($table)->where('id', $id);
$query = $this->applyContractorIsolation($query, $table);
$query->delete();
return response()->json([
'status' => 'success',
'message' => 'Record deleted successfully'
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Render Dynamic Blade View
*
* Renders a server-side Blade template and returns HTML.
*/
protected function renderView(Request $request, $table)
{
try {
$file = $request->input('file', 'index'); // Default to index.blade.php
// Determine view path
// 1. Path: admin.{table}.{file} (Direct folder)
// 2. Path: admin.type.{table}.{file} (Modular structure)
$viewPath = "";
$data = $request->all(); // Data to pass to the view
if (View::exists("admin.{$table}.{$file}")) {
$viewPath = "admin.{$table}.{$file}";
} elseif (View::exists("admin.type.{$table}.{$file}")) {
$viewPath = "admin.type.{$table}.{$file}";
} else {
return response()->json(['error' => 'View not found'], 404);
}
// Render view and return as HTML string
$html = View::make($viewPath, $data)->render();
return response()->json([
'html' => $html,
'view' => $viewPath,
'status' => 'success'
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Filter data keys to match table columns
*/
protected function filterData($table, $data)
{
if (empty($data)) {
return [];
}
// Cache column listing if possible, but for dynamic API, schema call is acceptable
$columns = Schema::getColumnListing($table);
// If table not found or error, return empty or handle exception
if (empty($columns)) {
return [];
}
$filtered = array_intersect_key($data, array_flip($columns));
return $filtered;
}
/**
* Calculate Aggregates (Sum, Avg, Min, Max, Count)
* Supports casting string columns to numbers.
*
* @param Request $request
* @param string $table
* @return \Illuminate\Http\JsonResponse
*/
protected function calculate(Request $request, $table)
{
try {
$column = $request->input('column');
$operation = strtolower($request->input('operation', 'count'));
if (!$column && $operation !== 'count') {
return response()->json(['error' => 'Column is required for this operation'], 400);
}
$allowedOperations = ['sum', 'avg', 'min', 'max', 'count'];
if (!in_array($operation, $allowedOperations)) {
return response()->json(['error' => 'Invalid operation'], 400);
}
// Start ID-based Query
$query = DB::table($table);
// Apply Filters
$query = $this->queryBuilder->apply(
$query,
$request->input('filter')
);
// Handle Operation
if ($operation === 'count') {
$result = $query->count();
} else {
// Handle String to Number Casting if needed
// We verify if column exists to avoid SQL injection
if (!Schema::hasColumn($table, $column)) {
return response()->json(['error' => "Column '$column' does not exist in table '$table'"], 400);
}
// Use DB::raw to cast string columns to decimal/float for calculation
// This approach works for MySQL/MariaDB. For SQLite it might differ slightly but usually works.
// We wrap it in a CAST or just rely on implicit conversion + 0 logic if strict mode is off.
// A safer universal way in Laravel for simple casting is usually raw SQL.
// "CAST(`column` AS DECIMAL(10,2))"
$castedColumn = DB::raw("CAST(`$column` AS DECIMAL(20, 2))");
switch ($operation) {
case 'sum':
$result = $query->sum($castedColumn);
break;
case 'avg':
$result = $query->avg($castedColumn);
break;
case 'min':
$result = $query->min($castedColumn);
break;
case 'max':
$result = $query->max($castedColumn);
break;
}
}
return response()->json([
'table' => $table,
'operation' => $operation,
'column' => $column,
'result' => $result,
'status' => 'success'
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Get distinct values for a table and column (API version).
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getTableColumnValues(Request $request)
{
$table = $request->input('table');
$column = $request->input('column');
if (!$table || !$column) {
return response()->json(['error' => 'Table and column parameters are required'], 400);
}
// Resolve Table Name & Check Permissions
$resolution = $this->resolveResource($table, 'values');
if (isset($resolution['error'])) {
return response()->json(['error' => $resolution['error']], 403);
}
$realTable = $resolution['table'];
// Call the internal getValues method
return $this->getValues($request, $realTable);
}
/**
* List all available tables (modules).
*
* @return \Illuminate\Http\JsonResponse
*/
public function listTables()
{
try {
// 1. Get all real database tables (like artisan.blade.php)
$dbTables = [];
foreach (DB::select('SHOW TABLES') as $table) {
$dbTables[] = array_values((array) $table)[0];
}
// 2. Build module lookup from types table
$modules = DB::table('types')
->where('enabled', true)
->whereNotNull('slug')
->where('slug', '!=', '')
->get(['id', 'slug', 'title']);
$moduleMap = [];
foreach ($modules as $module) {
$tableName = Str::snake($module->slug);
$moduleMap[$tableName] = $module;
}
// 3. Build result: all tables with columns
$tables = [];
foreach ($dbTables as $tableName) {
// Get columns for this table (like ai-sql-builder.blade.php)
$columns = Schema::getColumnListing($tableName);
$entry = [
'table_name' => $tableName,
'columns' => $columns,
];
// If this table belongs to a module, add module info
if (isset($moduleMap[$tableName])) {
$mod = $moduleMap[$tableName];
$entry['id'] = $mod->id;
$entry['slug'] = $mod->slug;
$entry['title'] = $mod->title;
$entry['is_module'] = true;
} else {
$entry['is_module'] = false;
}
$tables[] = $entry;
}
return response()->json([
'status' => 'success',
'data' => $tables
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Get columns for a specific table.
*
* @param Request $request
* @param string $table
* @return \Illuminate\Http\JsonResponse
*/
protected function getColumns(Request $request, $table)
{
try {
if (!Schema::hasTable($table)) {
return response()->json(['error' => "Table '$table' not found"], 404);
}
$columns = Schema::getColumnListing($table);
return response()->json([
'status' => 'success',
'table' => $table,
'data' => $columns
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Get distinct values for a specific column (for dropdowns).
*
* @param Request $request
* @param string $table
* @return \Illuminate\Http\JsonResponse
*/
protected function getValues(Request $request, $table)
{
try {
$column = $request->input('column');
if (!$column) {
return response()->json(['error' => 'Column param is required'], 400);
}
if (!Schema::hasColumn($table, $column)) {
return response()->json(['error' => "Column '$column' not found"], 404);
}
// Specialized Welder Lookup for weld_logs
if ($table === 'weld_logs' && in_array($column, ['welder_1', 'welder_2', 'real_welder_1', 'real_welder_2'])) {
// If we have material group info, filter welders based on qualification rules
if ($request->has('ru_material_group_1') || $request->has('ru_material_group_2')) {
$ru1 = trim($request->input('ru_material_group_1', ''));
$ru2 = trim($request->input('ru_material_group_2', ''));
$typeOfJoint = $request->input('type_of_joint', '');
// Logic similar to real_welder_1.php
if ($ru1 !== '' && $ru2 !== '' && $ru1 !== $ru2) {
$searchTerm = $ru1 . '+' . $ru2;
$searchTermReverse = $ru2 . '+' . $ru1;
} elseif ($ru1 !== '') {
$searchTerm = $ru1;
$searchTermReverse = $ru1;
} else {
$searchTerm = $ru2;
$searchTermReverse = $ru2;
}
// Step 1: Find material names in material_group_maps
$materialGroupMapRecords = DB::table('material_group_maps')
->where("qualified_materials", "like", "%{$searchTerm}%")
->orWhere("qualified_materials", "like", "%{$searchTermReverse}%")
->get();
if ($materialGroupMapRecords->isNotEmpty()) {
$materialNamesToSearch = [];
foreach ($materialGroupMapRecords as $record) {
$materialName = trim($record->material_name ?? '');
if (!empty($materialName)) {
$materialNamesToSearch[] = $materialName;
}
}
if (!empty($materialNamesToSearch)) {
// Step 2: Query for active welders matching these materials
$weldersQuery = DB::table('welder_tests')
->select('naks_id', 'name_surname')
->where('status', 'Active')
->where(function ($query) use ($materialNamesToSearch) {
foreach ($materialNamesToSearch as $materialName) {
$materialName = trim($materialName);
if (empty($materialName))
continue;
$query->orWhere(function ($q) use ($materialName) {
if (strpos($materialName, '+') !== false) {
$parts = array_map('trim', explode('+', $materialName));
if (count($parts) >= 2) {
$q->where(function ($q2) use ($parts) {
$q2->where("material_group_1", "like", "%{$parts[0]}%")
->where("material_group_2", "like", "%{$parts[1]}%");
})->orWhere(function ($q2) use ($parts) {
$q2->where("material_group_1", "like", "%{$parts[1]}%")
->where("material_group_2", "like", "%{$parts[0]}%");
});
}
} else {
$q->where("material_group_1", "like", "%{$materialName}%")
->orWhere("material_group_2", "like", "%{$materialName}%");
}
});
}
});
// Optional: isolation bypass for admin, otherwise filter by company
if (auth()->check()) {
$user = auth()->user();
if ($user->level !== 'Admin' && !empty($user->subcontructer)) {
$weldersQuery->join('welder_locations', 'welder_tests.naks_id', '=', 'welder_locations.naks_id')
->where("welder_locations.subcontructor", $user->subcontructer);
}
}
$welders = $weldersQuery->groupBy('naks_id')->get();
$values = [];
foreach ($welders as $welder) {
$values[] = "[{$welder->naks_id}] {$welder->name_surname}";
}
return response()->json([
'status' => 'success',
'table' => $table,
'column' => $column,
'data' => $values,
'source' => 'welder_tests (matching materials)'
]);
}
}
}
}
$query = DB::table($table);
// Apply Contractor Isolation
$query = $this->applyContractorIsolation($query, $table);
if ($request->has('filter')) {
$query = $this->queryBuilder->apply($query, $request->input('filter'));
}
// Get distinct values, limit increased to 10000, exclude empty
$values = $query
->select($column)
->distinct()
->whereNotNull($column)
->where($column, '<>', '')
->orderBy($column)
->limit(10000)
->pluck($column);
return response()->json([
'status' => 'success',
'table' => $table,
'column' => $column,
'data' => $values
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
protected function applyContractorIsolation($query, $table)
{
if (auth()->check()) {
$user = auth()->user();
// Admin always bypasses isolation
if ($user->level === 'Admin') {
return $query;
}
// Use getLevelIndex() to get numeric index and avoid PHP 8 string-to-int
// comparison bug (e.g. "Field Staff" >= 4 evaluates TRUE in PHP 8)
$levelIndex = getLevelIndex($user->level);
// Level index >= 4 (Subcontractor and below) are filtered by their company
if ($levelIndex !== null && $levelIndex >= 4 && !empty($user->subcontructer)) {
$company = $user->subcontructer;
// weld_logs stores company in three different columns depending on role.
// Check all company columns so users see their records regardless of column.
if ($table === 'weld_logs') {
$query->where(function ($q) use ($company) {
$q->where('contractor', $company)
->orWhere('ste_subcontractor', $company)
->orWhere('general_contractor', $company);
});
} elseif ($table === 'r_f_i_s') {
// r_f_i_s stores company in both contractor and subcontractor columns
$query->where(function ($q) use ($company) {
$q->where('contractor', $company)
->orWhere('subcontractor', $company);
});
} elseif (Schema::hasColumn($table, 'contractor')) {
$query->where('contractor', $company);
}
}
}
return $query;
}
/**
* Resolve the table name from the module's blade file.
* Parses $tableName = '...' from the blade template (same logic as MobileTypeController).
*
* @param string $slug Module slug
* @return string|null
*/
protected function resolveTableNameFromBlade($slug)
{
$bladePath = resource_path("views/admin/type/{$slug}.blade.php");
if (\Illuminate\Support\Facades\File::exists($bladePath)) {
$content = \Illuminate\Support\Facades\File::get($bladePath);
if (preg_match('/\$tableName\s*=\s*["\']([^"\']+)["\']\s*;/', $content, $matches)) {
return $matches[1];
}
}
return null;
}
/**
* Resolve the blockGroup array from the module's blade file.
* Parses $blockGroup = [...] from the blade template.
*
* @param string $slug Module slug
* @return array|null
*/
protected function resolveBlockGroupFromBlade($slug)
{
$bladePath = resource_path("views/admin/type/{$slug}.blade.php");
if (\Illuminate\Support\Facades\File::exists($bladePath)) {
$content = \Illuminate\Support\Facades\File::get($bladePath);
// Try to find in current file
if (preg_match('/\$blockGroup\s*=\s*(\[[\s\S]*?\])\s*;/', $content, $matches)) {
$phpCode = $matches[1];
try {
$blockGroup = eval("return $phpCode;");
return is_array($blockGroup) ? $blockGroup : null;
} catch (\Throwable $e) {
\Log::error("Error parsing blockGroup for slug {$slug}: " . $e->getMessage());
}
}
// If not found, check for @include and follow it (common for weldmap)
if (preg_match('/@include\s*\(\s*["\']([^"\']+)["\']\s*\)/', $content, $includeMatches)) {
$includedView = $includeMatches[1];
$includedPath = resource_path("views/" . str_replace('.', '/', $includedView) . ".blade.php");
if (\Illuminate\Support\Facades\File::exists($includedPath)) {
$includedContent = \Illuminate\Support\Facades\File::get($includedPath);
if (preg_match('/\$blockGroup\s*=\s*(\[[\s\S]*?\])\s*;/', $includedContent, $matches)) {
$phpCode = $matches[1];
try {
$blockGroup = eval("return $phpCode;");
return is_array($blockGroup) ? $blockGroup : null;
} catch (\Throwable $e) {
\Log::error("Error parsing blockGroup in included view {$includedView}: " . $e->getMessage());
}
}
}
}
}
return null;
}
/**
* Resolve the documentColumns (link-search types) from the module's blade file.
* Parses $relationDatas = [...] from the blade template.
*
* @param string $slug Module slug
* @return array
*/
protected function resolveDocumentColumnsFromBlade($slug)
{
$bladePath = resource_path("views/admin/type/{$slug}.blade.php");
if (\Illuminate\Support\Facades\File::exists($bladePath)) {
$content = \Illuminate\Support\Facades\File::get($bladePath);
$cols = $this->extractDocumentColumnsFromContent($content);
if (!empty($cols)) {
return $cols;
}
// Follow @include
if (preg_match('/@include\s*\(\s*["\']([^"\']+)["\']\s*\)/', $content, $includeMatches)) {
$includedView = $includeMatches[1];
$includedPath = resource_path("views/" . str_replace('.', '/', $includedView) . ".blade.php");
if (\Illuminate\Support\Facades\File::exists($includedPath)) {
$includedContent = \Illuminate\Support\Facades\File::get($includedPath);
return $this->extractDocumentColumnsFromContent($includedContent);
}
}
}
return [];
}
/**
* Helper to extract document columns from a content string.
*/
protected function extractDocumentColumnsFromContent($content)
{
if (preg_match('/\$relationDatas\s*=\s*(\[[\s\S]*?\])\s*;/', $content, $matches)) {
$phpCode = $matches[1];
$documentColumns = [];
// Identify keys followed by => [
preg_match_all('/([\'"])([^\'"]+)\1\s*=>\s*\[/', $phpCode, $keys, PREG_OFFSET_CAPTURE);
foreach ($keys[2] as $index => $keyInfo) {
$key = $keyInfo[0];
$startPos = $keyInfo[1];
$nextKeyPos = $keys[2][$index + 1][1] ?? strlen($phpCode);
$entryBody = substr($phpCode, $startPos, $nextKeyPos - $startPos);
if (preg_match('/([\'"])type\1\s*=>\s*([\'"])link-search\2/', $entryBody)) {
$documentColumns[] = $key;
}
}
return $documentColumns;
}
return [];
}
}