add jobs and workflow support with percentage progress

This commit is contained in:
E98Developer 2026-03-06 17:52:09 +01:00
parent 9e90feb662
commit 6c430afffe
11 changed files with 421 additions and 36 deletions

View File

@ -20,11 +20,15 @@ #### Architektúra és Struktúra
* **Requests**: Form Validation-re használd a Laravel FormRequest osztályait a kontrollerek metódusain belül.
* **DTO (Data Transfer Objects)**: Használj DTO-kat vagy Typed Array-eket nagyobb adatszerkezetek továbbításakor (pl. `Extra` library).
##### Modellek és Auditálás
##### Modellek, Auditálás és Workflow
- Minden fő modellnek a `App\Models\BaseAuditable` osztályból kell származnia.
- Ez biztosítja a `created_by`, `updated_by` és `deleted_by` mezők automatikus töltését a `ModelUserIdAppenderTrait` segítségével.
- Használjunk `SoftDeletes`-t, ahol az adatok megőrzése fontos.
- **Enums**: Amennyiben egy modell mezőjéhez meghatározott értékkészlet tartozik, kötelező PHP Enum használata és annak beállítása a `$casts` tömbben.
- **Workflow Kezelés**: Összetett, több lépésből álló folyamatok (pl. árlista feldolgozás) kezeléséhez PHP Enum-okat használunk:
- Az Enum definiálja a lépéseket, a címkéket (`label()`), a végrehajtó Job-okat (`jobClass()`) és a színeket (`color()`).
- A modell `workflow_steps` JSON mezője tárolja a folyamat állapotát, amit a modell `creating` eseményében inicializálunk az Enum `defaultSteps()` metódusával.
- A Service réteg felelős a státuszok léptetéséért a folyamat során.
##### Hibrid Frontend Architektúra (Mix & Vite) és a "Clean Cut" elv
A projekt fokozatos modernizáció alatt áll, ezért két párhuzamos build rendszert használunk a **"Clean Cut"** (tiszta vágás) elv mentén:

View File

