- 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>
153 lines
5.7 KiB
PHP
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
|
|
];
|
|
}
|
|
}
|