diff --git a/app/Filament/Resources/PricelistFiles/Pages/EditPricelistFile.php b/app/Filament/Resources/PricelistFiles/Pages/EditPricelistFile.php index e48f98f..ccc1bdb 100644 --- a/app/Filament/Resources/PricelistFiles/Pages/EditPricelistFile.php +++ b/app/Filament/Resources/PricelistFiles/Pages/EditPricelistFile.php @@ -2,7 +2,9 @@ namespace App\Filament\Resources\PricelistFiles\Pages; +use App\Enums\PricelistFileStatusEnum; use App\Filament\Resources\PricelistFiles\PricelistFileResource; +use App\Services\PricelistFileProcessService; use Filament\Actions\DeleteAction; use Filament\Actions\ForceDeleteAction; use Filament\Actions\RestoreAction; @@ -22,4 +24,11 @@ protected function getHeaderActions(): array RestoreAction::make(), ]; } + + protected function afterSave(): void + { + if ($this->record->status === PricelistFileStatusEnum::fail) { + app(PricelistFileProcessService::class)->dispatchInitialChain($this->record); + } + } } diff --git a/app/Filament/Resources/PricelistFiles/Pages/ViewPricelistFile.php b/app/Filament/Resources/PricelistFiles/Pages/ViewPricelistFile.php index 4bd41e0..fdc5bc9 100644 --- a/app/Filament/Resources/PricelistFiles/Pages/ViewPricelistFile.php +++ b/app/Filament/Resources/PricelistFiles/Pages/ViewPricelistFile.php @@ -5,6 +5,7 @@ use App\Filament\Resources\PricelistFiles\PricelistFileResource; use Filament\Actions\EditAction; use Filament\Resources\Pages\ViewRecord; +use App\Enums\PricelistFileStatusEnum; class ViewPricelistFile extends ViewRecord { @@ -13,7 +14,8 @@ class ViewPricelistFile extends ViewRecord protected function getHeaderActions(): array { return [ - EditAction::make(), + EditAction::make() + ->visible(fn ($record) => $record->status === PricelistFileStatusEnum::fail), ]; } diff --git a/app/Filament/Resources/PricelistFiles/PricelistFileResource.php b/app/Filament/Resources/PricelistFiles/PricelistFileResource.php index 483e4bf..579e9e9 100644 --- a/app/Filament/Resources/PricelistFiles/PricelistFileResource.php +++ b/app/Filament/Resources/PricelistFiles/PricelistFileResource.php @@ -15,7 +15,7 @@ use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; use Filament\Tables\Table; -use Filament\Support\Enums\MaxWidth; +use Filament\Support\Enums\Width; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; @@ -33,9 +33,9 @@ class PricelistFileResource extends Resource protected static ?string $modelLabel = 'Árlista fájl'; - public static function getMaxWidth(): MaxWidth|string|null + public static function getMaxWidth(): Width|string|null { - return MaxWidth::Full; + return Width::Full; } public static function form(Schema $schema): Schema diff --git a/app/Filament/Resources/PricelistFiles/RelationManagers/LinesRelationManager.php b/app/Filament/Resources/PricelistFiles/RelationManagers/LinesRelationManager.php index 39e923d..4d61518 100644 --- a/app/Filament/Resources/PricelistFiles/RelationManagers/LinesRelationManager.php +++ b/app/Filament/Resources/PricelistFiles/RelationManagers/LinesRelationManager.php @@ -4,18 +4,25 @@ use App\Enums\PricelistFileLineStatusEnum; use App\Enums\PricelistFileStatusEnum; +use App\Services\PriceListService; use Filament\Actions\ViewAction; -use Filament\Forms\Components\KeyValue; -use Filament\Forms\Components\Placeholder; use Filament\Infolists\Components\TextEntry; use Filament\Resources\RelationManagers\RelationManager; +use Filament\Schemas\Components\EmbeddedTable; use Filament\Schemas\Components\Grid; +use Filament\Schemas\Components\RenderHook; use Filament\Schemas\Components\Section; +use Filament\Schemas\Components\Text; use Filament\Schemas\Schema; +use Filament\Support\Facades\FilamentView; use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; +use Filament\Tables\View\TablesRenderHook; +use Filament\View\PanelsRenderHook; +use Filament\Support\Enums\Width; use Illuminate\Support\HtmlString; +use Filament\Schemas\Components\Tabs\Tab; +use Illuminate\Database\Eloquent\Builder; class LinesRelationManager extends RelationManager { @@ -23,37 +30,135 @@ class LinesRelationManager extends RelationManager protected static ?string $title = 'Feldolgozott sorok'; + public function booted() + { + FilamentView::registerRenderHook( + TablesRenderHook::TOOLBAR_START, + fn () => $this->getTabsContentComponent()->container($this->makeSchema())->toHtml(), + scopes: static::class, + ); + } + + public function content(Schema $schema): Schema + { + return $schema + ->components([ + RenderHook::make(PanelsRenderHook::RESOURCE_RELATION_MANAGER_BEFORE), + EmbeddedTable::make(), + RenderHook::make(PanelsRenderHook::RESOURCE_RELATION_MANAGER_AFTER), + ]); + } + + public function getTabs(): array + { + $ownerRecord = $this->getOwnerRecord(); + $counts = $ownerRecord->lines() + ->selectRaw('status, count(*) as count') + ->groupBy('status') + ->pluck('count', 'status'); + + return [ + 'all' => Tab::make('Mind') + ->badge($counts->sum()), + 'hibas' => Tab::make('Hibás') + ->icon(PricelistFileLineStatusEnum::error->icon()) + ->badgeColor(PricelistFileLineStatusEnum::error->color()) + ->badge($counts->get(PricelistFileLineStatusEnum::error->value, 0)) + ->modifyQueryUsing(fn (Builder $query) => $query->where('status', PricelistFileLineStatusEnum::error)), + 'warning' => Tab::make('Figyelmeztetések') + ->icon(PricelistFileLineStatusEnum::warning->icon()) + ->badgeColor(PricelistFileLineStatusEnum::warning->color()) + ->badge($counts->get(PricelistFileLineStatusEnum::warning->value, 0)) + ->modifyQueryUsing(fn (Builder $query) => $query->where('status', PricelistFileLineStatusEnum::warning)), + 'uj' => Tab::make('Új') + ->icon(PricelistFileLineStatusEnum::new_product->icon()) + ->badgeColor(PricelistFileLineStatusEnum::new_product->color()) + ->badge($counts->get(PricelistFileLineStatusEnum::new_product->value, 0)) + ->modifyQueryUsing(fn (Builder $query) => $query->where('status', PricelistFileLineStatusEnum::new_product)), + 'modosult' => Tab::make('Módosult') + ->icon(PricelistFileLineStatusEnum::updated->icon()) + ->badgeColor(PricelistFileLineStatusEnum::updated->color()) + ->badge($counts->get(PricelistFileLineStatusEnum::updated->value, 0)) + ->modifyQueryUsing(fn (Builder $query) => $query->where('status', PricelistFileLineStatusEnum::updated)), + ]; + } + public function form(Schema $schema): Schema { return $schema ->components([ - Grid::make(2) + Grid::make(1) ->schema([ - TextEntry::make('row_number') - ->label('Sor száma'), - TextEntry::make('status') - ->label('Státusz') - ->badge() - ->color(fn (PricelistFileLineStatusEnum $state) => $state->color()) - ->formatStateUsing(fn (PricelistFileLineStatusEnum $state) => $state->label()), - ]), + Section::make('Módosulások') + ->schema([ + Text::make(fn ($record) => new HtmlString( + '
' . + collect($record->diff ?? [])->map(function ($d, $field) { + $old = $d['old_label'] ?? $d['old'] ?? 'N/A'; + $new = $d['new_label'] ?? $d['new'] ?? 'N/A'; + $label = $d['label'] ?? $field; + + if ($old === '' || $old === null) $old = '(üres)'; + if ($new === '' || $new === null) $new = '(üres)'; + + // Ár formázása ha árról van szó + if ($field === 'price') { + $old = number_format((float)($d['old'] ?? 0), 2, ',', ' ') . ' Ft'; + $new = number_format((float)($d['new'] ?? 0), 2, ',', ' ') . ' Ft'; + } + + return "
+
$label
+
+ $old + + $new +
+
"; + })->join('') . + '
' + )), + ]) + ->visible(fn ($record) => !empty($record->diff)), + + Section::make('Validációs üzenetek') + ->schema([ + Text::make(fn ($record) => new HtmlString( + '' + )), + ]) + ->visible(fn ($record) => !empty($record->validation_messages)), + ]) + ->columnSpan(1) + ->visible(fn ($record) => !empty($record->diff) || !empty($record->validation_messages)), + Section::make('Adatok') ->schema([ - KeyValue::make('payload') - ->label('Excel adatok') - ->columnSpanFull(), - ]), - Section::make('Validációs üzenetek') - ->schema([ - Placeholder::make('validation_messages_list') - ->label(false) - ->content(fn ($record) => new HtmlString( - '' - )), + Text::make(fn ($record) => new HtmlString( + ' + + + + + + + + + ' . collect($record->payload ?? [])->map(function ($value, $key) { + $valStr = is_array($value) ? json_encode($value) : (string)$value; + return ' + + + '; + })->join('') . ' + +
OszlopÉrték
' . e($key) . '' . e($valStr) . '
+ ' + )), ]) - ->visible(fn ($record) => !empty($record->validation_messages)), + ->columnSpan(fn ($record) => (empty($record->diff) && empty($record->validation_messages)) ? 2 : 1), ]); } @@ -77,25 +182,41 @@ public function table(Table $table): Table TextColumn::make('product_info') ->label('Termék') - ->description(fn ($record) => $record->payload['Termék megnevezése'] ?? '') - ->getStateUsing(fn ($record) => $record->payload['Szállítói cikkszám'] ?? 'N/A') + ->description(fn ($record) => $record->payload[PriceListService::EXPECTED_HEADERS[4]] ?? '') + ->getStateUsing(fn ($record) => $record->payload[PriceListService::EXPECTED_HEADERS[0]] ?? 'N/A') + ->color(fn ($record) => !empty($record->diff) ? 'info' : null) + ->wrap() ->searchable(query: function ($query, string $search) { - $query->where('payload->Szállítói cikkszám', 'like', "%{$search}%") - ->orWhere('payload->Termék megnevezése', 'like', "%{$search}%"); + $sku = PriceListService::EXPECTED_HEADERS[0]; + $name = PriceListService::EXPECTED_HEADERS[4]; + $query->where("payload->$sku", 'like', "%{$search}%") + ->orWhere("payload->$name", 'like', "%{$search}%"); }), + TextColumn::make('price_diff') ->label('Árváltozás') ->html() ->getStateUsing(function ($record) { - $oldPrice = $record->diff['changes']['price']['old'] ?? null; - $newPrice = $record->payload['Új számlázási nettó ár'] ?? null; + // Biztonságos elérés data_get-tel, mert a diff lehet null + $oldPrice = data_get($record->diff, 'price.old'); - if ($oldPrice === null) { - return number_format((float)$newPrice, 2, ',', ' ') . ' Ft'; + // Az új árat a diff-ből vesszük (már float), vagy a payload-ból (string lehet) + $newPriceRaw = data_get($record->diff, 'price.new') ?? data_get($record->payload, PriceListService::EXPECTED_HEADERS[14]); + + // Normalizálás: szóközök eltávolítása és tizedesvessző cseréje + $newPrice = is_string($newPriceRaw) + ? (float) str_replace([' ', ','], ['', '.'], $newPriceRaw) + : (float) $newPriceRaw; + + // Ha nincs régi ár, vagy az nulla, akkor nincs százalékos számítás + if (empty($oldPrice) || (float)$oldPrice === 0.0) { + return number_format($newPrice, 2, ',', ' ') . ' Ft'; } + $oldPrice = (float) $oldPrice; $diffPercent = (($newPrice / $oldPrice) - 1) * 100; + $colorClass = $diffPercent > 0 ? 'text-danger-600' : ($diffPercent < 0 ? 'text-success-600' : 'text-gray-600'); $icon = $diffPercent > 0 ? '↑' : ($diffPercent < 0 ? '↓' : ''); @@ -105,7 +226,7 @@ public function table(Table $table): Table %s Ft %s %s%% ', number_format($oldPrice, 2, ',', ' '), - number_format((float)$newPrice, 2, ',', ' '), + number_format($newPrice, 2, ',', ' '), $colorClass, $icon, number_format(abs($diffPercent), 1, ',', ' ') @@ -117,20 +238,40 @@ public function table(Table $table): Table ->label('Megjegyzés') ->listWithLineBreaks() ->bulleted() + ->wrap() ->color(fn ($state) => collect($state)->contains(fn ($msg) => str_contains($msg, 'Hiányzó') || str_contains($msg, 'Ismeretlen') || str_contains($msg, 'Érvénytelen')) ? 'danger' : 'info') - ->getStateUsing(fn ($record) => $record->validation_messages ?? []), + ->getStateUsing(function ($record) { + $messages = collect($record->validation_messages ?? [])->values(); + + $diffMessages = collect($record->diff ?? [])->map(function ($d, $field) { + $label = $d['label'] ?? $field; + $old = $d['old_label'] ?? $d['old'] ?? 'N/A'; + $new = $d['new_label'] ?? $d['new'] ?? 'N/A'; + + if ($field === 'price') { + $old = number_format((float)($d['old'] ?? 0), 0, ',', ' ') . ' Ft'; + $new = number_format((float)($d['new'] ?? 0), 0, ',', ' ') . ' Ft'; + } + + return "Módosult $label: $old -> $new"; + })->values(); + + return $messages->merge($diffMessages)->toArray(); + }), ]) ->filters([ - SelectFilter::make('status') - ->label('Státusz') - ->options(collect(PricelistFileLineStatusEnum::cases())->mapWithKeys(fn ($e) => [$e->value => $e->label()])), + // ]) ->headerActions([ // ]) ->actions([ ViewAction::make() - ->slideOver(), + ->slideOver() + ->modalWidth(Width::FourExtraLarge) + ->iconButton() + ->modalHeading(fn ($record) => "Sor száma: #{$record->row_number}") + ->modalDescription(fn ($record) => "Státusz: " . $record->status->label()), ]) ->bulkActions([ // diff --git a/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileForm.php b/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileForm.php index 2e3bfbf..df190ff 100644 --- a/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileForm.php +++ b/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileForm.php @@ -2,6 +2,13 @@ namespace App\Filament\Resources\PricelistFiles\Schemas; +use App\Models\Supplier; +use Filament\Forms\Components\DatePicker; +use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\Textarea; +use Filament\Schemas\Components\Grid; +use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; class PricelistFileForm @@ -10,7 +17,32 @@ public static function configure(Schema $schema): Schema { return $schema ->components([ - // + Section::make('Árlista fájl adatai') + ->schema([ + Grid::make(2) + ->schema([ + Select::make('supplier_id') + ->label('Beszállító') + ->options(Supplier::query()->pluck('name', 'id')) + ->required() + ->searchable(), + DatePicker::make('available_date') + ->label('Életbelépés dátuma') + ->required() + ->native(false) + ->displayFormat('Y.m.d'), + ]), + Textarea::make('note') + ->label('Megjegyzés') + ->rows(3), + FileUpload::make('filename') + ->label('Fájl kiválasztása') + ->required() + ->acceptedFileTypes(['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/csv']) + ->disk('public') + ->directory('pricelist-uploads') + ->preserveFilenames(), + ]) ]); } } diff --git a/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileInfolist.php b/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileInfolist.php index 0413c7a..d2e25b7 100644 --- a/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileInfolist.php +++ b/app/Filament/Resources/PricelistFiles/Schemas/PricelistFileInfolist.php @@ -63,7 +63,7 @@ public static function configure(Schema $schema): Schema TextEntry::make('processing_current_step') ->label('Aktuális folyamat') ->weight('bold') - ->color('info') + ->color(fn ($record) => $record->status === PricelistFileStatusEnum::fail ? 'danger' : 'info') ->formatStateUsing(fn ($state, $record) => "$state ({$record->processing_current_step_percentage}%)"), ]), TextEntry::make('note') diff --git a/app/Filament/Resources/PricelistFiles/Tables/PricelistFilesTable.php b/app/Filament/Resources/PricelistFiles/Tables/PricelistFilesTable.php index ac47b32..0e0e125 100644 --- a/app/Filament/Resources/PricelistFiles/Tables/PricelistFilesTable.php +++ b/app/Filament/Resources/PricelistFiles/Tables/PricelistFilesTable.php @@ -58,7 +58,8 @@ public static function configure(Table $table): Table ]) ->recordActions([ ViewAction::make(), - EditAction::make(), + EditAction::make() + ->visible(fn ($record) => $record->status === PricelistFileStatusEnum::fail), ]) ->toolbarActions([ BulkActionGroup::make([ diff --git a/app/Services/PricelistFileProcessService.php b/app/Services/PricelistFileProcessService.php index 900bd84..fe7efd3 100644 --- a/app/Services/PricelistFileProcessService.php +++ b/app/Services/PricelistFileProcessService.php @@ -21,6 +21,49 @@ class PricelistFileProcessService { + protected const PRODUCT_FIELD_MAP = [ + 'name' => [ + 'header' => PriceListService::EXPECTED_HEADERS[4], // Termék megnevezése + 'type' => 'string' + ], + 'unitValue' => [ + 'header' => PriceListService::EXPECTED_HEADERS[6], // Súly/Űrtartalom (nettó) + 'type' => 'float' + ], + 'productUnit' => [ + 'header' => PriceListService::EXPECTED_HEADERS[7], // Mértékegység + 'type' => 'string' + ], + 'sellerUnit' => [ + 'header' => PriceListService::EXPECTED_HEADERS[9], // Legkisebb eladási egység + 'type' => 'string' + ], + 'unitMultiplier' => [ + 'header' => PriceListService::EXPECTED_HEADERS[10], // Egységszorzó + 'type' => 'float' + ], + 'amountUnit' => [ + 'header' => PriceListService::EXPECTED_HEADERS[11], // Mennyiségi egység + 'type' => 'string' + ], + 'vat' => [ + 'header' => PriceListService::EXPECTED_HEADERS[12], // ÁFA % + 'type' => 'float' + ], + 'hooreycaId' => [ + 'header' => PriceListService::EXPECTED_HEADERS[16], // Fix Hooreyca ID + 'type' => 'string' + ], + 'HooreycaUnit' => [ + 'header' => PriceListService::EXPECTED_HEADERS[17], // Hooreyca mértékegység + 'type' => 'string' + ], + 'HooreycaMultiplier' => [ + 'header' => PriceListService::EXPECTED_HEADERS[18], // Hooreyca mennyiségi szorzó + 'type' => 'float' + ], + ]; + public function __construct( protected PriceListService $priceListService ) {} @@ -455,7 +498,26 @@ public function validate(PricelistFile $pricelistFile): bool $hasData = false; foreach ($expectedHeaders as $index => $label) { - $value = $worksheet->getCellByColumnAndRow($index + 1, $row)->getValue(); + $cell = $worksheet->getCellByColumnAndRow($index + 1, $row); + $value = $cell->getValue(); + + // Ha képlet, próbáljuk kiszámolni a megjelenített értéket + if (str_starts_with((string)$value, "=")) { + try { + // Megpróbáljuk kiszámítani a képletet + $value = $cell->getCalculatedValue(); + } catch (\Exception $e) { + try { + // Ha a számítás nem sikerül (pl. hiányzó külső hivatkozás), + // próbáljuk a fájlban utoljára tárolt értéket (cache) lekérni + $value = $cell->getOldCalculatedValue(); + } catch (\Exception $e2) { + // Ha nincs korábbi érték sem, marad a képlet szövege fallback-ként + \Illuminate\Support\Facades\Log::warning("Képlet számítási hiba a(z) {$row}. sorban: " . $e->getMessage()); + } + } + } + $rowData[$label] = $value; if (!empty(trim((string)$value))) { $hasData = true; @@ -495,6 +557,21 @@ public function validate(PricelistFile $pricelistFile): bool $this->runBusinessValidation($pricelistFile); $duration = round(microtime(true) - $startTime, 2); + + // Ellenőrizzük, vannak-e hibás sorok + $hasErrors = $pricelistFile->lines()->where('status', PricelistFileLineStatusEnum::error)->exists(); + + if ($hasErrors) { + $this->updateStepStatus( + $pricelistFile, + PricelistWorkflowStep::Validation, + 'failed', + "befejeződött, de hibás sorokat találtunk. Kérjük, javítsa a hibákat a továbblépéshez!", + 100 + ); + return false; + } + $this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'completed', "Sikeres validálás ({$processedCount} sor, {$duration} mp).", 100); // A validálás végén átadjuk a lépést a jóváhagyásnak (Approval) @@ -562,6 +639,36 @@ protected function getProducerLookupMap(): array ->toArray(); } + /** + * Gyártó ID -> név mapping + */ + protected function getProducerIdMap(): array + { + return \App\Models\Producer::pluck('name', 'id')->toArray(); + } + + /** + * Termékcsoport ID -> útvonal mapping + */ + protected function getProductGroupIdMap(): array + { + $allGroups = (new \App\Models\ProductGroup)->allWithPath(); + $idMap = []; + + foreach ($allGroups as $group) { + $parentsNames = $group['parentsNames'] ?? []; + + if (isset($group['depth']) && $group['depth'] > 0 && !empty($parentsNames)) { + array_shift($parentsNames); + } + + $pathParts = array_merge($parentsNames, [$group['name']]); + $idMap[$group['id']] = implode(' > ', $pathParts); + } + + return $idMap; + } + /** * Termék lookup tábla felépítése a beszállítóhoz (cikkszám -> id) */ @@ -726,13 +833,59 @@ protected function runBusinessValidation(PricelistFile $pricelistFile): void $producerLookup = $this->getProducerLookupMap(); $productLookup = $this->getProductLookupMap($pricelistFile->supplier_id); - $pricelistFile->lines()->chunk(1000, function ($lines) use ($groupLookup, $producerLookup, $productLookup) { + $producerIdToName = $this->getProducerIdMap(); + $groupIdToPath = $this->getProductGroupIdMap(); + + $pricelistFile->lines()->chunk(1000, function ($lines) use ($groupLookup, $producerLookup, $productLookup, $producerIdToName, $groupIdToPath) { $updates = []; foreach ($lines as $line) { $payload = $line->payload; $validationMessages = []; + // 1. Kötelező mezők ellenőrzése (0-12 és 14) + $mandatoryIndexes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14]; + foreach ($mandatoryIndexes as $mIndex) { + $mLabel = PriceListService::EXPECTED_HEADERS[$mIndex]; + $mValue = $payload[$mLabel] ?? null; + if ($mValue === null || (is_string($mValue) && trim($mValue) === '')) { + // Megfelelő kulcsot választunk, hogy a specifikus validátorok felülírhassák + $mKey = match($mIndex) { + 7 => 'product_unit', + 8 => 'producer', + 9 => 'seller_unit', + 11 => 'amount_unit', + 12 => 'vat', + default => 'mandatory_' . $mIndex + }; + $validationMessages[$mKey] = "Hiányzó kötelező mező: $mLabel"; + } + } + + // 2. Hooreyca specifikus mezők függősége (16, 17, 18, 19) + $hooreycaIndexes = [16, 17, 18, 19]; + $hasAnyHooreyca = false; + $hooreycaData = []; + foreach ($hooreycaIndexes as $hIndex) { + $hLabel = PriceListService::EXPECTED_HEADERS[$hIndex]; + $hValue = $payload[$hLabel] ?? null; + if ($hValue !== null && !(is_string($hValue) && trim($hValue) === '')) { + $hasAnyHooreyca = true; + $hooreycaData[$hIndex] = $hValue; + } else { + $hooreycaData[$hIndex] = null; + } + } + + if ($hasAnyHooreyca) { + foreach ($hooreycaIndexes as $hIndex) { + if ($hooreycaData[$hIndex] === null) { + $hLabel = PriceListService::EXPECTED_HEADERS[$hIndex]; + $validationMessages['hooreyca_' . $hIndex] = "Hiányzó kötelező mező (Hooreyca adatok jelenléte miatt): $hLabel"; + } + } + } + // Termék ID kinyerése (cikkszám alapján) $sku = (string)($payload[PriceListService::EXPECTED_HEADERS[0]] ?? ''); $existingProduct = $productLookup[$sku] ?? null; @@ -788,6 +941,8 @@ protected function runBusinessValidation(PricelistFile $pricelistFile): void } $status = PricelistFileLineStatusEnum::ok; + $diff = []; + if ($hasError) { $status = PricelistFileLineStatusEnum::error; } elseif ($productId === null) { @@ -798,22 +953,97 @@ protected function runBusinessValidation(PricelistFile $pricelistFile): void $isUpdated = false; // 1. Gyártó módosulás + $oldProducerId = (int)($existingProduct['producer_id'] ?? 0); if ($hasNewProducer) { $isUpdated = true; - } elseif ($producerId !== null && (int)($existingProduct['producer_id'] ?? 0) !== (int)$producerId) { + $diff['producer_id'] = [ + 'old' => $oldProducerId, + 'new' => $producerId, + 'old_label' => $producerIdToName[$oldProducerId] ?? ($oldProducerId > 0 ? "Ismeretlen gyártó ($oldProducerId)" : 'N/A'), + 'new_label' => $producerName, + 'label' => PriceListService::EXPECTED_HEADERS[8] + ]; + } elseif ($producerId !== null && $oldProducerId !== (int)$producerId) { $isUpdated = true; - $validationMessages['producer_update'] = "Módosult gyártó"; + $diff['producer_id'] = [ + 'old' => $oldProducerId, + 'new' => (int)$producerId, + 'old_label' => $producerIdToName[$oldProducerId] ?? ($oldProducerId > 0 ? "Ismeretlen gyártó ($oldProducerId)" : 'N/A'), + 'new_label' => $producerIdToName[(int)$producerId] ?? ($producerId > 0 ? "Ismeretlen gyártó ($producerId)" : 'N/A'), + 'label' => PriceListService::EXPECTED_HEADERS[8] + ]; } // 2. Termékcsoport módosulás - if ($groupId !== null && (int)($existingProduct['product_group_id'] ?? 0) !== (int)$groupId) { + $oldGroupId = (int)($existingProduct['product_group_id'] ?? 0); + if ($groupId !== null && $oldGroupId !== (int)$groupId) { $isUpdated = true; - $validationMessages['product_group_update'] = "Módosult termékcsoport"; + $oldPath = $groupIdToPath[$oldGroupId] ?? null; + $newPath = $groupIdToPath[(int)$groupId] ?? null; + + $diff['product_group_id'] = [ + 'old' => $oldGroupId, + 'new' => (int)$groupId, + 'old_label' => $oldPath ?? ($oldGroupId > 0 ? "Ismeretlen csoport ($oldGroupId)" : 'N/A'), + 'new_label' => $newPath ?? ($groupId > 0 ? "Ismeretlen csoport ($groupId)" : 'N/A'), + 'label' => PriceListService::EXPECTED_HEADERS[1] + ]; + } + + // 3. Általános mezők összehasonlítása + foreach (self::PRODUCT_FIELD_MAP as $modelField => $config) { + $excelValue = $payload[$config['header']] ?? null; + $currentValue = $existingProduct[$modelField] ?? null; + + // Típuskonverzió és normalizálás az összehasonlításhoz + if ($config['type'] === 'float') { + $excelValue = is_string($excelValue) + ? (float)str_replace([' ', ','], ['', '.'], $excelValue) + : (float)$excelValue; + + // ÁFA speciális kezelése: 0.05 -> 5 + if ($modelField === 'vat') { + if ($excelValue > 0 && $excelValue <= 1) { + $excelValue *= 100; + } + $excelValue = (float) round($excelValue); + } + + $currentValue = (float)$currentValue; + } + + if ($excelValue != $currentValue) { + $diff[$modelField] = [ + 'old' => $currentValue, + 'new' => $excelValue, + 'label' => $config['header'] + ]; + $isUpdated = true; + } } if ($isUpdated) { $status = PricelistFileLineStatusEnum::updated; } + + // 4. Ár módosulás (mindig bekerül a diff-be ha van változás, de nem feltétlenül váltja ki az 'updated' státuszt, ha az árat külön kezeljük) + // Megjegyzés: Az áráltozás önmagában is lehet 'updated', de sokszor az árlista import lényege az árváltozás. + $oldPriceExcel = $payload[PriceListService::EXPECTED_HEADERS[13]] ?? null; + $newPriceExcel = $payload[PriceListService::EXPECTED_HEADERS[14]] ?? null; + + if ($oldPriceExcel !== null && $newPriceExcel !== null) { + // Szóközök és vesszők egységes kezelése string típus esetén + $oldPrice = (float)str_replace([' ', ','], ['', '.'], (string)$oldPriceExcel); + $newPrice = (float)str_replace([' ', ','], ['', '.'], (string)$newPriceExcel); + + if ($oldPrice != $newPrice) { + $diff['price'] = [ + 'old' => $oldPrice, + 'new' => $newPrice, + 'label' => PriceListService::EXPECTED_HEADERS[14] + ]; + } + } } $updates[] = [ @@ -826,6 +1056,7 @@ protected function runBusinessValidation(PricelistFile $pricelistFile): void 'producer_id' => $producerId, 'payload' => json_encode($line->payload), 'validation_messages' => json_encode($validationMessages), + 'diff' => json_encode($diff), 'created_at' => $line->created_at->toDateTimeString(), 'updated_at' => now()->toDateTimeString(), ]; @@ -836,7 +1067,7 @@ protected function runBusinessValidation(PricelistFile $pricelistFile): void PricelistFileLine::upsert( $updates, ['id'], // Egyedi azonosító - ['status', 'validation_messages', 'product_id', 'product_group_id', 'producer_id', 'updated_at'] // Frissítendő mezők + ['status', 'validation_messages', 'diff', 'product_id', 'product_group_id', 'producer_id', 'updated_at'] // Frissítendő mezők ); } });