1005 lines
34 KiB
PHP
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;
|
|
}
|
|
}
|
|
|