Compare commits

...

14 Commits

Author SHA1 Message Date
E98Developer
cbda830c5b ADD EV3-431 Profitcenter - betűkód phase2/4 fix speedButtonNavBar.blade.php to include SupplierCustomerCodes and FeatureFlagSeeder 2026-08-10 07:10:50 +02:00
E98Developer
e5933d0639 ADD EV3-431 Profitcenter - betűkód phase2/3 add modern supplier-customer-codes 2026-08-09 18:02:50 +02:00
E98Developer
1f1254c818 ADD EV3-431 Profitcenter - betűkód phase2/2 add hasCusetomer code switch to supplier administration 2026-08-09 10:46:46 +02:00
E98Developer
41758a81c4 ADD EV3-431 Profitcenter - betűkód phase2/1 add feature flag 2026-08-09 10:10:45 +02:00
E98Developer
e20a7a6a9d ADD EV3-431 Profitcenter - betűkód phase2/1 add feature flag 2026-08-09 10:10:24 +02:00
E98Developer
75ba62b1e1 ADD EV3-431 Profitcenter - betűkód phase2/1 add ProfitCenterSupplierCode data model 2026-08-09 09:41:07 +02:00
E98Developer
856b61da88 MOD change env call to config call in project 2026-08-09 07:15:37 +02:00
E98Developer
a6b11d2178 ADD EV3-431 Profitcenter - betűkód phase3 addd feature flag seeder 2026-08-09 06:44:25 +02:00
E98Developer
259df71c7b ADD EV3-431 Profitcenter - betűkód phase1 addd feature flag override relation manager 2026-08-08 16:46:34 +02:00
E98Developer
d6c6f0af1c EV3-431 Profitcenter - betűkód phase1 addd feature flag with laravel pennant 2026-08-08 16:09:54 +02:00
E98Developer
af6c02ad2e EV3-357 Árlista feltöltés és feldolgzás újra gondolása phase4 fejléc adatok akciós lehetőség bővítése 2026-08-08 12:37:51 +02:00
E98Developer
d3486cb699 ADD EV3-444 Hibás számítás Hooreyca adatok - add log analisator command 2026-08-04 10:50:14 +02:00
E98Developer
b28a224e8e FIX EV3-459 PC árlista admin view 2026-08-04 08:20:45 +02:00
E98Developer
a30aa2dd51 EV3-357 Árlista feltöltés és feldolgzás újra gondolása phase4 fejléc adatok akciós lehetőség bővítése 2026-08-03 12:09:41 +02:00
56 changed files with 2241 additions and 69 deletions

11
.claude/launch.json Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "t2t-dev",
"runtimeExecutable": "php",
"runtimeArgs": ["artisan", "serve", "--port=8123"],
"port": 8123
}
]
}

View File

@ -22,6 +22,16 @@ ### 3. Átállás és Modernizáció
- **Auditálás**: Használd a `BaseAuditable` osztályt minden új modellnél a konzisztens követhetőség érdekében.
- **Refaktorálás**: Ha egy legacy kontroller/nézet jelentős átalakítást igényel, javasold annak áthelyezését egy Filament Resource-ba.
# Feature flag rendszer (Laravel Pennant)
Az alkalmazásban deploy nélkül, admin felületről (`/admin/feature-flags`) ki/bekapcsolható funkciókhoz a `laravel/pennant` csomagra épülő, saját `feature_flags`/`feature_flag_overrides` táblákkal kiegészített rendszer van. Új feature flag bevezetésekor vagy a rendszer módosításakor:
- **NE** írj kézzel új `Feature::define()` hívást — a `FeatureFlagRegistrar::registerAll()` (hívva: `AppServiceProvider::boot()`) automatikusan regisztrálja az admin felületen létrehozott flageket.
- **NE** használd a Pennant saját tárolóját (`Feature::for($user)->activate()`) tartós, admin felületről törölhetetlen felhasználói döntésekhez — a `Feature::purge()` egy adott flag nevére **minden** scope-ot (minden felhasználó feloldását) töröl. Az explicit egyéni felülbírálásokat ezért a saját `feature_flag_overrides` tábla (`FeatureFlagOverride` modell) tárolja, amit a resolver néz meg elsőként, mielőtt a szabály-alapú (enabled/stage/role) logikára térne.
- `FeatureFlagOverride` (vagy bármi, ami Pennant-cache-invalidáló Observerrel rendelkezik) módosítása mindig **model-instance-on keresztül** történjen (`$model->save()`/`$model->delete()`), soha ne query builder `->update()`/`->delete()`-tel — csak az előbbi váltja ki az Eloquent modell-eseményeket, amikre az Observerek épülnek.
- A `FeatureFlagAdmin` nevű flag saját magát a feature flag admin felületet védi (`FeatureFlagPolicy`/`FeatureFlagOverridePolicy` → `Feature::for($user)->active('FeatureFlagAdmin')`) — jelenleg kizárólag `developer` szerepkör éri el, minden stage-en.
- Teljes technikai leírás (architektúra, talált hibák, tesztelési minták): `storage/app/private/docs/feature-flag-rendszer.md`.
# Projekt-specifikus szabályok
- **API válaszok**: Használd a `flugger/laravel-responder` csomagot (`responder()` helper) minden API válaszhoz. Kövesd az `OrderController.php`-ban látható mintát.

View File

