add PricelistFileLine business logic part2

This commit is contained in:
E98Developer 2026-03-14 07:39:38 +01:00
parent e4c1ea326d
commit a4a3f33082
8 changed files with 470 additions and 54 deletions

View File

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

View File

@ -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),
];
}

View File

@ -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

View File

@ -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(
'<div class="grid grid-cols-1 gap-4">' .
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 = '<i>(üres)</i>';
if ($new === '' || $new === null) $new = '<i>(üres)</i>';
// Á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 "<div class='text-sm border-l-4 border-info-500 pl-3 py-1 bg-info-50/50 dark:bg-info-950/20'>
<div class='font-bold text-gray-700 dark:text-gray-300'>$label</div>
<div class='flex items-center gap-2 mt-1'>
<span class='text-gray-500 line-through'>$old</span>
<span class='text-gray-400'></span>
<span class='text-info-600 font-bold'>$new</span>
</div>
</div>";
})->join('') .
'</div>'
)),
])
->visible(fn ($record) => !empty($record->diff)),
Section::make('Validációs üzenetek')
->schema([
Text::make(fn ($record) => new HtmlString(
'<ul class="list-disc ml-5">' .
collect($record->validation_messages ?? [])->map(fn ($msg) => "<li>$msg</li>")->join('') .
'</ul>'
)),
])
->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(
'<ul class="list-disc ml-5">' .
collect($record->validation_messages ?? [])->map(fn ($msg) => "<li>$msg</li>")->join('') .
'</ul>'
)),
Text::make(fn ($record) => new HtmlString(
'
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/3">Oszlop</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Érték</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-transparent divide-y divide-gray-200 dark:divide-white/10">
' . collect($record->payload ?? [])->map(function ($value, $key) {
$valStr = is_array($value) ? json_encode($value) : (string)$value;
return '<tr>
<td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400 font-medium bg-gray-50/50 dark:bg-white/5">' . e($key) . '</td>
<td class="px-3 py-2 text-sm text-gray-900 dark:text-gray-200 whitespace-normal break-words">' . e($valStr) . '</td>
</tr>';
})->join('') . '
</tbody>
</table>
'
)),
])
->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
<span class="font-bold">%s Ft <span class="%s">%s %s%%</span></span>
</div>',
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([
//

View File

@ -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(),
])
]);
}
}

View File

@ -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')

View File

@ -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([

View File

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