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 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.
|
- 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.
|
- **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
|
##### JavaScript Architektúra és Modulok
|
||||||
- A JS kód alapja egy egyedi objektumorientált rendszer.
|
- 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'],
|
'supplier_id' => $data['supplier_id'],
|
||||||
'available_date' => $data['available_date'],
|
'available_date' => $data['available_date'],
|
||||||
'note' => $data['note'],
|
'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()
|
Notification::make()
|
||||||
->title('Sikeres feltöltés')
|
->title('Sikeres feltöltés')
|
||||||
|
|||||||
@ -56,7 +56,7 @@ public static function table(Table $table): Table
|
|||||||
public static function getRelations(): array
|
public static function getRelations(): array
|
||||||
{
|
{
|
||||||
return [
|
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;
|
namespace App\Filament\Resources\PricelistFiles\Schemas;
|
||||||
|
|
||||||
use App\Enums\PricelistFileStatusEnum;
|
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\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\Schemas\Schema;
|
||||||
|
use Filament\Support\Enums\FontWeight;
|
||||||
|
use Filament\Support\Enums\IconPosition;
|
||||||
|
use Filament\Support\Enums\Width;
|
||||||
|
|
||||||
class PricelistFileInfolist
|
class PricelistFileInfolist
|
||||||
{
|
{
|
||||||
@ -17,119 +21,157 @@ public static function configure(Schema $schema): Schema
|
|||||||
->components([
|
->components([
|
||||||
Section::make(null)
|
Section::make(null)
|
||||||
->poll('5s')
|
->poll('5s')
|
||||||
|
->columnSpanFull()
|
||||||
|
->maxWidth(Width::Full)
|
||||||
->schema([
|
->schema([
|
||||||
Section::make('Fájl információk')
|
Grid::make(2)
|
||||||
|
->columnSpanFull()
|
||||||
|
->maxWidth(Width::Full)
|
||||||
->schema([
|
->schema([
|
||||||
Grid::make(3)
|
Section::make('Fájl információk')
|
||||||
->schema([
|
->schema([
|
||||||
TextEntry::make('filename')
|
Grid::make(3)
|
||||||
->label('Fájlnév')
|
->schema([
|
||||||
->icon('heroicon-o-document-text')
|
TextEntry::make('filename')
|
||||||
->color('primary'),
|
->label('Fájlnév')
|
||||||
TextEntry::make('supplier.name')
|
->icon('heroicon-o-document-text')
|
||||||
->label('Beszállító')
|
->color('primary'),
|
||||||
->icon('heroicon-o-user'),
|
TextEntry::make('supplier.name')
|
||||||
TextEntry::make('status')
|
->label('Beszállító')
|
||||||
->label('Státusz')
|
->icon('heroicon-o-user'),
|
||||||
->badge()
|
TextEntry::make('status')
|
||||||
->formatStateUsing(fn (PricelistFileStatusEnum $state): string => $state->label())
|
->label('Státusz')
|
||||||
->color(fn (PricelistFileStatusEnum $state): string => match ($state) {
|
->badge()
|
||||||
PricelistFileStatusEnum::todo => 'gray',
|
->formatStateUsing(fn (PricelistFileStatusEnum $state): string => $state->label())
|
||||||
PricelistFileStatusEnum::inprogress => 'info',
|
->color(fn (PricelistFileStatusEnum $state): string => match ($state) {
|
||||||
PricelistFileStatusEnum::done => 'success',
|
PricelistFileStatusEnum::todo => 'gray',
|
||||||
PricelistFileStatusEnum::fail => 'danger',
|
PricelistFileStatusEnum::inprogress => 'info',
|
||||||
PricelistFileStatusEnum::waiting_for_approval => 'primary',
|
PricelistFileStatusEnum::done => 'success',
|
||||||
PricelistFileStatusEnum::closed => 'warning',
|
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([
|
->schema([
|
||||||
TextEntry::make('available_date')
|
Grid::make(5)->schema([
|
||||||
->label('Életbelépés dátuma')
|
TextEntry::make('lines_count')
|
||||||
->date('Y.m.d'),
|
->label('Összes sor')
|
||||||
TextEntry::make('created_at')
|
->icon('heroicon-o-list-bullet')
|
||||||
->label('Feltöltés időpontja')
|
->getStateUsing(fn ($record) => $record->lines()->count()),
|
||||||
->dateTime('Y.m.d H:i'),
|
TextEntry::make('error_lines_count')
|
||||||
TextEntry::make('processing_current_step')
|
->label('Hibás sorok')
|
||||||
->label('Aktuális folyamat')
|
->icon('heroicon-o-x-circle')
|
||||||
->weight('bold')
|
->color('danger')
|
||||||
->color('info')
|
->getStateUsing(fn ($record) => $record->lines()->where('status', 'error')->count()),
|
||||||
->formatStateUsing(fn ($state, $record) => "$state ({$record->processing_current_step_percentage}%)"),
|
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
|
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 App\Enums\PricelistFileStatusEnum;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
class PricelistFile extends BaseAuditable
|
class PricelistFile extends BaseAuditable
|
||||||
@ -36,4 +37,9 @@ public function priceList(): BelongsTo
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(PriceList::class);
|
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\Enums\PricelistWorkflowStep;
|
||||||
use App\Models\PricelistFile;
|
use App\Models\PricelistFile;
|
||||||
use App\Enums\PricelistFileStatusEnum;
|
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\Bus;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@ -39,6 +44,8 @@ public function resetWorkflow(PricelistFile $pricelistFile): void
|
|||||||
*/
|
*/
|
||||||
public function dispatchInitialChain(PricelistFile $pricelistFile): void
|
public function dispatchInitialChain(PricelistFile $pricelistFile): void
|
||||||
{
|
{
|
||||||
|
$this->resetWorkflow($pricelistFile);
|
||||||
|
|
||||||
Bus::chain([
|
Bus::chain([
|
||||||
PricelistWorkflowStep::Preprocessing->getJob($pricelistFile),
|
PricelistWorkflowStep::Preprocessing->getJob($pricelistFile),
|
||||||
PricelistWorkflowStep::Validation->getJob($pricelistFile),
|
PricelistWorkflowStep::Validation->getJob($pricelistFile),
|
||||||
@ -95,9 +102,13 @@ public function updateStepStatus(
|
|||||||
|
|
||||||
$updateData = [
|
$updateData = [
|
||||||
'workflow_steps' => $steps,
|
'workflow_steps' => $steps,
|
||||||
'processing_current_step' => ($message && $message !== $stepEnum->label())
|
'processing_current_step' => mb_strcut(
|
||||||
? $stepEnum->label() . ': ' . $message
|
($message && $message !== $stepEnum->label())
|
||||||
: $effectiveMessage,
|
? $stepEnum->label() . ': ' . $message
|
||||||
|
: $effectiveMessage,
|
||||||
|
0,
|
||||||
|
255
|
||||||
|
),
|
||||||
'processing_current_step_percentage' => $percentage,
|
'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
|
* @param PricelistFile $pricelistFile
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function validate(PricelistFile $pricelistFile): bool
|
public function validate(PricelistFile $pricelistFile): bool
|
||||||
{
|
{
|
||||||
sleep(15); // Szimuláció
|
$startTime = microtime(true);
|
||||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', 'Üzleti szabályok ellenőrzése...', 60);
|
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', 'Sorok beolvasása és mentése...', 0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Validációs logika
|
// Korábbi sorok törlése az újraindíthatóság érdekében
|
||||||
sleep(15); // Szimuláció
|
$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)
|
// A validálás végén átadjuk a lépést a jóváhagyásnak (Approval)
|
||||||
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Approval, 'inprogress');
|
$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
|
* 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