add PricelistFileLine business logic part1
This commit is contained in:
parent
dd22d93d16
commit
e4c1ea326d
@ -51,6 +51,10 @@ ##### Modernizációs és Design Irányelvek (Filament & Tailwind)
|
||||
- A legacy és modern részek közötti váltáshoz sima linkeket használunk (teljes oldalújratöltés).
|
||||
- A navigációs linkeknél a `data-no-ajax="true"` attribútumot kell használni, hogy a legacy JS (`app.js`) ne próbálja AJAX-szal betölteni a modern tartalmat.
|
||||
- **Közös Elemek (Render Hooks)**: A megszokott elemeket (pl. `speedButtonNavBar`) Filamentben Render Hook-ok segítségével injektáljuk a fejlécbe (`TOPBAR_START`), biztosítva a funkcionális folytonosságot.
|
||||
- **Filament 4 és Schemas rendszer**: A projekt egy egyedi Filament 4 alapú architektúrát és a `Filament\Schemas` rendszert használja.
|
||||
- **Komponensek**: Sémák (Infolist/Form) definiálásakor elsődlegesen a `Filament\Schemas\Components` namespace alatti osztályokat (Section, Grid, Text stb.) kell használni a standard Filament komponensek helyett a kompatibilitás és a polling működése érdekében.
|
||||
- **Namespace korrekciók**: Figyeljünk a v3-hoz képest megváltozott namespace-ekre, például a `ViewAction` a `Filament\Actions\ViewAction` alatt érhető el (nem a `Filament\Tables\Actions` vagy `Filament\Infolists\Actions` alatt).
|
||||
- **Dinamikus frissítés (Polling)**: Az adatok automatikus frissítéséhez a `.poll('5s')` hívást a `Schemas\Components\Section` szinten kell alkalmazni a szülő konténeren, amely biztosítja a teljes nézet újrarenderelését.
|
||||
|
||||
##### JavaScript Architektúra és Modulok
|
||||
- A JS kód alapja egy egyedi objektumorientált rendszer.
|
||||
|
||||
45
app/Enums/PricelistFileLineStatusEnum.php
Normal file
45
app/Enums/PricelistFileLineStatusEnum.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PricelistFileLineStatusEnum: string
|
||||
{
|
||||
case ok = 'ok';
|
||||
case warning = 'warning';
|
||||
case error = 'error';
|
||||
case new_product = 'new_product';
|
||||
case updated = 'updated';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ok => 'rendben',
|
||||
self::warning => 'figyelmeztetés',
|
||||
self::error => 'hiba',
|
||||
self::new_product => 'új termék',
|
||||
self::updated => 'módosítás',
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ok => 'success',
|
||||
self::warning => 'warning',
|
||||
self::error => 'danger',
|
||||
self::new_product => 'info',
|
||||
self::updated => 'primary',
|
||||
};
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ok => 'heroicon-o-check-circle',
|
||||
self::warning => 'heroicon-o-exclamation-triangle',
|
||||
self::error => 'heroicon-o-x-circle',
|
||||
self::new_product => 'heroicon-o-sparkles',
|
||||
self::updated => 'heroicon-o-pencil-square',
|
||||
};
|
||||
}
|
||||
}
|
||||
28
app/Enums/PricelistSellerUnitEnum.php
Normal file
28
app/Enums/PricelistSellerUnitEnum.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PricelistSellerUnitEnum: string
|
||||
{
|
||||
case csom = 'csom';
|
||||
case kart = 'kart';
|
||||
case rúd = 'rúd';
|
||||
case db = 'db';
|
||||
case dob = 'dob';
|
||||
case zsák = 'zsák';
|
||||
case tálca = 'tálca';
|
||||
case kg = 'kg';
|
||||
case liter = 'liter';
|
||||
case pár = 'pár';
|
||||
case tekercs = 'tekercs';
|
||||
|
||||
/**
|
||||
* Visszaadja az összes értéket tömbként
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
21
app/Enums/PricelistUnitEnum.php
Normal file
21
app/Enums/PricelistUnitEnum.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PricelistUnitEnum: string
|
||||
{
|
||||
case l = 'l';
|
||||
case ml = 'ml';
|
||||
case kg = 'kg';
|
||||
case g = 'g';
|
||||
|
||||
/**
|
||||
* Visszaadja az összes értéket tömbként
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
30
app/Enums/PricelistVatEnum.php
Normal file
30
app/Enums/PricelistVatEnum.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PricelistVatEnum: int
|
||||
{
|
||||
case Percent5 = 5;
|
||||
case Percent18 = 18;
|
||||
case Percent27 = 27;
|
||||
|
||||
/**
|
||||
* Visszaadja az összes értéket tömbként (5, 18, 27)
|
||||
*
|
||||
* @return array<int>
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Visszaadja a megjelenítendő címkéket (pl. '5%')
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function labels(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->value . '%', self::cases());
|
||||
}
|
||||
}
|
||||
@ -87,16 +87,9 @@ public function save()
|
||||
'supplier_id' => $data['supplier_id'],
|
||||
'available_date' => $data['available_date'],
|
||||
'note' => $data['note'],
|
||||
'status' => \App\Enums\PricelistFileStatusEnum::todo,
|
||||
'workflow_steps' => [
|
||||
['name' => 'preprocess', 'label' => 'Előfeldolgozás', 'status' => 'pending'],
|
||||
['name' => 'validation', 'label' => 'Validálás', 'status' => 'pending'],
|
||||
['name' => 'approval', 'label' => 'Jóváhagyás', 'status' => 'pending'],
|
||||
['name' => 'execution', 'label' => 'Végrehajtás', 'status' => 'pending']
|
||||
],
|
||||
]);
|
||||
|
||||
PricelistPreProcessJob::dispatch($pricelistFile);
|
||||
app(\App\Services\PricelistFileProcessService::class)->dispatchInitialChain($pricelistFile);
|
||||
|
||||
Notification::make()
|
||||
->title('Sikeres feltöltés')
|
||||
|
||||
@ -56,7 +56,7 @@ public static function table(Table $table): Table
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
RelationManagers\LinesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PricelistFiles\RelationManagers;
|
||||
|
||||
use App\Enums\PricelistFileLineStatusEnum;
|
||||
use App\Enums\PricelistFileStatusEnum;
|
||||
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\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class LinesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'lines';
|
||||
|
||||
protected static ?string $title = 'Feldolgozott sorok';
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Grid::make(2)
|
||||
->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')
|
||||
->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>'
|
||||
)),
|
||||
])
|
||||
->visible(fn ($record) => !empty($record->validation_messages)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('id')
|
||||
->columns([
|
||||
TextColumn::make('row_number')
|
||||
->label('#')
|
||||
->sortable()
|
||||
->width('50px'),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label('Státusz')
|
||||
->badge()
|
||||
->color(fn (PricelistFileLineStatusEnum $state) => $state->color())
|
||||
->formatStateUsing(fn (PricelistFileLineStatusEnum $state) => $state->label())
|
||||
->icon(fn (PricelistFileLineStatusEnum $state) => $state->icon())
|
||||
->sortable(),
|
||||
|
||||
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')
|
||||
->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}%");
|
||||
}),
|
||||
|
||||
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;
|
||||
|
||||
if ($oldPrice === null) {
|
||||
return number_format((float)$newPrice, 2, ',', ' ') . ' Ft';
|
||||
}
|
||||
|
||||
$diffPercent = (($newPrice / $oldPrice) - 1) * 100;
|
||||
$colorClass = $diffPercent > 0 ? 'text-danger-600' : ($diffPercent < 0 ? 'text-success-600' : 'text-gray-600');
|
||||
$icon = $diffPercent > 0 ? '↑' : ($diffPercent < 0 ? '↓' : '');
|
||||
|
||||
return sprintf(
|
||||
'<div class="flex flex-col">
|
||||
<span class="text-xs text-gray-500 line-through">%s Ft</span>
|
||||
<span class="font-bold">%s Ft <span class="%s">%s %s%%</span></span>
|
||||
</div>',
|
||||
number_format($oldPrice, 2, ',', ' '),
|
||||
number_format((float)$newPrice, 2, ',', ' '),
|
||||
$colorClass,
|
||||
$icon,
|
||||
number_format(abs($diffPercent), 1, ',', ' ')
|
||||
);
|
||||
})
|
||||
->visible(fn ($livewire) => $livewire->getOwnerRecord()->status !== PricelistFileStatusEnum::todo),
|
||||
|
||||
TextColumn::make('validation_messages')
|
||||
->label('Megjegyzés')
|
||||
->listWithLineBreaks()
|
||||
->bulleted()
|
||||
->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 ?? []),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->label('Státusz')
|
||||
->options(collect(PricelistFileLineStatusEnum::cases())->mapWithKeys(fn ($e) => [$e->value => $e->label()])),
|
||||
])
|
||||
->headerActions([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
->slideOver(),
|
||||
])
|
||||
->bulkActions([
|
||||
//
|
||||
])
|
||||
->defaultSort('row_number', 'asc')
|
||||
->striped()
|
||||
->poll('5s');
|
||||
}
|
||||
}
|
||||
@ -3,11 +3,15 @@
|
||||
namespace App\Filament\Resources\PricelistFiles\Schemas;
|
||||
|
||||
use App\Enums\PricelistFileStatusEnum;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Infolists\Components\RepeatableEntry;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Text;
|
||||
use Filament\Infolists\Components\RepeatableEntry;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Enums\FontWeight;
|
||||
use Filament\Support\Enums\IconPosition;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class PricelistFileInfolist
|
||||
{
|
||||
@ -17,119 +21,157 @@ public static function configure(Schema $schema): Schema
|
||||
->components([
|
||||
Section::make(null)
|
||||
->poll('5s')
|
||||
->columnSpanFull()
|
||||
->maxWidth(Width::Full)
|
||||
->schema([
|
||||
Section::make('Fájl információk')
|
||||
Grid::make(2)
|
||||
->columnSpanFull()
|
||||
->maxWidth(Width::Full)
|
||||
->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)
|
||||
|
||||
Section::make('Validáció statisztika')
|
||||
->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}%)"),
|
||||
Grid::make(5)->schema([
|
||||
TextEntry::make('lines_count')
|
||||
->label('Összes sor')
|
||||
->icon('heroicon-o-list-bullet')
|
||||
->getStateUsing(fn ($record) => $record->lines()->count()),
|
||||
TextEntry::make('error_lines_count')
|
||||
->label('Hibás sorok')
|
||||
->icon('heroicon-o-x-circle')
|
||||
->color('danger')
|
||||
->getStateUsing(fn ($record) => $record->lines()->where('status', 'error')->count()),
|
||||
TextEntry::make('warning_lines_count')
|
||||
->label('Figyelmeztetések')
|
||||
->icon('heroicon-o-exclamation-triangle')
|
||||
->color('warning')
|
||||
->getStateUsing(fn ($record) => $record->lines()->where('status', 'warning')->count()),
|
||||
TextEntry::make('new_lines_count')
|
||||
->label('Új termékek')
|
||||
->icon('heroicon-o-sparkles')
|
||||
->color('info')
|
||||
->getStateUsing(fn ($record) => $record->lines()->where('status', 'new_product')->count()),
|
||||
TextEntry::make('updated_lines_count')
|
||||
->label('Módosult termékek')
|
||||
->icon('heroicon-o-pencil-square')
|
||||
->color('primary')
|
||||
->getStateUsing(fn ($record) => $record->lines()->where('status', 'updated')->count()),
|
||||
]),
|
||||
])
|
||||
->visible(fn ($record) => $record->lines()->exists()),
|
||||
|
||||
Section::make('Előfeldolgozás eredménye')
|
||||
->schema([
|
||||
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'] ?? [])),
|
||||
])
|
||||
->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(),
|
||||
]),
|
||||
TextEntry::make('note')
|
||||
->label('Megjegyzés')
|
||||
->placeholder('Nincs megjegyzés')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
|
||||
Section::make('Előfeldolgozás eredménye')
|
||||
->schema([
|
||||
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'] ?? [])),
|
||||
])
|
||||
->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(),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
@ -25,6 +25,8 @@ public function __construct(
|
||||
*/
|
||||
public function handle(\App\Services\PricelistFileProcessService $service): void
|
||||
{
|
||||
$service->preProcess($this->pricelistFile);
|
||||
if (!$service->preProcess($this->pricelistFile)) {
|
||||
throw new \Exception('Pricelist pre-processing failed.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Enums\PricelistFileStatusEnum;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PricelistFile extends BaseAuditable
|
||||
@ -36,4 +37,9 @@ public function priceList(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PriceList::class);
|
||||
}
|
||||
|
||||
public function lines(): HasMany
|
||||
{
|
||||
return $this->hasMany(PricelistFileLine::class);
|
||||
}
|
||||
}
|
||||
|
||||
51
app/Models/PricelistFileLine.php
Normal file
51
app/Models/PricelistFileLine.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\PricelistFileLineStatusEnum;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PricelistFileLine extends BaseAuditable
|
||||
{
|
||||
protected $casts = [
|
||||
'status' => PricelistFileLineStatusEnum::class,
|
||||
'row_number' => 'integer',
|
||||
'product_id' => 'integer',
|
||||
'product_group_id' => 'integer',
|
||||
'producer_id' => 'integer',
|
||||
'payload' => 'array',
|
||||
'validation_messages' => 'array',
|
||||
'diff' => 'array',
|
||||
];
|
||||
|
||||
public function pricelistFile(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PricelistFile::class);
|
||||
}
|
||||
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
public function productGroup(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ProductGroup::class);
|
||||
}
|
||||
|
||||
public function producer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Producer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hibaüzenet hozzáadása a sorhoz
|
||||
*/
|
||||
public function addValidationError(string $field, string $message): void
|
||||
{
|
||||
$messages = $this->validation_messages ?? [];
|
||||
$messages[$field] = $message;
|
||||
$this->validation_messages = $messages;
|
||||
$this->status = PricelistFileLineStatusEnum::error;
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,11 @@
|
||||
use App\Enums\PricelistWorkflowStep;
|
||||
use App\Models\PricelistFile;
|
||||
use App\Enums\PricelistFileStatusEnum;
|
||||
use App\Models\PricelistFileLine;
|
||||
use App\Enums\PricelistFileLineStatusEnum;
|
||||
use App\Enums\PricelistUnitEnum;
|
||||
use App\Enums\PricelistSellerUnitEnum;
|
||||
use App\Enums\PricelistVatEnum;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@ -39,6 +44,8 @@ public function resetWorkflow(PricelistFile $pricelistFile): void
|
||||
*/
|
||||
public function dispatchInitialChain(PricelistFile $pricelistFile): void
|
||||
{
|
||||
$this->resetWorkflow($pricelistFile);
|
||||
|
||||
Bus::chain([
|
||||
PricelistWorkflowStep::Preprocessing->getJob($pricelistFile),
|
||||
PricelistWorkflowStep::Validation->getJob($pricelistFile),
|
||||
@ -95,9 +102,13 @@ public function updateStepStatus(
|
||||
|
||||
$updateData = [
|
||||
'workflow_steps' => $steps,
|
||||
'processing_current_step' => ($message && $message !== $stepEnum->label())
|
||||
? $stepEnum->label() . ': ' . $message
|
||||
: $effectiveMessage,
|
||||
'processing_current_step' => mb_strcut(
|
||||
($message && $message !== $stepEnum->label())
|
||||
? $stepEnum->label() . ': ' . $message
|
||||
: $effectiveMessage,
|
||||
0,
|
||||
255
|
||||
),
|
||||
'processing_current_step_percentage' => $percentage,
|
||||
];
|
||||
|
||||
@ -410,23 +421,82 @@ private function humanFileSize(int $bytes): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Validálás (Validation) - Üzleti szabályok ellenőrzése
|
||||
* Validálás (Validation) - Üzleti szabályok ellenőrzése és sorok rögzítése
|
||||
*
|
||||
* @param PricelistFile $pricelistFile
|
||||
* @return bool
|
||||
*/
|
||||
public function validate(PricelistFile $pricelistFile): bool
|
||||
{
|
||||
sleep(15); // Szimuláció
|
||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', 'Üzleti szabályok ellenőrzése...', 60);
|
||||
$startTime = microtime(true);
|
||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', 'Sorok beolvasása és mentése...', 0);
|
||||
|
||||
try {
|
||||
// TODO: Validációs logika
|
||||
sleep(15); // Szimuláció
|
||||
// Korábbi sorok törlése az újraindíthatóság érdekében
|
||||
$pricelistFile->lines()->delete();
|
||||
|
||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'completed', 'Validálás sikeres.', 100);
|
||||
$fullPath = Storage::disk('public')->path($pricelistFile->filename);
|
||||
$spreadsheet = IOFactory::load($fullPath);
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
$highestRow = $worksheet->getHighestDataRow();
|
||||
$expectedHeaders = PriceListService::EXPECTED_HEADERS;
|
||||
|
||||
$batchSize = 50;
|
||||
$dataToInsert = [];
|
||||
$processedCount = 0;
|
||||
$totalDataRows = $highestRow >= 5 ? ($highestRow - 4) : 0;
|
||||
|
||||
if ($totalDataRows === 0) {
|
||||
throw new \Exception('A fájl nem tartalmaz feldolgozható adatsort az 5. sortól.');
|
||||
}
|
||||
|
||||
for ($row = 5; $row <= $highestRow; $row++) {
|
||||
$rowData = [];
|
||||
$hasData = false;
|
||||
|
||||
foreach ($expectedHeaders as $index => $label) {
|
||||
$value = $worksheet->getCellByColumnAndRow($index + 1, $row)->getValue();
|
||||
$rowData[$label] = $value;
|
||||
if (!empty(trim((string)$value))) {
|
||||
$hasData = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Csak akkor mentjük, ha a sor nem teljesen üres
|
||||
if ($hasData) {
|
||||
$dataToInsert[] = [
|
||||
'pricelist_file_id' => $pricelistFile->id,
|
||||
'row_number' => $row,
|
||||
'status' => PricelistFileLineStatusEnum::ok->value,
|
||||
'payload' => json_encode($rowData),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
// A BaseAuditable trait miatt ezeket is érdemes lehet kitölteni,
|
||||
// de a tömeges beszúrásnál (insert) nem futnak le a boot események.
|
||||
'created_by' => auth()->id() ?? 1,
|
||||
];
|
||||
}
|
||||
|
||||
$processedCount++;
|
||||
|
||||
if (count($dataToInsert) >= $batchSize) {
|
||||
PricelistFileLine::insert($dataToInsert);
|
||||
$dataToInsert = [];
|
||||
|
||||
$percentage = round(($processedCount / $totalDataRows) * 100);
|
||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', "Sorok mentése ({$processedCount}/{$totalDataRows})...", $percentage);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($dataToInsert)) {
|
||||
PricelistFileLine::insert($dataToInsert);
|
||||
}
|
||||
|
||||
$this->runBusinessValidation($pricelistFile);
|
||||
|
||||
$duration = round(microtime(true) - $startTime, 2);
|
||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'completed', "Sikeres validálás ({$processedCount} sor, {$duration} mp).", 100);
|
||||
|
||||
sleep(15); // Szimuláció
|
||||
// A validálás végén átadjuk a lépést a jóváhagyásnak (Approval)
|
||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Approval, 'inprogress');
|
||||
|
||||
@ -438,6 +508,340 @@ public function validate(PricelistFile $pricelistFile): bool
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Szöveg normalizálása az összehasonlításhoz (mb_strtoupper, trim, speciális karakterek kezelése)
|
||||
*/
|
||||
protected function normalizeForComparison(string $text): string
|
||||
{
|
||||
// 1. Kis/Nagybetű érzéketlenség UTF-8 támogatással
|
||||
$text = mb_strtoupper(trim($text), 'UTF-8');
|
||||
|
||||
// 2. Szeparátorok egységesítése (perjel és alulvonás cseréje)
|
||||
// A hibaüzenetben látható alulvonás és a felhasználó által említett perjel miatt
|
||||
$text = str_replace(['/', '_'], ' ', $text);
|
||||
|
||||
// 3. Felesleges szóközök eltávolítása (ha a csere után több szóköz maradt)
|
||||
$text = preg_replace('/\s+/', ' ', $text);
|
||||
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Termékcsoport lookup tábla felépítése az adatbázisból (hierarchia útvonala -> id)
|
||||
*/
|
||||
protected function getProductGroupLookupMap(): array
|
||||
{
|
||||
$allGroups = (new \App\Models\ProductGroup)->allWithPath(3);
|
||||
$groupLookup = [];
|
||||
|
||||
foreach ($allGroups as $group) {
|
||||
$parentsNames = $group['parentsNames'] ?? [];
|
||||
|
||||
// Technikai gyökér elhagyása (0. szint, pl. "Főcsoportok")
|
||||
if (isset($group['depth']) && $group['depth'] > 0 && !empty($parentsNames)) {
|
||||
array_shift($parentsNames);
|
||||
}
|
||||
|
||||
$pathParts = array_merge($parentsNames, [$group['name']]);
|
||||
$normalizedParts = array_map(fn($v) => $this->normalizeForComparison((string)$v), $pathParts);
|
||||
$pathKey = implode('|', $normalizedParts);
|
||||
$groupLookup[$pathKey] = $group['id'];
|
||||
}
|
||||
|
||||
return $groupLookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gyártó lookup tábla felépítése (név -> id)
|
||||
*/
|
||||
protected function getProducerLookupMap(): array
|
||||
{
|
||||
return \App\Models\Producer::all()
|
||||
->keyBy(fn($p) => $this->normalizeForComparison($p->name))
|
||||
->map->id
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Termék lookup tábla felépítése a beszállítóhoz (cikkszám -> id)
|
||||
*/
|
||||
protected function getProductLookupMap(int $supplierId): array
|
||||
{
|
||||
return \App\Models\Product::where('supplier_id', $supplierId)
|
||||
->whereNotNull('supplierProductNumber')
|
||||
->get()
|
||||
->keyBy('supplierProductNumber')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Termékcsoport útvonal összeállítása a payload-ból
|
||||
*/
|
||||
protected function getRowPathFromPayload(array $payload): string
|
||||
{
|
||||
$pathComponents = [
|
||||
$payload[PriceListService::EXPECTED_HEADERS[1]] ?? null,
|
||||
$payload[PriceListService::EXPECTED_HEADERS[2]] ?? null,
|
||||
$payload[PriceListService::EXPECTED_HEADERS[3]] ?? null
|
||||
];
|
||||
|
||||
return collect($pathComponents)
|
||||
->filter(fn($v) => !empty(trim((string)$v)))
|
||||
->map(fn($v) => $this->normalizeForComparison((string)$v))
|
||||
->implode('|');
|
||||
}
|
||||
|
||||
/**
|
||||
* Termékcsoport validáció egy sorra
|
||||
*/
|
||||
protected function validateProductGroup(string $rowPath, array $groupLookup): ?string
|
||||
{
|
||||
if (empty($rowPath)) {
|
||||
return "Hiányzó termékcsoport adatok.";
|
||||
}
|
||||
|
||||
if (!isset($groupLookup[$rowPath])) {
|
||||
return "Ismeretlen termékcsoport útvonal: $rowPath";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mértékegység validáció egy sorra
|
||||
*/
|
||||
protected function validateUnit(array $payload): ?string
|
||||
{
|
||||
$unitValue = $payload[PriceListService::EXPECTED_HEADERS[7]] ?? null;
|
||||
|
||||
if (empty(trim((string)$unitValue))) {
|
||||
return "Hiányzó mértékegység.";
|
||||
}
|
||||
|
||||
$normalizedUnit = mb_strtolower(trim((string)$unitValue));
|
||||
if (!PricelistUnitEnum::tryFrom($normalizedUnit)) {
|
||||
return "Érvénytelen mértékegység: $unitValue. Elfogadott: " . implode(', ', PricelistUnitEnum::values());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legkisebb eladási egység validáció egy sorra
|
||||
*/
|
||||
protected function validateSellerUnit(array $payload): ?string
|
||||
{
|
||||
$sellerUnitValue = $payload[PriceListService::EXPECTED_HEADERS[9]] ?? null;
|
||||
|
||||
if (empty(trim((string)$sellerUnitValue))) {
|
||||
return "Hiányzó legkisebb eladási egység.";
|
||||
}
|
||||
|
||||
$normalizedUnit = mb_strtolower(trim((string)$sellerUnitValue));
|
||||
if (!PricelistSellerUnitEnum::tryFrom($normalizedUnit)) {
|
||||
return "Érvénytelen legkisebb eladási egység: $sellerUnitValue. Elfogadott: " . implode(', ', PricelistSellerUnitEnum::values());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mennyiségi egység validáció egy sorra
|
||||
*/
|
||||
protected function validateAmountUnit(array $payload): ?string
|
||||
{
|
||||
$amountUnitValue = $payload[PriceListService::EXPECTED_HEADERS[11]] ?? null;
|
||||
|
||||
if (empty(trim((string)$amountUnitValue))) {
|
||||
return "Hiányzó mennyiségi egység.";
|
||||
}
|
||||
|
||||
$normalizedUnit = mb_strtolower(trim((string)$amountUnitValue));
|
||||
if (!PricelistSellerUnitEnum::tryFrom($normalizedUnit)) {
|
||||
return "Érvénytelen mennyiségi egység: $amountUnitValue. Elfogadott: " . implode(', ', PricelistSellerUnitEnum::values());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ÁFA % validáció egy sorra
|
||||
*/
|
||||
protected function validateVat(array $payload): ?string
|
||||
{
|
||||
$vatValue = $payload[PriceListService::EXPECTED_HEADERS[12]] ?? null;
|
||||
|
||||
if ($vatValue === null || (is_string($vatValue) && trim($vatValue) === '')) {
|
||||
return "Hiányzó ÁFA %.";
|
||||
}
|
||||
|
||||
// Kinyerjük a numerikus értéket, kezelve a százalékjelet, szóközöket és a tizedesvesszőt
|
||||
$numericValue = (float) str_replace(['%', ' ', ','], ['', '', '.'], (string)$vatValue);
|
||||
|
||||
// Ha az érték 0 és 1 közé esik (pl. 0.05, 0.27), akkor valószínűleg Excel százalék formátum
|
||||
// Ebben az esetben megszorozzuk 100-zal, hogy megkapjuk az egész értéket (pl. 5, 27)
|
||||
if ($numericValue > 0 && $numericValue <= 1) {
|
||||
$numericValue *= 100;
|
||||
}
|
||||
|
||||
$finalVat = (int) round($numericValue);
|
||||
|
||||
if (!PricelistVatEnum::tryFrom($finalVat)) {
|
||||
return "Érvénytelen ÁFA %: $vatValue. Elfogadott: " . implode(', ', PricelistVatEnum::labels());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gyártó validáció egy sorra
|
||||
*/
|
||||
protected function validateProducer(array $payload, array $producerLookup): ?string
|
||||
{
|
||||
$producerName = $payload[PriceListService::EXPECTED_HEADERS[8]] ?? null;
|
||||
|
||||
if (empty(trim((string)$producerName))) {
|
||||
return "Hiányzó gyártó megnevezés.";
|
||||
}
|
||||
|
||||
$normalizedName = $this->normalizeForComparison((string)$producerName);
|
||||
|
||||
if (!isset($producerLookup[$normalizedName])) {
|
||||
return "NEW_PRODUCER";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Üzleti validáció futtatása a mentett sorokon (Batch feldolgozás)
|
||||
*
|
||||
* @param PricelistFile $pricelistFile
|
||||
*/
|
||||
protected function runBusinessValidation(PricelistFile $pricelistFile): void
|
||||
{
|
||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', 'Üzleti validálás...', 100);
|
||||
|
||||
$groupLookup = $this->getProductGroupLookupMap();
|
||||
$producerLookup = $this->getProducerLookupMap();
|
||||
$productLookup = $this->getProductLookupMap($pricelistFile->supplier_id);
|
||||
|
||||
$pricelistFile->lines()->chunk(1000, function ($lines) use ($groupLookup, $producerLookup, $productLookup) {
|
||||
$updates = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$payload = $line->payload;
|
||||
$validationMessages = [];
|
||||
|
||||
// Termék ID kinyerése (cikkszám alapján)
|
||||
$sku = (string)($payload[PriceListService::EXPECTED_HEADERS[0]] ?? '');
|
||||
$existingProduct = $productLookup[$sku] ?? null;
|
||||
$productId = $existingProduct['id'] ?? null;
|
||||
|
||||
// Termékcsoport útvonal és ID kinyerése
|
||||
$rowPath = $this->getRowPathFromPayload($payload);
|
||||
$groupId = $groupLookup[$rowPath] ?? null;
|
||||
|
||||
// Gyártó ID kinyerése
|
||||
$producerName = $payload[PriceListService::EXPECTED_HEADERS[8]] ?? null;
|
||||
$producerId = $producerLookup[$this->normalizeForComparison((string)$producerName)] ?? null;
|
||||
|
||||
// Különálló validációk meghívása
|
||||
if ($groupError = $this->validateProductGroup($rowPath, $groupLookup)) {
|
||||
$validationMessages['product_group'] = $groupError;
|
||||
}
|
||||
|
||||
$hasNewProducer = false;
|
||||
if ($producerError = $this->validateProducer($payload, $producerLookup)) {
|
||||
if ($producerError === 'NEW_PRODUCER') {
|
||||
$validationMessages['producer'] = "Új gyártó: " . ($payload[PriceListService::EXPECTED_HEADERS[8]] ?? '');
|
||||
$hasNewProducer = true;
|
||||
} else {
|
||||
$validationMessages['producer'] = $producerError;
|
||||
}
|
||||
}
|
||||
|
||||
if ($unitError = $this->validateUnit($payload)) {
|
||||
$validationMessages['product_unit'] = $unitError;
|
||||
}
|
||||
|
||||
if ($sellerUnitError = $this->validateSellerUnit($payload)) {
|
||||
$validationMessages['seller_unit'] = $sellerUnitError;
|
||||
}
|
||||
|
||||
if ($amountUnitError = $this->validateAmountUnit($payload)) {
|
||||
$validationMessages['amount_unit'] = $amountUnitError;
|
||||
}
|
||||
|
||||
if ($vatError = $this->validateVat($payload)) {
|
||||
$validationMessages['vat'] = $vatError;
|
||||
}
|
||||
|
||||
// Státusz meghatározása
|
||||
$hasError = false;
|
||||
|
||||
foreach ($validationMessages as $key => $msg) {
|
||||
if (str_contains($msg, 'Hiányzó') || str_contains($msg, 'Ismeretlen') || str_contains($msg, 'Érvénytelen')) {
|
||||
$hasError = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$status = PricelistFileLineStatusEnum::ok;
|
||||
if ($hasError) {
|
||||
$status = PricelistFileLineStatusEnum::error;
|
||||
} elseif ($productId === null) {
|
||||
// Ha nincs meglévő termék -> Új termék
|
||||
$status = PricelistFileLineStatusEnum::new_product;
|
||||
} else {
|
||||
// Meglévő termék esetén nézzük a módosulásokat
|
||||
$isUpdated = false;
|
||||
|
||||
// 1. Gyártó módosulás
|
||||
if ($hasNewProducer) {
|
||||
$isUpdated = true;
|
||||
} elseif ($producerId !== null && (int)($existingProduct['producer_id'] ?? 0) !== (int)$producerId) {
|
||||
$isUpdated = true;
|
||||
$validationMessages['producer_update'] = "Módosult gyártó";
|
||||
}
|
||||
|
||||
// 2. Termékcsoport módosulás
|
||||
if ($groupId !== null && (int)($existingProduct['product_group_id'] ?? 0) !== (int)$groupId) {
|
||||
$isUpdated = true;
|
||||
$validationMessages['product_group_update'] = "Módosult termékcsoport";
|
||||
}
|
||||
|
||||
if ($isUpdated) {
|
||||
$status = PricelistFileLineStatusEnum::updated;
|
||||
}
|
||||
}
|
||||
|
||||
$updates[] = [
|
||||
'id' => $line->id,
|
||||
'pricelist_file_id' => $line->pricelist_file_id,
|
||||
'row_number' => $line->row_number,
|
||||
'status' => $status->value,
|
||||
'product_id' => $productId,
|
||||
'product_group_id' => $groupId,
|
||||
'producer_id' => $producerId,
|
||||
'payload' => json_encode($line->payload),
|
||||
'validation_messages' => json_encode($validationMessages),
|
||||
'created_at' => $line->created_at->toDateTimeString(),
|
||||
'updated_at' => now()->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
|
||||
// Kötegelt frissítés (upsert) a teljesítmény optimalizálásához
|
||||
if (!empty($updates)) {
|
||||
PricelistFileLine::upsert(
|
||||
$updates,
|
||||
['id'], // Egyedi azonosító
|
||||
['status', 'validation_messages', 'product_id', 'product_group_id', 'producer_id', 'updated_at'] // Frissítendő mezők
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Végrehajtás (Execution) - Módosítások tényleges alkalmazása tranzakcióban
|
||||
*
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\PricelistFileLineStatusEnum;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('pricelist_file_lines', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('pricelist_file_id')->constrained()->cascadeOnDelete();
|
||||
$table->integer('row_number');
|
||||
$table->enum('status', array_column(PricelistFileLineStatusEnum::cases(), 'value'))->index();
|
||||
$table->json('payload');
|
||||
$table->json('validation_messages')->nullable();
|
||||
$table->json('diff')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['pricelist_file_id', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('pricelist_file_lines');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('pricelist_file_lines', function (Blueprint $table) {
|
||||
$table->unsignedInteger('created_by')->nullable()->after('updated_at');
|
||||
$table->unsignedInteger('updated_by')->nullable()->after('created_by');
|
||||
$table->unsignedInteger('deleted_by')->nullable()->after('updated_by');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('pricelist_file_lines', function (Blueprint $table) {
|
||||
$table->dropColumn(['created_by', 'updated_by', 'deleted_by']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('pricelist_file_lines', function (Blueprint $table) {
|
||||
$table->foreignId('product_id')->nullable()->constrained('products')->nullOnDelete();
|
||||
$table->foreignId('product_group_id')->nullable()->constrained('product_groups')->nullOnDelete();
|
||||
$table->foreignId('producer_id')->nullable()->constrained('producers')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('pricelist_file_lines', function (Blueprint $table) {
|
||||
$table->dropForeign(['product_id']);
|
||||
$table->dropForeign(['product_group_id']);
|
||||
$table->dropForeign(['producer_id']);
|
||||
$table->dropColumn(['product_id', 'product_group_id', 'producer_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\PricelistFileStatusEnum;
|
||||
use App\Enums\PricelistFileLineStatusEnum;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 1. PricelistFileStatusEnum frissítése a pricelist_files táblában
|
||||
$fileStatuses = array_map(fn($case) => "'{$case->value}'", PricelistFileStatusEnum::cases());
|
||||
$fileStatusesStr = implode(', ', $fileStatuses);
|
||||
DB::statement("ALTER TABLE pricelist_files MODIFY COLUMN status ENUM({$fileStatusesStr}) DEFAULT 'todo' NOT NULL");
|
||||
|
||||
// 2. PricelistFileLineStatusEnum frissítése a pricelist_file_lines táblában
|
||||
$lineStatuses = array_map(fn($case) => "'{$case->value}'", PricelistFileLineStatusEnum::cases());
|
||||
$lineStatusesStr = implode(', ', $lineStatuses);
|
||||
DB::statement("ALTER TABLE pricelist_file_lines MODIFY COLUMN status ENUM({$lineStatusesStr}) NOT NULL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// A visszagörgetésnél nem csinálunk semmit, mert az enumok csökkentése adatvesztéssel járhatna
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user