user() for Sanctum compatibility $u = $request->user(); if (!$u || !isset($u->level)) { return response()->json([ 'success' => false, 'message' => 'Authentication required' ], 401); } try { // Validate the request // Manually validate using Validator facade for conditional logic $validator = \Illuminate\Support\Facades\Validator::make($request->all(), [ 'table_name' => 'required|string', 'record_id' => 'required|numeric', 'comment' => 'required|string', 'module_slug' => 'nullable|string', 'photo' => 'nullable', // Allow both, will validate structure below ]); // Add conditional validation for photo $validator->after(function ($validator) use ($request) { if ($request->hasFile('photo')) { $photos = $request->file('photo'); if (is_array($photos)) { // Validate array of photos $photoValidator = \Illuminate\Support\Facades\Validator::make($request->all(), [ 'photo.*' => 'image|mimes:jpg,jpeg,png,gif,webp|max:20480', ]); if ($photoValidator->fails()) { foreach ($photoValidator->errors()->all() as $error) { $validator->errors()->add('photo', $error); } } } else { // Validate single photo $photoValidator = \Illuminate\Support\Facades\Validator::make($request->all(), [ 'photo' => 'image|mimes:jpg,jpeg,png,gif,webp|max:20480', ]); if ($photoValidator->fails()) { foreach ($photoValidator->errors()->all() as $error) { $validator->errors()->add('photo', $error); } } } } }); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => $validator->errors()->first() ], 422); } $validated = $validator->validated(); $tableName = $validated['table_name']; $recordId = $validated['record_id']; $comment = $validated['comment']; $moduleSlug = $validated['module_slug'] ?? null; // Get settings configuration if module_slug provided $inspectionConfig = null; $fileNamePattern = null; $pathPattern = null; if ($moduleSlug) { $inspectionConfig = getInspectionHistoryConfig($moduleSlug); // Get patterns from config if ($inspectionConfig) { $pathPattern = $inspectionConfig['path_pattern'] ?? null; $fileNamePattern = $inspectionConfig['file_pattern'] ?? $inspectionConfig['pattern'] ?? null; } } // Determine default path $basePath = 'inspection_history'; // Default path if ($inspectionConfig && isset($inspectionConfig['path'])) { $basePath = $inspectionConfig['path']; } // Get row data for pattern processing (only needed once) $rowData = null; if (!empty($fileNamePattern) || !empty($pathPattern)) { try { $rowData = DB::table($tableName)->where('id', $recordId)->first(); if ($rowData) { $rowData = (array) $rowData; } } catch (\Exception $e) { \Log::warning("InspectionHistoryController: Could not fetch row data for pattern processing. Table: $tableName, ID: $recordId"); $rowData = null; } } $createdRecords = []; // Handle photos if provided if ($request->hasFile('photo')) { $photos = $request->file('photo'); // Ensure it's an array even if single file (though validation expects array) if (!is_array($photos)) { $photos = [$photos]; } foreach ($photos as $photo) { $filePath = null; $fileName = null; $fileSize = null; // Generate file name using pattern or original name $originalName = $photo->getClientOriginalName(); $originalExtension = $photo->getClientOriginalExtension(); // --- 1. Determine Folder Name (Path) --- $folderName = ''; if (!empty($pathPattern)) { // Process path pattern (allow slashes for subdirectories) $folderName = $this->processFileNamePattern($pathPattern, $rowData, '', true); // Sanitize but keep slashes $folderName = preg_replace('/[^A-Za-z0-9_\-\/]/', '_', $folderName); $folderName = preg_replace('/_+/', '_', $folderName); $folderName = trim($folderName, '_'); } // Fallback to record_id if folder name is empty if (empty($folderName)) { $folderName = ($moduleSlug === 'Other' && $recordId == 0) ? date('Y-m-d') : $recordId; } // --- 2. Determine File Name --- $fileNameForSave = ''; if (!empty($fileNamePattern)) { // Process file pattern (STRICT sanitization, no slashes) $fileNameForSave = $this->processFileNamePattern($fileNamePattern, $rowData, $originalExtension, false); } // Fallback to original filename if (empty($fileNameForSave)) { $fileNameForSave = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', pathinfo($originalName, PATHINFO_FILENAME)); if ($originalExtension) { $fileNameForSave .= '.' . $originalExtension; } } // --- 3. Save File --- // Ensure unique filename - use pattern-based folder name $folderPath = storage_path("documents/{$basePath}/{$folderName}"); if (!File::isDirectory($folderPath)) { File::makeDirectory($folderPath, 0777, true, true); } $finalFileName = $fileNameForSave; $counter = 1; while (File::exists($folderPath . '/' . $finalFileName)) { $baseName = pathinfo($fileNameForSave, PATHINFO_FILENAME); $extension = pathinfo($fileNameForSave, PATHINFO_EXTENSION); $finalFileName = "{$baseName}_{$counter}.{$extension}"; $counter++; } // Move file $photo->move($folderPath, $finalFileName); // Set file information $filePath = "documents/{$basePath}/" . ($folderName !== '' ? "{$folderName}/" : "") . $finalFileName; $fileName = $originalName; // Store original name in database $fileSize = File::size($folderPath . '/' . $finalFileName); // Create inspection history record for each photo $inspectionHistory = InspectionHistory::create([ 'table_name' => $tableName, 'record_id' => $recordId, 'column_name' => null, 'comment' => $comment, 'file_path' => $filePath, 'file_name' => $fileName, 'file_size' => $fileSize, 'user_id' => $u->id, 'ip_address' => $request->ip(), ]); $createdRecords[] = $inspectionHistory; } } else { // No photos, just comment $inspectionHistory = InspectionHistory::create([ 'table_name' => $tableName, 'record_id' => $recordId, 'column_name' => null, 'comment' => $comment, 'file_path' => null, 'file_name' => null, 'file_size' => null, 'user_id' => $u->id, 'ip_address' => $request->ip(), ]); $createdRecords[] = $inspectionHistory; } return response()->json([ 'success' => true, 'message' => 'Inspection history saved successfully', 'data' => $createdRecords ]); } catch (\Exception $e) { \Log::error("InspectionHistory upload error: " . $e->getMessage(), [ 'trace' => $e->getTraceAsString() ]); return response()->json([ 'success' => false, 'message' => 'Failed to save inspection history: ' . $e->getMessage() ], 500); } } /** * List inspection history for a specific record * * @group Inspection History * @queryParam table_name string required The database table name. Example: weld_logs * @queryParam record_id integer required The primary key of the record. Example: 15 * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse */ public function list(Request $request) { // Check authentication - using $request->user() for Sanctum compatibility $u = $request->user(); if (!$u || !isset($u->level)) { return response()->json([ 'success' => false, 'message' => 'Authentication required' ], 401); } try { $request->validate([ 'table_name' => 'required|string', 'record_id' => 'required|numeric', ]); $tableName = $request->input('table_name'); $recordId = $request->input('record_id'); // Get inspection history records with user information $historyRecords = InspectionHistory::where('table_name', $tableName) ->where('record_id', $recordId) ->with('user:id,name') ->orderBy('created_at', 'DESC') ->get() ->map(function ($record) { return [ 'id' => $record->id, 'table_name' => $record->table_name, 'record_id' => $record->record_id, 'comment' => $record->comment, 'file_path' => $record->file_path, 'file_name' => $record->file_name, 'file_size' => $record->file_size, 'user_name' => $record->user ? $record->user->name : 'Unknown', 'user_id' => $record->user_id, 'ip_address' => $record->ip_address, 'created_at' => $record->created_at->format('Y-m-d H:i:s'), ]; }); return response()->json($historyRecords); } catch (\Exception $e) { \Log::error("InspectionHistory list error: " . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to retrieve inspection history: ' . $e->getMessage() ], 500); } } /** * Delete inspection history record * * @group Inspection History * @urlParam id integer required The ID of the inspection history record. Example: 1 * @param int $id * @return \Illuminate\Http\JsonResponse */ public function delete(Request $request, $id) { // Check authentication - using $request->user() for Sanctum compatibility $u = $request->user(); if (!$u || !isset($u->level)) { return response()->json([ 'success' => false, 'message' => 'Authentication required' ], 401); } try { $inspectionHistory = InspectionHistory::findOrFail($id); // Delete file if exists if ($inspectionHistory->file_path) { $fullPath = storage_path($inspectionHistory->file_path); if (File::exists($fullPath)) { File::delete($fullPath); } } // Delete record $inspectionHistory->delete(); return response()->json([ 'success' => true, 'message' => 'Inspection history deleted successfully' ]); } catch (\Exception $e) { \Log::error("InspectionHistory delete error: " . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to delete inspection history: ' . $e->getMessage() ], 500); } } /** * Bulk delete inspection history records * * @group Inspection History * @bodyParam ids int[] required Array of inspection history IDs to delete. Example: [1, 2] * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse */ public function bulkDelete(Request $request) { // Check authentication $u = $request->user(); if (!$u || !isset($u->level)) { return response()->json([ 'success' => false, 'message' => 'Authentication required' ], 401); } try { $request->validate([ 'ids' => 'required|array', 'ids.*' => 'numeric' ]); $ids = $request->input('ids'); $deletedCount = 0; foreach ($ids as $id) { try { $inspectionHistory = InspectionHistory::find($id); if ($inspectionHistory) { // Delete file if exists if ($inspectionHistory->file_path) { $fullPath = storage_path($inspectionHistory->file_path); if (File::exists($fullPath)) { File::delete($fullPath); } } // Delete record $inspectionHistory->delete(); $deletedCount++; } } catch (\Exception $e) { \Log::error("InspectionHistory bulk delete error for ID $id: " . $e->getMessage()); // Continue with other records } } return response()->json([ 'success' => true, 'message' => "$deletedCount records deleted successfully", 'deleted_count' => $deletedCount ]); } catch (\Exception $e) { \Log::error("InspectionHistory bulk delete error: " . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to delete records: ' . $e->getMessage() ], 500); } } /** * List active inspection history modules * * @group Inspection History * @return \Illuminate\Http\JsonResponse */ public function modules(Request $request) { // Check authentication $u = $request->user(); if (!$u || !isset($u->level)) { return response()->json([ 'success' => false, 'message' => 'Authentication required' ], 401); } try { // Get current settings $currentSettingsJson = setting('inspection_history_management', false, '{}'); $currentSettings = json_decode($currentSettingsJson, true); if (!is_array($currentSettings)) { $currentSettings = []; } // Get all enabled modules from Types $modules = \App\Types::where("enabled", true) ->whereNotNull("slug") ->where("slug", "!=", "") ->orderBy("s", "ASC") ->get(['id', 'slug', 'title']); $activeModules = []; foreach ($modules as $module) { $slug = $module->slug; $config = $currentSettings[$slug] ?? null; $isActive = isset($config['active']) ? (bool)$config['active'] : false; if ($isActive) { $activeModules[] = [ 'id' => $module->id, 'slug' => $module->slug, 'title' => $module->title, 'path_pattern' => $config['path_pattern'] ?? null, 'file_pattern' => $config['file_pattern'] ?? $config['pattern'] ?? null ]; } } return response()->json([ 'success' => true, 'data' => $activeModules ]); } catch (\Exception $e) { \Log::error("InspectionHistory modules error: " . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to retrieve modules: ' . $e->getMessage() ], 500); } } /** * Process file name pattern and replace placeholders with row data * * @param string $pattern Pattern string with {column_name} placeholders * @param array $rowData Row data array * @param string $originalExtension Original file extension * @param bool $allowSlashes Whether to allow slashes in the output (for directory paths) * @return string Processed file name */ private function processFileNamePattern($pattern, $rowData, $originalExtension = '', $allowSlashes = false) { // Replace Date/Time placeholders first $replacements = [ '{Y}' => date('Y'), '{m}' => date('m'), '{d}' => date('d'), '{H}' => date('H'), '{i}' => date('i'), '{s}' => date('s'), '{date}' => date('Y-m-d'), '{time}' => date('H-i-s'), '{timestamp}' => time(), ]; $processedPattern = str_replace(array_keys($replacements), array_values($replacements), $pattern); // Find all placeholders in pattern preg_match_all('/\{(\w+)\}/', $processedPattern, $matches); $placeholders = $matches[1]; // Replace each placeholder with corresponding value from row data foreach ($placeholders as $columnName) { $value = isset($rowData[$columnName]) ? $rowData[$columnName] : ''; // Sanitize value for filename $value = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', (string)$value); // Replace placeholder in pattern $processedPattern = str_replace('{' . $columnName . '}', $value, $processedPattern); } // Check if extension is already in pattern $hasExtension = preg_match('/\.[a-zA-Z0-9]+$/', $processedPattern); // Add extension if not present and original extension exists (and not a path) if (!$hasExtension && $originalExtension && !$allowSlashes) { $processedPattern .= '.' . $originalExtension; } // Final sanitization if ($allowSlashes) { $processedPattern = preg_replace('/[^A-Za-z0-9_\-\.\/]/', '_', $processedPattern); } else { $processedPattern = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', $processedPattern); } return $processedPattern; } /** * Upload custom photo for "Other" module (no specific record required) * * @group Inspection History * @bodyParam comment string required Inspection comment. * @bodyParam photo file[] nullable Photos to upload. * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse */ public function uploadOther(Request $request) { // Set defaults for "Other" upload $request->merge([ 'table_name' => 'Other', 'record_id' => 0, 'module_slug' => 'Other' ]); // Re-use existing upload logic return $this->upload($request); } }