@ -0,0 +1,336 @@
<?php
namespace App\Console\Commands;
use App\Models\ProfitCenter;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Monolog\Formatter\LineFormatter;
class CheckApiLog extends Command
{
protected $signature = 'app:check-api-log {file? : A storage/logs alatti API log fájl neve (pl. api-2026-07-21.log). Ha nincs megadva, a fájlnév szerint legutolsó api-*.log fájlt választja.}';
protected $description = 'A storage/logs/api-*.log fájlban szereplő /api/v1/order/unprocessed válaszok ellenőrzése: profit center hooreycaDataActive állapota és a HooreycaVolumen/HooreycaUnitPrice számított mezők helyessége.';
public function handle(): int
{
$filePath = $this->resolveLogFile($this->argument('file'));
if (! $filePath) {
$this->error($this->argument('file')
? 'A megadott log fájl nem található: storage/logs/'.$this->argument('file')
: 'Nem található api-*.log fájl a storage/logs könyvtárban.');
return self::FAILURE;
}
$this->info('Elemzett fájl: '.$filePath);
[$totalQueries, $totalOrders] = $this->preprocess($filePath);
if ($totalQueries === 0) {
$this->warn('Nem található "unprocessed" API válasz (pagination mezővel) ebben a fájlban.');
return self::SUCCESS;
}
$this->info("Talált lekérdezések (API hívások): {$totalQueries}, összesen {$totalOrders} megrendeléssel.");
$errorLogPath = $this->errorLogPathFor($filePath);
$errorLogger = Log::build([
'driver' => 'single',
'path' => $errorLogPath,
]);
$formatter = new LineFormatter(null, null, true, true);
$formatter->setJsonPrettyPrint(true);
foreach ($errorLogger->getLogger()->getHandlers() as $handler) {
$handler->setFormatter($formatter);
}
$bar = $this->output->createProgressBar($totalOrders);
$bar->start();
$queryIndex = 0;
$queriesWithErrors = 0;
$ordersWithErrors = 0;
$itemErrorCount = 0;
$profitCenterErrorCount = 0;
$handle = fopen($filePath, 'r');
while (($line = fgets($handle)) !== false) {
$responseBody = $this->extractUnprocessedResponse($line);
if ($responseBody === null) {
continue;
}
$queryIndex++;
$queryHasError = false;
foreach ($responseBody['data'] ?? [] as $order) {
$orderErrors = $this->checkProfitCenter($order);
if ($orderErrors) {
$profitCenterErrorCount++;
}
foreach ($order['items'] ?? [] as $item) {
$itemErrors = $this->checkItemCalculations($item);
if ($itemErrors) {
$itemErrorCount++;
foreach ($itemErrors as $err) {
$orderErrors[] = 'item id='.($item['id'] ?? '?').': '.$err;
}
}
}
if ($orderErrors) {
$ordersWithErrors++;
$queryHasError = true;
$errorLogger->error('Hibás megrendelés találat', [
'sourceQueryIndex' => $queryIndex,
'orderId' => $order['id'] ?? null,
'errors' => $orderErrors,
'order' => $order,
]);
}
$bar->advance();
}
if ($queryHasError) {
$queriesWithErrors++;
}
}
fclose($handle);
$bar->finish();
$this->newLine(2);
$this->table(['Metrika', 'Érték'], [
['Vizsgált lekérdezések (API hívások) száma', $queryIndex],
['Hibát tartalmazó lekérdezések száma', $queriesWithErrors],
['Vizsgált megrendelések száma', $totalOrders],
['Hibás megrendelések száma', $ordersWithErrors],
['Hibás tétel(item) számítás találatok száma', $itemErrorCount],
['Profit center hiba találatok száma', $profitCenterErrorCount],
]);
if ($ordersWithErrors > 0) {
$this->warn('Hibás rendelések naplózva ide: '.$errorLogPath);
} else {
$this->info('Nem található hiba.');
}
return self::SUCCESS;
}
private function resolveLogFile(?string $file): ?string
{
$logsDir = storage_path('logs');
if ($file) {
$path = $logsDir.DIRECTORY_SEPARATOR.$file;
return is_file($path) ? $path : null;
}
$dated = [];
foreach (glob($logsDir.DIRECTORY_SEPARATOR.'api-*.log') ?: [] as $path) {
if (preg_match('/api-(\d{4}-\d{2}-\d{2})\.log$/', basename($path), $m)) {
$dated[$m[1]] = $path;
}
}
if (! $dated) {
return null;
}
krsort($dated);
return reset($dated);
}
private function errorLogPathFor(string $filePath): string
{
$suffix = preg_match('/api-(\d{4}-\d{2}-\d{2})\.log$/', basename($filePath), $m)
? $m[1]
: now()->format('Y-m-d');
return storage_path("logs/api-check-errors-{$suffix}.log");
}
private function preprocess(string $filePath): array
{
$totalQueries = 0;
$totalOrders = 0;
$handle = fopen($filePath, 'r');
while (($line = fgets($handle)) !== false) {
$responseBody = $this->extractUnprocessedResponse($line);
if ($responseBody === null) {
continue;
}
$totalQueries++;
$totalOrders += count($responseBody['data'] ?? []);
}
fclose($handle);
return [$totalQueries, $totalOrders];
}
/**
* Csak a /api/v1/order/unprocessed válaszokat ismeri fel (a "pagination" kulcs csak ott van jelen,
* a setProcessed/status válaszokban nincs).
*/
private function extractUnprocessedResponse(string $line): ?array
{
if (! str_contains($line, 'API response')) {
return null;
}
$context = $this->extractBalancedJson($line, 'API response');
if ($context === null || ! isset($context['responseBody']) || ! is_array($context['responseBody'])) {
return null;
}
$body = $context['responseBody'];
if (! isset($body['pagination'], $body['data']) || ! is_array($body['data'])) {
return null;
}
return $body;
}
/**
* A Monolog sorvégi %context%/%extra% JSON-jából kiszedi az első kapcsos zárójel-pártól
* a hozzá tartozó záró zárójelig terjedő, kiegyensúlyozott JSON-t (idézőjelen belüli
* kapcsos zárójeleket figyelmen kívül hagyva), hogy a sor végén álló extra tartalom
* (pl. üres %extra% tömb) ne zavarja a dekódolást.
*/
private function extractBalancedJson(string $line, string $marker): ?array
{
$pos = strpos($line, $marker);
if ($pos === false) {
return null;
}
$jsonStart = strpos($line, '{', $pos);
if ($jsonStart === false) {
return null;
}
$depth = 0;
$inString = false;
$escape = false;
$end = null;
$len = strlen($line);
for ($i = $jsonStart; $i < $len; $i++) {
$ch = $line[$i];
if ($escape) {
$escape = false;
continue;
}
if ($ch === '\\') {
$escape = true;
continue;
}
if ($ch === '"') {
$inString = ! $inString;
continue;
}
if ($inString) {
continue;
}
if ($ch === '{') {
$depth++;
} elseif ($ch === '}') {
$depth--;
if ($depth === 0) {
$end = $i;
break;
}
}
}
if ($end === null) {
return null;
}
$decoded = json_decode(substr($line, $jsonStart, $end - $jsonStart + 1), true);
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
}
/**
* A profit center hooreycaDataActive állapotát a JELENLEGI DB-állapot alapján ellenőrzi
* (a log maga nem tartalmazza a nyers flag értékét), és jelzi, ha a válasz tartalma
* (profitCenterHooreycaId/profitCenterName jelenléte) nem egyezik a DB jelenlegi állapotával.
*/
private function checkProfitCenter(array $order): array
{
$errors = [];
$profitCenterId = $order['profit_center_id'] ?? null;
$profitCenter = $profitCenterId ? ProfitCenter::withTrashed()->find($profitCenterId) : null;
$dbActive = $profitCenter?->hooreycaDataActive;
$hasResponseData = ! empty($order['profitCenterHooreycaId']) || ! empty($order['profitCenterName']);
if (! $profitCenter) {
$errors[] = "profit_center_id={$profitCenterId} nem található a DB-ben (törölve vagy soha nem létezett)";
} elseif ($profitCenter->trashed()) {
$errors[] = "profit_center_id={$profitCenterId} soft-deleted a DB-ben, mégis szerepelt a válaszban";
} elseif (! $dbActive) {
$errors[] = "profit_center_id={$profitCenterId} hooreycaDataActive JELENLEG nem aktív a DB-ben, mégis szerepelt a válaszban";
}
if ($profitCenter && ! $profitCenter->trashed() && $dbActive && ! $hasResponseData) {
$errors[] = "profit_center_id={$profitCenterId} a DB-ben aktív, de a válaszból hiányzik a profitCenterHooreycaId/profitCenterName";
}
return $errors;
}
/**
* A HooreycaVolumen/HooreycaUnitPrice mezőket a jelenleg érvényes (OrderController::
* convertItemCollectionToHooreycaAPIItems-ben lévő) képlettel újraszámolja a naplózott
* nyers mezőkből, és összeveti a naplózott számított értékkel.
*/
private function checkItemCalculations(array $item): array
{
$errors = [];
$quantity = $item['quantity'] ?? null;
$unitMultiplier = $item['unitMultiplier'] ?? null;
$hooreycaMultiplier = $item['HooreycaMultiplier'] ?? null;
$price = $item['price'] ?? null;
$loggedVolumen = $item['HooreycaVolumen'] ?? null;
$loggedUnitPrice = $item['HooreycaUnitPrice'] ?? null;
if ($hooreycaMultiplier === null) {
$expectedVolumen = null;
$expectedUnitPrice = null;
} else {
$expectedVolumen = $quantity * $hooreycaMultiplier;
$expectedUnitPrice = $expectedVolumen != 0
? ($quantity * $unitMultiplier * $price) / $expectedVolumen
: null;
}
if (! $this->numbersMatch($expectedVolumen, $loggedVolumen)) {
$errors[] = sprintf('HooreycaVolumen eltér: várt=%s, naplózott=%s', json_encode($expectedVolumen), json_encode($loggedVolumen));
}
if (! $this->numbersMatch($expectedUnitPrice, $loggedUnitPrice)) {
$errors[] = sprintf('HooreycaUnitPrice eltér: várt=%s, naplózott=%s', json_encode($expectedUnitPrice), json_encode($loggedUnitPrice));
}
return $errors;
}
private function numbersMatch($expected, $actual, float $epsilon = 0.0001): bool
{
if ($expected === null || $actual === null) {
return $expected === $actual;
}
return abs((float) $expected - (float) $actual) < $epsilon;
}
}

View File

