documentsMapping = []; // Build dynamic mapping from documents array // Each document has an 'order' field (0-based from JavaScript) foreach ($documents as $document) { $type = $document['type'] ?? null; $order = $document['order'] ?? null; if ($type !== null && $order !== null) { // Order comes as 0-based from JavaScript, we'll convert to 1-based when using it $this->documentsMapping[$type] = (int) $order; } } Log::debug("Documents mapping set", [ 'mapping' => $this->documentsMapping ]); } /** * Add a new row to Excel table with document information * * @param array $search File paths to search for * @param array $selectDocument Selected document information (title, path, etc.) * @param string $fullFolder Target folder path * @param string $lineNumber Line number or identifier * @param string $documentDate Document date * @param int $rowNo Row number (sequence) * @param Worksheet $sheet PhpSpreadsheet worksheet object * @param int $currentRow Current row position * @param bool $override Whether to overwrite existing files * * @return int Next row position or current position if row was not added * @throws Exception */ public function addRow( array $search, array $selectDocument, string $fullFolder, string $lineNumber, string $documentDate, int $rowNo, Worksheet &$sheet, int $currentRow, bool $override = false ): int { try { $logPrefix = "[ROW-$rowNo]"; Log::info("🔵 ExcelRowHandler->addRow() CALLED --", [ 'row_no' => $rowNo, 'title1' => $selectDocument['title1'] ?? 'N/A', 'title2' => $selectDocument['title2'] ?? 'N/A', 'title3' => $selectDocument['title3'] ?? 'N/A', 'title4' => $selectDocument['title4'] ?? 'N/A', 'path' => $selectDocument['path'] ?? 'N/A', 'type' => $selectDocument['type'] ?? 'N/A', 'line' => $lineNumber, 'search_count' => count($search) ]); Log::debug("$logPrefix Processing document: " . $selectDocument['title2'] . ', Line: ' . $lineNumber); // Check for duplicate reports if ($this->isDuplicateReport($selectDocument, $lineNumber)) { $this->logDuplicateReport($selectDocument, $lineNumber, $fullFolder); return $currentRow; } // Mark this report as processed $this->markReportAsProcessed($selectDocument, $lineNumber); // Calculate document numbering ONCE for consistency $typeOrderNo = $this->getTypeOrderNumber($selectDocument); $subNumber = $this->getAndIncrementSubNumber($selectDocument); $fullNumber = "{$typeOrderNo}.{$subNumber}"; Log::info("📝 Document numbering calculated for rowNo={$rowNo}", [ 'rowNo' => $rowNo, 'type' => $selectDocument['type'] ?? 'N/A', 'order_from_document' => $selectDocument['order'] ?? 'N/A', 'type_order_no' => $typeOrderNo, 'sub_number' => $subNumber, 'full_number' => $fullNumber, 'title2' => $selectDocument['title2'] ?? 'N/A' ]); // Get contractor and page information from cache $contractor = Cache::get("rc_contractor", ""); $firstPage = Cache::get("rc_firstPage", 0); $lastPage = Cache::get("rc_lastPage", 0); // Determine row title $rowTitle = $this->determineRowTitle($selectDocument, $lineNumber); // Find existing files $foundFiles = $this->findExistingFiles($search); if (empty($foundFiles)) { $this->logNotFoundFiles($search, $rowTitle, $lineNumber, $rowNo, $fullFolder); return $currentRow; } // Process first found file $firstFoundFile = $foundFiles[0]; $this->updateFileTypeStats($firstFoundFile); // Generate file name with pre-calculated numbers $fileName = $this->generateFileName($selectDocument, $lineNumber, $rowNo, $typeOrderNo, $subNumber, $fullNumber); $fullPath = $fullFolder . $fileName; Log::debug("File name: " . $fileName); // Format document date $documentDate = df($documentDate); // Get page count $pageCount = $this->getPageCount($firstFoundFile); // Calculate page indices [$firstPage, $lastPage] = $this->calculatePageIndices($firstPage, $lastPage, $pageCount); // Update cache with new page numbers Cache::put("rc_lastPage", $lastPage); Cache::put("rc_firstPage", $firstPage); // Create log message $simpleLog = $this->createLogMessage($rowNo, $rowTitle, $lineNumber, $foundFiles, $search); // Get page index string $pageIndex = $this->getPageIndexString($firstPage, $lastPage, $pageCount); // Add row to Excel $this->addExcelRow( $sheet, $currentRow, $rowNo, $rowTitle, $lineNumber, $documentDate, $contractor, $pageCount, $pageIndex, $selectDocument, $typeOrderNo, $subNumber, $fullNumber ); // Copy files $this->copyFiles( $firstFoundFile, $fullFolder, $fileName, $override, $selectDocument, $simpleLog ); // Don't echo successful operations - keep console clean // Only errors and NOT FOUND messages will be shown // Update success counter self::$successfulOperations++; return $currentRow + 1; } catch (\Throwable $th) { Log::error("Unexpected error in addRow: " . $th->getMessage()); $this->logError($th, $rowNo ?? 0, $fullFolder); throw $th; } } /** * Check if report is duplicate */ private function isDuplicateReport(array $selectDocument, string $lineNumber): bool { $reportKey = $selectDocument['title2'] . '_' . $lineNumber; return in_array($reportKey, self::$addedReports); } /** * Mark report as processed */ private function markReportAsProcessed(array $selectDocument, string $lineNumber): void { $reportKey = $selectDocument['title2'] . '_' . $lineNumber; self::$addedReports[] = $reportKey; } /** * Log duplicate report */ private function logDuplicateReport(array $selectDocument, string $lineNumber, string $fullFolder): void { // Only log to debug - don't clutter console Log::debug("Duplicate report detected, skipping: " . $selectDocument['title2'] . ' - ' . $lineNumber); } /** * Determine row title from document */ private function determineRowTitle(array $selectDocument, string $lineNumber): string { $rowTitle = $selectDocument['title2']; if(isset($selectDocument['title3'])) { $rowTitle = $selectDocument['title3']; $selectDocument['title2'] = $selectDocument['title3']; } if(isset($selectDocument['title4'])) { $rowTitle = $selectDocument['title4']; } $convertTerm = $selectDocument['path']; if(strpos($convertTerm, "Naks_Consumables") !== false) { $convertTerm = $selectDocument['path'] . '_' . $selectDocument['type']; } if(strpos($convertTerm, "Procedure") !== false) { $convertTerm = $lineNumber; } if(isset($selectDocument['incoming_control_description'])) { $convertTerm = $selectDocument['incoming_control_description']; } if($selectDocument['type'] == 'drawings') { $convertTerm = convertRu($selectDocument['title2']); } // Convert and normalize $rowTitleRaw = convertRu($convertTerm); Log::info("rowTitleRaw", ['rowTitleRaw' => $rowTitleRaw]); $rowTitle = is_array($rowTitleRaw) ? json_encode($rowTitleRaw) : (string)$rowTitleRaw; // Log if conversion returned unexpected type if (is_array($rowTitleRaw)) { Log::warning('convertRu returned array', [ 'term' => $convertTerm, 'result' => $rowTitleRaw ]); } /* Log::info("rowTitleSelectDocument", [ 'title1' => $selectDocument['title1'] ?? 'N/A', 'title2' => $selectDocument['title2'] ?? 'N/A', 'title3' => $selectDocument['title3'] ?? 'N/A', 'title4' => $selectDocument['title4'] ?? 'N/A', 'path' => $selectDocument['path'] ?? 'N/A', 'type' => $selectDocument['type'] ?? 'N/A', ] ); */ Log::info("rowTitle", ['rowTitle' => $rowTitle]); return $rowTitle; } /** * Find existing files from search paths */ private function findExistingFiles(array $search): array { $foundFiles = []; foreach ($search as $searchPath) { $relativePath = str_replace("storage/documents/", "", $searchPath); if (file_exists($searchPath) || Storage::exists($relativePath)) { $foundFiles[] = $searchPath; } } return $foundFiles; } /** * Update file type statistics */ private function updateFileTypeStats(string $filePath): void { $fileExtension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); if (!isset(self::$fileTypeStats[$fileExtension])) { self::$fileTypeStats[$fileExtension] = 0; } self::$fileTypeStats[$fileExtension]++; } /** * Generate file name based on document information * Uses pattern from settings with placeholders: {type}, {number}, {title}, {line}, {file_name} * New placeholders: {type_order_no}, {sub_number}, {full_number} (e.g., "1.1") */ private function generateFileName( array $selectDocument, string $lineNumber, int $rowNo, int $typeOrderNo, int $subNumber, string $fullNumber ): string { $order = $rowNo; // Get pattern from settings, fallback to legacy format $pattern = setting('register_creator_file_name_pattern'); // Debug logging Log::debug("File naming pattern", [ 'pattern' => $pattern, 'order' => $order, 'type' => $selectDocument['type'] ?? 'N/A', 'type_order_no' => $typeOrderNo, 'sub_number' => $subNumber, 'full_number' => $fullNumber, 'title2' => $selectDocument['title2'] ?? 'N/A', 'line' => $lineNumber, 'real_line' => $selectDocument['real_line_identifier'] ?? $lineNumber ]); // Use the real line identifier if provided (fixes issue where processors override $lineNumber) $actualLine = $selectDocument['real_line_identifier'] ?? $lineNumber; if (empty($pattern)) { Log::debug("Using legacy file naming format (pattern is empty)"); // Legacy format for backward compatibility $addInLineNumber = ['по сварке трубопроводов(ЖСР)']; if (in_array($selectDocument['title2'], $addInLineNumber)) { return "$fullNumber - {$selectDocument['title2']} - $actualLine.pdf"; } if (isset($selectDocument['file_name'])) { return "$fullNumber - {$selectDocument['file_name']} - $actualLine.pdf"; } $title = $selectDocument['title2'] ?? 'document'; // If the title already contains the line number, don't append it again if (!empty($actualLine) && strpos($title, $actualLine) === false) { return "$fullNumber - {$title} - $actualLine.pdf"; } return "$fullNumber - {$title}.pdf"; } // Prepare replacements for placeholders $replacements = [ '{type}' => $selectDocument['type'] ?? '', '{number}' => (string) $order, '{type_order_no}' => (string) $typeOrderNo, '{sub_number}' => (string) $subNumber, '{full_number}' => $fullNumber, '{title}' => $selectDocument['title2'] ?? 'document', '{line}' => $actualLine, '{file_name}' => $selectDocument['file_name'] ?? '', ]; // Replace placeholders in pattern $fileName = $pattern; foreach ($replacements as $placeholder => $value) { $fileName = str_replace($placeholder, $value, $fileName); } // Clean up multiple spaces and trim $fileName = preg_replace('/\s+/', ' ', $fileName); $fileName = trim($fileName); // Remove invalid filename characters $fileName = preg_replace('/[<>:"\/\\|?*]/', '', $fileName); // Ensure .pdf extension if (!str_ends_with(strtolower($fileName), '.pdf')) { $fileName .= '.pdf'; } Log::debug("Generated file name", ['file_name' => $fileName]); return $fileName; } /** * Get type order number for document type * Maps document types to their order numbers */ private function getTypeOrderNumber(array $selectDocument): int { // Get type order mapping from settings or use default mapping $typeOrderMapping = $this->getTypeOrderMapping(); $docType = $selectDocument['type'] ?? 'unknown'; $orderId = $selectDocument['order'] ?? null; // If order is set in document, use it // Add 1 because JavaScript index starts from 0 but we want numbering from 1 if ($orderId !== null) { return (int) $orderId + 1; } // Otherwise use type mapping return $typeOrderMapping[$docType] ?? 99; } /** * Get and increment sub number for document type * Returns the current sub number and increments for next use */ private function getAndIncrementSubNumber(array $selectDocument): int { $docType = $selectDocument['type'] ?? 'unknown'; $typeOrderNo = $this->getTypeOrderNumber($selectDocument); // Use combination of type and type_order_no as key $key = "{$docType}_{$typeOrderNo}"; // Initialize if not set if (!isset(self::$typeSubNumbers[$key])) { self::$typeSubNumbers[$key] = 0; } // Increment and return self::$typeSubNumbers[$key]++; Log::debug("Sub number tracking", [ 'key' => $key, 'sub_number' => self::$typeSubNumbers[$key], 'type' => $docType, 'type_order_no' => $typeOrderNo ]); return self::$typeSubNumbers[$key]; } /** * Get type order mapping * Returns mapping of document types to their order numbers * * Priority: * 1. Dynamic mapping from frontend documents (if available) * 2. Settings-based mapping * 3. Default fallback mapping */ private function getTypeOrderMapping(): array { // Priority 1: Use dynamic mapping from frontend documents if available if (!empty($this->documentsMapping)) { Log::debug("Using dynamic documents mapping", [ 'mapping' => $this->documentsMapping ]); // Convert 0-based order to 1-based for return $convertedMapping = []; foreach ($this->documentsMapping as $type => $order) { $convertedMapping[$type] = $order + 1; // Convert to 1-based } return $convertedMapping; } // Priority 2: Try to get from settings $mappingSetting = setting('register_creator_type_order_mapping'); if (!empty($mappingSetting)) { $mapping = json_decode($mappingSetting, true); if (is_array($mapping)) { Log::debug("Using settings-based mapping"); return $mapping; } } // Priority 3: Default mapping (fallback) Log::debug("Using default fallback mapping"); return [ 'qa' => 1, 'wdb' => 2, 'drawings' => 3, 'materials' => 4, 'incoming_control_materials' => 4, 'prikaz' => 5, 'document-procedure' => 6, 'template' => 7, 'wps_naks_technology' => 8, 'naks_consumables_certificate' => 9, 'naks_consumables_inspection_test_report' => 10, ]; } /** * Get PDF page count */ private function getPageCount(string $filePath): int { try { $command = "pdftk '$filePath' dump_data | grep NumberOfPages"; putenv('LANG=ru_RU.UTF-8'); $output = shell_exec($command); $pageCount = (int) trim(explode(" ", $output)[1]); return $pageCount > 0 ? $pageCount : 1; } catch (\Throwable $th) { return 1; } } /** * Calculate page indices */ private function calculatePageIndices(int $firstPage, int $lastPage, int $pageCount): array { if ($firstPage == 0) { $firstPage = 1; } else { $firstPage = $lastPage + 1; } $lastPage = $firstPage + $pageCount - 1; return [$firstPage, $lastPage]; } /** * Get page index string */ private function getPageIndexString(int $firstPage, int $lastPage, int $pageCount): string { if ($pageCount == 1) { if ($firstPage == 1) { return (string) $firstPage; } else { $prevPage = $firstPage - 1; return "$prevPage - $firstPage"; } } return "$firstPage - $lastPage"; } /** * Create log message */ private function createLogMessage( int $rowNo, string $rowTitle, string $lineNumber, array $foundFiles, array $allSearchPaths ): string { $notFoundFiles = array_diff($allSearchPaths, $foundFiles); $simpleLog = ""; $simpleLog .= "📋 " . $rowNo . ". " . $rowTitle . " - " . $lineNumber . " | " . count($foundFiles) . " file(s)\n"; foreach ($foundFiles as $file) { $simpleLog .= " -- " . basename($file) . "\n"; } if (!empty($notFoundFiles)) { $simpleLog .= " ❓ Not found: " . count($notFoundFiles) . " file(s)\n"; foreach ($notFoundFiles as $file) { $simpleLog .= " -- " . basename($file) . " (missing)\n"; } } $simpleLog .= "\n"; return $simpleLog; } /** * Add row to Excel sheet */ private function addExcelRow( Worksheet &$sheet, int $currentRow, int $rowNo, string $rowTitle, string $lineNumber, string $documentDate, string $contractor, int $pageCount, string $pageIndex, array $selectDocument, int $typeOrderNo, int $subNumber, string $fullNumber ): void { $templateRow = Cache::get("rc_template_row", $currentRow); // Get template row cells and formulas $templateRowCells = []; $templateFormulas = []; foreach ($sheet->getRowIterator($templateRow, $templateRow)->current()->getCellIterator() as $cell) { $colIndex = $cell->getColumn(); // Get cell value - handle RichText objects $cellValue = $cell->getValue(); if ($cellValue instanceof \PhpOffice\PhpSpreadsheet\RichText\RichText) { $cellValue = $cellValue->getPlainText(); } $templateRowCells[$colIndex] = $cellValue; if ($cell->isFormula()) { $templateFormulas[$colIndex] = $cell->getValue(); } } // Get merged cells $mergedCells = []; foreach ($sheet->getMergeCells() as $mergeRange) { if (preg_match('/^\D*'.$templateRow.'$/', explode(':', $mergeRange)[0])) { $mergedCells[] = $mergeRange; } } // Insert new row $sheet->insertNewRowBefore($currentRow + 1, 1); // Prepare replacements $replacements = $this->prepareReplacements( $rowNo, $rowTitle, $lineNumber, $documentDate, $contractor, $pageCount, $pageIndex, $selectDocument, $typeOrderNo, $subNumber, $fullNumber ); // Fill cells with data foreach ($templateRowCells as $colIndex => $cellValue) { if (isset($templateFormulas[$colIndex])) { // Handle formulas $formula = $templateFormulas[$colIndex]; $updatedFormula = $this->updateFormula($formula, $currentRow, $templateRow); $sheet->setCellValue($colIndex . ($currentRow + 1), $updatedFormula); } else { // Handle regular values - Convert to string first $cellValueStr = (string)$cellValue; // Replace placeholders foreach ($replacements as $placeholder => $replacement) { if (strpos($cellValueStr, $placeholder) !== false) { $cellValueStr = str_replace($placeholder, $replacement, $cellValueStr); } } // Trim whitespace from the final value $cellValueStr = trim($cellValueStr); // Set the value with appropriate type // If the value is purely numeric, set it as a number if (is_numeric($cellValueStr)) { // Set as numeric value $sheet->setCellValueExplicit( $colIndex . ($currentRow + 1), $cellValueStr, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC ); } else { // Set as string value $sheet->setCellValue($colIndex . ($currentRow + 1), $cellValueStr); } } } // Merge cells foreach ($mergedCells as $mergeRange) { $adjustedMergeRange = preg_replace_callback('/\d+/', function($matches) use ($currentRow, $templateRow) { return $matches[0] == $templateRow ? $currentRow + 1 : $matches[0]; }, $mergeRange); try { $sheet->mergeCells($adjustedMergeRange); } catch (\Throwable $th) { // Merge error is not critical } } // Copy styles foreach ($sheet->getRowIterator($templateRow, $templateRow)->current()->getCellIterator() as $cell) { $colIndex = $cell->getColumn(); try { $style = $sheet->getStyle($colIndex . $templateRow); $sheet->duplicateStyle($style, $colIndex . ($currentRow + 1)); } catch (\Throwable $th) { // Style error is not critical } } // Copy row height $templateRowHeight = $sheet->getRowDimension($templateRow)->getRowHeight(); $sheet->getRowDimension($currentRow + 1)->setRowHeight($templateRowHeight); } /** * Prepare placeholder replacements */ private function prepareReplacements( int $rowNo, string $rowTitle, string $lineNumber, string $documentDate, string $contractor, int $pageCount, string $pageIndex, array $selectDocument, int $typeOrderNo, int $subNumber, string $fullNumber ): array { $replacements = []; $replacements['{rowNo}'] = $rowNo; $replacements['{type_order_no}'] = $typeOrderNo; $replacements['{sub_number}'] = $subNumber; $replacements['{full_number}'] = $fullNumber; $replacements['{rowTitle}'] = $rowTitle; $replacements['{documentReportNumber}'] = $lineNumber; $replacements['{documentDate}'] = $documentDate; $replacements['{constructor}'] = $contractor; $replacements['{pageCount}'] = $pageCount; $replacements['{pageIndex}'] = $pageIndex; // Safe get with type checking for all title fields $replacements['{title1}'] = $this->safeGetValue($selectDocument, 'title1'); $replacements['{ndt_report_no}'] = $this->safeGetValue($selectDocument, 'title2'); $replacements['{pdf_document_title}'] = $this->safeGetValue($selectDocument, 'title3'); $replacements['{ndt_register_title}'] = $this->safeGetValue($selectDocument, 'title4'); Log::info("📋 Prepared replacements for Excel row", [ 'rowNo' => $rowNo, 'type_order_no' => $typeOrderNo, 'sub_number' => $subNumber, 'full_number' => $fullNumber ]); return $replacements; } /** * Safely get value from array, converting arrays to JSON */ private function safeGetValue(array $data, string $key): string { if (!isset($data[$key])) { return ''; } $value = $data[$key]; return is_array($value) ? json_encode($value) : (string)$value; } /** * Update formula with new row number */ private function updateFormula(string $formula, int $currentRow, int $templateRow): string { $rowOffset = ($currentRow + 1) - $templateRow; return preg_replace_callback( '/([A-Z]+)(\d+)/', function($matches) use ($rowOffset) { $col = $matches[1]; $row = intval($matches[2]); $row += $rowOffset; return $col . $row; }, $formula ); } /** * Copy files to target location */ private function copyFiles( string $sourceFile, string $fullFolder, string $fileName, bool $override, array $selectDocument, string &$simpleLog ): void { $sourcePath = str_replace("storage/documents/", "", $sourceFile); $targetPath = $this->getStorageRelativePath($fullFolder) . basename($fileName); try { // Create directory if not exists $targetDir = dirname($targetPath); if (!Storage::exists($targetDir)) { Storage::makeDirectory($targetDir); } // Copy PDF file $copyStatus = $this->copyFile($sourceFile, $sourcePath, $targetPath, $override); $simpleLog .= " 📁 File: " . $copyStatus . "\n"; // Copy template XLSX file if exists if (isset($selectDocument['type']) && $selectDocument['type'] === 'template') { $xlsxStatus = $this->copyTemplateXlsx($sourceFile, $sourcePath, $targetPath, $override); $simpleLog .= " " . $xlsxStatus . "\n"; } } catch (\Throwable $th) { $simpleLog .= " ❌ Copy error: " . $th->getMessage() . "\n"; } $simpleLog .= "\n"; } /** * Copy single file */ private function copyFile(string $originalPath, string $sourcePath, string $targetPath, bool $override): string { if (Storage::exists($targetPath)) { if ($override) { Storage::delete($targetPath); $this->performCopy($originalPath, $sourcePath, $targetPath); return "overwritten"; } else { // Check if content is different $currentContent = md5(Storage::get($targetPath)); $newContent = $this->getFileHash($originalPath, $sourcePath); if ($currentContent !== $newContent) { Storage::delete($targetPath); $this->performCopy($originalPath, $sourcePath, $targetPath); return "updated"; } return "same content, skipped"; } } $this->performCopy($originalPath, $sourcePath, $targetPath); return "copied"; } /** * Perform file copy operation */ private function performCopy(string $originalPath, string $sourcePath, string $targetPath): void { if (Storage::exists($sourcePath)) { Storage::copy($sourcePath, $targetPath); } else if (file_exists($originalPath)) { $fileContent = file_get_contents($originalPath); Storage::put($targetPath, $fileContent); } } /** * Get file hash */ private function getFileHash(string $originalPath, string $sourcePath): string { if (Storage::exists($sourcePath)) { return md5(Storage::get($sourcePath)); } else if (file_exists($originalPath)) { return md5(file_get_contents($originalPath)); } return ''; } /** * Copy template XLSX file */ private function copyTemplateXlsx(string $pdfPath, string $pdfSourcePath, string $pdfTargetPath, bool $override): string { $xlsxOriginalPath = str_replace('.pdf', '.xlsx', $pdfPath); $xlsxSourcePath = str_replace('.pdf', '.xlsx', $pdfSourcePath); $xlsxTargetPath = str_replace('.pdf', '.xlsx', $pdfTargetPath); if (!Storage::exists($xlsxSourcePath) && !file_exists($xlsxOriginalPath)) { return "❓ XLSX: not found"; } $xlsxStatus = $this->copyFile($xlsxOriginalPath, $xlsxSourcePath, $xlsxTargetPath, $override); return "📊 XLSX: " . $xlsxStatus; } /** * Log not found files */ private function logNotFoundFiles( array $search, string $rowTitle, string $lineNumber, int $rowNo, string $fullFolder ): void { // Console output - simplified (same format as file log) $message = "❌ NOT FOUND: {$rowTitle} - {$lineNumber}\n"; echo($message); // File log - same simplified format try { $storagePath = $this->getStorageRelativePath($fullFolder); Storage::append($storagePath . 'log.txt', $message); } catch (\Throwable $logError) { // Silent fail - don't clutter console } } /** * Log error */ private function logError(\Throwable $th, int $rowNo, string $fullFolder): void { try { // Console and file log - same simplified format $message = "❌ ERROR: Row {$rowNo} - {$th->getMessage()}\n"; echo($message); // File log $storagePath = $this->getStorageRelativePath($fullFolder); Storage::append($storagePath . 'log.txt', $message); } catch (\Throwable $logError) { // Silent fail } } /** * Get storage relative path */ private function getStorageRelativePath(string $fullPath): string { return str_replace("storage/documents/", "", $fullPath); } /** * Get file type statistics */ public static function getFileTypeStats(): array { return self::$fileTypeStats; } /** * Get successful operations count */ public static function getSuccessfulOperations(): int { return self::$successfulOperations; } /** * Reset statistics */ public static function resetStatistics(): void { self::$addedReports = []; self::$fileTypeStats = []; self::$successfulOperations = 0; } }