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('' . e($record->shopify_product_id) . '')
: new HtmlString('Noch nicht in Shopify erstellt')
),
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('✓ Shopify')
: new HtmlString('Lokal')
),
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'),
'view' => Pages\ViewProduct::route('/{record}'),
'edit' => Pages\EditProduct::route('/{record}/edit'),
'recipe-history' => Pages\RecipeHistory::route('/{record}/rezept-versionen'),
];
}
public static function getPluralLabel(): string {
return 'Produkte';
}
}