@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use App\Models\PricelistFile;
use App\Services\PricelistFileProcessService;
use Illuminate\Console\Command;
class PricelistProcessCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'pricelist:process {id}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Elindítja a megadott azonosítójú árlista fájl kezdeti feldolgozási láncát (Pre-process -> Validation)';
/**
* Execute the console command.
*/
public function handle(PricelistFileProcessService $service): int
{
$id = $this->argument('id');
$pricelistFile = PricelistFile::find($id);
if (!$pricelistFile) {
$this->error("A(z) {$id} ID-vel rendelkező árlista fájl nem található.");
return 1;
}
$this->info("Folyamat elindítása: {$pricelistFile->filename} (ID: {$id})");
try {
$service->dispatchInitialChain($pricelistFile);
$this->info("Az árlista feldolgozási lánc (Pre-process -> Validation) sikeresen ütemezve.");
return 0;
} catch (\Exception $e) {
$this->error("Hiba történt a folyamat indításakor: " . $e->getMessage());
return 1;
}
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
use Illuminate\Support\Facades\Schema;
class QueueListCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'queue:list {--queue= : Szűrés a queue nevére} {--failed : A hibás jobok kilistázása}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Kilistázza a várólistán lévő (vagy hibás) jobokat táblázatos formában';
/**
* Execute the console command.
*/
public function handle(): int
{
$queueFilter = $this->option('queue');
$showFailed = $this->option('failed');
$tableName = $showFailed ? 'failed_jobs' : 'jobs';
if (!Schema::hasTable($tableName)) {
$this->error("A(z) {$tableName} tábla nem létezik.");
return 1;
}
$query = DB::table($tableName);
if ($queueFilter) {
$query->where('queue', $queueFilter);
}
$jobs = $query->orderBy($showFailed ? 'failed_at' : 'created_at', 'desc')->get();
if ($jobs->isEmpty()) {
$this->info("Nincsenek " . ($showFailed ? "hibás " : "") . "jobok a várólistán.");
return 0;
}
$tableData = $jobs->map(function ($job) use ($showFailed) {
$payload = json_decode($job->payload, true);
$displayName = $payload['displayName'] ?? 'Ismeretlen';
if ($showFailed) {
return [
'ID' => $job->id,
'Queue' => $job->queue,
'Job Class' => $displayName,
'Failed At' => $job->failed_at,
'Exception' => substr($job->exception, 0, 70) . '...',
];
}
return [
'ID' => $job->id,
'Queue' => $job->queue,
'Job Class' => $displayName,
'Attempts' => $job->attempts,
'Reserved At' => $job->reserved_at ? Carbon::createFromTimestamp($job->reserved_at)->format('Y.m.d H:i:s') : 'Nincs lefoglalva',
'Available At' => Carbon::createFromTimestamp($job->available_at)->format('Y.m.d H:i:s'),
'Created At' => Carbon::createFromTimestamp($job->created_at)->format('Y.m.d H:i:s'),
];
});
if ($showFailed) {
$headers = ['ID', 'Queue', 'Job Class', 'Failed At', 'Exception'];
} else {
$headers = ['ID', 'Queue', 'Job Class', 'Próbálkozások', 'Lefoglalva', 'Elérhető', 'Létrehozva'];
}
$this->table($headers, $tableData);
return 0;
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Enums;
use App\Jobs\PricelistPreProcessJob;
use App\Jobs\PricelistValidationJob;
use App\Jobs\PricelistExecutionJob;
enum PricelistWorkflowStep: string
{
case Preprocessing = 'preprocess';
case Validation = 'validation';
case Approval = 'approval';
case Execution = 'execution';
/**
* Magyar megnevezés (UI megjelenítéshez)
*/
public function label(): string
{
return match($this) {
self::Preprocessing => 'Előfeldolgozás',
self::Validation => 'Validálás',
self::Approval => 'Jóváhagyás',
self::Execution => 'Végrehajtás',
};
}
/**
* A lépéshez tartozó Job osztály neve
*/
public function jobClass(): ?string
{
return match($this) {
self::Preprocessing => PricelistPreProcessJob::class,
self::Validation => PricelistValidationJob::class,
self::Execution => PricelistExecutionJob::class,
self::Approval => null, // Manuális lépésnél nincs automatikus job
};
}
/**
* A lépéshez tartozó Job példány visszaadása
*/
public function getJob(\App\Models\PricelistFile $file): ?object
{
$jobClass = $this->jobClass();
return $jobClass ? new $jobClass($file) : null;
}
/**
* Státusz színek (UI megjelenítéshez)
*/
public function color(string $status): string
{
return match ($status) {
'pending' => 'gray',
'inprogress' => 'primary',
'completed' => 'success',
'failed' => 'danger',
default => 'gray',
};
}
/**
* Segédfüggvény az alapértelmezett workflow struktúra legenerálásához
*/
public static function defaultSteps(): array
{
return array_map(fn($step) => [
'name' => $step->value,
'label' => $step->label(),
'status' => 'pending',
], self::cases());
}
}

View File

@ -8,4 +8,9 @@
class CreatePricelistFile extends CreateRecord
{
protected static string $resource = PricelistFileResource::class;
protected function afterCreate(): void
{
app(\App\Services\PricelistFileProcessService::class)->dispatchInitialChain($this->record);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Jobs;
use App\Models\PricelistFile;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class PricelistExecutionJob implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(
public PricelistFile $pricelistFile
)
{
//
}
/**
* Execute the job.
*/
public function handle(\App\Services\PricelistFileProcessService $service): void
{
$service->execute($this->pricelistFile);
}
}

View File

@ -23,8 +23,8 @@ public function __construct(
/**
* Execute the job.
*/
public function handle(): void
public function handle(\App\Services\PricelistFileProcessService $service): void
{
// Ide kerül majd az üzleti logika a PricelistFileProcessService segítségével
$service->preProcess($this->pricelistFile);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Jobs;
use App\Models\PricelistFile;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class PricelistValidationJob implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(
public PricelistFile $pricelistFile
)
{
//
}
/**
* Execute the job.
*/
public function handle(\App\Services\PricelistFileProcessService $service): void
{
$service->validate($this->pricelistFile);
}
}

View File

@ -17,6 +17,15 @@ class PricelistFile extends BaseAuditable
'workflow_steps' => 'array',
];
protected static function booted()
{
static::creating(function ($pricelistFile) {
if (empty($pricelistFile->workflow_steps)) {
$pricelistFile->workflow_steps = \App\Enums\PricelistWorkflowStep::defaultSteps();
}
});
}
public function supplier(): BelongsTo
{
return $this->belongsTo(Supplier::class);

View File

@ -2,8 +2,10 @@
namespace App\Services;
use App\Enums\PricelistWorkflowStep;
use App\Models\PricelistFile;
use App\Enums\PricelistFileStatusEnum;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@ -14,40 +16,138 @@ public function __construct(
) {}
/**
* Előfeldolgozás (Pre-processing) - Adatok ellenőrzése és validálása
* Elindítja az árlista feldolgozás kezdeti láncát (Pre-processing -> Validation)
*/
public function dispatchInitialChain(PricelistFile $pricelistFile): void
{
Bus::chain([
PricelistWorkflowStep::Preprocessing->getJob($pricelistFile),
PricelistWorkflowStep::Validation->getJob($pricelistFile),
])->dispatch();
}
/**
* Elindítja a végrehajtási jobot (Approval után)
*/
public function dispatchExecutionJob(PricelistFile $pricelistFile): void
{
$job = PricelistWorkflowStep::Execution->getJob($pricelistFile);
if ($job) {
dispatch($job);
}
}
/**
* Frissíti egy workflow lépés állapotát és az aktuális folyamat jelzőket
*/
public function updateStepStatus(
PricelistFile $pricelistFile,
PricelistWorkflowStep $stepEnum,
string $status,
?string $message = null,
int $percentage = 0
): void {
$effectiveMessage = $message ?? $stepEnum->label();
// Frissítjük a modellt az adatbázisból, hogy a legfrissebb workflow_steps-szel dolgozzunk
// Ez különösen fontos a láncolt joboknál (Bus::chain), ahol az objektum "stale" maradhat a sorban következő job számára
if ($pricelistFile->exists) {
$pricelistFile->refresh();
}
$steps = $pricelistFile->workflow_steps ?? [];
$found = false;
foreach ($steps as &$step) {
if ($step['name'] === $stepEnum->value) {
$step['status'] = $status;
$step['message'] = $effectiveMessage;
$found = true;
break;
}
}
unset($step);
// Ha valamiért nem találtuk meg a lépést (pl. régi rekord), adjuk hozzá a listához
if (!$found) {
$steps[] = [
'name' => $stepEnum->value,
'label' => $stepEnum->label(),
'status' => $status,
'message' => $effectiveMessage,
];
}
$updateData = [
'workflow_steps' => $steps,
'processing_current_step' => ($message && $message !== $stepEnum->label())
? $stepEnum->label() . ': ' . $message
: $effectiveMessage,
'processing_current_step_percentage' => $percentage,
];
if ($status === 'inprogress') {
$updateData['status'] = PricelistFileStatusEnum::inprogress;
} elseif ($status === 'failed') {
$updateData['status'] = PricelistFileStatusEnum::fail;
}
$pricelistFile->update($updateData);
}
/**
* Előfeldolgozás (Pre-processing) - Adatok ellenőrzése és beolvasása
*
* @param PricelistFile $pricelistFile
* @param array $data Az XLS-ből beolvasott adatok
* @return bool
*/
public function preProcess(PricelistFile $pricelistFile, array $data): bool
public function preProcess(PricelistFile $pricelistFile): bool
{
$pricelistFile->update([
'status' => PricelistFileStatusEnum::inprogress,
'processing_current_step' => 'Előfeldolgozás: Adatok ellenőrzése...',
'processing_current_step_percentage' => 10,
]);
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'inprogress', 'Adatok ellenőrzése...', 10);
try {
// Itt hívjuk majd meg a PriceListService ellenőrző metódusait
// TODO: Itt hívjuk majd meg a PriceListService-t a fájl beolvasásához
// $data = $this->priceListService->readFromFile($pricelistFile->path);
// $results = $this->priceListService->importPriceListCheck($pricelistFile->supplier_id, $data);
// TODO: Eredmények mentése (pl. preview_data JSON mezőbe vagy fájlba)
sleep(15); // Szimuláció
$pricelistFile->update([
'status' => PricelistFileStatusEnum::todo, // Visszaállítjuk, vagy egy új 'waiting_for_approval' státuszra, ha létezik
'processing_current_step' => 'Előfeldolgozás kész, jóváhagyásra vár.',
'processing_current_step_percentage' => 100,
]);
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'completed', 'Adatok ellenőrizve.', 100);
return true;
} catch (\Exception $e) {
Log::error('Pricelist pre-process error: ' . $e->getMessage());
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Preprocessing, 'failed', $e->getMessage());
throw $e;
}
}
/**
* Validálás (Validation) - Üzleti szabályok ellenőrzése
*
* @param PricelistFile $pricelistFile
* @return bool
*/
public function validate(PricelistFile $pricelistFile): bool
{
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', 'Üzleti szabályok ellenőrzése...', 60);
try {
// TODO: Validációs logika
sleep(1); // Szimuláció
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'completed', 'Validálás sikeres.', 100);
$pricelistFile->update([
'status' => PricelistFileStatusEnum::fail,
'processing_current_step' => 'Hiba az előfeldolgozás során: ' . $e->getMessage(),
'status' => PricelistFileStatusEnum::waiting_for_approval,
'processing_current_step' => 'Előfeldolgozás és validálás kész, jóváhagyásra vár.',
]);
return false;
return true;
} catch (\Exception $e) {
Log::error('Pricelist validation error: ' . $e->getMessage());
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'failed', $e->getMessage());
throw $e;
}
}
@ -55,36 +155,29 @@ public function preProcess(PricelistFile $pricelistFile, array $data): bool
* Végrehajtás (Execution) - Módosítások tényleges alkalmazása tranzakcióban
*
* @param PricelistFile $pricelistFile
* @param array $dataToImport A véglegesítendő adatok
* @return bool
*/
public function execute(PricelistFile $pricelistFile, array $dataToImport): bool
public function execute(PricelistFile $pricelistFile): bool
{
$pricelistFile->update([
'status' => PricelistFileStatusEnum::inprogress,
'processing_current_step' => 'Végrehajtás: Árlista frissítése...',
'processing_current_step_percentage' => 0,
]);
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Execution, 'inprogress', 'Árlista frissítése...', 0);
DB::beginTransaction();
try {
// Tényleges importálás végrehajtása a PriceListService segítségével
// TODO: Tényleges importálás végrehajtása
// $this->priceListService->importPriceList(...);
sleep(1); // Szimuláció
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Execution, 'completed', 'Sikeresen befejezve.', 100);
DB::commit();
$pricelistFile->update([
'status' => PricelistFileStatusEnum::done,
'processing_current_step' => 'Sikeresen befejezve.',
'processing_current_step_percentage' => 100,
]);
return true;
} catch (\Exception $e) {
DB::rollBack();
Log::error('Pricelist execution error: ' . $e->getMessage());
$pricelistFile->update([
'status' => PricelistFileStatusEnum::fail,
'processing_current_step' => 'Hiba a végrehajtás során: ' . $e->getMessage(),
]);
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Execution, 'failed', $e->getMessage());
return false;
}
}

View File

@ -5,7 +5,7 @@
"keywords": ["framework", "laravel"],
"license": "MIT",
"require": {
"php": "^8.2",
"php": "^8.4",
"ahmedhakeem/extra": "dev-master",
"barryvdh/laravel-dompdf": "^3.1",
"barryvdh/laravel-ide-helper": "^3.5",