@ -158,7 +158,7 @@ private function sendNotification(int $orderArchiveId)
$to[] = $orderData['customNotificationEmail'];
}
if (env('APP_STAGE', 'DEV') == 'PROD') {
if (config('app.stage', 'DEV') == 'PROD') {
$to[] = $orderData['supplier']['orderEmail'];
if ($orderData['supplier']['orderEmail2']) {
$to[] = $orderData['supplier']['orderEmail2'];

View File

@ -0,0 +1,172 @@
<?php
namespace App\Filament\Pages;
use App\Models\ProfitCenter;
use App\Models\ProfitCenterSupplierCode;
use App\Models\Supplier;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Laravel\Pennant\Feature;
class SupplierCustomerCodes extends Page implements HasForms
{
use InteractsWithForms;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-identification';
protected string $view = 'filament.pages.supplier-customer-codes';
protected static ?string $title = 'Beszállítói vevőkódok';
protected static ?string $navigationLabel = 'Beszállítói vevőkódok';
protected static bool $shouldRegisterNavigation = false;
public ?array $data = [];
/**
* Rollout-védelem: a SupplierCustomerCode Pennant flag alapján dől el, ki éri el ezt
* az oldalt - a topbar menüpont is ezt hívja (ld. speed-button-nav-bar.blade.php).
*/
public static function canAccess(): bool
{
if (! $user = auth()->user()) {
return false;
}
return Feature::for($user)->active('SupplierCustomerCode');
}
public function mount(): void
{
abort_unless(static::canAccess(), 403);
$this->form->fill();
}
public function form(Schema $form): Schema
{
return $form
->schema([
Grid::make(2)
->schema([
Select::make('supplier_id')
->label('Beszállító')
->options(Supplier::query()->orderBy('name')->pluck('name', 'id'))
->required()
->searchable()
->live()
->afterStateUpdated(fn (?string $state, Set $set) => $this->loadSupplierData($state, $set)),
TextInput::make('search')
->label('Profitcenter keresése')
->placeholder('Kezdjen el gépelni a szűküléshez...')
->live(debounce: 300)
->visible(fn (Get $get) => filled($get('supplier_id'))),
]),
Toggle::make('hasCustomerCode')
->label('Vevőkód funkció aktív ennél a beszállítónál')
->visible(fn (Get $get) => filled($get('supplier_id'))),
Section::make('Profitcenterenkénti vevőkód')
->visible(fn (Get $get) => filled($get('supplier_id')))
->columns(3)
->schema(fn (Get $get) => $this->profitCenterCodeFields($get('search'))),
])
->statePath('data');
}
/**
* @return array<int, TextInput>
*/
private function profitCenterCodeFields(?string $search = null): array
{
return ProfitCenter::query()
->when(filled($search), fn ($query) => $query->where('name', 'like', '%'.$search.'%'))
->orderBy('name')
->get()
->map(fn (ProfitCenter $profitCenter) => TextInput::make("codes.{$profitCenter->id}")
->label($profitCenter->name)
->inlineLabel()
->maxLength(255))
->all();
}
private function loadSupplierData(?string $supplierId, Set $set): void
{
$codes = [];
if ($supplierId) {
$supplier = Supplier::find($supplierId);
$set('hasCustomerCode', (bool) $supplier?->hasCustomerCode);
$codes = ProfitCenterSupplierCode::query()
->where('supplier_id', $supplierId)
->pluck('customerCode', 'profit_center_id')
->all();
} else {
$set('hasCustomerCode', false);
}
foreach (ProfitCenter::query()->pluck('id') as $profitCenterId) {
$set("codes.{$profitCenterId}", $codes[$profitCenterId] ?? null);
}
}
protected function getFormActions(): array
{
return [
Action::make('save')
->label('Mentés')
->submit('save')
->color('primary'),
];
}
public function save(): void
{
$state = $this->form->getState();
$supplierId = $state['supplier_id'];
Supplier::whereKey($supplierId)->update([
'hasCustomerCode' => (bool) ($state['hasCustomerCode'] ?? false),
]);
// A form aktuális szűrés (keresés) miatt csak a látható mezőket adná vissza a getState() -
// a $this->data-ban viszont minden profitcenter kódja benne marad, mert a loadSupplierData()
// mindegyiket explicit beállítja, függetlenül attól, hogy éppen renderelve van-e.
foreach ($this->data['codes'] ?? [] as $profitCenterId => $customerCode) {
$customerCode = is_string($customerCode) ? trim($customerCode) : $customerCode;
if (blank($customerCode)) {
ProfitCenterSupplierCode::query()
->where('supplier_id', $supplierId)
->where('profit_center_id', $profitCenterId)
->delete();
continue;
}
ProfitCenterSupplierCode::updateOrCreate(
['supplier_id' => $supplierId, 'profit_center_id' => $profitCenterId],
['customerCode' => $customerCode],
);
}
Notification::make()
->title('Mentve')
->body('A beszállító vevőkód adatai elmentve.')
->success()
->send();
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Filament\Resources\FeatureFlagOverrides;
use App\Filament\Resources\FeatureFlagOverrides\Pages\CreateFeatureFlagOverride;
use App\Filament\Resources\FeatureFlagOverrides\Pages\EditFeatureFlagOverride;
use App\Filament\Resources\FeatureFlagOverrides\Pages\ListFeatureFlagOverrides;
use App\Filament\Resources\FeatureFlagOverrides\Schemas\FeatureFlagOverrideForm;
use App\Filament\Resources\FeatureFlagOverrides\Tables\FeatureFlagOverridesTable;
use App\Models\FeatureFlagOverride;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
class FeatureFlagOverrideResource extends Resource
{
protected static ?string $model = FeatureFlagOverride::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-user-circle';
protected static string|\UnitEnum|null $navigationGroup = 'Rendszer';
protected static ?string $navigationLabel = 'Egyéni felülbírálások';
public static function form(Schema $schema): Schema
{
return FeatureFlagOverrideForm::configure($schema);
}
public static function table(Table $table): Table
{
return FeatureFlagOverridesTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListFeatureFlagOverrides::route('/'),
'create' => CreateFeatureFlagOverride::route('/create'),
'edit' => EditFeatureFlagOverride::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\FeatureFlagOverrides\Pages;
use App\Filament\Resources\FeatureFlagOverrides\FeatureFlagOverrideResource;
use Filament\Resources\Pages\CreateRecord;
class CreateFeatureFlagOverride extends CreateRecord
{
protected static string $resource = FeatureFlagOverrideResource::class;
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\FeatureFlagOverrides\Pages;
use App\Filament\Resources\FeatureFlagOverrides\FeatureFlagOverrideResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditFeatureFlagOverride extends EditRecord
{
protected static string $resource = FeatureFlagOverrideResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\FeatureFlagOverrides\Pages;
use App\Filament\Resources\FeatureFlagOverrides\FeatureFlagOverrideResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListFeatureFlagOverrides extends ListRecords
{
protected static string $resource = FeatureFlagOverrideResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Filament\Resources\FeatureFlagOverrides\Schemas;
use App\Models\FeatureFlag;
use App\Models\User;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
class FeatureFlagOverrideForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('feature_flag_id')
->label('Feature flag')
->searchable()
->options(fn () => FeatureFlag::pluck('label', 'id'))
->disabledOn('edit')
->required(),
Select::make('user_id')
->label('Felhasználó')
->searchable()
->options(fn () => User::pluck('name', 'id'))
->disabledOn('edit')
->required(),
Toggle::make('active')
->label('Bekapcsolva')
->required(),
]);
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Filament\Resources\FeatureFlagOverrides\Tables;
use App\Models\FeatureFlag;
use App\Models\User;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
class FeatureFlagOverridesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('featureFlag.label')
->label('Feature flag')
->searchable()
->sortable(),
TextColumn::make('user.name')
->label('Felhasználó')
->searchable()
->sortable(),
IconColumn::make('active')
->label('Állapot')
->boolean(),
TextColumn::make('updated_at')
->label('Módosítva')
->dateTime()
->sortable(),
])
->filters([
SelectFilter::make('feature_flag_id')
->label('Feature flag')
->options(fn () => FeatureFlag::pluck('label', 'id')),
SelectFilter::make('user_id')
->label('Felhasználó')
->options(fn () => User::pluck('name', 'id'))
->searchable(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Filament\Resources\FeatureFlags;
use App\Filament\Resources\FeatureFlags\Pages\CreateFeatureFlag;
use App\Filament\Resources\FeatureFlags\Pages\EditFeatureFlag;
use App\Filament\Resources\FeatureFlags\Pages\ListFeatureFlags;
use App\Filament\Resources\FeatureFlags\RelationManagers\OverridesRelationManager;
use App\Filament\Resources\FeatureFlags\Schemas\FeatureFlagForm;
use App\Filament\Resources\FeatureFlags\Tables\FeatureFlagsTable;
use App\Models\FeatureFlag;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
class FeatureFlagResource extends Resource
{
protected static ?string $model = FeatureFlag::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-flag';
protected static string|\UnitEnum|null $navigationGroup = 'Rendszer';
protected static ?string $navigationLabel = 'Feature flagek';
public static function form(Schema $schema): Schema
{
return FeatureFlagForm::configure($schema);
}
public static function table(Table $table): Table
{
return FeatureFlagsTable::configure($table);
}
public static function getRelations(): array
{
return [
OverridesRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => ListFeatureFlags::route('/'),
'create' => CreateFeatureFlag::route('/create'),
'edit' => EditFeatureFlag::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\FeatureFlags\Pages;
use App\Filament\Resources\FeatureFlags\FeatureFlagResource;
use Filament\Resources\Pages\CreateRecord;
class CreateFeatureFlag extends CreateRecord
{
protected static string $resource = FeatureFlagResource::class;
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\FeatureFlags\Pages;
use App\Filament\Resources\FeatureFlags\FeatureFlagResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditFeatureFlag extends EditRecord
{
protected static string $resource = FeatureFlagResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\FeatureFlags\Pages;
use App\Filament\Resources\FeatureFlags\FeatureFlagResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListFeatureFlags extends ListRecords
{
protected static string $resource = FeatureFlagResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Filament\Resources\FeatureFlags\RelationManagers;
use App\Models\User;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class OverridesRelationManager extends RelationManager
{
protected static string $relationship = 'overrides';
protected static ?string $title = 'Egyéni felülbírálások';
public function form(Schema $schema): Schema
{
return $schema
->components([
Select::make('user_id')
->label('Felhasználó')
->searchable()
->options(fn () => User::pluck('name', 'id'))
->disabledOn('edit')
->required(),
Toggle::make('active')
->label('Bekapcsolva')
->required(),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('user.name')
->columns([
TextColumn::make('user.name')
->label('Felhasználó')
->searchable(),
IconColumn::make('active')
->label('Állapot')
->boolean(),
TextColumn::make('updated_at')
->label('Módosítva')
->dateTime()
->sortable(),
])
->headerActions([
CreateAction::make(),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
]);
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Filament\Resources\FeatureFlags\Schemas;
use App\Models\Role;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class FeatureFlagForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Section::make('Alapadatok')
->columnSpanFull()
->schema([
TextInput::make('name')
->label('Kulcs')
->helperText('Ezt a kulcsot kell használni a kódban: Feature::active(\'kulcs\')')
->required()
->unique(ignoreRecord: true)
->maxLength(255),
TextInput::make('label')
->label('Megnevezés')
->required()
->maxLength(255),
Textarea::make('description')
->label('Leírás')
->columnSpanFull(),
Toggle::make('enabled')
->label('Bekapcsolva (rendszer szintű kapcsoló)')
->default(true),
]),
Section::make('Célzás')
->columnSpanFull()
->schema([
TagsInput::make('stages')
->label('Stage-ek')
->helperText('Üresen hagyva minden környezeten aktív. Ismert értékek: DEV, TEST, PROD, d2dtst, e2etst.')
->placeholder('pl. TEST'),
Select::make('roles')
->label('Szerepkörök')
->helperText('Üresen hagyva minden szerepkörnek aktív.')
->multiple()
->options(fn () => Role::pluck('display_name', 'name')->filter()->all()),
]),
]);
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Filament\Resources\FeatureFlags\Tables;
use App\Models\FeatureFlag;
use App\Models\FeatureFlagOverride;
use App\Models\User;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Radio;
use Filament\Forms\Components\Select;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Table;
class FeatureFlagsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Kulcs')
->searchable()
->sortable(),
TextColumn::make('label')
->label('Megnevezés')
->searchable(),
ToggleColumn::make('enabled')
->label('Bekapcsolva'),
TextColumn::make('stages')
->label('Stage-ek')
->badge()
->placeholder('mind'),
TextColumn::make('roles')
->label('Szerepkörök')
->badge()
->placeholder('mind'),
TextColumn::make('updated_at')
->label('Módosítva')
->dateTime()
->sortable(),
])
->recordActions([
EditAction::make(),
Action::make('userOverride')
->label('Egyéni felülbírálás')
->icon('heroicon-o-user')
->form([
Select::make('user_id')
->label('Felhasználó')
->searchable()
->options(fn () => User::pluck('name', 'id'))
->required(),
Radio::make('override')
->label('Állapot')
->options([
'inherit' => 'Örökölt (alapértelmezett szabály érvényes)',
'on' => 'Bekapcsolva',
'off' => 'Kikapcsolva',
])
->default('inherit')
->required(),
])
->action(function (array $data, FeatureFlag $record) {
$user = User::findOrFail($data['user_id']);
if ($data['override'] === 'inherit') {
// model-instance delete() kell, hogy a FeatureFlagOverrideObserver
// (Pennant purge) ténylegesen kiváltódjon - a query builder delete()
// nem hívja meg a modell eseményeit
FeatureFlagOverride::where('feature_flag_id', $record->id)
->where('user_id', $user->id)
->first()
?->delete();
} else {
FeatureFlagOverride::updateOrCreate(
['feature_flag_id' => $record->id, 'user_id' => $user->id],
['active' => $data['override'] === 'on'],
);
}
}),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@ -90,13 +90,13 @@ public function index(): View|Response|JsonResponse
$m->subject('Árlista változás ');
$m->to($emailAddress);
/*
if(env('APP_STAGE')=='PROD'){
if(config('app.stage')=='PROD'){
$m->to($emailAddress);
}else{
if(env('APP_STAGE')=='TEST'){
if(config('app.stage')=='TEST'){
$m->to('beszerzes@delirest.hu');
}
if(env('APP_STAGE')=='DEV'){
if(config('app.stage')=='DEV'){
$m->to('city@e98.hu');
}
@ -303,13 +303,13 @@ private function notificationNewPriceList($priceListId)
$destEmail->each(function ($emailAddress) use ($emailData) {
\Mail::send('email.priceListChangeNotify', $emailData, function ($m) use ($emailAddress) {
$m->subject('Árlista változás ');
if (env('APP_STAGE') == 'PROD') {
if (config('app.stage') == 'PROD') {
$m->to($emailAddress);
} else {
if (env('APP_STAGE') == 'TEST') {
if (config('app.stage') == 'TEST') {
$m->to('beszerzes@delirest.hu');
}
if (env('APP_STAGE') == 'DEV') {
if (config('app.stage') == 'DEV') {
$m->to('city@e98.hu');
}
}

View File

@ -102,6 +102,12 @@ private function getSupplierDataFromRequest(Request $request)
$data[$key] = $request->get($key);
}
$data['status'] = DbStatusFieldEnum::active;
// A hasCustomerCode mező csak akkor jelenik meg a formon, ha a SupplierCustomerCode
// feature flag aktív - ilyenkor nem szabad null-t írni a NOT NULL boolean oszlopba.
if ($request->has('hasCustomerCode')) {
$data['hasCustomerCode'] = $request->boolean('hasCustomerCode');
}
if ($contactId = $request->get('contactId')) {
$data['contactId'] = $contactId;
} else {

View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
class FeatureFlag extends BaseAuditable
{
protected $casts = [
'enabled' => 'boolean',
'stages' => 'array',
'roles' => 'array',
];
public function overrides(): HasMany
{
return $this->hasMany(FeatureFlagOverride::class);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class FeatureFlagOverride extends BaseAuditable
{
protected $casts = [
'active' => 'boolean',
];
public function featureFlag(): BelongsTo
{
return $this->belongsTo(FeatureFlag::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -33,6 +34,11 @@ public function suppliers(): BelongsToMany
return $this->belongsToMany(Supplier::class)->withTimestamps();
}
public function supplierCustomerCodes(): HasMany
{
return $this->hasMany(ProfitCenterSupplierCode::class);
}
public function contact(): MorphMany
{
return $this->morphMany(Contact::class, 'contactable');

View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class ProfitCenterSupplierCode extends BaseAuditable
{
use SoftDeletes;
protected $guarded = ['id', 'created_at', 'updated_at'];
public function profitCenter(): BelongsTo
{
return $this->belongsTo(ProfitCenter::class);
}
public function supplier(): BelongsTo
{
return $this->belongsTo(Supplier::class);
}
}

View File

@ -44,6 +44,11 @@ public function profitCenter(): BelongsToMany
return $this->belongsToMany(ProfitCenter::class)->withTimestamps();
}
public function customerCodes(): HasMany
{
return $this->hasMany(ProfitCenterSupplierCode::class);
}
public function users(): HasMany
{
return $this->hasMany(User::class);

View File

@ -0,0 +1,23 @@
<?php
namespace App\Observers;
use App\Models\FeatureFlag;
use Laravel\Pennant\Feature;
class FeatureFlagObserver
{
/**
* A Pennant lazy módon, scope-onként gyorsítótárazza a feloldott értékeket -
* mentés/törlés után purge-elni kell, hogy a módosítás azonnal érvényesüljön.
*/
public function saved(FeatureFlag $flag): void
{
Feature::purge($flag->name);
}
public function deleted(FeatureFlag $flag): void
{
Feature::purge($flag->name);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Observers;
use App\Models\FeatureFlagOverride;
use Laravel\Pennant\Feature;
class FeatureFlagOverrideObserver
{
/**
* Létrehozás/módosítás/törlés után csak az érintett usernek a gyorsítótárazott
* Pennant-feloldását érvénytelenítjük, a többi felhasználót nem érinti.
*/
public function saved(FeatureFlagOverride $override): void
{
$this->forget($override);
}
public function deleted(FeatureFlagOverride $override): void
{
$this->forget($override);
}
private function forget(FeatureFlagOverride $override): void
{
$user = $override->user;
$flag = $override->featureFlag;
if ($user && $flag) {
Feature::for($user)->forget($flag->name);
}
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Policies;
use App\Models\FeatureFlagOverride;
use App\Models\User;
use Laravel\Pennant\Feature;
class FeatureFlagOverridePolicy
{
/**
* Ugyanaz a "FeatureFlagAdmin" flag szabályozza a hozzáférést, mint a
* FeatureFlagPolicy-nál - a felülbírálások ugyanannak a jogosultsági
* körnek a részei, mint maguk a flagek.
*/
protected function hasAccess(User $user): bool
{
return Feature::for($user)->active('FeatureFlagAdmin');
}
public function viewAny(User $user): bool
{
return $this->hasAccess($user);
}
public function view(User $user, FeatureFlagOverride $featureFlagOverride): bool
{
return $this->hasAccess($user);
}
public function create(User $user): bool
{
return $this->hasAccess($user);
}
public function update(User $user, FeatureFlagOverride $featureFlagOverride): bool
{
return $this->hasAccess($user);
}
public function delete(User $user, FeatureFlagOverride $featureFlagOverride): bool
{
return $this->hasAccess($user);
}
public function deleteAny(User $user): bool
{
return $this->hasAccess($user);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Policies;
use App\Models\FeatureFlag;
use App\Models\User;
use Laravel\Pennant\Feature;
class FeatureFlagPolicy
{
/**
* A feature flag admin felület elérését magára a "FeatureFlagAdmin" flagre bízzuk,
* hogy a hozzáférés szabályozása (stage/szerepkör/felhasználó szinten) deploy
* nélkül, az admin felületről állítható legyen.
*/
protected function hasAccess(User $user): bool
{
return Feature::for($user)->active('FeatureFlagAdmin');
}
public function viewAny(User $user): bool
{
return $this->hasAccess($user);
}
public function view(User $user, FeatureFlag $featureFlag): bool
{
return $this->hasAccess($user);
}
public function create(User $user): bool
{
return $this->hasAccess($user);
}
public function update(User $user, FeatureFlag $featureFlag): bool
{
return $this->hasAccess($user);
}
public function delete(User $user, FeatureFlag $featureFlag): bool
{
return $this->hasAccess($user);
}
public function deleteAny(User $user): bool
{
return $this->hasAccess($user);
}
}

View File

@ -2,6 +2,10 @@
namespace App\Providers;
use App\Models\FeatureFlag;
use App\Models\FeatureFlagOverride;
use App\Observers\FeatureFlagObserver;
use App\Observers\FeatureFlagOverrideObserver;
use App\Repositories\AddressRepository;
use App\Repositories\AddressRepositoryInterface;
use App\Repositories\ContactRepository;
@ -20,6 +24,8 @@
use App\Repositories\ProfitCenterRepositoryInterface;
use App\Repositories\SupplierRepository;
use App\Repositories\SupplierRepositoryInterface;
use App\Services\FeatureFlagRegistrar;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
@ -53,6 +59,15 @@ public function boot(): void
{
URL::forceScheme('https');
FeatureFlag::observe(FeatureFlagObserver::class);
FeatureFlagOverride::observe(FeatureFlagOverrideObserver::class);
// Schema::hasTable védi a friss telepítést / migrate-et megelőző state-et,
// amikor a feature_flags tábla még nem létezik.
if (Schema::hasTable('feature_flags')) {
app(FeatureFlagRegistrar::class)->registerAll();
}
\Illuminate\Database\Schema\Blueprint::macro('userIdFields', function () {
$this->unsignedInteger('created_by')->nullable();
$this->unsignedInteger('updated_by')->nullable();
@ -62,6 +77,7 @@ public function boot(): void
\Livewire\Livewire::component('app.filament.pages.price-list-processor', \App\Filament\Pages\PriceListProcessor::class);
\Livewire\Livewire::component('app.filament.auth.login', \App\Filament\Auth\Login::class);
\Livewire\Livewire::component('app.filament.pages.calendar-test', \App\Filament\Pages\CalendarTest::class);
\Livewire\Livewire::component('app.filament.pages.supplier-customer-codes', \App\Filament\Pages\SupplierCustomerCodes::class);
\Livewire\Livewire::component('app.filament.widgets.delivery-calendar-widget', \App\Filament\Widgets\DeliveryCalendarWidget::class);
\Livewire\Livewire::component('app.filament.supplier-portal.resources.product-stocks.pages.list-product-stocks', \App\Filament\SupplierPortal\Resources\ProductStocks\Pages\ListProductStocks::class);
@ -88,6 +104,15 @@ public function boot(): void
\Livewire\Livewire::component('app.filament.resources.profit-center-supplier-schedules.pages.create-profit-center-supplier-schedule', \App\Filament\Resources\ProfitCenterSupplierSchedules\Pages\CreateProfitCenterSupplierSchedule::class);
\Livewire\Livewire::component('app.filament.resources.profit-center-supplier-schedules.pages.edit-profit-center-supplier-schedule', \App\Filament\Resources\ProfitCenterSupplierSchedules\Pages\EditProfitCenterSupplierSchedule::class);
\Livewire\Livewire::component('app.filament.resources.feature-flags.pages.list-feature-flags', \App\Filament\Resources\FeatureFlags\Pages\ListFeatureFlags::class);
\Livewire\Livewire::component('app.filament.resources.feature-flags.pages.create-feature-flag', \App\Filament\Resources\FeatureFlags\Pages\CreateFeatureFlag::class);
\Livewire\Livewire::component('app.filament.resources.feature-flags.pages.edit-feature-flag', \App\Filament\Resources\FeatureFlags\Pages\EditFeatureFlag::class);
\Livewire\Livewire::component('app.filament.resources.feature-flags.relation-managers.overrides-relation-manager', \App\Filament\Resources\FeatureFlags\RelationManagers\OverridesRelationManager::class);
\Livewire\Livewire::component('app.filament.resources.feature-flag-overrides.pages.list-feature-flag-overrides', \App\Filament\Resources\FeatureFlagOverrides\Pages\ListFeatureFlagOverrides::class);
\Livewire\Livewire::component('app.filament.resources.feature-flag-overrides.pages.create-feature-flag-override', \App\Filament\Resources\FeatureFlagOverrides\Pages\CreateFeatureFlagOverride::class);
\Livewire\Livewire::component('app.filament.resources.feature-flag-overrides.pages.edit-feature-flag-override', \App\Filament\Resources\FeatureFlagOverrides\Pages\EditFeatureFlagOverride::class);
\Livewire\Livewire::component('filament.livewire.notifications', \Filament\Livewire\Notifications::class);
\Livewire\Livewire::component('filament.livewire.database-notifications', \Filament\Livewire\DatabaseNotifications::class);
\Livewire\Livewire::component('filament.livewire.sidebar', \Filament\Livewire\Sidebar::class);

View File

@ -13,7 +13,8 @@ class AuthServiceProvider extends ServiceProvider
* @var array<class-string, class-string>
*/
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
\App\Models\FeatureFlag::class => \App\Policies\FeatureFlagPolicy::class,
\App\Models\FeatureFlagOverride::class => \App\Policies\FeatureFlagOverridePolicy::class,
];
/**

View File

@ -0,0 +1,53 @@
<?php
namespace App\Services;
use App\Models\FeatureFlag;
use App\Models\FeatureFlagOverride;
use App\Models\User;
use Laravel\Pennant\Feature;
class FeatureFlagRegistrar
{
/**
* Regisztrálja az összes adatbázisban tárolt feature flaget a Pennant-ban.
*
* Az egyéni felhasználói felülbírálásokat szándékosan NEM a Pennant saját
* tárolóján (Feature::for($user)->activate()) keresztül kezeljük, mert a
* Pennant purge() minden scope-ra töröl egy adott flag névre - egy admin
* mentés (pl. enabled kikapcsolása) így törölné az explicit felülbírálásokat
* is. Ezért az override-okat saját táblában tároljuk, a resolver pedig ezt
* nézi meg elsőként; a Pennant tárolója így csak a szabály-alapú
* alapértelmezett feloldást cache-eli, amit bármikor biztonságos purge-elni.
*/
public function registerAll(): void
{
FeatureFlag::all()->each(function (FeatureFlag $flag) {
Feature::define($flag->name, function (?User $user) use ($flag) {
if ($user) {
$override = FeatureFlagOverride::where('feature_flag_id', $flag->id)
->where('user_id', $user->id)
->first();
if ($override) {
return $override->active;
}
}
if (! $flag->enabled) {
return false;
}
if ($flag->stages && ! in_array(config('app.stage'), $flag->stages, true)) {
return false;
}
if ($flag->roles && (! $user || ! $user->hasRole($flag->roles))) {
return false;
}
return true;
});
});
}
}

View File

@ -106,6 +106,7 @@ public function getExcelFieldPointer(): array
19 => 'Vevői megnevezés',
20 => 'Megjegyzés',
21 => 'KREL',
22 => 'Akció',
];
private array $excelFieldPointer = [
@ -131,6 +132,7 @@ public function getExcelFieldPointer(): array
'buyerProductName' => 19,
'note' => 20,
'krel' => 21,
'specialOffer' => 22,
];
@ -157,6 +159,7 @@ public function getExcelFieldPointer(): array
'buyerProductName' => ['string'], // 19
'note' => ['string'], // 20
'krel' => ['string'], // 21
'specialOffer' => ['string'], // 22
];
private array $excelFieldConverter = [
@ -165,6 +168,7 @@ public function getExcelFieldPointer(): array
'vat' => ['percentage'],
'note' => ['null'],
'krel' => ['booleanCustom'],
'specialOffer' => ['booleanCustom'],
'hooreycaId' => ['null'],
'HooreycaUnit' => ['null'],
'HooreycaMultiplier' => ['null'],

View File

@ -187,6 +187,15 @@ public function updateStepStatus(
private const HEADER_ROW = 3;
private const DATA_START_ROW = 5;
/**
* Opcionális (visszafelé kompatibilis) oszlopfejlécek indexei.
* Ha egy ilyen oszlop hiányzik a 3. sorból, az nem blokkoló hiba, csak figyelmeztetés,
* és a hozzá tartozó feldolgozás (pl. akciós jelölés) kimarad.
*/
private const OPTIONAL_HEADER_INDEXES = [
22, // Akció (specialOffer)
];
/**
* Előfeldolgozás (Pre-processing) - Strukturális és technikai ellenőrzés
*
@ -384,6 +393,12 @@ private function buildColumnMap(Worksheet $worksheet): array
if (isset($headerByName[$normalizedExpected])) {
$map[$index] = $headerByName[$normalizedExpected];
$usedColumns[$headerByName[$normalizedExpected]] = true;
} elseif (in_array($index, self::OPTIONAL_HEADER_INDEXES, true)) {
// Opcionális oszlop hiánya nem blokkoló, csak figyelmeztetés (visszafelé kompatibilitás)
$warnings[] = $this->buildWarning(
PreProcessErrorCode::COLUMN_MISSING,
"Hiányzó opcionális oszlopfejléc: '{$expectedLabel}' (a " . self::HEADER_ROW . ". sorban nem található); a feldolgozás e nélkül folytatódik."
);
} else {
// Hiányzó kötelező fejléc: blokkoló hiba
$errors[] = $this->buildError(
@ -626,7 +641,9 @@ public function validate(PricelistFile $pricelistFile): bool
PricelistFileLine::insert($dataToInsert);
}
$this->runBusinessValidation($pricelistFile);
// Az "Akció" (specialOffer) opcionális oszlop: csak akkor kezeljük, ha ténylegesen szerepel a fájlban
$hasSpecialOfferColumn = isset($columnMap[22]);
$this->runBusinessValidation($pricelistFile, $hasSpecialOfferColumn);
$duration = round(microtime(true) - $startTime, 2);
@ -897,7 +914,7 @@ protected function validateProducer(array $payload, array $producerLookup): ?str
*
* @param PricelistFile $pricelistFile
*/
protected function runBusinessValidation(PricelistFile $pricelistFile): void
protected function runBusinessValidation(PricelistFile $pricelistFile, bool $hasSpecialOfferColumn = false): void
{
$this->updateStepStatus($pricelistFile, PricelistWorkflowStep::Validation, 'inprogress', 'Üzleti validálás...', 100);
@ -908,15 +925,17 @@ protected function runBusinessValidation(PricelistFile $pricelistFile): void
$producerIdToName = $this->getProducerIdMap();
$groupIdToPath = $this->getProductGroupIdMap();
$pricelistFile->lines()->chunk(1000, function ($lines) use ($groupLookup, $producerLookup, $productLookup, $producerIdToName, $groupIdToPath) {
$pricelistFile->lines()->chunk(1000, function ($lines) use ($groupLookup, $producerLookup, $productLookup, $producerIdToName, $groupIdToPath, $hasSpecialOfferColumn) {
$updates = [];
foreach ($lines as $line) {
$payload = $line->payload;
$validationMessages = [];
// 1. Kötelező mezők ellenőrzése (0-12 és 14)
$mandatoryIndexes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14];
// 1. Kötelező mezők ellenőrzése (0-2, 4-12 és 14)
// Megjegyzés: az "Alcsoport 2" (3. index) NEM kötelező, mivel vannak
// olyan termékek, amelyek csak az "Alcsoport 1" szinthez vannak rendelve.
$mandatoryIndexes = [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14];
foreach ($mandatoryIndexes as $mIndex) {
$mLabel = PriceListService::EXPECTED_HEADERS[$mIndex];
$mValue = $payload[$mLabel] ?? null;
@ -1116,6 +1135,25 @@ protected function runBusinessValidation(PricelistFile $pricelistFile): void
];
}
}
// 5. Akciós (specialOffer) módosulás - csak ha az "Akció" oszlop szerepel a fájlban.
// booleanCustom logika: üres érték => false, bármilyen más érték => true.
if ($hasSpecialOfferColumn) {
$specialOfferRaw = $payload[PriceListService::EXPECTED_HEADERS[22]] ?? null;
$newSpecialOffer = strlen(trim((string)$specialOfferRaw)) > 0;
$oldSpecialOffer = (bool)($existingProduct['specialOffer'] ?? false);
if ($newSpecialOffer !== $oldSpecialOffer) {
$isUpdated = true;
$diff['specialOffer'] = [
'old' => (int)$oldSpecialOffer,
'new' => (int)$newSpecialOffer,
'old_label' => $oldSpecialOffer ? 'Akciós' : 'Nem akciós',
'new_label' => $newSpecialOffer ? 'Akciós' : 'Nem akciós',
'label' => PriceListService::EXPECTED_HEADERS[22],
];
}
}
}
$updates[] = [

View File

@ -18,6 +18,7 @@
"laravel-json-api/laravel": "^5.1",
"laravel-lang/lang": "^15.0",
"laravel/framework": "^12.48",
"laravel/pennant": "^1.24",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.10.1",
"league/flysystem-aws-s3-v3": "^3.0",

79
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "05f09a61a6722503ee896c57c530bf6b",
"content-hash": "e6b0d54f37a81c8c859c977ad5d3527c",
"packages": [
{
"name": "ahmedhakeem/extra",
@ -5084,6 +5084,83 @@
},
"time": "2026-03-26T14:51:54+00:00"
},
{
"name": "laravel/pennant",
"version": "v1.24.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pennant.git",
"reference": "b99fbc8038eb2c0a2642e9e13077a75e892b61d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pennant/zipball/b99fbc8038eb2c0a2642e9e13077a75e892b61d9",
"reference": "b99fbc8038eb2c0a2642e9e13077a75e892b61d9",
"shasum": ""
},
"require": {
"illuminate/console": "^10.0|^11.0|^12.0|^13.0",
"illuminate/container": "^10.0|^11.0|^12.0|^13.0",
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"illuminate/database": "^10.0|^11.0|^12.0|^13.0",
"illuminate/queue": "^10.0|^11.0|^12.0|^13.0",
"illuminate/support": "^10.0|^11.0|^12.0|^13.0",
"php": "^8.1",
"symfony/console": "^6.0|^7.0|^8.0",
"symfony/finder": "^6.0|^7.0|^8.0"
},
"require-dev": {
"laravel/octane": "^1.4|^2.0",
"orchestra/testbench": "^8.36|^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Feature": "Laravel\\Pennant\\Feature"
},
"providers": [
"Laravel\\Pennant\\PennantServiceProvider"
]
},
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"files": [
"src/helpers.php",
"src/functions.php"
],
"psr-4": {
"Laravel\\Pennant\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "A simple, lightweight library for managing feature flags.",
"homepage": "https://github.com/laravel/pennant",
"keywords": [
"feature",
"flags",
"laravel",
"pennant"
],
"support": {
"issues": "https://github.com/laravel/pennant/issues",
"source": "https://github.com/laravel/pennant"
},
"time": "2026-06-28T17:14:13+00:00"
},
{
"name": "laravel/prompts",
"version": "v0.3.16",

44
config/pennant.php Normal file
View File

@ -0,0 +1,44 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Pennant Store
|--------------------------------------------------------------------------
|
| Here you will specify the default store that Pennant should use when
| storing and resolving feature flag values. Pennant ships with the
| ability to store flag values in an in-memory array or database.
|
| Supported: "array", "database"
|
*/
'default' => env('PENNANT_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Pennant Stores
|--------------------------------------------------------------------------
|
| Here you may configure each of the stores that should be available to
| Pennant. These stores shall be used to store resolved feature flag
| values - you may configure as many as your application requires.
|
*/
'stores' => [
'array' => [
'driver' => 'array',
],
'database' => [
'driver' => 'database',
'connection' => null,
'table' => 'features',
],
],
];

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Laravel\Pennant\Migrations\PennantMigration;
return new class extends PennantMigration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('features', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('scope');
$table->text('value');
$table->timestamps();
$table->unique(['name', 'scope']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('features');
}
};

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('feature_flags', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('label');
$table->text('description')->nullable();
$table->boolean('enabled')->default(true);
$table->json('stages')->nullable();
$table->json('roles')->nullable();
// Audit mezők a BaseAuditable-hoz
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
$table->unsignedBigInteger('deleted_by')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('feature_flags');
}
};

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('feature_flag_overrides', function (Blueprint $table) {
$table->id();
$table->foreignId('feature_flag_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->boolean('active');
// Audit mezők a BaseAuditable-hoz
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
$table->unsignedBigInteger('deleted_by')->nullable();
$table->timestamps();
$table->unique(['feature_flag_id', 'user_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('feature_flag_overrides');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('suppliers', function (Blueprint $table) {
$table->boolean('hasCustomerCode')->default(false)->after('deliveryLeadTime');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('suppliers', function (Blueprint $table) {
$table->dropColumn('hasCustomerCode');
});
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('profit_center_supplier_codes', function (Blueprint $table) {
$table->id();
$table->foreignId('profit_center_id')->constrained()->onDelete('cascade');
$table->foreignId('supplier_id')->constrained()->onDelete('cascade');
$table->string('customerCode')->nullable();
$table->userIdFields();
$table->timestamps();
$table->softDeletes();
$table->unique(['profit_center_id', 'supplier_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('profit_center_supplier_codes');
}
};

View File

@ -0,0 +1,50 @@
<?php
namespace Database\Seeders;
use App\Models\FeatureFlag;
use App\Models\Role;
use Illuminate\Database\Seeder;
class FeatureFlagSeeder extends Seeder
{
/**
* A "developer" role és a "FeatureFlagAdmin" flag nélkül a feature flag
* admin felület (/admin/feature-flags) senki számára nem elérhető - a
* FeatureFlagPolicy/FeatureFlagOverridePolicy ezt a flaget nézi, egy
* ismeretlen (még nem seedelt) flag pedig mindenkinek inaktívnak számít.
*/
public function run(): void
{
Role::firstOrCreate(
['name' => 'developer'],
['display_name' => 'Developer', 'description' => 'Fejlesztői jogosultság'],
);
FeatureFlag::updateOrCreate(
['name' => 'FeatureFlagAdmin'],
[
'label' => 'Feature flag admin felület',
'description' => 'A feature flag admin felület (/admin/feature-flags) elérését szabályozza. Enélkül senki nem éri el a felületet.',
'enabled' => true,
'stages' => null,
'roles' => ['developer'],
],
);
FeatureFlag::updateOrCreate(
['name' => 'SupplierCustomerCode'],
[
'label' => 'Beszállítói vevőkód (profitcenterenként)',
'description' => 'A profitcenterenkénti beszállítói vevőkód kezelését szabályozza a legacy és modern felületen a fejlesztés/rollout alatt. Átmeneti kapcsoló, a funkció véglegesítése után eltávolítható.',
// enabled=true + roles=['developer'] a FeatureFlagAdmin mintáját követi: az "enabled"
// egy globális kapcsoló (false esetén roles-tól függetlenül SENKI nem látja), tehát a
// "csak developer lássa rollout alatt" állapotot enabled=true + roles szűkítéssel kell
// kifejezni, nem enabled=false-fal (az utóbbi a developer usereket is kizárja).
'enabled' => true,
'stages' => null,
'roles' => ['developer'],
],
);
}
}

View File

@ -82,6 +82,9 @@ window.PriceListProfitCenterAdmin=function (options) {
$('.sidebar-menu .sidebar-dropdown .sidebar-submenu li a.categoryItem.clickable').on('click',
function () {
let groupId=$(this).data('id');
if (groupId === 'all') {
groupId = null;
}
selectedProductGroup=groupId;
root.setDatatableHeaderText(root.getTreePathName(this));
/*
@ -91,7 +94,12 @@ window.PriceListProfitCenterAdmin=function (options) {
root.refreshDatatable();
});
$('.sidebar-wrapper a.categoryItem.categoryParent').off('click');
$('.sidebar-wrapper a.categoryItem.categoryParent').on('click', function (e) {
e.preventDefault();
$(this).next('ul.subgroup-list').slideToggle(200);
$(this).toggleClass('open');
});
}
this.refreshProductGroup=function (){
let ajaxData={

View File

@ -18,6 +18,9 @@ if(typeof Supplier==='undefined'){
if(typeof this.hasDeliveryConstraint ==="undefined"){
this.hasDeliveryConstraint=0;
}
if(typeof this.hasCustomerCode ==="undefined"){
this.hasCustomerCode=0;
}
if(typeof this.orderCutOffTime ==="undefined"){
this.orderCutOffTime=12;
}
@ -229,6 +232,7 @@ window.SupplierAdmin=function (options,SupplierItem) {
$(CSSSelectorForm+' *[data-toggle=canSeeInOrder][data-title='+root.Supplier.canSeeInOrder+']').click();
$(CSSSelectorForm+' *[data-toggle=hooreycaDataActive][data-title='+root.Supplier.hooreycaDataActive+']').click();
$(CSSSelectorForm+' *[data-toggle=hasDeliveryConstraint][data-title='+root.Supplier.hasDeliveryConstraint+']').click();
$(CSSSelectorForm+' *[data-toggle=hasCustomerCode][data-title='+root.Supplier.hasCustomerCode+']').click();
$(CSSSelectorForm+' input[name=orderCutOffTime]').val(root.Supplier.orderCutOffTime);
$(CSSSelectorForm+' select[name=deliveryLeadTime]').val(root.Supplier.deliveryLeadTime);
let productGroup=[];

View File

@ -463,6 +463,22 @@
}
.sidebar-wrapper .sidebar-menu .sidebar-dropdown .sidebar-submenu li a.categoryItem.categoryParent {
cursor: pointer;
position: relative;
}
.sidebar-wrapper .sidebar-menu .sidebar-dropdown .sidebar-submenu li a.categoryItem.categoryParent:after {
font-family: "Font Awesome 5 Free";
font-weight: 900;
content: "\f105"; /* jobbra mutató nyíl */
position: absolute;
right: 15px;
transition: transform 0.2s ease;
}
.sidebar-wrapper .sidebar-menu .sidebar-dropdown .sidebar-submenu li a.categoryItem.categoryParent.open:after {
transform: rotate(90deg); /* nyitott állapotban lefelé fordul */
}
/*--------------------------side-footer------------------------------*/
.sidebar-footer {
@ -652,6 +668,31 @@
color: #b8bfce;
}
/*----- rugalmas magasság: a Csoportok tölti ki a szabad helyet -----*/
.sidebar-wrapper .sidebar-content {
display: flex;
flex-direction: column;
}
.sidebar-wrapper .sidebar-menu,
.sidebar-wrapper .sidebar-menu > ul {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0; /* flexben a görgetés feltétele */
}
/* a Csoportok li nőjjon, a többi (Kosár, Extra) megtartja a méretét */
.sidebar-wrapper .sidebar-menu .sidebar-dropdown.active:not(.extra-block) {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
}
.sidebar-wrapper .sidebar-menu .sidebar-dropdown.active:not(.extra-block) .sidebar-submenu {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
}
</style>

View File

@ -350,6 +350,20 @@ class="form-control" id="openHours"
</div>
</div>
@feature('SupplierCustomerCode')
<div class="form-group row">
<label for="hasCustomerCode" class="col-sm-2 col-form-label">Vevőkód funkció:</label>
<div class="col-sm-10">
<div class="input-group">
<div class="btn-group customRadioBtnGroup">
<a class="btn btn-primary btn-sm notActive" data-toggle="hasCustomerCode" data-title="1">Igen</a>
<a class="btn btn-primary btn-sm active" data-toggle="hasCustomerCode" data-title="0">Nem</a>
</div>
<input type="hidden" name="hasCustomerCode" value="0">
</div>
</div>
</div>
@endfeature
<div class="form-group row">
<label for="hasDeliveryConstraint" class="col-sm-2 col-form-label">Szállítási korlátozások:</label>
<div class="col-sm-10">

View File

@ -82,7 +82,7 @@
</ul>
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-05-17 11:53";
@endphp
@ -102,7 +102,7 @@
{{--
This is displayed
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-09-30 11:14";
@ -123,7 +123,7 @@
<hr>
<p class="lead">ChangeLog </p>
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-10-17 10:51";
@ -135,7 +135,7 @@
</ul>
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-10-15 19:11";
@endphp
@ -146,7 +146,7 @@
</ul>
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-10-04 12:45";
@ -161,7 +161,7 @@
@endif
@if (env('APP_STAGE')=='PROD')
@if (config('app.stage')=='PROD')
@php
$featureStartTime="2023-09-30 09:02";
@endphp
@ -179,7 +179,7 @@
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-09-13 19:32";
@ -190,7 +190,7 @@
</ul>
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-09-10 10:21";
@ -208,7 +208,7 @@
</ul>
@endif
@if (env('APP_STAGE')=='PROD')
@if (config('app.stage')=='PROD')
@php
$featureStartTime="2023-06-18 17:20";
@ -236,7 +236,7 @@
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-06-18 17:20";
@ -249,7 +249,7 @@
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-06-09 08:10";
@endphp
@ -261,7 +261,7 @@
</ul>
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-06-08 09:22";
@endphp
@ -275,7 +275,7 @@
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-05-15 13:10";
@endphp
@ -287,7 +287,7 @@
</ul>
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-05-04 11:17";
@endphp
@ -298,7 +298,7 @@
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-04-26 07:28";
@endphp
@ -309,7 +309,7 @@
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-04-26 05:51";
@endphp
@ -322,7 +322,7 @@
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-03-30 19:41";
@endphp
@ -333,7 +333,7 @@
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-03-29 18:01";
@endphp
@ -344,7 +344,7 @@
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-03-28 23:12";
@endphp
@ -382,7 +382,7 @@
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-02-19 20:01";
@endphp
@ -397,7 +397,7 @@
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-02-15 08:12";
@endphp
@ -412,7 +412,7 @@
@endphp
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2023-01-30 05:53";
@endphp
@ -426,7 +426,7 @@
@endphp
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-11-22 13:45";
@endphp
@ -442,7 +442,7 @@
<li>NEW "Megrendelések szűrő"<a class="pmLink" href="http://pm.e98.hu/issue/EV3-63">[EV3-63]</a></li>
</ul>
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-11-02 07:12";
@endphp
@ -458,7 +458,7 @@
<li>CHANGE Termékek kiválasztásánál oszlopok szélességének átállítása</li>
</ul>
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-10-06 10:58";
@endphp
@ -472,7 +472,7 @@
<li>NEW "Legkisebb eladási egység - mennyiségi egység"<a class="pmLink" href="http://pm.e98.hu/issue/EV3-62">[EV3-62]</a></li>
</ul>
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-07-06 07:58";
@endphp
@ -486,7 +486,7 @@
<li>CHANGE,FIX "Új termékcsoport felvétel"<a class="pmLink" href="http://pm.e98.hu/issue/EV3-59">[EV3-59]</a></li>
</ul>
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-06-20 06:19";
@endphp
@ -508,7 +508,7 @@
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-06-08 11:35";
@endphp
@ -522,7 +522,7 @@
@endphp
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-06-01 06:11";
@endphp
@ -543,7 +543,7 @@
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-05-17 06:32";
@endphp
@ -559,7 +559,7 @@
</ul>
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-05-12 09:45";
@endphp
@ -580,7 +580,7 @@
@endif
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-05-10 11:53";
@endphp
@ -603,7 +603,7 @@
<li>FIX "Megrendelés visszaigazolás időpontja"<a class="pmLink" href="http://pm.e98.hu/issue/EV3-50">[EV3-50]</a></li>
</ul>
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-04-11 06:53";
@endphp
@ -620,7 +620,7 @@
<li>NEW PART "Beszállító megjegyzés rovat"<a class="pmLink" href="http://pm.e98.hu/issue/EV3-46">[EV3-46]</a></li>
</ul>
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-03-31 12:30";
@endphp
@ -640,7 +640,7 @@
</li>
</ul>
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-03-25 05:57";
@endphp
@ -672,7 +672,7 @@
</ul>
@if (env('APP_STAGE')!='PROD')
@if (config('app.stage')!='PROD')
@php
$featureStartTime="2022-03-09 07:30";
@endphp

View File

@ -1,4 +1,17 @@
@php
$deliveryChildren = [
['displayName' => 'Szállítási sablonok', 'link' => \App\Filament\Resources\DeliverySchedules\DeliveryScheduleResource::getUrl()],
['displayName' => 'Beszállítói felülbírálások', 'link' => \App\Filament\Resources\DeliveryCalendarOverrides\DeliveryCalendarOverrideResource::getUrl()],
['displayName' => 'Profitcenter ütemezés', 'link' => \App\Filament\Resources\ProfitCenterSupplierSchedules\ProfitCenterSupplierScheduleResource::getUrl()],
['displayName' => 'Munkanaptárak', 'link' => \App\Filament\Resources\WorkCalendars\WorkCalendarResource::getUrl()],
];
// A vevőkód-kezelő oldalt a Szállítás menü utolsó elemeként jelenítjük meg,
// a SupplierCustomerCode feature flagre bízva a láthatóságot (rollout-védelem).
if (\App\Filament\Pages\SupplierCustomerCodes::canAccess()) {
$deliveryChildren[] = ['displayName' => 'Beszállítói vevőkódok', 'link' => \App\Filament\Pages\SupplierCustomerCodes::getUrl()];
}
$modules = [
['name' => 'home', 'displayName' => 'Főoldal', 'icon' => 'home', 'link' => route('legacy.show', ['path' => 'dashboard']), 'roles' => ['root','admin','developer','profit-center']],
['name' => 'order', 'displayName' => 'Új megrendelés', 'icon' => 'bevetelezes', 'link' => route('legacy.show', ['path' => 'order']), 'roles' => ['root','profit-center']],
@ -13,14 +26,20 @@
['name' => 'statistics', 'displayName' => 'Statisztikák', 'icon' => 'kimutatasok', 'link' => route('legacy.show', ['path' => 'admin/statistics']), 'roles' => ['root','developer','admin']],
['name' => 'export', 'displayName' => 'Export', 'icon' => 'export', 'link' => route('legacy.show', ['path' => 'admin/export']), 'roles' => ['root','developer','admin']],
['name' => 'priceListProcessor', 'displayName' => 'Árlista feldolgozó', 'icon' => 'price_list', 'link' => '/admin/pricelist-files', 'roles' => ['root','developer','admin']],
['name' => 'delivery', 'displayName' => 'Szállítás', 'icon' => 'timetable3', 'roles' => ['root','admin','developer'], 'children' => [
['displayName' => 'Szállítási sablonok', 'link' => \App\Filament\Resources\DeliverySchedules\DeliveryScheduleResource::getUrl()],
['displayName' => 'Beszállítói felülbírálások', 'link' => \App\Filament\Resources\DeliveryCalendarOverrides\DeliveryCalendarOverrideResource::getUrl()],
['displayName' => 'Profitcenter ütemezés', 'link' => \App\Filament\Resources\ProfitCenterSupplierSchedules\ProfitCenterSupplierScheduleResource::getUrl()],
['displayName' => 'Munkanaptárak', 'link' => \App\Filament\Resources\WorkCalendars\WorkCalendarResource::getUrl()],
]],
['name' => 'delivery', 'displayName' => 'Szállítás', 'icon' => 'timetable3', 'roles' => ['root','admin','developer'], 'children' => $deliveryChildren],
['name' => 'calendarTest', 'displayName' => 'Naptár Teszt', 'icon' => 'idopontok', 'link' => \App\Filament\Pages\CalendarTest::getUrl(), 'roles' => ['root','developer','admin']],
];
// A feature flag admin felület megjelenítését nem statikus role-listával,
// hanem a FeatureFlagResource jogosultság-ellenőrzésével (FeatureFlagPolicy,
// ami a "FeatureFlagAdmin" flaget nézi) döntjük el - így a hozzáférés
// deploy nélkül, az admin felületről szabályozható.
if (\App\Filament\Resources\FeatureFlags\FeatureFlagResource::canViewAny()) {
$modules[] = ['name' => 'featureFlags', 'displayName' => 'Feature flagek', 'icon' => 'setup', 'roles' => ['root','developer','admin','profit-center'], 'children' => [
['displayName' => 'Flagek', 'link' => \App\Filament\Resources\FeatureFlags\FeatureFlagResource::getUrl()],
['displayName' => 'Egyéni felülbírálások', 'link' => \App\Filament\Resources\FeatureFlagOverrides\FeatureFlagOverrideResource::getUrl()],
]];
}
@endphp
<nav class="fi-speed-button-nav flex items-center h-full ml-4 w-full">
@ -85,8 +104,8 @@ class="w-16 h-16 object-contain mb-1"
@endrole
@endforeach
</ul>
@if (env('APP_STAGE', 'DEV') != 'PROD')
<h2 class="text-[#b83400] font-bold text-lg ml-4">site:{{ env('APP_STAGE', 'DEV') }}</h2>
@if (config('app.stage', 'DEV') != 'PROD')
<h2 class="text-[#b83400] font-bold text-lg ml-4">site:{{ config('app.stage', 'DEV') }}</h2>
@endif
<div class="ml-auto flex items-center pr-2">
<img style="max-width: none; height: 59px; vertical-align: middle; margin-top: 20px;"

View File

@ -0,0 +1,20 @@
<x-filament-panels::page>
<div class="space-y-6">
<div class="p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Beszállítói vevőkódok</h5>
<p class="mb-3 font-normal text-gray-700 dark:text-gray-400">Válassza ki a beszállítót, majd adja meg a profitcenterenkénti vevőkódokat.</p>
</div>
<div class="p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
<form wire:submit="save">
{{ $this->form }}
<div class="mt-6">
<x-filament::actions
:actions="$this->getFormActions()"
/>
</div>
</form>
</div>
</div>
</x-filament-panels::page>

View File

@ -13,6 +13,19 @@
$priceListRoute=route('priceList.index');
$supplierRoute=route('supplier.index');
};
$deliveryChildren=[
['DisplayName' => 'Szállítási sablonok', 'link' => '/admin/delivery-schedules'],
['DisplayName' => 'Beszállítói felülbírálások', 'link' => '/admin/delivery-calendar-overrides'],
['DisplayName' => 'Profitcenter ütemezés', 'link' => '/admin/profit-center-supplier-schedules'],
['DisplayName' => 'Munkanaptárak', 'link' => '/admin/work-calendars'],
['DisplayName' => 'Naptár teszt', 'link' => '/admin/calendar-test'],
];
// A vevőkód-kezelő oldalt a Szállítás menü utolsó elemeként jelenítjük meg,
// a SupplierCustomerCode feature flagre bízva a láthatóságot (rollout-védelem) -
// ugyanaz a canAccess() hívás, mint a modern speed-button-nav-bar.blade.php-ban.
if (\App\Filament\Pages\SupplierCustomerCodes::canAccess()) {
$deliveryChildren[] = ['DisplayName' => 'Beszállítói vevőkódok', 'link' => \App\Filament\Pages\SupplierCustomerCodes::getUrl()];
}
$Modules=[
'home'=>[
'name'=>'home',
@ -121,7 +134,7 @@
'icon' => 'price_list',
'link' => '/admin/pricelist-files',
'noAjax' => true,
'roles' => ['developer'],
'roles' => ['developer','admin'],
],
'delivery' => [
'name' => 'delivery',
@ -130,13 +143,7 @@
'link' => '#',
'roles' => ['root', 'admin', 'developer'],
'noAjax' => true,
'children' => [
['DisplayName' => 'Szállítási sablonok', 'link' => '/admin/delivery-schedules'],
['DisplayName' => 'Beszállítói felülbírálások', 'link' => '/admin/delivery-calendar-overrides'],
['DisplayName' => 'Profitcenter ütemezés', 'link' => '/admin/profit-center-supplier-schedules'],
['DisplayName' => 'Munkanaptárak', 'link' => '/admin/work-calendars'],
['DisplayName' => 'Naptár teszt', 'link' => '/admin/calendar-test']
],
'children' => $deliveryChildren,
],
/*
'calendarTest' => [

View File

@ -25,7 +25,7 @@
// auth()->loginUsingId(1);
// auth()->logout();
if (in_array(strtolower(env('APP_STAGE')), ['d2dtst', 'e2etst'])) {
if (in_array(strtolower(config('app.stage')), ['d2dtst', 'e2etst'])) {
echo view('layout.wherehere')->render();
exit();
}
@ -38,7 +38,7 @@
]);
});
/*
if (! in_array(strtolower(env('APP_STAGE')), ['d2dtst', 'e2etst', 'dev','local'])) {
if (! in_array(strtolower(config('app.stage')), ['d2dtst', 'e2etst', 'dev','local'])) {
URL::forceScheme('https');
} else {
URL::forceScheme('http');

View File

@ -0,0 +1,70 @@
<?php
use App\Filament\Resources\FeatureFlags\FeatureFlagResource;
use App\Filament\Resources\FeatureFlags\Pages\ListFeatureFlags;
use App\Models\FeatureFlag;
use App\Models\Role;
use App\Models\User;
use App\Services\FeatureFlagRegistrar;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Livewire;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
test('a lista oldal hiba nélkül renderel, ha egy flagnek van szerepköre és stage-e', function () {
$devRole = Role::create(['name' => 'developer', 'display_name' => 'Developer']);
$developer = User::factory()->create();
$developer->addRole($devRole);
FeatureFlag::create([
'name' => 'FeatureFlagAdmin',
'label' => 'Feature flag admin felület',
'enabled' => true,
'stages' => null,
'roles' => ['developer'],
]);
FeatureFlag::create([
'name' => 'ui-test-flag',
'label' => 'UI teszt flag',
'enabled' => true,
'stages' => ['TEST'],
'roles' => ['developer'],
]);
app(FeatureFlagRegistrar::class)->registerAll();
$this->actingAs($developer);
Livewire::test(ListFeatureFlags::class)
->assertSuccessful();
});
test('csak a FeatureFlagAdmin flaggel rendelkező szerepkör éri el a feature flag admin felületet', function () {
$adminRole = Role::create(['name' => 'admin', 'display_name' => 'Admin']);
$devRole = Role::create(['name' => 'developer', 'display_name' => 'Developer']);
$admin = User::factory()->create();
$admin->addRole($adminRole);
$developer = User::factory()->create();
$developer->addRole($devRole);
FeatureFlag::create([
'name' => 'FeatureFlagAdmin',
'label' => 'Feature flag admin felület',
'enabled' => true,
'stages' => null,
'roles' => ['developer'],
]);
app(FeatureFlagRegistrar::class)->registerAll();
expect(Gate::forUser($admin)->allows('viewAny', FeatureFlag::class))->toBeFalse();
expect(Gate::forUser($developer)->allows('viewAny', FeatureFlag::class))->toBeTrue();
$this->actingAs($admin);
$this->get(FeatureFlagResource::getUrl())->assertForbidden();
$this->actingAs($developer);
$this->get(FeatureFlagResource::getUrl())->assertSuccessful();
});

View File

@ -0,0 +1,103 @@
<?php
use App\Filament\Resources\FeatureFlagOverrides\FeatureFlagOverrideResource;
use App\Filament\Resources\FeatureFlagOverrides\Pages\ListFeatureFlagOverrides;
use App\Filament\Resources\FeatureFlags\Pages\EditFeatureFlag;
use App\Models\FeatureFlag;
use App\Models\FeatureFlagOverride;
use App\Models\Role;
use App\Models\User;
use App\Services\FeatureFlagRegistrar;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Laravel\Pennant\Feature;
use Livewire\Livewire;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function actAsDeveloper(): User
{
$devRole = Role::firstOrCreate(['name' => 'developer'], ['display_name' => 'Developer']);
$developer = User::factory()->create();
$developer->addRole($devRole);
FeatureFlag::firstOrCreate(
['name' => 'FeatureFlagAdmin'],
['label' => 'Feature flag admin felület', 'enabled' => true, 'stages' => null, 'roles' => ['developer']],
);
app(FeatureFlagRegistrar::class)->registerAll();
return $developer;
}
test('a flag szerkesztő oldalán megjelenik az egyéni felülbírálások relation manager', function () {
$developer = actAsDeveloper();
$flag = FeatureFlag::create([
'name' => 'rm-test-flag',
'label' => 'RM teszt flag',
'enabled' => true,
]);
app(FeatureFlagRegistrar::class)->registerAll();
$targetUser = User::factory()->create();
FeatureFlagOverride::create([
'feature_flag_id' => $flag->id,
'user_id' => $targetUser->id,
'active' => true,
]);
$this->actingAs($developer);
Livewire::test(EditFeatureFlag::class, ['record' => $flag->getRouteKey()])
->assertSuccessful();
});
test('a globális felülbírálás lista renderel, és csak fejlesztők érik el', function () {
$developer = actAsDeveloper();
$adminRole = Role::firstOrCreate(['name' => 'admin'], ['display_name' => 'Admin']);
$admin = User::factory()->create();
$admin->addRole($adminRole);
expect(Gate::forUser($admin)->allows('viewAny', FeatureFlagOverride::class))->toBeFalse();
expect(Gate::forUser($developer)->allows('viewAny', FeatureFlagOverride::class))->toBeTrue();
$this->actingAs($admin);
$this->get(FeatureFlagOverrideResource::getUrl())->assertForbidden();
$this->actingAs($developer);
Livewire::test(ListFeatureFlagOverrides::class)->assertSuccessful();
});
test('az "örökölt"-re állítás (mass delete) is érvényteleníti a gyorsítótárazott Pennant-feloldást', function () {
actAsDeveloper();
$flag = FeatureFlag::create([
'name' => 'purge-test-flag',
'label' => 'Purge teszt flag',
'enabled' => false,
]);
app(FeatureFlagRegistrar::class)->registerAll();
$user = User::factory()->create();
FeatureFlagOverride::create([
'feature_flag_id' => $flag->id,
'user_id' => $user->id,
'active' => true,
]);
Feature::for($user)->forget('purge-test-flag');
expect(Feature::for($user)->active('purge-test-flag'))->toBeTrue();
// ugyanaz a mintázat, mint amit a FeatureFlagsTable "Egyéni felülbírálás"
// akciója az "örökölt" választásnál használ - model-instance delete()-en
// keresztül, hogy a FeatureFlagOverrideObserver kiváltódjon
FeatureFlagOverride::where('feature_flag_id', $flag->id)
->where('user_id', $user->id)
->first()
?->delete();
expect(Feature::for($user)->active('purge-test-flag'))->toBeFalse();
});

View File

@ -0,0 +1,123 @@
<?php
use App\Models\FeatureFlag;
use App\Models\FeatureFlagOverride;
use App\Models\Role;
use App\Models\User;
use App\Services\FeatureFlagRegistrar;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Config;
use Laravel\Pennant\Feature;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function defineFlag(array $attributes): FeatureFlag
{
$flag = FeatureFlag::create(array_merge([
'name' => 'test-flag',
'label' => 'Test flag',
'enabled' => true,
'stages' => null,
'roles' => null,
], $attributes));
app(FeatureFlagRegistrar::class)->registerAll();
return $flag;
}
test('a kikapcsolt flag mindenkinek inaktív', function () {
defineFlag(['enabled' => false]);
$user = User::factory()->create();
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
});
test('a stage-mismatch inaktívvá teszi a flaget', function () {
Config::set('app.stage', 'PROD');
defineFlag(['stages' => ['TEST']]);
$user = User::factory()->create();
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
});
test('a stage-match aktívvá teszi a flaget', function () {
Config::set('app.stage', 'TEST');
defineFlag(['stages' => ['TEST']]);
$user = User::factory()->create();
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
});
test('a role-mismatch inaktívvá teszi a flaget', function () {
Role::create(['name' => 'admin', 'display_name' => 'Admin']);
defineFlag(['roles' => ['admin']]);
$user = User::factory()->create();
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
});
test('a role-match aktívvá teszi a flaget', function () {
$role = Role::create(['name' => 'admin', 'display_name' => 'Admin']);
defineFlag(['roles' => ['admin']]);
$user = User::factory()->create();
$user->addRole($role);
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
});
test('a user-szintű explicit felülbírálás felülír mindent', function () {
$flag = defineFlag(['enabled' => false]);
$user = User::factory()->create();
FeatureFlagOverride::create([
'feature_flag_id' => $flag->id,
'user_id' => $user->id,
'active' => true,
]);
Feature::for($user)->forget('test-flag');
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
});
test('a flag admin-mentése nem törli az egyéni felülbírálásokat', function () {
$flag = defineFlag(['enabled' => true]);
$user = User::factory()->create();
FeatureFlagOverride::create([
'feature_flag_id' => $flag->id,
'user_id' => $user->id,
'active' => false,
]);
Feature::for($user)->forget('test-flag');
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
// Admin módosítja a flaget (pl. leírás) - ez kiváltja a FeatureFlagObserver purge-öt
$flag->update(['description' => 'módosítva']);
app(FeatureFlagRegistrar::class)->registerAll();
expect(FeatureFlagOverride::where('feature_flag_id', $flag->id)->where('user_id', $user->id)->exists())
->toBeTrue();
expect(Feature::for($user)->active('test-flag'))->toBeFalse();
});
test('üres stages/roles esetén mindenkinek aktív', function () {
defineFlag([]);
$user = User::factory()->create();
expect(Feature::for($user)->active('test-flag'))->toBeTrue();
});

View File

@ -81,6 +81,41 @@ function makeWorksheetWithHeaderOrder(array $order): Worksheet
expect($result['map'])->toHaveCount(count($expected));
});
test('a hiányzó opcionális "Akció" oszlop csak figyelmeztetést ad, nem blokkol', function () {
$expected = PriceListService::EXPECTED_HEADERS;
$worksheet = (new Spreadsheet())->getActiveSheet();
$col = 1;
foreach ($expected as $fieldIndex => $label) {
if ($fieldIndex === 22) {
// Az "Akció" oszlopot szándékosan kihagyjuk (visszafelé kompatibilis fájl)
continue;
}
$worksheet->setCellValueByColumnAndRow($col, 3, $label);
$col++;
}
$result = invokeBuildColumnMap($worksheet);
$hasBlocker = collect($result['errors'])->contains(fn ($error) => ($error['severity'] ?? '') === 'blocker');
expect($hasBlocker)->toBeFalse();
expect($result['map'])->not->toHaveKey(22);
expect(collect($result['warnings']))->not->toBeEmpty();
});
test('a jelen lévő "Akció" oszlop leképeződik', function () {
$expected = PriceListService::EXPECTED_HEADERS;
$worksheet = makeWorksheetWithHeaderOrder(array_keys($expected));
$result = invokeBuildColumnMap($worksheet);
expect($result['errors'])->toBe([]);
expect($result['map'])->toHaveKey(22);
$value = $worksheet->getCellByColumnAndRow($result['map'][22], 5)->getValue();
expect($value)->toBe('VAL_22');
});
test('hiányzó kötelező fejléc blokkoló hibát ad', function () {
$expected = PriceListService::EXPECTED_HEADERS;