From 6c430afffe30069e569d06f3b2aee78a0f4fe35f Mon Sep 17 00:00:00 2001 From: E98Developer Date: Fri, 6 Mar 2026 17:52:09 +0100 Subject: [PATCH] add jobs and workflow support with percentage progress --- .junie/guidelines.md | 6 +- .../Commands/PricelistProcessCommand.php | 49 ++++++ app/Console/Commands/QueueListCommand.php | 89 ++++++++++ app/Enums/PricelistWorkflowStep.php | 76 +++++++++ .../Pages/CreatePricelistFile.php | 5 + app/Jobs/PricelistExecutionJob.php | 30 ++++ app/Jobs/PricelistPreProcessJob.php | 4 +- app/Jobs/PricelistValidationJob.php | 30 ++++ app/Models/PricelistFile.php | 9 + app/Services/PricelistFileProcessService.php | 157 ++++++++++++++---- composer.json | 2 +- 11 files changed, 421 insertions(+), 36 deletions(-) create mode 100644 app/Console/Commands/PricelistProcessCommand.php create mode 100644 app/Console/Commands/QueueListCommand.php create mode 100644 app/Enums/PricelistWorkflowStep.php create mode 100644 app/Jobs/PricelistExecutionJob.php create mode 100644 app/Jobs/PricelistValidationJob.php diff --git a/.junie/guidelines.md b/.junie/guidelines.md index 31a96d7..5069ec4 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -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: diff --git a/app/Console/Commands/PricelistProcessCommand.php b/app/Console/Commands/PricelistProcessCommand.php new file mode 100644 index 0000000..b4495c7 --- /dev/null +++ b/app/Console/Commands/PricelistProcessCommand.php @@ -0,0 +1,49 @@ + 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; + } + } +} diff --git a/app/Console/Commands/QueueListCommand.php b/app/Console/Commands/QueueListCommand.php new file mode 100644 index 0000000..3192c38 --- /dev/null +++ b/app/Console/Commands/QueueListCommand.php @@ -0,0 +1,89 @@ +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; + } +} diff --git a/app/Enums/PricelistWorkflowStep.php b/app/Enums/PricelistWorkflowStep.php new file mode 100644 index 0000000..1b974d6 --- /dev/null +++ b/app/Enums/PricelistWorkflowStep.php @@ -0,0 +1,76 @@ + '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()); + } +} diff --git a/app/Filament/Resources/PricelistFiles/Pages/CreatePricelistFile.php b/app/Filament/Resources/PricelistFiles/Pages/CreatePricelistFile.php index da55ecd..454d392 100644 --- a/app/Filament/Resources/PricelistFiles/Pages/CreatePricelistFile.php +++ b/app/Filament/Resources/PricelistFiles/Pages/CreatePricelistFile.php @@ -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); + } } diff --git a/app/Jobs/PricelistExecutionJob.php b/app/Jobs/PricelistExecutionJob.php new file mode 100644 index 0000000..a3f3874 --- /dev/null +++ b/app/Jobs/PricelistExecutionJob.php @@ -0,0 +1,30 @@ +execute($this->pricelistFile); + } +} diff --git a/app/Jobs/PricelistPreProcessJob.php b/app/Jobs/PricelistPreProcessJob.php index 8596d86..ea669c0 100644 --- a/app/Jobs/PricelistPreProcessJob.php +++ b/app/Jobs/PricelistPreProcessJob.php @@ -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); } } diff --git a/app/Jobs/PricelistValidationJob.php b/app/Jobs/PricelistValidationJob.php new file mode 100644 index 0000000..15bf696 --- /dev/null +++ b/app/Jobs/PricelistValidationJob.php @@ -0,0 +1,30 @@ +validate($this->pricelistFile); + } +} diff --git a/app/Models/PricelistFile.php b/app/Models/PricelistFile.php index 39a5eff..0c3a6a6 100644 --- a/app/Models/PricelistFile.php +++ b/app/Models/PricelistFile.php @@ -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); diff --git a/app/Services/PricelistFileProcessService.php b/app/Services/PricelistFileProcessService.php index f8174a7..6f56d08 100644 --- a/app/Services/PricelistFileProcessService.php +++ b/app/Services/PricelistFileProcessService.php @@ -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; } } diff --git a/composer.json b/composer.json index 206e8f6..9efbf21 100644 --- a/composer.json +++ b/composer.json @@ -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",