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 ]; } }