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

518 lines
20 KiB
PHP

<?php
namespace App\Services\RegisterCreator;
use App\Services\RegisterCreator\DocumentProcessors\DocumentProcessorFactory;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Exception;
class RegisterCreatorService
{
private ExcelHandler $excelHandler;
private PdfConverter $pdfConverter;
private PlaceholderReplacer $placeholderReplacer;
private ProgressTracker $progressTracker;
private array $settings;
private array $statistics;
public function __construct()
{
$this->excelHandler = new ExcelHandler();
$this->pdfConverter = new PdfConverter();
$this->placeholderReplacer = new PlaceholderReplacer();
// Initialize statistics
$this->statistics = [
'documents_processed' => 0,
'documents_success' => 0,
'documents_failed' => 0,
'by_type' => []
];
}
/**
* Process single line with all documents
*/
public function processLine(array $lineData, array $documents, array $settings): array
{
$startTime = microtime(true);
$this->settings = $settings;
// Initialize progress tracker
$jobId = $settings['job_id'] ?? uniqid('rc_', true);
$registerColumnBased = $settings['register_column_based'] ?? 'line_number';
$lineIdentifier = $lineData[$registerColumnBased] ?? 'unknown';
// Set total to document count for proper progress tracking
$totalDocuments = count($documents);
$this->progressTracker = new ProgressTracker($jobId, $totalDocuments, 0, $lineIdentifier);
Log::info("Processing line started", [
'job_id' => $jobId,
'line' => $lineIdentifier,
'documents_count' => $totalDocuments
]);
$this->progressTracker->update("Loading template for {$lineIdentifier}", 0, 0);
try {
// Get document template info
$documentInfo = document_template("register");
if (!$documentInfo) {
throw new Exception("Register template not found!");
}
// Prepare folder structure - matching blade structure
$path = $settings['path'] ?? '';
$basePath = "storage/documents/{$path}";
$fullFolder = "{$basePath}/{$lineIdentifier}/";
$fullFolder2 = str_replace("storage/documents/", "", $fullFolder);
$justFolder = "{$path}/{$lineIdentifier}/";
// Clean target directory before processing
if (Storage::exists($fullFolder2)) {
Storage::deleteDirectory($fullFolder2);
}
Storage::makeDirectory($fullFolder2);
// Create log file using fullFolder2 (storage-relative path)
$this->initializeLogFile($fullFolder2, $lineIdentifier);
// Create info.txt file with job and user information
try {
Log::info("About to create info.txt", ['folder' => $fullFolder2, 'line' => $lineIdentifier]);
$this->createInfoFile($fullFolder2, $lineIdentifier, $settings, $totalDocuments, $lineData);
Log::info("info.txt creation completed", ['folder' => $fullFolder2]);
} catch (\Throwable $th) {
Log::error("Failed to create info.txt", [
'error' => $th->getMessage(),
'file' => $th->getFile(),
'line' => $th->getLine()
]);
}
// Load Excel template
$this->excelHandler->loadTemplate($documentInfo->files);
// Prepare placeholders
$replacements = $this->placeholderReplacer->prepareReplacements($lineData);
// Replace placeholders in Excel
$this->excelHandler->replacePlaceholders($replacements);
// Get contractor information
$contractor = $this->getContractorName($lineData);
Cache::put("rc_contractor", $contractor);
// Get WPS data if available
$wpsData = $this->getWpsData($lineData);
// Set template row
$templateRow = setting("register_creator_start_row") ?: 16;
Cache::put("rc_template_row", $templateRow);
$currentRow = $templateRow;
$rowNo = 1; // Initialize global row number counter
// DEBUG: Log documents BEFORE sorting
Log::info("📋 Documents BEFORE sorting", [
'documents' => array_map(function($doc, $idx) {
return [
'index' => $idx,
'type' => $doc['type'] ?? 'N/A',
'order' => $doc['order'] ?? 'N/A',
'title2' => $doc['title2'] ?? 'N/A'
];
}, $documents, array_keys($documents))
]);
// Sort documents by order field before processing
// This ensures Excel rows are created in the correct document type order
usort($documents, function($a, $b) {
$orderA = $a['order'] ?? 999;
$orderB = $b['order'] ?? 999;
// If orders are equal, maintain original order by using type as secondary sort
if ($orderA === $orderB) {
$typeA = $a['type'] ?? '';
$typeB = $b['type'] ?? '';
return strcmp($typeA, $typeB);
}
return $orderA <=> $orderB;
});
// DEBUG: Log documents AFTER sorting
Log::info("📋 Documents AFTER sorting", [
'documents' => array_map(function($doc, $idx) {
return [
'index' => $idx,
'type' => $doc['type'] ?? 'N/A',
'order' => $doc['order'] ?? 'N/A',
'title2' => $doc['title2'] ?? 'N/A'
];
}, $documents, array_keys($documents))
]);
Log::debug("Documents sorted by order field", [
'documents_count' => count($documents),
'first_order' => $documents[0]['order'] ?? 'N/A',
'last_order' => $documents[count($documents) - 1]['order'] ?? 'N/A'
]);
// Process each document
foreach ($documents as $index => $document) {
$startDocTime = microtime(true);
try {
// Safe string conversion for title2
$docTitle = $document['title2'] ?? 'unknown';
if (is_array($docTitle)) {
$docTitle = json_encode($docTitle);
}
$currentDoc = $index + 1;
$progressPercent = (int) (($currentDoc) / count($documents) * 100);
$this->progressTracker->update(
"Processing document {$currentDoc}/" . count($documents) . ": {$docTitle}",
$progressPercent,
$currentDoc // Update current to show which document we're on
);
Log::debug("Starting document processing", [
'job_id' => $this->progressTracker->getJobId(),
'line' => $lineIdentifier,
'document' => $docTitle,
'index' => $index + 1,
'total' => count($documents)
]);
$oldRowNo = $rowNo;
$rowNo = $this->processDocument(
$lineData,
$document,
$this->excelHandler->getSheet(),
$currentRow,
$wpsData,
$rowNo,
$documents // Pass all documents array for dynamic mapping
);
$docDuration = round(microtime(true) - $startDocTime, 2);
Log::info("📊 Document processed", [
'job_id' => $this->progressTracker->getJobId(),
'line' => $lineIdentifier,
'document' => $docTitle,
'document_type' => $document['type'] ?? 'N/A',
'document_order' => $document['order'] ?? 'N/A',
'rowNo_before' => $oldRowNo,
'rowNo_after' => $rowNo,
'rows_added' => ($rowNo - $oldRowNo),
'duration' => $docDuration . 's'
]);
$this->statistics['documents_success']++;
} catch (\Throwable $th) {
$this->statistics['documents_failed']++;
// Safe string conversion for error logging
$docTitle = $document['title2'] ?? 'unknown';
if (is_array($docTitle)) {
$docTitle = json_encode($docTitle);
}
$docType = $document['type'] ?? 'unknown';
if (is_array($docType)) {
$docType = json_encode($docType);
}
$docPath = $document['path'] ?? 'unknown';
if (is_array($docPath)) {
$docPath = json_encode($docPath);
}
// Detailed error logging for documents
Log::error("Document processing error", [
'job_id' => $this->progressTracker->getJobId(),
'line' => $lineIdentifier,
'document' => $docTitle,
'document_type' => $docType,
'document_path' => $docPath,
'error_type' => get_class($th),
'error_message' => $th->getMessage(),
'file' => $th->getFile(),
'line_number' => $th->getLine(),
'trace' => $th->getTraceAsString()
]);
// Write detailed error to log file
$errorLog = "❌ Document error: {$docTitle}\n";
$errorLog .= " Type: {$docType}\n";
$errorLog .= " Path: {$docPath}\n";
$errorLog .= " Error: " . get_class($th) . " - {$th->getMessage()}\n";
$errorLog .= " File: {$th->getFile()}:{$th->getLine()}\n";
$this->writeLog($fullFolder2, $errorLog);
continue;
}
$this->statistics['documents_processed']++;
}
// Apply work permit replacements
try {
$spreadsheet = workPermitReplacerExcel2(
$this->excelHandler->getSpreadsheet(),
$lineData,
$documentInfo,
"replacer"
);
} catch (\Throwable $th) {
Log::warning("Work permit replacer error", [
'error' => $th->getMessage()
]);
}
// Remove template row
$this->excelHandler->removeTemplateRow($templateRow);
// Save Excel file
$registerFileName = $justFolder . "Register.xlsx";
$this->excelHandler->save($registerFileName, $settings['override'] ?? true);
// Convert to PDF
$pdfPath = $justFolder;
$this->pdfConverter->convert($registerFileName, $pdfPath, $settings['override'] ?? true);
// Calculate duration
$duration = round(microtime(true) - $startTime, 2);
// Write summary to log
$this->writeSummary($justFolder, $lineIdentifier, $duration);
// Mark as complete - will show total/total (e.g., 23/23)
$this->progressTracker->complete("Completed successfully");
Log::info("Line processing completed", [
'job_id' => $this->progressTracker->getJobId(),
'line' => $lineIdentifier,
'duration' => $duration . 's',
'documents_processed' => $this->statistics['documents_processed'],
'documents_success' => $this->statistics['documents_success'],
'documents_failed' => $this->statistics['documents_failed']
]);
return [
'status' => 'success',
'line' => $lineIdentifier,
'duration' => $duration,
'statistics' => $this->statistics
];
} catch (\Throwable $th) {
$this->progressTracker->fail($th->getMessage());
Log::error("Line processing failed", [
'job_id' => $this->progressTracker->getJobId(),
'line' => $lineIdentifier,
'error' => $th->getMessage(),
'trace' => $th->getTraceAsString()
]);
throw $th;
} finally {
// Cleanup
$this->excelHandler->cleanup();
Cache::forget("rc_lastPage");
Cache::forget("rc_firstPage");
Cache::forget("rc_contractor");
Cache::forget("rc_template_row");
}
}
/**
* Process single document
*/
private function processDocument(
array $lineData,
array $document,
$sheet,
int &$currentRow,
$wpsData,
int $rowNo,
array $allDocuments = [] // All documents array for dynamic type order mapping
): int {
// Validate document structure - ensure 'type' field exists
if (!isset($document['type']) || empty($document['type'])) {
// Log detailed document information for debugging
Log::warning("⚠️ Document missing 'type' field, attempting to infer", [
'document_keys' => array_keys($document),
'document_id' => $document['id'] ?? 'unknown',
'document_path' => $document['path'] ?? 'unknown'
]);
// Try to infer type from document structure
if (isset($document['is_dynamic']) && $document['is_dynamic']) {
$document['type'] = 'dynamic';
Log::info(" ✓ Inferred type as 'dynamic' based on is_dynamic flag");
} else if (isset($document['sql_query'])) {
// If has SQL query, it's likely a dynamic document
$document['type'] = 'dynamic';
Log::info(" ✓ Inferred type as 'dynamic' based on sql_query presence");
} else if (isset($document['id']) && strpos($document['id'], 'template') !== false) {
$document['type'] = 'template';
Log::info(" ✓ Inferred type as 'template' based on id");
} else {
// Default fallback
$document['type'] = 'qa';
Log::info(" ✓ Using default type 'qa' as fallback");
}
}
$processor = DocumentProcessorFactory::make($document['type'], $document);
// Prepare settings for processor
$processorSettings = array_merge($this->settings, [
'wps_data' => $wpsData,
'row_no' => $rowNo, // Pass global row number to processor
'all_documents' => $allDocuments // Pass all documents for dynamic mapping
]);
$newRow = $processor->process(
$lineData,
$document,
$sheet,
$currentRow,
$processorSettings
);
// Update current row if changed
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
// Update statistics
$docType = $document['type'];
if (!isset($this->statistics['by_type'][$docType])) {
$this->statistics['by_type'][$docType] = 0;
}
$this->statistics['by_type'][$docType]++;
// Get number of documents added from processor and increment rowNo accordingly
$documentsAdded = $processor->getDocumentsAdded();
Log::debug("Documents added by processor", [
'processor' => get_class($processor),
'documents_added' => $documentsAdded,
'current_rowNo' => $rowNo,
'next_rowNo' => $rowNo + $documentsAdded
]);
return $rowNo + $documentsAdded;
}
/**
* Get contractor name
*/
private function getContractorName(array $lineData): string
{
$subcontractors = Cache::get("subcontractors", []);
$contractorKey = $lineData['contractor'] ?? '';
if (isset($subcontractors[$contractorKey])) {
return $subcontractors[$contractorKey]->company_name_ru ?? $contractorKey;
}
return $contractorKey;
}
/**
* Get WPS data
*/
private function getWpsData(array $lineData)
{
$wpsNo = $lineData['wps_no'] ?? '';
if (empty($wpsNo)) {
return null;
}
return db("w_p_s")->where("details", $wpsNo)->first();
}
/**
* Initialize log file
*/
private function initializeLogFile(string $folder, string $lineIdentifier): void
{
$logPath = $folder . 'log.txt';
Storage::delete($logPath);
$header = str_repeat("🚀", 30) . "\n";
$header .= "REGISTER CREATOR LOG FILE\n";
$header .= "Line: {$lineIdentifier}\n";
$header .= "Started: " . now()->toDateTimeString() . "\n";
$header .= str_repeat("🚀", 30) . "\n\n";
Storage::put($logPath, $header);
}
/**
* Create info.txt file with user information (JSON format)
*/
private function createInfoFile(string $folder, string $lineIdentifier, array $settings, int $totalDocuments, array $lineData): void
{
$infoPath = $folder . 'info.txt';
// Delete old info file if exists
Storage::delete($infoPath);
// Create simple JSON with user information
$infoData = [
'user_name' => $settings['user_name'] ?? 'Unknown',
'user_id' => $settings['user_id'] ?? null,
'created_at' => now()->format('Y-m-d H:i:s'),
'line_identifier' => $lineIdentifier
];
Storage::put($infoPath, json_encode($infoData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
/**
* Write to log file
*/
private function writeLog(string $folder, string $message): void
{
$logPath = $folder . 'log.txt';
Storage::append($logPath, $message . "\n");
}
/**
* Write summary to log file
*/
private function writeSummary(string $folder, string $lineIdentifier, float $duration): void
{
$summary = "\n" . str_repeat("=", 80) . "\n";
$summary .= "PROCESSING SUMMARY\n";
$summary .= str_repeat("=", 80) . "\n";
$summary .= "Line: {$lineIdentifier}\n";
$summary .= "Duration: {$duration}s\n";
$summary .= "Documents Processed: {$this->statistics['documents_processed']}\n";
$summary .= "Successful: {$this->statistics['documents_success']}\n";
$summary .= "Failed: {$this->statistics['documents_failed']}\n";
$summary .= "\nBy Type:\n";
foreach ($this->statistics['by_type'] as $type => $count) {
$summary .= " - {$type}: {$count}\n";
}
$summary .= str_repeat("=", 80) . "\n";
$this->writeLog($folder, $summary);
}
}