From dd22d93d16ca47b6daebb7dc09f9706de8a3d946 Mon Sep 17 00:00:00 2001 From: E98Developer Date: Sun, 8 Mar 2026 06:46:54 +0100 Subject: [PATCH] add preprocessing workflow and meta data --- .gitignore | 1 + .../Commands/PricelistProcessCommand.php | 2 + app/Enums/PreProcessErrorCode.php | 53 +++ .../Schemas/PricelistFileInfolist.php | 185 ++++++----- app/Models/PricelistFile.php | 1 + app/Services/PriceListService.php | 30 ++ app/Services/PricelistFileProcessService.php | 301 +++++++++++++++++- ...add_file_meta_to_pricelist_files_table.php | 28 ++ 8 files changed, 517 insertions(+), 84 deletions(-) create mode 100644 app/Enums/PreProcessErrorCode.php create mode 100644 database/migrations/2026_03_07_072716_add_file_meta_to_pricelist_files_table.php diff --git a/.gitignore b/.gitignore index b71b1ea..c265e30 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ Homestead.json Homestead.yaml Thumbs.db +/storage/tmp/ diff --git a/app/Console/Commands/PricelistProcessCommand.php b/app/Console/Commands/PricelistProcessCommand.php index b4495c7..a09438e 100644 --- a/app/Console/Commands/PricelistProcessCommand.php +++ b/app/Console/Commands/PricelistProcessCommand.php @@ -36,8 +36,10 @@ public function handle(PricelistFileProcessService $service): int } $this->info("Folyamat elindítása: {$pricelistFile->filename} (ID: {$id})"); + $this->info("Workflow állapotok alaphelyzetbe állítása..."); try { + $service->resetWorkflow($pricelistFile); $service->dispatchInitialChain($pricelistFile); $this->info("Az árlista feldolgozási lánc (Pre-process -> Validation) sikeresen ütemezve."); return 0; diff --git a/app/Enums/PreProcessErrorCode.php b/app/Enums/PreProcessErrorCode.php new file mode 100644 index 0000000..300d027 --- /dev/null +++ b/app/Enums/PreProcessErrorCode.php @@ -0,0 +1,53 @@ + 'A fájl nem található a szerveren.', + self::FILE_CORRUPTED => 'A fájl sérült vagy nem nyitható meg.', + self::FILE_TOO_LARGE => 'A fájl mérete meghaladja a megengedett határt.', + self::FILE_EMPTY => 'A fájl üres.', + self::INVALID_EXTENSION => 'A fájl kiterjesztése nem megfelelő.', + self::NO_WORKSHEET => 'Az Excel fájl nem tartalmaz munkalapot.', + self::COLUMN_COUNT_MISMATCH => 'Az oszlopok száma nem egyezik az elvárt struktúrával.', + self::COLUMN_MISSING => 'Hiányzó oszlop(ok) az elvárt struktúrából.', + self::COLUMN_EXTRA => 'Többlet oszlop(ok) az elvárt struktúrához képest.', + self::HEADER_MISMATCH => 'Egy vagy több oszlopfejléc nem egyezik az elvárttal.', + self::NO_DATA_ROWS => 'A fájl nem tartalmaz feldolgozható adatsort.', + }; + } + + public function severity(): string + { + return match ($this) { + self::FILE_NOT_FOUND, + self::FILE_CORRUPTED, + self::FILE_EMPTY, + self::INVALID_EXTENSION, + self::NO_WORKSHEET, + self::COLUMN_COUNT_MISMATCH, + self::HEADER_MISMATCH, + self::NO_DATA_ROWS => 'blocker', + + self::COLUMN_MISSING => 'error', + self::COLUMN_EXTRA, + self::FILE_TOO_LARGE => 'warning', + }; + } +} diff --git a/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileInfolist.php b/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileInfolist.php index 3c6eb7c..42c6b1a 100644 --- a/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileInfolist.php +++ b/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileInfolist.php @@ -15,89 +15,122 @@ public static function configure(Schema $schema): Schema { return $schema ->components([ - Section::make('Fájl információk') + Section::make(null) ->poll('5s') ->schema([ - Grid::make(3) + Section::make('Fájl információk') ->schema([ - TextEntry::make('filename') - ->label('Fájlnév') - ->icon('heroicon-o-document-text') - ->color('primary'), - TextEntry::make('supplier.name') - ->label('Beszállító') - ->icon('heroicon-o-user'), - TextEntry::make('status') - ->label('Státusz') - ->badge() - ->formatStateUsing(fn (PricelistFileStatusEnum $state): string => $state->label()) - ->color(fn (PricelistFileStatusEnum $state): string => match ($state) { - PricelistFileStatusEnum::todo => 'gray', - PricelistFileStatusEnum::inprogress => 'info', - PricelistFileStatusEnum::done => 'success', - PricelistFileStatusEnum::fail => 'danger', - PricelistFileStatusEnum::waiting_for_approval => 'primary', - PricelistFileStatusEnum::closed => 'warning', - }), + Grid::make(3) + ->schema([ + TextEntry::make('filename') + ->label('Fájlnév') + ->icon('heroicon-o-document-text') + ->color('primary'), + TextEntry::make('supplier.name') + ->label('Beszállító') + ->icon('heroicon-o-user'), + TextEntry::make('status') + ->label('Státusz') + ->badge() + ->formatStateUsing(fn (PricelistFileStatusEnum $state): string => $state->label()) + ->color(fn (PricelistFileStatusEnum $state): string => match ($state) { + PricelistFileStatusEnum::todo => 'gray', + PricelistFileStatusEnum::inprogress => 'info', + PricelistFileStatusEnum::done => 'success', + PricelistFileStatusEnum::fail => 'danger', + PricelistFileStatusEnum::waiting_for_approval => 'primary', + PricelistFileStatusEnum::closed => 'warning', + }), + ]), + Grid::make(3) + ->schema([ + TextEntry::make('available_date') + ->label('Életbelépés dátuma') + ->date('Y.m.d'), + TextEntry::make('created_at') + ->label('Feltöltés időpontja') + ->dateTime('Y.m.d H:i'), + TextEntry::make('processing_current_step') + ->label('Aktuális folyamat') + ->weight('bold') + ->color('info') + ->formatStateUsing(fn ($state, $record) => "$state ({$record->processing_current_step_percentage}%)"), + ]), + TextEntry::make('note') + ->label('Megjegyzés') + ->placeholder('Nincs megjegyzés') + ->columnSpanFull(), ]), - Grid::make(3) - ->schema([ - TextEntry::make('available_date') - ->label('Életbelépés dátuma') - ->date('Y.m.d'), - TextEntry::make('created_at') - ->label('Feltöltés időpontja') - ->dateTime('Y.m.d H:i'), - TextEntry::make('processing_current_step') - ->label('Aktuális folyamat') - ->weight('bold') - ->color('info') - ->formatStateUsing(fn ($state, $record) => "$state ({$record->processing_current_step_percentage}%)"), - ]), - TextEntry::make('note') - ->label('Megjegyzés') - ->placeholder('Nincs megjegyzés') - ->columnSpanFull(), - ]), - Section::make('Feldolgozási folyamat (Workflow)') - ->poll('5s') - ->schema([ - RepeatableEntry::make('workflow_steps') - ->label(false) + Section::make('Előfeldolgozás eredménye') ->schema([ - TextEntry::make('label') - ->label('Lépés') - ->weight('bold') - ->inlineLabel(), - TextEntry::make('status') - ->label('Állapot') - ->badge() - ->inlineLabel() - ->formatStateUsing(fn (string $state): string => match ($state) { - 'pending' => 'várakozik', - 'inprogress' => 'folyamatban', - 'completed' => 'kész', - 'failed' => 'hiba', - default => $state, - }) - ->color(fn (string $state): string => match ($state) { - 'pending' => 'gray', - 'inprogress' => 'info', - 'completed' => 'success', - 'failed' => 'danger', - default => 'gray', - }) - ->icon(fn (string $state): string => match ($state) { - 'pending' => 'heroicon-o-clock', - 'inprogress' => 'heroicon-o-arrow-path', - 'completed' => 'heroicon-o-check-circle', - 'failed' => 'heroicon-o-x-circle', - default => 'heroicon-o-question-mark-circle', - }), + Grid::make(4)->schema([ + TextEntry::make('file_meta.preprocess.statistics.data_rows') + ->label('Adatsorok') + ->icon('heroicon-o-table-cells'), + TextEntry::make('file_meta.preprocess.statistics.column_count') + ->label('Oszlopok száma'), + TextEntry::make('file_meta.preprocess.file_info.file_size_human') + ->label('Fájlméret'), + TextEntry::make('file_meta.preprocess.duration_seconds') + ->label('Feldolgozási idő') + ->suffix(' mp'), + ]), + RepeatableEntry::make('file_meta.preprocess.warnings') + ->label('Figyelmeztetések') + ->schema([ + TextEntry::make('message')->label('Leírás')->color('warning'), + TextEntry::make('severity')->label('Súlyosság')->badge()->color('warning'), + ]) + ->visible(fn ($record) => !empty($record->file_meta['preprocess']['warnings'] ?? [])), + RepeatableEntry::make('file_meta.preprocess.errors') + ->label('Hibák') + ->schema([ + TextEntry::make('message')->label('Leírás')->color('danger'), + TextEntry::make('severity')->label('Súlyosság')->badge()->color('danger'), + ]) + ->visible(fn ($record) => !empty($record->file_meta['preprocess']['errors'] ?? [])), ]) - ->columns(2) - ->columnSpanFull(), + ->visible(fn ($record) => !empty($record->file_meta['preprocess'] ?? [])), + + Section::make('Feldolgozási folyamat (Workflow)') + ->schema([ + RepeatableEntry::make('workflow_steps') + ->label(false) + ->schema([ + TextEntry::make('label') + ->label('Lépés') + ->weight('bold') + ->inlineLabel(), + TextEntry::make('status') + ->label('Állapot') + ->badge() + ->inlineLabel() + ->formatStateUsing(fn (string $state): string => match ($state) { + 'pending' => 'várakozik', + 'inprogress' => 'folyamatban', + 'completed' => 'kész', + 'failed' => 'hiba', + default => $state, + }) + ->color(fn (string $state): string => match ($state) { + 'pending' => 'gray', + 'inprogress' => 'info', + 'completed' => 'success', + 'failed' => 'danger', + default => 'gray', + }) + ->icon(fn (string $state): string => match ($state) { + 'pending' => 'heroicon-o-clock', + 'inprogress' => 'heroicon-o-arrow-path', + 'completed' => 'heroicon-o-check-circle', + 'failed' => 'heroicon-o-x-circle', + default => 'heroicon-o-question-mark-circle', + }), + ]) + ->columns(2) + ->columnSpanFull(), + ]), ]), ]); } diff --git a/app/Models/PricelistFile.php b/app/Models/PricelistFile.php index 0c3a6a6..bca5685 100644 --- a/app/Models/PricelistFile.php +++ b/app/Models/PricelistFile.php @@ -15,6 +15,7 @@ class PricelistFile extends BaseAuditable 'processing_current_step_percentage' => 'integer', 'available_date' => 'date', 'workflow_steps' => 'array', + 'file_meta' => 'array', ]; protected static function booted() diff --git a/app/Services/PriceListService.php b/app/Services/PriceListService.php index a7b0f87..4bef5b4 100644 --- a/app/Services/PriceListService.php +++ b/app/Services/PriceListService.php @@ -78,6 +78,36 @@ public function getError() return $this->error->toArray(); } + public function getExcelFieldPointer(): array + { + return $this->excelFieldPointer; + } + + public const EXPECTED_HEADERS = [ + 0 => 'Szállítói cikkszám', + 1 => 'Fő termékcsoport', + 2 => 'Alcsoport 1', + 3 => 'Alcsoport 2', + 4 => 'Termék megnevezése', + 5 => 'Kiszerelés', + 6 => 'Súly/Űrtartalom (nettó)', + 7 => 'Mértékegység', + 8 => 'Gyártó', + 9 => 'Legkisebb eladási egység', + 10 => 'Egységszorzó', + 11 => 'Mennyiségi egység', + 12 => 'ÁFA %', + 13 => 'Előző számlázási nettó ár/mennyiségi egység', + 14 => 'Új számlázási nettó ár/mennyiségi egység', + 15 => 'Áráltozás %', + 16 => 'Fix Hooreyca ID', + 17 => 'Hooreyca mértékegység', + 18 => 'Hooreyca mennyiségi szorzó', + 19 => 'Vevői megnevezés', + 20 => 'Megjegyzés', + 21 => 'KREL', + ]; + private array $excelFieldPointer = [ 'supplierProductNumber' => 0, 'productGroupMain' => 1, diff --git a/app/Services/PricelistFileProcessService.php b/app/Services/PricelistFileProcessService.php index bbd68f3..358f4ab 100644 --- a/app/Services/PricelistFileProcessService.php +++ b/app/Services/PricelistFileProcessService.php @@ -2,12 +2,17 @@ namespace App\Services; +use App\Enums\PreProcessErrorCode; use App\Enums\PricelistWorkflowStep; use App\Models\PricelistFile; use App\Enums\PricelistFileStatusEnum; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class PricelistFileProcessService { @@ -15,6 +20,20 @@ public function __construct( protected PriceListService $priceListService ) {} + /** + * Alaphelyzetbe állítja a workflow-t és az állapotot az újraindításhoz + */ + public function resetWorkflow(PricelistFile $pricelistFile): void + { + $pricelistFile->update([ + 'workflow_steps' => PricelistWorkflowStep::defaultSteps(), + 'status' => PricelistFileStatusEnum::todo, + 'processing_current_step' => null, + 'processing_current_step_percentage' => null, + 'file_meta' => null, + ]); + } + /** * Elindítja az árlista feldolgozás kezdeti láncát (Pre-processing -> Validation) */ @@ -97,33 +116,299 @@ public function updateStepStatus( $pricelistFile->update($updateData); } + private const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB + private const MIN_FILE_SIZE_BYTES = 1024; // 1 KB + private const ALLOWED_EXTENSIONS = ['xlsx', 'xls']; + /** - * Előfeldolgozás (Pre-processing) - Adatok ellenőrzése és beolvasása + * Előfeldolgozás (Pre-processing) - Strukturális és technikai ellenőrzés * * @param PricelistFile $pricelistFile * @return bool */ public function preProcess(PricelistFile $pricelistFile): bool { - $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Adatok ellenőrzése...', 10); + $startTime = microtime(true); + $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Fájl ellenőrzése...', 10); + + $meta = [ + 'completed_at' => null, + 'duration_seconds' => null, + 'file_info' => [], + 'statistics' => [], + 'errors' => [], + 'warnings' => [], + 'is_processable' => true, + ]; try { - // TODO: Itt hívjuk majd meg a PriceListService-t a fájl beolvasásához - // $data = $this->priceListService->readFromFile($pricelistFile->path); - // $results = $this->priceListService->importPriceListCheck($pricelistFile->supplier_id, $data); + // 1. Fájl fizikai ellenőrzése (10-20%) + $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Fájl fizikai ellenőrzése...', 15); + $fileCheckResult = $this->checkFilePhysical($pricelistFile); + $meta['file_info'] = $fileCheckResult['info']; + $meta['errors'] = array_merge($meta['errors'], $fileCheckResult['errors']); + $meta['warnings'] = array_merge($meta['warnings'], $fileCheckResult['warnings']); - sleep(15); // Szimuláció + if ($this->hasBlockerError($meta['errors'])) { + $meta['is_processable'] = false; + $this->saveMetaAndFail($pricelistFile, $meta, $startTime); + return false; + } - $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'completed', 'Adatok ellenőrizve.', 100); + // 2. Excel megnyitás és munkalap ellenőrzés (20-40%) + $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Munkalap ellenőrzése...', 30); + $fullPath = Storage::disk('public')->path($pricelistFile->filename); + + try { + $spreadsheet = IOFactory::load($fullPath); + $worksheet = $spreadsheet->getActiveSheet(); + $meta['file_info']['sheet_name'] = $worksheet->getTitle(); + } catch (\Exception $e) { + $meta['errors'][] = $this->buildError(PreProcessErrorCode::FILE_CORRUPTED, $e->getMessage()); + $meta['is_processable'] = false; + $this->saveMetaAndFail($pricelistFile, $meta, $startTime); + return false; + } + + // 3. Oszlopstruktúra validálás (40-70%) + $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Oszlopstruktúra ellenőrzése...', 50); + $structureResult = $this->validateColumnStructure($worksheet); + $meta['errors'] = array_merge($meta['errors'], $structureResult['errors']); + $meta['warnings'] = array_merge($meta['warnings'], $structureResult['warnings']); + + if ($this->hasBlockerError($meta['errors'])) { + $meta['is_processable'] = false; + $this->saveMetaAndFail($pricelistFile, $meta, $startTime); + return false; + } + + // 4. Statisztika gyűjtés (70-90%) + $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Statisztikai adatok gyűjtése...', 75); + $meta['statistics'] = $this->collectStatistics($worksheet); + + if ($meta['statistics']['data_rows'] === 0) { + $meta['errors'][] = $this->buildError(PreProcessErrorCode::NO_DATA_ROWS); + $meta['is_processable'] = false; + $this->saveMetaAndFail($pricelistFile, $meta, $startTime); + return false; + } + + // 5. Sikeres befejezés (100%) + $meta['completed_at'] = now()->toDateTimeString(); + $meta['duration_seconds'] = round(microtime(true) - $startTime, 2); + + $pricelistFile->update(['file_meta' => ['preprocess' => $meta]]); + $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'completed', 'Előfeldolgozás sikeres.', 100); return true; + } catch (\Exception $e) { Log::error('Pricelist pre-process error: ' . $e->getMessage()); - $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'failed', $e->getMessage()); + $meta['errors'][] = [ + 'code' => 'UNEXPECTED_ERROR', + 'message' => $e->getMessage(), + 'severity' => 'blocker', + ]; + $meta['is_processable'] = false; + $this->saveMetaAndFail($pricelistFile, $meta, $startTime); throw $e; } } + /** + * Fájl fizikai ellenőrzése (létezés, kiterjesztés, méret) + */ + private function checkFilePhysical(PricelistFile $pricelistFile): array + { + $errors = []; + $warnings = []; + $info = [ + 'original_filename' => $pricelistFile->filename, + 'file_size_bytes' => null, + 'file_size_human' => null, + 'mime_type' => null, + ]; + + $disk = Storage::disk('public'); + + if (!$disk->exists($pricelistFile->filename)) { + $errors[] = $this->buildError(PreProcessErrorCode::FILE_NOT_FOUND); + return ['info' => $info, 'errors' => $errors, 'warnings' => $warnings]; + } + + $fullPath = $disk->path($pricelistFile->filename); + $extension = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION)); + + if (!in_array($extension, self::ALLOWED_EXTENSIONS)) { + $errors[] = $this->buildError( + PreProcessErrorCode::INVALID_EXTENSION, + "Kiterjesztés: '{$extension}', elfogadott: " . implode(', ', self::ALLOWED_EXTENSIONS) + ); + return ['info' => $info, 'errors' => $errors, 'warnings' => $warnings]; + } + + $fileSize = $disk->size($pricelistFile->filename); + $info['file_size_bytes'] = $fileSize; + $info['file_size_human'] = $this->humanFileSize($fileSize); + $info['mime_type'] = $disk->mimeType($pricelistFile->filename); + + if ($fileSize < self::MIN_FILE_SIZE_BYTES) { + $errors[] = $this->buildError(PreProcessErrorCode::FILE_EMPTY, "Fájlméret: {$info['file_size_human']}"); + } + + if ($fileSize > self::MAX_FILE_SIZE_BYTES) { + $warnings[] = $this->buildWarning(PreProcessErrorCode::FILE_TOO_LARGE, "Fájlméret: {$info['file_size_human']}"); + } + + return ['info' => $info, 'errors' => $errors, 'warnings' => $warnings]; + } + + /** + * Oszlopstruktúra és fejléc validálása + */ + private function validateColumnStructure(Worksheet $worksheet): array + { + $errors = []; + $warnings = []; + + $expectedColumnCount = count($this->priceListService->getExcelFieldPointer()); + $highestColumn = $worksheet->getHighestDataColumn(); + $columnCount = Coordinate::columnIndexFromString($highestColumn); + + if ($columnCount < $expectedColumnCount) { + $errors[] = $this->buildError( + PreProcessErrorCode::COLUMN_COUNT_MISMATCH, + "Kevés oszlop ({$columnCount}), elvárt legalább: {$expectedColumnCount}" + ); + } elseif ($columnCount > $expectedColumnCount) { + $warnings[] = $this->buildWarning( + PreProcessErrorCode::COLUMN_EXTRA, + "Többlet oszlopok ({$columnCount}), az első {$expectedColumnCount} kerül feldolgozásra." + ); + } + + // Fejléc ellenőrzés (4. sor a specifikáció szerint) + $headerRow = 4; + $expectedHeaders = PriceListService::EXPECTED_HEADERS; + $checkCount = min($columnCount, count($expectedHeaders)); + + for ($col = 0; $col < $checkCount; $col++) { + $cellValue = trim((string) $worksheet->getCellByColumnAndRow($col + 1, $headerRow)->getValue()); + $expectedValue = $expectedHeaders[$col] ?? null; + + if ($expectedValue !== null && $cellValue !== $expectedValue) { + // Az oszlopnevek eltérése blocker hiba + $errors[] = $this->buildError( + PreProcessErrorCode::HEADER_MISMATCH, + "A(z) " . ($col + 1) . ". oszlop fejléce '{$cellValue}' az elvárt '{$expectedValue}' helyett (a {$headerRow}. sorban)." + ); + } + } + + return ['errors' => $errors, 'warnings' => $warnings]; + } + + /** + * Statisztikai adatok gyűjtése a munkalapról + */ + private function collectStatistics(Worksheet $worksheet): array + { + $highestRow = $worksheet->getHighestDataRow(); + $highestColumn = $worksheet->getHighestDataColumn(); + $columnCount = Coordinate::columnIndexFromString($highestColumn); + + $dataRows = 0; + $emptyRows = 0; + + // A 4. sort fejlécnek tekintjük, az 5. sortól indul az adat + for ($row = 5; $row <= $highestRow; $row++) { + $supplierProductNumber = $worksheet->getCellByColumnAndRow(1, $row)->getValue(); + if (!empty(trim((string) $supplierProductNumber))) { + $dataRows++; + } else { + $emptyRows++; + } + } + + return [ + 'total_rows' => max(0, $highestRow - 4), // 4 sor fejléc/leírás után + 'data_rows' => $dataRows, + 'empty_rows' => $emptyRows, + 'column_count' => $columnCount, + 'expected_column_count' => count($this->priceListService->getExcelFieldPointer()), + ]; + } + + /** + * Hiba rekord összeállítása + */ + private function buildError(PreProcessErrorCode $code, ?string $customMessage = null): array + { + return [ + 'code' => $code->value, + 'message' => $customMessage ?? $code->label(), + 'severity' => $code->severity(), + ]; + } + + /** + * Figyelmeztetés rekord összeállítása + */ + private function buildWarning(PreProcessErrorCode $code, ?string $customMessage = null, array $extra = []): array + { + return array_merge([ + 'code' => $code->value, + 'message' => $customMessage ?? $code->label(), + 'severity' => $code->severity(), + ], $extra); + } + + /** + * Blocker típusú hiba keresése a hibalistában + */ + private function hasBlockerError(array $errors): bool + { + foreach ($errors as $error) { + if (($error['severity'] ?? '') === 'blocker') { + return true; + } + } + return false; + } + + /** + * Meta adatok mentése és a workflow lépés hibásra állítása + */ + private function saveMetaAndFail(PricelistFile $pricelistFile, array $meta, float $startTime): void + { + $meta['completed_at'] = now()->toDateTimeString(); + $meta['duration_seconds'] = round(microtime(true) - $startTime, 2); + + $pricelistFile->update(['file_meta' => ['preprocess' => $meta]]); + + $firstBlocker = collect($meta['errors'])->firstWhere('severity', 'blocker'); + $failMessage = $firstBlocker['message'] ?? 'Előfeldolgozás sikertelen.'; + + $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'failed', $failMessage); + } + + /** + * Fájlméret olvasható formátumban + */ + private function humanFileSize(int $bytes): string + { + $units = ['B', 'KB', 'MB', 'GB']; + $i = 0; + $size = (float) $bytes; + + while ($size >= 1024 && $i < count($units) - 1) { + $size /= 1024; + $i++; + } + + return round($size, 1) . ' ' . $units[$i]; + } + /** * Validálás (Validation) - Üzleti szabályok ellenőrzése * diff --git a/database/migrations/2026_03_07_072716_add_file_meta_to_pricelist_files_table.php b/database/migrations/2026_03_07_072716_add_file_meta_to_pricelist_files_table.php new file mode 100644 index 0000000..9137fd9 --- /dev/null +++ b/database/migrations/2026_03_07_072716_add_file_meta_to_pricelist_files_table.php @@ -0,0 +1,28 @@ +json('file_meta')->nullable()->after('workflow_steps'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('pricelist_files', function (Blueprint $table) { + $table->dropColumn('file_meta'); + }); + } +};