d2d.emegrendeles.hu/app/Filament/Resources/PricelistFiles/Pages/ViewPricelistFile.php
E98Developer eee27aa659 ADD EV3-357 Árlista feldolgozás phase5/2 jóváhagyás/elutasítás akció
- PricelistFile::canBeApproved() / hasExecutionStarted(): a jóváhagyás feltételei
  egy helyen, hogy a felületi gomb és a service ugyanazt ellenőrizze
- approve(): a státusz MÉG A DISPATCH ELŐTT billen át, különben a queue-latency
  alatt a gomb látszana és egy második kattintás párhuzamos importot indítana
- reject(): új 'rejected' lépésstátusz - nem 'failed', mert azt az
  updateStepStatus fail fájlstátuszra fordítaná, az elutasítás viszont closed
- a döntés (ki, mikor, milyen indokkal) a file_meta.approval-ba kerül, mert a
  BaseAuditable a nevével ellentétben nem naplóz
- Edit gomb szűkítése hasExecutionStarted()-tel három helyen, köztük az
  EditPricelistFile::afterSave()-ben, ami a teljes láncot újraindítja

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 15:00:33 +02:00

153 lines
5.7 KiB
PHP

<?php
namespace App\Filament\Resources\PricelistFiles\Pages;
use App\Enums\PricelistFileLineStatusEnum;
use App\Enums\PricelistFileStatusEnum;
use App\Filament\Resources\PricelistFiles\PricelistFileResource;
use App\Models\PricelistFile;
use App\Services\PricelistFileProcessService;
use Filament\Actions\Action;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ViewRecord;
use Laravel\Pennant\Feature;
class ViewPricelistFile extends ViewRecord
{
protected static string $resource = PricelistFileResource::class;
protected function getHeaderActions(): array
{
return [
$this->approveAction(),
$this->rejectAction(),
EditAction::make()
// A `fail` a validálásig tartó szakasz hibája, onnan a fájl újratöltése
// biztonságosan újraindítja a láncot. A hasExecutionStarted() extra
// feltétel a maradék kockázatot zárja ki: ha a végrehajtás egyszer már
// elindult, a nulláról induló újrafuttatás duplikált termékeket és
// árakat hozna létre.
->visible(fn (PricelistFile $record) => $record->status === PricelistFileStatusEnum::fail
&& ! $record->hasExecutionStarted()),
];
}
protected function approveAction(): Action
{
return Action::make('approve')
->label('Jóváhagyás')
->icon('heroicon-o-check-circle')
->color('success')
->visible(fn (PricelistFile $record) => $this->canDecide($record))
->requiresConfirmation()
->modalHeading('Árlista jóváhagyása')
->modalDescription(fn (PricelistFile $record) => 'A jóváhagyás elindítja a tényleges importot: '
. $this->summarizeLines($record)
. ' A művelet a termékadatokat is módosítja.')
->modalSubmitActionLabel('Jóváhagyás és végrehajtás')
->action(function (PricelistFile $record) {
if (! app(PricelistFileProcessService::class)->approve($record)) {
$this->notifyStateChanged();
return;
}
Notification::make()
->title('Végrehajtás elindítva')
->body('Az árlista importja elindult, a haladás ezen az oldalon követhető.')
->success()
->send();
});
}
protected function rejectAction(): Action
{
return Action::make('reject')
->label('Elutasítás')
->icon('heroicon-o-x-circle')
->color('danger')
->visible(fn (PricelistFile $record) => $this->canDecide($record))
->requiresConfirmation()
->modalHeading('Árlista elutasítása')
->modalDescription('A fájl lezárva státuszba kerül, import nem indul. A művelet nem vonható vissza.')
->modalSubmitActionLabel('Elutasítás')
->schema([
Textarea::make('reason')
->label('Elutasítás indoka')
->rows(3)
->maxLength(1000),
])
->action(function (PricelistFile $record, array $data) {
if (! app(PricelistFileProcessService::class)->reject($record, $data['reason'] ?? null)) {
$this->notifyStateChanged();
return;
}
Notification::make()
->title('Árlista elutasítva')
->body('A fájl lezárva státuszba került.')
->success()
->send();
});
}
/**
* A döntési gombok közös feltétele: a rollout flag és a fájl jóváhagyhatósága.
* A canBeApproved() a modellben él, mert ugyanezt a service is ellenőrzi a
* művelet végrehajtásakor - a felület csak elrejti a gombot, a védelmet a
* service adja.
*/
protected function canDecide(PricelistFile $record): bool
{
return Feature::for(auth()->user())->active('PricelistExecution')
&& $record->canBeApproved();
}
/**
* A jóváhagyás előtti összegzés: mit fog csinálni az import.
*/
protected function summarizeLines(PricelistFile $record): string
{
$counts = $record->lineStatusCounts();
$parts = [];
foreach ([
PricelistFileLineStatusEnum::new_product,
PricelistFileLineStatusEnum::updated,
PricelistFileLineStatusEnum::ok,
PricelistFileLineStatusEnum::warning,
] as $status) {
$count = (int) ($counts[$status->value] ?? 0);
if ($count > 0) {
$parts[] = $count . ' ' . $status->label();
}
}
return $parts === [] ? 'nincs feldolgozandó sor.' : implode(', ', $parts) . '.';
}
/**
* A felület 5 mp-enként pollozik, így a gomb megjelenítése és a kattintás között
* változhat az állapot (pl. más felhasználó közben jóváhagyta). Ilyenkor a service
* elutasítja a műveletet, a felhasználónak pedig ezt meg kell mondani.
*/
protected function notifyStateChanged(): void
{
Notification::make()
->title('A művelet nem hajtható végre')
->body('A fájl állapota időközben megváltozott. Frissítsd az oldalt a jelenlegi állapotért.')
->warning()
->send();
}
protected function getHeaderWidgets(): array
{
return [
// Később ide jöhetnek widgetek, pl. statisztika a feldolgozásról
];
}
}