Files
citrus-cms/app/Services/RegisterCreator/ExcelRowHandler.php
T
2026-04-28 21:14:25 +03:00

1005 lines
34 KiB
PHP

<?php
namespace App\Services\RegisterCreator;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Cache;
use Exception;
/**
* Excel Row Handler Service
*
* This service handles adding formatted rows to Excel sheets for Register Creator,
* including file copying, page counting, and logging operations.
*/
class ExcelRowHandler
{
private static array $addedReports = [];
private static array $fileTypeStats = [];
private static int $successfulOperations = 0;
private static array $typeSubNumbers = []; // Track sub numbers for each document type
private array $documentsMapping = []; // Store documents array for dynamic type order mapping
/**
* Set documents array for dynamic type order mapping
*
* @param array $documents Documents array from frontend
* @return void
*/
public function setDocuments(array $documents): void
{
$this->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;
}
}