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); } }