add PricelistFileLine business logic part2
This commit is contained in:
parent
e4c1ea326d
commit
a4a3f33082
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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('Adatok')
|
||||
Section::make('Módosulások')
|
||||
->schema([
|
||||
KeyValue::make('payload')
|
||||
->label('Excel adatok')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
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([
|
||||
Placeholder::make('validation_messages_list')
|
||||
->label(false)
|
||||
->content(fn ($record) => new HtmlString(
|
||||
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([
|
||||
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>
|
||||
'
|
||||
)),
|
||||
])
|
||||
->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([
|
||||
//
|
||||
|
||||
@ -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(),
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user