Deploy / deploy (push) Successful in 14s
When the recipe is changed in a batch form, keep origin data for spices that exist in both the old and new recipe. Only add empty rows for spices that are new to the selected recipe version. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
230 lines
9.9 KiB
PHP
230 lines
9.9 KiB
PHP
<?php
|
||
|
||
namespace App\Filament\User\Resources;
|
||
|
||
use App\Filament\User\Resources\BatchResource\Pages;
|
||
use App\Models\Batch;
|
||
use App\Models\Farmer;
|
||
use App\Models\Product;
|
||
use App\Models\ProductSize;
|
||
use App\Models\Recipe;
|
||
use App\Models\Spice;
|
||
use Filament\Forms;
|
||
use Filament\Forms\Form;
|
||
use Filament\Forms\Get;
|
||
use Filament\Resources\Resource;
|
||
use Illuminate\Support\HtmlString;
|
||
use Filament\Tables;
|
||
use Filament\Tables\Table;
|
||
|
||
class BatchResource extends Resource
|
||
{
|
||
protected static ?string $model = Batch::class;
|
||
protected static ?string $navigationIcon = 'heroicon-o-qr-code';
|
||
protected static ?string $navigationLabel = 'Chargen (LOT)';
|
||
protected static ?string $modelLabel = 'Charge';
|
||
protected static ?string $pluralModelLabel = 'Chargen';
|
||
protected static ?string $navigationGroup = 'Produkte';
|
||
protected static ?int $navigationSort = 3;
|
||
|
||
public static function form(Form $form): Form
|
||
{
|
||
return $form->schema([
|
||
Forms\Components\Section::make('Charge')->schema([
|
||
Forms\Components\TextInput::make('lot_number')
|
||
->label('LOT-Nummer')
|
||
->required()
|
||
->unique(ignoreRecord: true)
|
||
->maxLength(255),
|
||
|
||
Forms\Components\Select::make('product_id')
|
||
->label('Produkt')
|
||
->relationship('product', 'title')
|
||
->searchable()
|
||
->preload()
|
||
->required()
|
||
->live()
|
||
->afterStateUpdated(function ($state, $set) {
|
||
$set('size_id', null);
|
||
$set('recipe_id', null);
|
||
$set('origins', []);
|
||
if (! $state) {
|
||
return;
|
||
}
|
||
$product = Product::find($state);
|
||
if ($product?->recipes_id) {
|
||
$set('recipe_id', $product->recipes_id);
|
||
$recipe = Recipe::with('spices')->find($product->recipes_id);
|
||
if ($recipe) {
|
||
$set('origins', $recipe->spices->map(fn ($s) => [
|
||
'spice_id' => $s->id,
|
||
'entries' => [['supplier_id' => null, 'origin_country' => null, 'farmer_id' => null, 'notes' => null]],
|
||
])->toArray());
|
||
}
|
||
}
|
||
}),
|
||
|
||
Forms\Components\Select::make('size_id')
|
||
->label('Variante (Verpackung)')
|
||
->options(fn (Get $get) => ProductSize::with('packing')
|
||
->where('product_id', $get('product_id'))
|
||
->get()
|
||
->mapWithKeys(fn ($s) => [$s->id => $s->packing->name . ' ' . $s->packing->size . 'g'])
|
||
)
|
||
->required()
|
||
->live(),
|
||
|
||
Forms\Components\Select::make('recipe_id')
|
||
->label('Rezeptur')
|
||
->options(fn (Get $get) => Recipe::where('product_id', $get('product_id') ?? 0)
|
||
->orderByDesc('version')
|
||
->get()
|
||
->mapWithKeys(fn ($r) => [$r->id => 'v' . $r->version . ' · ' . $r->created_at->format('d.m.Y')])
|
||
)
|
||
->required()
|
||
->live()
|
||
->afterStateUpdated(function ($state, $set, $get) {
|
||
if (! $state) {
|
||
$set('origins', []);
|
||
return;
|
||
}
|
||
$recipe = Recipe::with('spices')->find($state);
|
||
if ($recipe) {
|
||
$existing = collect($get('origins') ?? [])->keyBy('spice_id');
|
||
$set('origins', $recipe->spices->map(fn ($s) => $existing->has($s->id)
|
||
? $existing[$s->id]
|
||
: ['spice_id' => $s->id, 'entries' => [['supplier_id' => null, 'origin_country' => null, 'farmer_id' => null, 'notes' => null]]]
|
||
)->toArray());
|
||
}
|
||
}),
|
||
|
||
Forms\Components\TextInput::make('units_produced')
|
||
->label('Anzahl Einheiten')
|
||
->numeric()
|
||
->required()
|
||
->live(debounce: 400),
|
||
|
||
Forms\Components\Placeholder::make('quantity_g_preview')
|
||
->label('Gesamtgewicht (berechnet)')
|
||
->content(function (Get $get): HtmlString {
|
||
$units = (int) $get('units_produced');
|
||
$sizeId = $get('size_id');
|
||
if (! $units || ! $sizeId) {
|
||
return new HtmlString('<span class="text-gray-400">—</span>');
|
||
}
|
||
$size = ProductSize::with('packing')->find($sizeId);
|
||
$totalG = $units * ($size?->packing?->size ?? 0);
|
||
return new HtmlString("<strong>{$totalG} g</strong> ({$units} × {$size?->packing?->size}g)");
|
||
}),
|
||
|
||
Forms\Components\DatePicker::make('produced_at')
|
||
->label('Produktionsdatum'),
|
||
|
||
Forms\Components\DatePicker::make('expires_at')
|
||
->label('Mindesthaltbarkeitsdatum'),
|
||
])->columns(2),
|
||
|
||
Forms\Components\Section::make('Herkunft der Rohstoffe')->schema([
|
||
Forms\Components\Repeater::make('origins')
|
||
->label('')
|
||
->relationship()
|
||
->schema([
|
||
Forms\Components\Hidden::make('spice_id'),
|
||
Forms\Components\Placeholder::make('spice_name')
|
||
->label('Rohstoff')
|
||
->content(fn (Get $get): string =>
|
||
Spice::find($get('spice_id'))?->name ?? '—'
|
||
)
|
||
->columnSpanFull(),
|
||
|
||
Forms\Components\Repeater::make('entries')
|
||
->label('Herkunftsangaben')
|
||
->relationship()
|
||
->schema([
|
||
Forms\Components\Select::make('supplier_id')
|
||
->label('Lieferant')
|
||
->options(fn () => \App\Models\Supplier::orderBy('name')->pluck('name', 'id'))
|
||
->searchable()
|
||
->createOptionForm([
|
||
Forms\Components\TextInput::make('name')->label('Name')->required(),
|
||
])
|
||
->createOptionUsing(fn ($data) => \App\Models\Supplier::create($data)->id)
|
||
->nullable(),
|
||
|
||
Forms\Components\TextInput::make('origin_country')
|
||
->label('Herkunftsland')
|
||
->maxLength(100),
|
||
|
||
Forms\Components\Select::make('farmer_id')
|
||
->label('Landwirt')
|
||
->options(fn () => Farmer::orderBy('name')->pluck('name', 'id'))
|
||
->searchable()
|
||
->createOptionForm([
|
||
Forms\Components\TextInput::make('name')->label('Name')->required(),
|
||
])
|
||
->createOptionUsing(fn ($data) => Farmer::create($data)->id)
|
||
->nullable(),
|
||
|
||
Forms\Components\Textarea::make('notes')
|
||
->label('Notizen')
|
||
->rows(2)
|
||
->columnSpanFull(),
|
||
])
|
||
->columns(3)
|
||
->addActionLabel('Eintrag hinzufügen')
|
||
->reorderable(false)
|
||
->columnSpanFull(),
|
||
])
|
||
->reorderable(false),
|
||
]),
|
||
]);
|
||
}
|
||
|
||
public static function table(Table $table): Table
|
||
{
|
||
return $table
|
||
->columns([
|
||
Tables\Columns\TextColumn::make('lot_number')
|
||
->label('LOT-Nummer')
|
||
->searchable()
|
||
->sortable(),
|
||
|
||
Tables\Columns\TextColumn::make('product.title')
|
||
->label('Produkt')
|
||
->searchable(),
|
||
|
||
Tables\Columns\TextColumn::make('recipe.name')
|
||
->label('Rezeptur'),
|
||
|
||
Tables\Columns\TextColumn::make('produced_at')
|
||
->label('Produktion')
|
||
->date('d.m.Y')
|
||
->sortable(),
|
||
|
||
Tables\Columns\TextColumn::make('expires_at')
|
||
->label('MHD')
|
||
->date('d.m.Y')
|
||
->sortable(),
|
||
])
|
||
->defaultSort('produced_at', 'desc')
|
||
->filters([
|
||
Tables\Filters\SelectFilter::make('product')
|
||
->relationship('product', 'title')
|
||
->label('Produkt'),
|
||
])
|
||
->actions([
|
||
Tables\Actions\EditAction::make(),
|
||
Tables\Actions\DeleteAction::make(),
|
||
]);
|
||
}
|
||
|
||
public static function getPages(): array
|
||
{
|
||
return [
|
||
'index' => Pages\ListBatches::route('/'),
|
||
'create' => Pages\CreateBatch::route('/create'),
|
||
'edit' => Pages\EditBatch::route('/{record}/edit'),
|
||
];
|
||
}
|
||
}
|