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 []; } }