Files
SpiceManagementSystem/app/Filament/User/Resources/ProductResource.php
T
Malek TellissiandClaude Sonnet 4.6 596d66b366
Deploy / deploy (push) Successful in 16s
Apply EnforcesRoles trait in ProductResource and ProductSaleResource
Import was present but use statement inside class body was missing,
so role checks had no effect on these two resources.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-13 02:37:55 +02:00

254 lines
12 KiB
PHP

<?php
namespace App\Filament\User\Resources;
use App\Traits\EnforcesRoles;
use App\Filament\User\Resources\ProductResource\Pages;
use App\Filament\User\Resources\ProductResource\RelationManagers;
use App\Models\Packing;
use App\Models\Product;
use App\Models\ProductSize;
use App\Models\Spice;
use App\Rules\SumEquals100;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\HtmlString;
use Picqer\Barcode\BarcodeGeneratorSVG;
class ProductResource extends Resource {
use EnforcesRoles;
protected static ?string $model = Product::class;
protected static ?string $navigationIcon = 'heroicon-o-inbox';
protected static ?string $navigationLabel = 'Produkte';
protected static ?string $modelLabel = 'Produkt';
protected static ?string $slug = 'produkt';
public static function form(Form $form): Form {
return $form
->schema([
Forms\Components\TextInput::make('title')
->label('Name')
->required(),
Forms\Components\TextInput::make('lot_prefix')
->label('Chargen-Präfix')
->placeholder('z.B. 112')
->unique(ignoreRecord: true)
->maxLength(20),
Placeholder::make('shopify_product_id')
->label('Shopify ID')
->content(fn ($record) => $record?->shopify_product_id
? new HtmlString('<span class="font-mono text-sm">' . e($record->shopify_product_id) . '</span>')
: new HtmlString('<span class="text-sm text-gray-400 italic">Noch nicht in Shopify erstellt</span>')
),
FileUpload::make('images')
->label('Bilder')
->multiple()
->image()
->maxFiles(15)
->directory('product-images')
->panelLayout('grid')
->imagePreviewHeight('200')
->columnSpanFull(),
Placeholder::make('description')
->label('Beschreibung')
->content(fn ($record) => $record?->description ?? '—')
->columnSpanFull(),
Forms\Components\Section::make('Rezept')
->schema([
Placeholder::make('recipe_version')
->label('Version')
->content(fn ($record) => $record?->recipe
? 'v' . $record->recipe->version . ' · ' . $record->recipe->created_at->format('d.m.Y')
: '—'),
Repeater::make('recipe_spices')
->label('Zutaten')
->schema([
Select::make('spice_id')
->label('Rohstoff')
->options(Spice::all()->pluck('name', 'id'))
->searchable()
->required(),
TextInput::make('amount_in_percent')
->label('Anteil (%)')
->numeric()
->minValue(0.01)
->maxValue(100)
->required(),
])
->columns(2)
->addActionLabel('Zutat hinzufügen')
->dehydrated(false)
->rules([new SumEquals100()])
->afterStateHydrated(function ($component, $record) {
if ($record?->recipe) {
$record->recipe->load('spices');
$component->state(
$record->recipe->spices->map(fn ($s) => [
'spice_id' => $s->id,
'amount_in_percent' => $s->pivot->amount_in_percent,
])->toArray()
);
}
}),
])
->columns(2)
->columnSpanFull(),
Repeater::make('sizes')
->schema([
Hidden::make('product_size_id'),
Hidden::make('shopify_variant_id'),
Placeholder::make('shopify_status')
->label('Shopify')
->content(fn (Get $get): HtmlString =>
$get('shopify_variant_id')
? new HtmlString('<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">✓ Shopify</span>')
: new HtmlString('<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-500">Lokal</span>')
),
Select::make('packing_id')
->label('Verpackung')
->options(function () {
return Packing::all()->mapWithKeys(function ($packing) {
return [$packing->id => $packing->name . ' (' . $packing->size . ')'];
});
})
->searchable()
->required(),
TextInput::make('price')
->label('Preis (in EUR)')
->numeric()
->required(),
TextInput::make('sku')
->label('SKU'),
Placeholder::make('barcode_preview')
->label('Barcode')
->content(function (Get $get): HtmlString {
$gtin = preg_replace('/\D/', '', (string) $get('gtin'));
if (strlen($gtin) < 8) {
return new HtmlString('');
}
$type = match (strlen($gtin)) {
8 => BarcodeGeneratorSVG::TYPE_EAN_8,
13 => BarcodeGeneratorSVG::TYPE_EAN_13,
12 => BarcodeGeneratorSVG::TYPE_UPC_A,
14 => BarcodeGeneratorSVG::TYPE_CODABAR,
default => BarcodeGeneratorSVG::TYPE_CODE_128,
};
try {
$svg = (new BarcodeGeneratorSVG())->getBarcode($gtin, $type, 2, 60);
return new HtmlString($svg);
} catch (\Throwable) {
return new HtmlString('');
}
})
->columnSpanFull()
->visible(fn (Get $get) => strlen(preg_replace('/\D/', '', (string) $get('gtin'))) >= 8),
TextInput::make('gtin')
->label('GTIN')
->mask('9 999999 999999')
->dehydrateStateUsing(fn ($state) => preg_replace('/\D/', '', (string) $state))
->live(debounce: 500),
])
->label('Größen & Preise')
->columns(2)
->addActionLabel('Option hinzufügen')
->extraItemActions([
FormAction::make('shopify')
->label('Shopify')
->icon('heroicon-o-arrow-path')
->color('gray')
->requiresConfirmation()
->action(function (array $arguments, Repeater $component): void {
$item = $component->getState()[$arguments['item']] ?? [];
$sizeId = (int) ($item['product_size_id'] ?? 0);
if (! $sizeId) {
\Filament\Notifications\Notification::make()
->title('Zuerst speichern')
->warning()
->send();
return;
}
$page = $component->getContainer()->getLivewire();
if ($item['shopify_variant_id'] ?? null) {
$page->syncVariantWithShopify($sizeId);
} else {
$page->publishVariantToShopify($sizeId);
}
}),
])
->afterStateHydrated(function ($state, $set, $record) {
if ($record) {
$sizesData = ProductSize::with('packing')
->where('product_id', $record->id)
->get()
->map(fn ($ps) => [
'product_size_id' => $ps->id,
'shopify_variant_id' => $ps->shopify_variant_id,
'packing_id' => $ps->packing_id,
'price' => $ps->price,
'sku' => $ps->sku,
'gtin' => $ps->gtin,
'description' => $ps->description,
])
->toArray();
$set('sizes', $sizesData);
}
return $state;
}),
]);
}
public static function table(Table $table): Table {
return $table
->columns([
Tables\Columns\ImageColumn::make('images')
->label('Bild')
->disk('public')
->limit(1),
Tables\Columns\TextColumn::make('title')
->label('Name')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('recipe.version')
->label('Rezept')
->formatStateUsing(fn ($state, $record) => $record->recipe
? 'v' . $record->recipe->version . ' · ' . $record->recipe->created_at->format('d.m.Y')
: '—'
),
]);
}
public static function getRelations(): array {
return [
RelationManagers\BatchesRelationManager::class,
];
}
public static function getPages(): array {
return [
'index' => Pages\ListProducts::route('/'),
'create' => Pages\CreateProduct::route('/create'),
'edit' => Pages\EditProduct::route('/{record}/edit'),
'recipe-history' => Pages\RecipeHistory::route('/{record}/rezept-versionen'),
];
}
public static function getPluralLabel(): string {
return 'Produkte';
}
}