1229 lines
47 KiB
PHP
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 [];
|
|
}
|
|
}
|