İlk temizlik tamamlandı bir önceki projeden

This commit is contained in:
Ümit Tunç
2026-04-28 21:14:25 +03:00
commit f80443aec0
10000 changed files with 959965 additions and 0 deletions
@@ -0,0 +1,207 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use App\Services\RegisterCreator\ExcelRowHandler;
abstract class AbstractDocumentProcessor
{
protected array $weldLogData;
protected array $document;
protected Worksheet $sheet;
protected int $currentRow;
protected string $registerColumnBased;
protected array $settings;
protected string $logFilePath;
protected ExcelRowHandler $excelRowHandler;
protected int $documentsAdded = 0; // Counter for documents added to Excel
/**
* Process the document
*/
abstract public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int;
/**
* Initialize processor
*/
protected function initialize(
array $weldLogData,
array $document,
Worksheet $sheet,
int $currentRow,
array $settings
): void {
$this->weldLogData = $weldLogData;
$this->document = $document;
$this->sheet = $sheet;
$this->currentRow = $currentRow;
$this->settings = $settings;
$this->registerColumnBased = $settings['register_column_based'] ?? 'line_number';
$this->excelRowHandler = new ExcelRowHandler();
$this->documentsAdded = 0; // Reset counter for each document type
// Set documents array for dynamic type order mapping
if (isset($settings['all_documents']) && is_array($settings['all_documents'])) {
$this->excelRowHandler->setDocuments($settings['all_documents']);
Log::debug("ExcelRowHandler initialized with documents", [
'processor' => class_basename($this),
'documents_count' => count($settings['all_documents'])
]);
}
// Setup log file path
$lineIdentifier = $weldLogData[$this->registerColumnBased] ?? 'unknown';
$basePath = $settings['path'] ?? '';
$this->logFilePath = "{$basePath}/{$lineIdentifier}/log.txt";
}
/**
* Normalize search term for file matching
*/
protected function normalizeSearchTerm(string $term): string
{
$term = str_replace("/", "*", $term);
$term = str_replace(" ", "*", $term);
$term = str_replace("\\", "*", $term);
return $term;
}
/**
* Search for files using glob pattern
*/
protected function searchFiles(string $pattern): array
{
Log::debug('Searching for files', ['pattern' => $pattern]);
$files = glob($pattern);
if ($files === false) {
$files = [];
}
Log::debug('Files found', ['count' => count($files)]);
return $files;
}
/**
* Get full folder path for current line
*/
protected function getFullFolder(): string
{
$path = $this->settings['path'] ?? '';
$lineIdentifier = $this->weldLogData[$this->registerColumnBased] ?? 'unknown';
$basePath = "storage/documents/{$path}";
return "{$basePath}/{$lineIdentifier}/";
}
/**
* Write to log file
* Note: This method is now silent to keep log files clean
* Only errors and NOT FOUND messages are logged
*/
protected function log(string $message, string $level = 'info'): void
{
// Don't write info messages to log file anymore - keep it clean
// Only errors will be logged via ExcelRowHandler
// Only log to Laravel debug for development purposes
Log::debug($message, [
'processor' => class_basename($this),
'line' => $this->weldLogData[$this->registerColumnBased] ?? 'unknown'
]);
}
/**
* Get contractor name
*/
protected function getContractor(): string
{
$subcontractors = \Cache::get("subcontractors", []);
$contractorKey = $this->weldLogData['contractor'] ?? '';
if (isset($subcontractors[$contractorKey])) {
return $subcontractors[$contractorKey]->company_name_ru ?? $contractorKey;
}
return $contractorKey;
}
/**
* Add row to Excel using ExcelRowHandler service
*
* @param array $search File paths to search for
* @param string $lineNumber Line number or identifier
* @param string $documentDate Document date
*
* @return int Next row position
*/
protected function addRowToExcel(
array $search,
string $lineNumber,
string $documentDate
): int {
$fullFolder = $this->getFullFolder();
$override = $this->settings['override'] ?? false;
// Calculate current row number based on starting row + documents added so far
$startRowNo = $this->settings['row_no'] ?? 1;
$currentRowNo = $startRowNo + $this->documentsAdded;
Log::info("🟢 AbstractDocumentProcessor->addRowToExcel() CALLED", [
'processor' => class_basename($this),
'row_no' => $currentRowNo,
'start_row_no' => $startRowNo,
'documents_added' => $this->documentsAdded,
'doc_type' => $this->document['type'] ?? 'N/A',
'doc_title2' => $this->document['title2'] ?? 'N/A',
'line' => $lineNumber,
'search_files' => count($search)
]);
$oldCurrentRow = $this->currentRow;
// Ensure the actual line identifier is available in the document array
// This allows processors (like TemplateProcessor) to override $lineNumber for Excel
// while still preserving the real line number for filename generation.
$this->document['real_line_identifier'] = $this->weldLogData[$this->registerColumnBased] ?? $lineNumber;
$newRow = $this->excelRowHandler->addRow(
$search,
$this->document,
$fullFolder,
$lineNumber,
$documentDate,
$currentRowNo,
$this->sheet,
$this->currentRow,
$override
);
// If a row was added, increment the documents counter and update current row
if ($newRow > $oldCurrentRow) {
$this->documentsAdded++;
$this->currentRow = $newRow; // ← UPDATE CURRENT ROW!
}
return $newRow;
}
/**
* Get the number of documents added during processing
*/
public function getDocumentsAdded(): int
{
return $this->documentsAdded;
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DocumentProcedureProcessor extends AbstractDocumentProcessor
{
public function process(array $weldLogData, array $document, Worksheet $sheet, int &$currentRow, array $settings): int
{
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$procedures = [];
if (!empty($document['title2'])) {
$procedures = array_map('trim', explode(",", $document['title2']));
}
// Prepare procedures with dates for sorting
$proceduresWithDates = [];
foreach ($procedures as $procedureNo) {
$procedure = db("document_procedures")->where("document_no", $procedureNo)->first();
if (!$procedure) continue;
$proceduresWithDates[] = [
'procedure' => $procedure,
'date' => $procedure->publish_date ?? ''
];
}
// Sort by publish_date (oldest first) - reverse insertion order
usort($proceduresWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
foreach ($proceduresWithDates as $procData) {
$procedure = $procData['procedure'];
$normalized = $this->normalizeSearchTerm($procedure->document_no);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$document['title2'] = $procedure->document_no;
$this->document = $document;
$documentDate = $procData['date'];
$newRow = $this->addRowToExcel($search, $procedure->document_no, $documentDate);
if ($newRow > $currentRow) { $currentRow = $newRow; }
}
return $currentRow;
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use Exception;
class DocumentProcessorFactory
{
/**
* Create document processor instance based on type
*/
public static function make(string $type, ?array $document = null): AbstractDocumentProcessor
{
// Check if this is a dynamic document
if ($type === 'dynamic' || ($document && isset($document['is_dynamic']) && $document['is_dynamic'])) {
return new DynamicMappingProcessor();
}
return match($type) {
'qa' => new QaDocumentProcessor(),
'wdb' => new WdbDocumentProcessor(),
'wps_naks_technology' => new WpsNaksTechnologyProcessor(),
'naks_consumables_certificate' => new NaksConsumablesCertificateProcessor(),
'naks_consumables_inspection_test_report' => new NaksConsumablesInspectionProcessor(),
'drawings' => new DrawingsProcessor(),
'materials' => new MaterialsProcessor(),
'incoming_control_materials' => new IncomingControlProcessor(),
'template' => new TemplateProcessor(),
'prikaz' => new PrikazProcessor(),
'document-procedure' => new DocumentProcedureProcessor(),
'dynamic' => new DynamicMappingProcessor(),
default => new GenericDocumentProcessor(),
};
}
/**
* Check if processor exists for given type
*/
public static function exists(string $type): bool
{
try {
self::make($type);
return true;
} catch (\Throwable $th) {
return false;
}
}
}
@@ -0,0 +1,50 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DrawingsProcessor extends AbstractDocumentProcessor
{
/**
* Process drawings documents
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing Drawings");
$lineIdentifier = $weldLogData[$this->registerColumnBased];
$normalized = $this->normalizeSearchTerm($lineIdentifier);
$search = $this->searchFiles("{$document['path']}/*{$normalized}*.pdf");
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
$documentDate = $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
// Use the new ExcelRowHandler service method
$newRow = $this->addRowToExcel(
$search,
$lineIdentifier,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
return $currentRow;
}
}
@@ -0,0 +1,233 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class DynamicMappingProcessor extends AbstractDocumentProcessor
{
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing dynamic mapped document: {$document['title2']}");
// Check if this document has SQL query configuration
if (empty($document['sql_query'])) {
$this->log("No SQL query found for dynamic document", 'warning');
return $currentRow;
}
return $this->processSqlBasedMapping($currentRow);
}
private function processSqlBasedMapping(int &$currentRow): int
{
try {
// Execute SQL query with placeholder replacement
$results = $this->executeSqlQuery();
if (empty($results)) {
$this->log("SQL query returned no results", 'warning');
return $currentRow;
}
$this->log("Found " . count($results) . " records from SQL query");
// Process each result
foreach ($results as $result) {
try {
$identifier = $result['identifier'];
$recordData = $result['data'];
$documentDate = $result['document_date'];
// Generate row title using pattern
$rowTitle = $this->generateRowTitle($recordData);
// Generate file search pattern
$searchPattern = $this->generateFileSearchPattern($recordData);
$fullPath = "storage/documents/{$this->document['path']}/{$searchPattern}";
// Search for files
$files = $this->searchFiles($fullPath);
if (empty($files)) {
$this->log("No files found for: {$identifier} (pattern: {$searchPattern})", 'warning');
continue;
}
$this->log("✓ Found " . count($files) . " files for: {$identifier}");
// Update document titles
$this->document['title2'] = $identifier;
$this->document['title4'] = $rowTitle;
// Add row to Excel
$newRow = $this->addRowToExcel(
$files,
$identifier,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
$this->log("✓ Processed: {$identifier} (from SQL query)");
} catch (\Throwable $th) {
$this->log("Error processing SQL result: {$th->getMessage()}", 'error');
Log::error("Dynamic mapping result processing error", [
'identifier' => $result['identifier'] ?? 'unknown',
'error' => $th->getMessage(),
'trace' => $th->getTraceAsString()
]);
continue;
}
}
return $currentRow;
} catch (\Throwable $th) {
$this->log("SQL query execution error: {$th->getMessage()}", 'error');
Log::error("Dynamic mapping SQL execution error", [
'sql_query' => $this->document['sql_query'] ?? 'N/A',
'error' => $th->getMessage(),
'trace' => $th->getTraceAsString()
]);
throw $th;
}
}
/**
* Execute SQL query with placeholder replacement
*/
private function executeSqlQuery(): array
{
$sqlQuery = $this->document['sql_query'] ?? '';
if (empty($sqlQuery)) {
throw new \Exception("No SQL query defined for dynamic document");
}
// Replace placeholders
$executedQuery = $this->replacePlaceholders($sqlQuery);
Log::info("Executing dynamic SQL query", [
'original_query' => $sqlQuery,
'executed_query' => $executedQuery
]);
try {
$startTime = microtime(true);
$results = DB::select($executedQuery);
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
Log::info("Dynamic SQL query executed successfully", [
'record_count' => count($results),
'execution_time' => $executionTime . 'ms'
]);
return $this->formatResults($results);
} catch (\Throwable $th) {
Log::error("Dynamic SQL query execution failed", [
'query' => $executedQuery,
'error' => $th->getMessage()
]);
throw $th;
}
}
/**
* Replace :placeholder with actual values from register data
*/
private function replacePlaceholders(string $query): string
{
$result = $query;
// Find all :placeholder patterns
preg_match_all('/:(\w+)/', $query, $matches);
foreach ($matches[1] as $placeholder) {
$value = $this->weldLogData[$placeholder] ?? null;
if ($value !== null) {
// Escape value for SQL
$escapedValue = DB::getPdo()->quote($value);
$result = str_replace(":{$placeholder}", $escapedValue, $result);
} else {
Log::warning("Placeholder value not found", [
'placeholder' => $placeholder,
'available_fields' => array_keys($this->weldLogData)
]);
}
}
return $result;
}
/**
* Format SQL results to standard structure
*/
private function formatResults(array $results): array
{
$identifierField = $this->document['identifier_field'] ?? 'identifier';
$dateField = $this->document['date_field'] ?? 'document_date';
$formatted = [];
foreach ($results as $result) {
$recordArray = (array) $result;
$identifier = $recordArray[$identifierField] ?? $recordArray['id'] ?? 'unknown';
$date = $recordArray[$dateField] ?? '';
$formatted[] = [
'identifier' => $identifier,
'document_date' => $date,
'data' => $recordArray
];
}
return $formatted;
}
/**
* Generate file search pattern with field replacements
*/
private function generateFileSearchPattern(array $recordData): string
{
$pattern = $this->document['file_search_pattern'] ?? '*{identifier}*.pdf';
// Replace {field_name} with actual values
foreach ($recordData as $key => $value) {
$pattern = str_replace("{{$key}}", $value, $pattern);
}
return $pattern;
}
/**
* Generate row title with pattern
*/
private function generateRowTitle(array $recordData): string
{
$pattern = $this->document['title4_pattern'] ?? '{identifier}';
// Replace {field_name} with actual values
foreach ($recordData as $key => $value) {
$pattern = str_replace("{{$key}}", $value, $pattern);
}
return $pattern;
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* Generic processor for document types that don't have specific processors
*/
class GenericDocumentProcessor extends AbstractDocumentProcessor
{
/**
* Process generic document
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing generic document: {$document['type']}");
$lineIdentifier = $weldLogData[$this->registerColumnBased];
$normalized = $this->normalizeSearchTerm($lineIdentifier);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
$documentDate = $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$newRow = $this->addRowToExcel(
$search,
$lineIdentifier,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
return $currentRow;
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
class IncomingControlProcessor extends AbstractDocumentProcessor
{
public function process(array $weldLogData, array $document, Worksheet $sheet, int &$currentRow, array $settings): int
{
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$project = $weldLogData['line_number'] ?? '';
if (empty($project)) return $currentRow;
$incomingControls = db("incoming_controls")->where("project", $project)->groupBy("certificate_no", "description_ru")->get()->toArray();
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
// Sort by certificate_date (oldest first) - reverse insertion order
usort($incomingControls, function($a, $b) use ($placeholderReplacer, $weldLogData) {
$a = (object) $a;
$b = (object) $b;
$dateA = $a->certificate_date ?? $a->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$dateB = $b->certificate_date ?? $b->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$timestampA = !empty($dateA) ? strtotime($dateA) : 0;
$timestampB = !empty($dateB) ? strtotime($dateB) : 0;
return $timestampA <=> $timestampB;
});
foreach ($incomingControls as $control) {
$control = (object) $control;
if (empty($control->certificate_no)) continue;
$normalized = $this->normalizeSearchTerm($control->certificate_no);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$document['title2'] = $control->description_ru;
$document['incoming_control_description'] = $control->description_ru;
$document['title3'] = $control->certificate_no;
$this->document = $document;
$documentDate = $control->certificate_date ?? $control->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$newRow = $this->addRowToExcel($search, $control->certificate_no, $documentDate);
if ($newRow > $currentRow) { $currentRow = $newRow; }
}
return $currentRow;
}
}
@@ -0,0 +1,113 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class MaterialsProcessor extends AbstractDocumentProcessor
{
/**
* Process materials documents
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing Materials");
// Get all certificates from same line
$allRecords = db("weld_logs")
->where($this->registerColumnBased, $weldLogData['line_number'])
->get();
$uniqueCertificates = [];
foreach ($allRecords as $record) {
if (!empty($record->certificate_number_of_1) && !in_array($record->certificate_number_of_1, $uniqueCertificates)) {
$uniqueCertificates[] = $record->certificate_number_of_1;
}
if (!empty($record->certificate_number_of_2) && !in_array($record->certificate_number_of_2, $uniqueCertificates)) {
$uniqueCertificates[] = $record->certificate_number_of_2;
}
}
$this->log("Found " . count($uniqueCertificates) . " unique certificates");
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
// Prepare certificates with dates for sorting
$certificatesWithDates = [];
foreach ($uniqueCertificates as $certificateNumber) {
$incomingControl = db("incoming_controls")
->where("certificate_no", $certificateNumber)
->first();
$documentDate = $incomingControl->certificate_date ?? $incomingControl->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$certificatesWithDates[] = [
'certificate_no' => $certificateNumber,
'incoming_control' => $incomingControl,
'date' => $documentDate
];
}
// Sort by date (oldest first) - reverse insertion order
usort($certificatesWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
$this->log("Certificates sorted by date (oldest first for reverse insertion)");
foreach ($certificatesWithDates as $certData) {
$certificateNumber = $certData['certificate_no'];
$incomingControl = $certData['incoming_control'];
$documentDate = $certData['date'];
try {
if ($incomingControl) {
$document['title2'] = $incomingControl->description_ru;
$document['incoming_control_description'] = $incomingControl->description_ru;
$document['file_name'] = $certificateNumber;
} else {
$document['title2'] = "-";
$document['incoming_control_description'] = "-";
$document['file_name'] = $certificateNumber;
}
$normalized = $this->normalizeSearchTerm($certificateNumber);
$search = $this->searchFiles("{$document['path']}/*{$normalized}*.pdf");
// Update document reference for current iteration
$this->document = $document;
$newRow = $this->addRowToExcel(
$search,
$certificateNumber,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing certificate: {$certificateNumber} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class NaksConsumablesCertificateProcessor extends AbstractDocumentProcessor
{
public function process(array $weldLogData, array $document, Worksheet $sheet, int &$currentRow, array $settings): int
{
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$certificates = array_filter([
$weldLogData['welding_materials_1_certificate_no'] ?? '',
$weldLogData['welding_materials_2_certificate_no'] ?? '',
$weldLogData['welding_materials_3_certificate_no'] ?? ''
]);
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
foreach ($certificates as $certNo) {
$normalized = $this->normalizeSearchTerm($certNo);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$document['title2'] = $certNo;
$this->document = $document;
$documentDate = $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$newRow = $this->addRowToExcel($search, $certNo, $documentDate);
if ($newRow > $currentRow) { $currentRow = $newRow; }
}
return $currentRow;
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class NaksConsumablesInspectionProcessor extends AbstractDocumentProcessor
{
public function process(array $weldLogData, array $document, Worksheet $sheet, int &$currentRow, array $settings): int
{
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$lotNumbers = array_filter([
$weldLogData['welding_materials_1_lot_no'] ?? '',
$weldLogData['welding_materials_2_lot_no'] ?? '',
$weldLogData['welding_materials_3_lot_no'] ?? ''
]);
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
foreach ($lotNumbers as $lotNo) {
$normalized = $this->normalizeSearchTerm($lotNo);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$document['title2'] = $lotNo;
$this->document = $document;
$documentDate = $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$newRow = $this->addRowToExcel($search, $lotNo, $documentDate);
if ($newRow > $currentRow) { $currentRow = $newRow; }
}
return $currentRow;
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class PrikazProcessor extends AbstractDocumentProcessor
{
public function process(array $weldLogData, array $document, Worksheet $sheet, int &$currentRow, array $settings): int
{
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$project = $weldLogData['project'] ?? '';
if (empty($project)) return $currentRow;
$workPermitDocs = db("work_permit_documents")->where("zone", "like", "%" . $project . "%")->get()->toArray();
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
// Sort by issue_date (oldest first) - reverse insertion order
usort($workPermitDocs, function($a, $b) use ($placeholderReplacer, $weldLogData) {
$a = (object) $a;
$b = (object) $b;
$dateA = $a->issue_date ?? $a->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$dateB = $b->issue_date ?? $b->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$timestampA = !empty($dateA) ? strtotime($dateA) : 0;
$timestampB = !empty($dateB) ? strtotime($dateB) : 0;
return $timestampA <=> $timestampB;
});
foreach ($workPermitDocs as $doc) {
$doc = (object) $doc;
$normalized = $this->normalizeSearchTerm($doc->document_number);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$document['title2'] = $doc->document_number;
$document['title3'] = $doc->title ?? $doc->document_number;
$this->document = $document;
$documentDate = $doc->issue_date ?? $doc->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$newRow = $this->addRowToExcel($search, $doc->document_number, $documentDate);
if ($newRow > $currentRow) { $currentRow = $newRow; }
}
return $currentRow;
}
}
@@ -0,0 +1,209 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
class QaDocumentProcessor extends AbstractDocumentProcessor
{
/**
* Process QA type documents (NDT reports, procedures, etc.)
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing QA document: {$document['path']}");
// Get all joints with same line number
$allJoints = apply_welded_filter(
db("weld_logs")->where(
$this->registerColumnBased,
$this->weldLogData[$this->registerColumnBased]
)
)->get();
$this->log("Found {$allJoints->count()} joints for processing");
// Check if this is a procedure document
if (strpos($document['path'], "Procedure") !== false) {
return $this->processProcedureDocuments($currentRow);
}
// Process NDT reports
return $this->processNdtReports($allJoints, $currentRow);
}
/**
* Process NDT (Non-Destructive Testing) reports
*/
private function processNdtReports($allJoints, int &$currentRow): int
{
$allReports = [];
$uniqueReports = [];
$duplicateCount = 0;
// LAYER 1: Get reports from weld_logs (synced data)
$this->log("Layer 1: Extracting reports from weld_logs");
foreach ($allJoints as $joint) {
$jointArray = (array) $joint;
$logTypes = array_keys(log_test_types());
foreach ($logTypes as $logType) {
try {
if (strpos(strtolower($this->document['path']), $logType) !== false) {
$reportNoPrefix = $logType . "_report";
if ($logType == "pmi") {
$reportNoPrefix = "no_of_testing_report";
}
$reportNo = $jointArray[$reportNoPrefix] ?? '';
if (!empty($reportNo) && !in_array($reportNo, $uniqueReports)) {
$uniqueReports[] = $reportNo;
$allReports[] = [
'report_no' => $reportNo,
'document_date' => $jointArray[$logType . '_test_date'] ?? '',
'log_type' => $logType,
'weld_log_array' => $jointArray
];
$this->log("Layer 1 - Report added: {$reportNo} ({$logType})");
} else if (!empty($reportNo)) {
$duplicateCount++;
}
}
} catch (\Throwable $th) {
Log::error("Error processing test type: {$logType}", [
'error' => $th->getMessage()
]);
continue;
}
}
}
$this->log("Layer 1 complete: " . count($allReports) . " unique reports from weld_logs");
// Sort reports by joint number (A-Z)
// Note: Excel rows are inserted in reverse (insertNewRowBefore),
// so A-Z order becomes Z-A in Excel (correct order)
usort($allReports, function($a, $b) {
$jointNoA = $a['weld_log_array']['no_of_the_joint_as_per_as_built_survey'] ?? '';
$jointNoB = $b['weld_log_array']['no_of_the_joint_as_per_as_built_survey'] ?? '';
// Check if joint numbers are empty - throw error
if (empty($jointNoA)) {
throw new \Exception("Joint number is empty for report: {$a['report_no']}");
}
if (empty($jointNoB)) {
throw new \Exception("Joint number is empty for report: {$b['report_no']}");
}
// Sort by joint number (natural/numeric order: 1, 2, 3, 10, 11)
return strnatcmp($jointNoA, $jointNoB);
});
$this->log("Total " . count($allReports) . " unique reports sorted by joint number (A-Z)");
$this->log("Duplicate reports skipped: {$duplicateCount}");
// Process sorted reports
foreach ($allReports as $reportData) {
try {
$normalizedReportNo = $this->normalizeSearchTerm($reportData['report_no']);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalizedReportNo}*.pdf");
$this->document['title2'] = $reportData['report_no'];
// Get translation safely - handle array return
$translationKey = $reportData['log_type'] . "_register_title";
$translatedValue = e2($translationKey);
// Ensure we have a string, not an array
if (is_array($translatedValue)) {
$this->document['title4'] = $translationKey; // Use key as fallback
Log::warning("Translation returned array for key: {$translationKey}, using key as fallback");
} else {
$this->document['title4'] = (string) $translatedValue;
}
$lineNumber = $reportData['report_no'];
$newRow = $this->addRowToExcel(
$search,
$lineNumber,
$reportData['document_date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing report: {$reportData['report_no']} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process procedure documents
*/
private function processProcedureDocuments(int &$currentRow): int
{
$this->log("Processing Document Procedure");
$documentProcedures = [];
if (!empty($this->document['title2'])) {
$customCertificates = array_map('trim', explode(",", $this->document['title2']));
$documentProcedures = $customCertificates;
}
foreach ($documentProcedures as $procedureNo) {
try {
$procedure = db("document_procedures")
->where("document_no", $procedureNo)
->first();
if ($procedure) {
$this->log("Procedure found: {$procedure->document_no}");
$normalizedDocNo = $this->normalizeSearchTerm($procedure->document_no);
$searchPath = "storage/documents/{$this->document['path']}/*{$normalizedDocNo}*.pdf";
$search = $this->searchFiles($searchPath);
$documentDate = $procedure->publish_date ?? '';
$lineNumber = $procedure->document_no;
$this->document['title2'] = $procedure->document_no;
$newRow = $this->addRowToExcel(
$search,
$lineNumber,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} else {
$this->log("Procedure not found: {$procedureNo}", 'warning');
}
} catch (\Throwable $th) {
$this->log("Error processing procedure: {$procedureNo} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
}
@@ -0,0 +1,152 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class TemplateProcessor extends AbstractDocumentProcessor
{
/**
* Process template type documents
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing Template document: {$document['path']}");
$lineIdentifier = $weldLogData[$this->registerColumnBased];
// Search by line number
$normalized1 = $this->normalizeSearchTerm($lineIdentifier);
$search1 = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized1}*.pdf");
// Search by test package numbers
$testPackageNumbers = db("weld_logs")
->where($this->registerColumnBased, $lineIdentifier)
->whereNotNull("test_package_no")
->where("test_package_no", "!=", "")
->distinct()
->pluck("test_package_no")
->toArray();
$this->log("Found " . count($testPackageNumbers) . " test package numbers for search");
$search2 = [];
foreach ($testPackageNumbers as $testPackageNo) {
$normalized2 = $this->normalizeSearchTerm($testPackageNo);
$result = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized2}*.pdf");
if (!empty($result)) {
$search2 = array_merge($search2, $result);
}
}
// Merge and remove duplicates
$allFiles = array_unique(array_merge($search1, $search2));
$this->log("Total files found: " . count($allFiles));
if (empty($allFiles)) {
$this->log("No template files found", 'warning');
return $currentRow;
}
// Sort files by date extracted from filename (oldest first)
// Note: Excel rows are inserted in reverse (insertNewRowBefore),
// so oldest first becomes newest last in Excel (correct order)
$self = $this;
usort($allFiles, function($a, $b) use ($self) {
$dateA = strtotime($self->extractDateFromFilename(basename($a))) ?: 0;
$dateB = strtotime($self->extractDateFromFilename(basename($b))) ?: 0;
// Sort ascending (oldest first)
return $dateA <=> $dateB;
});
$this->log("Files sorted by date (oldest first for reverse insertion)");
foreach ($allFiles as $filePath) {
try {
$fileName = basename($filePath);
$documentDate = $this->extractDateFromFilename($fileName);
// Process single file
$templateFileName = str_replace(".pdf", "", $fileName);
$lineNumber = $templateFileName;
$document['title2'] = $templateFileName;
// Remove date from filename
$templateFileName = preg_replace('/_\d{2}\.\d{2}\.\d{4}/', '', $templateFileName);
$templateFileName = preg_replace('/_\d{4}-\d{2}-\d{2}/', '', $templateFileName);
$document['templateFileName'] = $templateFileName;
// Special handling for Incoming_Control
if ($document['id'] == "Incoming_Control") {
$incomingControl = db("incoming_controls")
->where("certificate_no", $lineNumber)
->first();
if ($incomingControl) {
$document['title2'] = $incomingControl->description_ru;
$this->log("Incoming Control found: {$document['title2']}");
}
}
// Update document reference for current iteration
$this->document = $document;
$newRow = $this->addRowToExcel(
[$filePath],
$lineNumber,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing template file: " . basename($filePath) . " - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Extract date from filename
*/
private function extractDateFromFilename(string $fileName): string
{
// Try YYYY-MM-DD format
if (preg_match('/(\d{4}-\d{2}-\d{2})/', $fileName, $matches)) {
return $matches[1];
}
// Try DD.MM.YYYY format
if (preg_match('/(\d{2}\.\d{2}\.\d{4})/', $fileName, $matches)) {
try {
return Carbon::createFromFormat('d.m.Y', $matches[1])->format('Y-m-d');
} catch (\Throwable $th) {
Log::debug("Could not parse date: {$matches[1]}");
}
}
// Default to latest date
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
return $placeholderReplacer->getLatestDate($this->weldLogData, $this->registerColumnBased);
}
}
@@ -0,0 +1,459 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
class WdbDocumentProcessor extends AbstractDocumentProcessor
{
/**
* Process WDB (Welding Database) type documents
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing WDB document: {$document['path']}");
$path = $document['path'];
$wpsData = $settings['wps_data'] ?? null;
$startRowNo = $settings['row_no'] ?? 1;
// Process based on sub-type
if (strpos($path, "Naks Technology") !== false) {
return $this->processNaksTechnology($currentRow, $wpsData, $startRowNo);
}
if (strpos($path, "Naks_Welder") !== false) {
return $this->processNaksWelder($currentRow, $startRowNo);
}
if (strpos($path, "Naks_Equipments") !== false) {
return $this->processNaksEquipments($currentRow, $startRowNo);
}
if (strpos($path, "Naks_Consumables") !== false) {
return $this->processNaksConsumables($currentRow, $startRowNo);
}
if (strpos($path, "Welding Experts") !== false) {
return $this->processWeldingExperts($currentRow, $startRowNo);
}
if (strpos($path, "WPQ") !== false) {
return $this->processWpq($currentRow, $startRowNo);
}
if (strpos($path, "WPS") !== false) {
return $this->processWps($currentRow, $wpsData, $startRowNo);
}
if (strpos($path, "PQR") !== false) {
return $this->processPqr($currentRow, $wpsData, $startRowNo);
}
return $currentRow;
}
/**
* Process Naks Technology certificates
*/
private function processNaksTechnology(int &$currentRow, $wpsData, int $startRowNo = 1): int
{
if (!$wpsData) {
$this->log("WPS data not found, skipping Naks Technology", 'warning');
return $currentRow;
}
$naksCertificates = explode(" + ", $wpsData->naks_certificate_no);
$this->log("Processing " . count($naksCertificates) . " Naks Technology certificates");
foreach ($naksCertificates as $naksCertificate) {
try {
$normalized = $this->normalizeSearchTerm($naksCertificate);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title3'] = $naksCertificate;
$newRow = $this->addRowToExcel(
$search,
$naksCertificate,
$wpsData->date ?? ''
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing Naks certificate: {$naksCertificate} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process Naks Welder certificates
*/
private function processNaksWelder(int &$currentRow, int $startRowNo = 1): int
{
$this->log("Processing Naks Welders");
$weldersData = apply_welded_filter(
db("weld_logs")
->select("certificate_no_1", "certificate_no_2")
->where($this->registerColumnBased, $this->weldLogData[$this->registerColumnBased])
)->get();
$naksWelders = [];
foreach ($weldersData as $welderData) {
if (!empty($welderData->certificate_no_1) && !in_array($welderData->certificate_no_1, $naksWelders)) {
$naksWelders[] = $welderData->certificate_no_1;
}
if (!empty($welderData->certificate_no_2) && !in_array($welderData->certificate_no_2, $naksWelders)) {
$naksWelders[] = $welderData->certificate_no_2;
}
}
$this->log("Found " . count($naksWelders) . " welder certificates");
// Prepare welders with their dates for sorting
$weldersWithDates = [];
foreach ($naksWelders as $naksWelder) {
$welderInfo = db("naks_welders")->where("naks_certificate_no", $naksWelder)->first();
$weldersWithDates[] = [
'certificate_no' => $naksWelder,
'date' => $welderInfo->period_of_validity ?? ''
];
}
// Sort by date (oldest first) - reverse insertion order
usort($weldersWithDates, function($a, $b) {
$dateA = !empty($a['date']) ? strtotime($a['date']) : 0;
$dateB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $dateA <=> $dateB;
});
$this->log("Welders sorted by date (oldest first for reverse insertion)");
foreach ($weldersWithDates as $welderData) {
try {
$naksWelder = $welderData['certificate_no'];
$normalized = $this->normalizeSearchTerm($naksWelder);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title2'] = $naksWelder;
$newRow = $this->addRowToExcel(
$search,
$naksWelder,
$welderData['date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing welder: {$naksWelder} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process Naks Equipment certificates
*/
private function processNaksEquipments(int &$currentRow, int $startRowNo = 1): int
{
$this->log("Processing Naks Equipments");
$certificates = [];
if (!empty($this->document['title2'])) {
$certificates = array_map('trim', explode(",", $this->document['title2']));
}
// Prepare equipments with dates for sorting
$equipmentsWithDates = [];
foreach ($certificates as $certificate) {
$equipmentInfo = db("welding_equipment")->where("attestation", $certificate)->first();
$equipmentsWithDates[] = [
'certificate' => $certificate,
'date' => $equipmentInfo->valid_until ?? ''
];
}
// Sort by valid_until date (oldest first) - reverse insertion order
usort($equipmentsWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
$this->log("Equipments sorted by date (oldest first for reverse insertion)");
foreach ($equipmentsWithDates as $equipData) {
try {
$certificate = $equipData['certificate'];
$normalized = $this->normalizeSearchTerm($certificate);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title2'] = $certificate;
$this->document['title3'] = $certificate;
$newRow = $this->addRowToExcel(
$search,
$certificate,
$equipData['date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing equipment: {$certificate} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process Naks Consumables
*/
private function processNaksConsumables(int &$currentRow, int $startRowNo = 1): int
{
$this->log("Processing Naks Consumables");
$consumables = [];
if (!empty($this->document['title2'])) {
$consumables = array_map('trim', explode(",", $this->document['title2']));
}
// Prepare consumables with dates for sorting
$consumablesWithDates = [];
foreach ($consumables as $consumable) {
$consumableInfo = db("naks_consumables")->where("naks_certificate_no", $consumable)->first();
$consumablesWithDates[] = [
'consumable' => $consumable,
'date' => $consumableInfo->certificate_date ?? ''
];
}
// Sort by certificate_date (oldest first) - reverse insertion order
usort($consumablesWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
$this->log("Consumables sorted by date (oldest first for reverse insertion)");
foreach ($consumablesWithDates as $consData) {
try {
$consumable = $consData['consumable'];
$normalized = $this->normalizeSearchTerm($consumable);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title2'] = $consumable;
$newRow = $this->addRowToExcel(
$search,
$consumable,
$consData['date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing consumable: {$consumable} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process Welding Experts
*/
private function processWeldingExperts(int &$currentRow, int $startRowNo = 1): int
{
$this->log("Processing Welding Experts");
$certificates = array_map('trim', explode(',', $this->document['title2'] ?? ''));
// Prepare experts with dates for sorting
$expertsWithDates = [];
foreach ($certificates as $certificate) {
$expertInfo = db("register_of_experts")->where("certificate_no", $certificate)->first();
$expertsWithDates[] = [
'certificate' => $certificate,
'date' => $expertInfo->expration_of_the_certificate ?? ''
];
}
// Sort by expiration date (oldest first) - reverse insertion order
usort($expertsWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
$this->log("Experts sorted by date (oldest first for reverse insertion)");
foreach ($expertsWithDates as $expertData) {
try {
$certificate = $expertData['certificate'];
$normalized = $this->normalizeSearchTerm($certificate);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title2'] = $certificate;
$newRow = $this->addRowToExcel(
$search,
$certificate,
$expertData['date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing expert: {$certificate} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process WPQ documents
*/
private function processWpq(int &$currentRow, int $startRowNo = 1): int
{
$this->log("Processing WPQ");
$wpqs = db("welder_tests")->whereIn("wpq_document_no", [
$this->weldLogData['wpq_report_1'] ?? '',
$this->weldLogData['wpq_report_2'] ?? '',
])->get()->toArray();
$this->log("Found " . count($wpqs) . " WPQ documents");
// Sort by naks_validity date (oldest first) - reverse insertion order
usort($wpqs, function($a, $b) {
$a = (object) $a;
$b = (object) $b;
$timestampA = !empty($a->naks_validity) ? strtotime($a->naks_validity) : 0;
$timestampB = !empty($b->naks_validity) ? strtotime($b->naks_validity) : 0;
return $timestampA <=> $timestampB;
});
$this->log("WPQ documents sorted by date (oldest first for reverse insertion)");
foreach ($wpqs as $wpq) {
$wpq = (object) $wpq;
try {
$normalized = $this->normalizeSearchTerm($wpq->wpq_document_no);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title3'] = $wpq->wpq_document_no;
$newRow = $this->addRowToExcel(
$search,
$wpq->wpq_document_no,
$wpq->naks_validity ?? ''
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing WPQ: {$wpq->wpq_document_no} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process WPS documents
*/
private function processWps(int &$currentRow, $wpsData, int $startRowNo = 1): int
{
if (!$wpsData) {
$this->log("WPS data not found", 'warning');
return $currentRow;
}
$this->log("Processing WPS: {$wpsData->details}");
$normalized = $this->normalizeSearchTerm($wpsData->details);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title3'] = $wpsData->details;
$newRow = $this->addRowToExcel(
$search,
$wpsData->details,
$wpsData->date ?? ''
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
return $currentRow;
}
/**
* Process PQR documents
*/
private function processPqr(int &$currentRow, $wpsData, int $startRowNo = 1): int
{
if (!$wpsData) {
$this->log("WPS data not found, cannot process PQR", 'warning');
return $currentRow;
}
$this->log("Processing PQR");
$pqr = db("prosedure_qualification_records")->where("pqr_no", $wpsData->pqr_no)->first();
if ($pqr) {
$this->log("PQR found: {$pqr->pqr_no}");
$normalized = $this->normalizeSearchTerm($pqr->pqr_no);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title2'] = $pqr->pqr_no;
$newRow = $this->addRowToExcel(
$search,
$pqr->pqr_no,
$pqr->approved_date ?? ''
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
}
return $currentRow;
}
}
@@ -0,0 +1,229 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
class WpsNaksTechnologyProcessor extends AbstractDocumentProcessor
{
/**
* Search files using the same algorithm as pdf-db-naks-technology-sync
* This algorithm handles zero-prefix patterns and multiple search strategies
*/
private function searchFilesWithFallback(string $basePath, string $certificateNo): array
{
$files = [];
// Parse certificate number to extract short_number and cert_no parts
// Format examples: АЦСТ-20-01934, АЦСТ-161-00050
$certParts = explode('-', $certificateNo);
if (count($certParts) >= 2) {
$shortNumber = $certParts[0]; // e.g., АЦСТ
$certNumber = isset($certParts[1]) ? $certParts[1] : '';
// If there's a third part, combine with second
if (count($certParts) >= 3) {
$certNumber = $certParts[1]; // e.g., 20 or 161
$thirdPart = $certParts[2]; // e.g., 01934 or 00050
// Generate certificate patterns with different zero prefixes
$certPatterns = [$thirdPart];
// Clean leading zeros and generate additional search patterns
$trimmedCertNo = ltrim($thirdPart, '0');
if ($trimmedCertNo != $thirdPart && $trimmedCertNo != '') {
$certPatterns[] = $trimmedCertNo;
// Add versions with different numbers of leading zeros
for ($i = 1; $i <= 5; $i++) {
$certPatterns[] = str_pad($trimmedCertNo, $i, '0', STR_PAD_LEFT);
}
}
// Try each pattern until we find a match
foreach ($certPatterns as $certPattern) {
// Format: short_number-certNumber-certPattern
$searchData = "*{$shortNumber}-{$certNumber}-{$certPattern}*";
$searchData = str_replace("/", "*", $searchData);
$searchData = str_replace(" ", "*", $searchData);
$searchData = str_replace("\\", "*", $searchData);
$path = "{$basePath}/{$searchData}";
$searchPDF = glob($path);
if (!empty($searchPDF)) {
Log::debug("Found with pattern", [
'pattern' => $searchData,
'files' => count($searchPDF)
]);
return $searchPDF;
}
}
} else {
// Format with only 2 parts: АЦСТ-161
$certPatterns = [$certNumber];
$trimmedCertNo = ltrim($certNumber, '0');
if ($trimmedCertNo != $certNumber && $trimmedCertNo != '') {
$certPatterns[] = $trimmedCertNo;
for ($i = 1; $i <= 5; $i++) {
$certPatterns[] = str_pad($trimmedCertNo, $i, '0', STR_PAD_LEFT);
}
}
foreach ($certPatterns as $certPattern) {
$searchData = "*{$shortNumber}-{$certPattern}*";
$searchData = str_replace("/", "*", $searchData);
$searchData = str_replace(" ", "*", $searchData);
$searchData = str_replace("\\", "*", $searchData);
$path = "{$basePath}/{$searchData}";
$searchPDF = glob($path);
if (!empty($searchPDF)) {
Log::debug("Found with 2-part pattern", [
'pattern' => $searchData,
'files' => count($searchPDF)
]);
return $searchPDF;
}
}
}
// Fallback: Try just the short_number
$searchData = "*{$shortNumber}*";
$searchData = str_replace("/", "*", $searchData);
$searchData = str_replace(" ", "*", $searchData);
$searchData = str_replace("\\", "*", $searchData);
$path = "{$basePath}/{$searchData}";
$searchPDF = glob($path);
if (!empty($searchPDF)) {
Log::debug("Found with short_number fallback", [
'pattern' => $searchData,
'files' => count($searchPDF)
]);
return $searchPDF;
}
}
// Final fallback: Try exact certificate number
$searchData = "*{$certificateNo}*";
$searchData = str_replace("/", "*", $searchData);
$searchData = str_replace(" ", "*", $searchData);
$searchData = str_replace("\\", "*", $searchData);
$path = "{$basePath}/{$searchData}";
$searchPDF = glob($path);
if (!empty($searchPDF)) {
Log::debug("Found with exact match", [
'pattern' => $searchData,
'files' => count($searchPDF)
]);
return $searchPDF;
}
Log::warning("No files found for certificate", [
'certificate' => $certificateNo,
'base_path' => $basePath
]);
return $files;
}
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$wpsData = $settings['wps_data'] ?? null;
if (!$wpsData || empty($wpsData->naks_certificate_no)) {
$this->log("WPS Naks certificate not found", 'warning');
return $currentRow;
}
$naksCertificates = explode(" + ", $wpsData->naks_certificate_no);
// Prepare certificates with dates for sorting
$certificatesWithDates = [];
foreach ($naksCertificates as $naksCertificate) {
$certInfo = db("naks_certificates")->where("certificate_no", $naksCertificate)->first();
$certificatesWithDates[] = [
'certificate_no' => $naksCertificate,
'date' => $certInfo->valid_from ?? ''
];
}
// Sort by valid_from date (oldest first) - reverse insertion order
usort($certificatesWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
$this->log("Certificates sorted by date (oldest first for reverse insertion)");
foreach ($certificatesWithDates as $certData) {
$naksCertificate = $certData['certificate_no'];
try {
Log::debug("Processing Naks certificate: " . $naksCertificate);
// Try multiple search strategies for Cyrillic characters and URL encoding
$search = [];
$basePath = "storage/documents/{$document['path']}";
// Strategy 1: Try with URL decoded path
$decodedPath = urldecode($basePath);
$files = $this->searchFilesWithFallback($decodedPath, $naksCertificate);
if (!empty($files)) {
$search = $files;
Log::debug("Found files with decoded path", ['count' => count($files), 'path' => $decodedPath]);
}
// Strategy 2: Try with original path if first strategy failed
if (empty($search) && $decodedPath !== $basePath) {
$files = $this->searchFilesWithFallback($basePath, $naksCertificate);
if (!empty($files)) {
$search = $files;
Log::debug("Found files with original path", ['count' => count($files), 'path' => $basePath]);
}
}
$document['title2'] = $naksCertificate;
$this->document = $document;
$newRow = $this->addRowToExcel(
$search,
$naksCertificate,
$certData['date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error: {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
}
@@ -0,0 +1,200 @@
<?php
namespace App\Services\RegisterCreator;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
use Exception;
class ExcelHandler
{
private Spreadsheet $spreadsheet;
private Worksheet $sheet;
private string $templatePath;
/**
* Load Excel template
*/
public function loadTemplate(string $templatePath): self
{
$this->templatePath = $templatePath;
$fullPath = storage_path('documents/' . $templatePath);
if (!file_exists($fullPath)) {
throw new Exception("Excel template not found: {$fullPath}");
}
Log::debug('Loading Excel template', ['path' => $fullPath]);
$this->spreadsheet = IOFactory::load($fullPath);
$this->sheet = $this->spreadsheet->getActiveSheet();
Log::debug('Excel template loaded', [
'type' => get_class($this->spreadsheet),
'sheet_name' => $this->sheet->getTitle()
]);
return $this;
}
/**
* Get spreadsheet instance
*/
public function getSpreadsheet(): Spreadsheet
{
return $this->spreadsheet;
}
/**
* Get active sheet
*/
public function getSheet(): Worksheet
{
return $this->sheet;
}
/**
* Replace placeholders in Excel sheet
*/
public function replacePlaceholders(array $replacements): self
{
Log::debug('Replacing placeholders in Excel', [
'count' => count($replacements)
]);
foreach ($this->sheet->getRowIterator() as $row) {
foreach ($row->getCellIterator() as $cell) {
$cellValue = $cell->getValue();
// Handle RichText objects
if ($cellValue instanceof \PhpOffice\PhpSpreadsheet\RichText\RichText) {
$cellValue = $cellValue->getPlainText();
}
// Convert to string for processing
$cellValueStr = (string)$cellValue;
if (!empty($cellValueStr)) {
$originalValue = $cellValueStr;
foreach ($replacements as $placeholder => $replacement) {
if (strpos($cellValueStr, $placeholder) !== false) {
$cellValueStr = str_replace($placeholder, $replacement, $cellValueStr);
}
}
// Only update if value changed
if ($cellValueStr !== $originalValue) {
$cell->setValue($cellValueStr);
Log::debug('Placeholder replaced', [
'cell' => $cell->getCoordinate(),
'original' => $originalValue,
'new' => $cellValueStr
]);
}
}
}
}
return $this;
}
/**
* Save Excel file
*/
public function save(string $outputPath, bool $override = true): string
{
$fullPath = storage_path('documents/' . $outputPath);
// Create directory if not exists
$directory = dirname($fullPath);
if (!file_exists($directory)) {
mkdir($directory, 0777, true);
Log::debug('Created directory', ['path' => $directory]);
}
// Check override setting
if (!$override && file_exists($fullPath)) {
$fullPath = $this->generateUniqueFilename($fullPath);
$outputPath = str_replace(storage_path('documents/'), '', $fullPath);
Log::debug('File exists and override is false, using unique name', [
'path' => $fullPath
]);
}
// Check write permissions
if (!is_writable($directory)) {
throw new Exception("Directory is not writable: {$directory}");
}
Log::debug('Saving Excel file', [
'path' => $fullPath,
'memory_usage' => round(memory_get_usage(true) / 1024 / 1024, 2) . ' MB'
]);
$writer = IOFactory::createWriter($this->spreadsheet, 'Xlsx');
$writer->save($fullPath);
Log::debug('Excel file saved successfully', [
'size' => filesize($fullPath) . ' bytes'
]);
return $outputPath;
}
/**
* Remove template row from sheet
*/
public function removeTemplateRow(int $templateRow): self
{
$this->sheet->removeRow($templateRow);
Log::debug('Template row removed', ['row' => $templateRow]);
return $this;
}
/**
* Generate unique filename if file exists
*/
private function generateUniqueFilename(string $filePath): string
{
$counter = 1;
$pathInfo = pathinfo($filePath);
do {
$newFileName = $pathInfo['dirname'] . '/' .
$pathInfo['filename'] . '_' . $counter . '.' .
$pathInfo['extension'];
$counter++;
} while (file_exists($newFileName));
return $newFileName;
}
/**
* Cleanup resources
*/
public function cleanup(): void
{
if (isset($this->spreadsheet)) {
$this->spreadsheet->disconnectWorksheets();
unset($this->spreadsheet);
gc_collect_cycles();
Log::debug('Excel resources cleaned up');
}
}
/**
* Destructor
*/
public function __destruct()
{
$this->cleanup();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
<?php
namespace App\Services\RegisterCreator;
use Illuminate\Support\Facades\Log;
use Exception;
class PdfConverter
{
/**
* Convert Excel to PDF using xlsx_to_pdf_legacy helper
*/
public function convert(string $excelPath, string $pdfPath, bool $override = true): string
{
$fullExcelPath = storage_path('documents/' . $excelPath);
$fullPdfPath = storage_path('documents/' . $pdfPath);
if (!file_exists($fullExcelPath)) {
throw new Exception("Excel file not found: {$fullExcelPath}");
}
// Prepare PDF filename
$pdfFileName = rtrim($fullPdfPath, '/') . 'Register.pdf';
// Check override setting
if (!$override && file_exists($pdfFileName)) {
$pdfFileName = $this->generateUniquePdfFilename($pdfFileName);
$pdfPath = str_replace(storage_path('documents/'), '', dirname($pdfFileName)) . '/';
Log::debug('PDF exists and override is false, using unique name', [
'path' => $pdfFileName
]);
}
Log::debug('Converting Excel to PDF', [
'excel' => $fullExcelPath,
'pdf_dir' => $fullPdfPath
]);
try {
// Use existing helper function
$result = xlsx_to_pdf_legacy($fullExcelPath, $fullPdfPath);
if (!$result) {
throw new Exception("PDF conversion failed");
}
// Fix file permissions if PDF was created
if (file_exists($pdfFileName)) {
try {
// Change owner to www-data for web access
chown($pdfFileName, 'www-data');
chgrp($pdfFileName, 'www-data');
chmod($pdfFileName, 0644);
} catch (\Throwable $th) {
Log::warning('Could not change PDF file permissions', [
'file' => $pdfFileName,
'error' => $th->getMessage()
]);
}
}
Log::debug('PDF conversion successful', [
'pdf_file' => $pdfFileName,
'exists' => file_exists($pdfFileName)
]);
return $pdfPath;
} catch (\Throwable $th) {
Log::error('PDF conversion error', [
'error' => $th->getMessage(),
'excel' => $fullExcelPath,
'pdf_dir' => $fullPdfPath
]);
throw $th;
}
}
/**
* Generate unique PDF filename if file exists
*/
private function generateUniquePdfFilename(string $filePath): string
{
$counter = 1;
$pathInfo = pathinfo($filePath);
do {
$newFileName = $pathInfo['dirname'] . '/' .
$pathInfo['filename'] . '_' . $counter . '.' .
$pathInfo['extension'];
$counter++;
} while (file_exists($newFileName));
return $newFileName;
}
}
@@ -0,0 +1,100 @@
<?php
namespace App\Services\RegisterCreator;
use Illuminate\Support\Facades\Log;
class PlaceholderReplacer
{
/**
* Prepare replacements array from weld log data
*/
public function prepareReplacements(array $weldLogData, array $additionalData = []): array
{
$replacements = [];
// Add project name
$replacements['{project_name}'] = setting('project_name_ru') ?? '';
// Add all weld log fields
foreach ($weldLogData as $column => $value) {
$replacements['{' . $column . '}'] = $value ?? '';
}
// Add additional data (title placeholders, etc.)
foreach ($additionalData as $key => $value) {
if (!str_starts_with($key, '{')) {
$key = '{' . $key . '}';
}
$replacements[$key] = $value ?? '';
}
Log::debug('Placeholders prepared', [
'count' => count($replacements),
'keys' => array_keys($replacements)
]);
return $replacements;
}
/**
* Get latest test date from weld log
*/
public function getLatestDate(array $weldLogData, string $registerColumnBased): string
{
try {
$latestDateQuery = db("weld_logs")
->where($registerColumnBased, $weldLogData[$registerColumnBased])
->select(
\DB::raw("GREATEST(
IFNULL(vt_test_date, '0000-00-00'),
IFNULL(rt_test_date, '0000-00-00'),
IFNULL(ut_test_date, '0000-00-00'),
IFNULL(pt_test_date, '0000-00-00'),
IFNULL(mt_test_date, '0000-00-00'),
IFNULL(pmi_test_date, '0000-00-00'),
IFNULL(ht_test_date, '0000-00-00'),
IFNULL(pwht_test_date, '0000-00-00'),
IFNULL(ferrite_test_date, '0000-00-00')
) AS latest_date")
)
->first();
// If query returns result, use latest date, otherwise use welding_date
$latestDate = ($latestDateQuery && $latestDateQuery->latest_date != '0000-00-00')
? $latestDateQuery->latest_date
: ($weldLogData['welding_date'] ?? now()->format('Y-m-d'));
Log::debug('Latest date calculated', [
'line' => $weldLogData[$registerColumnBased],
'date' => $latestDate
]);
return $latestDate;
} catch (\Throwable $th) {
Log::error('Error calculating latest date', [
'error' => $th->getMessage()
]);
return $weldLogData['welding_date'] ?? now()->format('Y-m-d');
}
}
}
@@ -0,0 +1,260 @@
<?php
namespace App\Services\RegisterCreator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
class ProgressTracker
{
private string $jobId;
private int $totalSteps;
private int $currentStep = 0;
private int $totalRegisters = 0;
private ?string $lineData = null;
public function __construct(string $jobId, int $totalSteps = 100, int $totalRegisters = 0, ?string $lineData = null)
{
$this->jobId = $jobId;
$this->totalSteps = $totalSteps;
$this->totalRegisters = $totalRegisters;
$this->lineData = $lineData;
// Update queue status to "running" when job starts processing
$this->updateQueueToRunning();
}
/**
* Update progress in cache
*/
public function update(string $description, ?int $customProgress = null, ?int $customCurrent = null): void
{
try {
$progress = $customProgress ?? $this->calculateProgress();
// If customCurrent is provided, use it; otherwise use currentStep
$current = $customCurrent ?? $this->currentStep;
$progressData = [
'total' => $this->totalSteps,
'current' => $current,
'progress' => $progress,
'description' => $description,
'line_data' => $this->lineData,
'updated_at' => now()->toDateTimeString(),
'status' => 'running'
];
Cache::put("register-creator-progress-{$this->jobId}", $progressData, now()->addHours(24));
Log::debug("Progress updated for job {$this->jobId}", [
'line_data' => $this->lineData,
'progress' => $progress,
'current' => $current,
'total' => $this->totalSteps,
'description' => $description
]);
} catch (\Throwable $th) {
// Don't let progress tracking failure stop the job
Log::error("Failed to update progress for job {$this->jobId}", [
'error' => $th->getMessage()
]);
}
}
/**
* Increment current step
*/
public function increment(string $description): void
{
$this->currentStep++;
$this->update($description);
}
/**
* Set current step
*/
public function setStep(int $step, string $description): void
{
$this->currentStep = $step;
$this->update($description);
}
/**
* Mark as completed
*/
public function complete(string $message = 'Completed'): void
{
// Set current to total when completing
$this->update($message, 100, $this->totalSteps);
Log::info("Job {$this->jobId} completed", [
'line_data' => $this->lineData
]);
// Remove from queue after completion
$this->removeFromQueue();
}
/**
* Mark as failed
*/
public function fail(string $error): void
{
Cache::put("register-creator-progress-{$this->jobId}", [
'total' => $this->totalSteps,
'current' => $this->currentStep,
'progress' => $this->calculateProgress(),
'description' => "Error: {$error}",
'line_data' => $this->lineData,
'status' => 'failed',
'updated_at' => now()->toDateTimeString()
], now()->addHours(24));
Log::error("Job {$this->jobId} failed", [
'line_data' => $this->lineData,
'current' => $this->currentStep,
'total' => $this->totalSteps,
'error' => $error
]);
// Remove from queue after failure
$this->removeFromQueue();
}
/**
* Calculate progress percentage
*/
private function calculateProgress(): int
{
if ($this->totalSteps === 0) {
return 0;
}
return min(100, (int) round(($this->currentStep / $this->totalSteps) * 100));
}
/**
* Get current progress
*/
public function get(): ?array
{
return Cache::get("register-creator-progress-{$this->jobId}");
}
/**
* Clear progress from cache
*/
public function clear(): void
{
Cache::forget("register-creator-progress-{$this->jobId}");
}
/**
* Update queue status to running when job starts processing
* Note: Only updates status fields, preserves user and line_identifier from controller
*/
private function updateQueueToRunning(): void
{
$queue = Cache::get('register-creator-queue-2', []);
if (isset($queue[$this->jobId])) {
// Update only status-related fields, preserve user and line_identifier from controller
$queue[$this->jobId]['status'] = 'running';
$queue[$this->jobId]['started_at'] = now()->toDateTimeString();
// Update line_identifier only if we have lineData and it's not already set
if ($this->lineData && empty($queue[$this->jobId]['line_identifier'])) {
$queue[$this->jobId]['line_identifier'] = $this->lineData;
}
// Sync total_registers with totalSteps if available
if ($this->totalSteps > 0) {
$queue[$this->jobId]['total_registers'] = $this->totalSteps;
}
Cache::put('register-creator-queue-2', $queue, now()->addHours(24));
Log::info("Job {$this->jobId} status updated to running", [
'line_data' => $this->lineData,
'total_documents' => $this->totalSteps,
'preserved_user' => isset($queue[$this->jobId]['user']) ? 'yes' : 'no',
'preserved_line_identifier' => $queue[$this->jobId]['line_identifier'] ?? 'none'
]);
} else {
// Fallback: If queue entry doesn't exist, create it
// This shouldn't happen in normal flow, but added for safety
Log::warning("Queue entry not found for job {$this->jobId}, creating new entry");
try {
$user = Auth::user();
} catch (\Throwable $th) {
$user = null;
}
$queue[$this->jobId] = [
'user' => $user,
'total_registers' => $this->totalSteps > 0 ? $this->totalSteps : $this->totalRegisters,
'line_identifier' => $this->lineData,
'started_at' => now()->toDateTimeString(),
'status' => 'running'
];
Cache::put('register-creator-queue-2', $queue, now()->addHours(24));
}
}
/**
* Remove this job from the queue
*/
private function removeFromQueue(): void
{
$queue = Cache::get('register-creator-queue-2', []);
if (isset($queue[$this->jobId])) {
unset($queue[$this->jobId]);
Cache::put('register-creator-queue-2', $queue, now()->addHours(24));
Log::info("Job {$this->jobId} removed from queue", [
'line_data' => $this->lineData
]);
}
}
/**
* Get job ID
*/
public function getJobId(): string
{
return $this->jobId;
}
/**
* Get total registers count
*/
public function getTotalRegisters(): int
{
return $this->totalRegisters;
}
/**
* Get line data
*/
public function getLineData(): ?string
{
return $this->lineData;
}
}
@@ -0,0 +1,517 @@
<?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);
}
}