Deploy / deploy (push) Successful in 14s
Products now have a configurable lot_prefix (e.g. "112"). In the batch form the prefix is shown read-only next to the suffix input (JJMMTT), and the full lot_number (e.g. "112-260912") is composed automatically and stored. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
227 lines
9.8 KiB
PHP
227 lines
9.8 KiB
PHP
<?php
|
||
|
||
namespace App\Filament\User\Resources\ProductResource\RelationManagers;
|
||
|
||
use App\Models\Farmer;
|
||
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\RelationManagers\RelationManager;
|
||
use Filament\Tables;
|
||
use Filament\Tables\Table;
|
||
use Illuminate\Support\HtmlString;
|
||
|
||
class BatchesRelationManager extends RelationManager
|
||
{
|
||
protected static string $relationship = 'batches';
|
||
protected static ?string $title = 'Chargen (LOT)';
|
||
protected static ?string $modelLabel = 'Charge';
|
||
protected static ?string $pluralModelLabel = 'Chargen';
|
||
|
||
public function form(Form $form): Form
|
||
{
|
||
return $form->schema([
|
||
Forms\Components\TextInput::make('lot_suffix')
|
||
->label('LOT-Nummer')
|
||
->prefix(fn ($livewire): ?string => $livewire->getOwnerRecord()->lot_prefix
|
||
? $livewire->getOwnerRecord()->lot_prefix . ' -'
|
||
: null
|
||
)
|
||
->placeholder('JJMMTT')
|
||
->maxLength(50)
|
||
->required()
|
||
->dehydrated(false)
|
||
->live()
|
||
->afterStateHydrated(function ($component, $record) {
|
||
if (! $record?->lot_number) return;
|
||
$pos = strrpos($record->lot_number, '-');
|
||
$component->state($pos !== false
|
||
? substr($record->lot_number, $pos + 1)
|
||
: $record->lot_number
|
||
);
|
||
})
|
||
->afterStateUpdated(function ($state, $set, $livewire) {
|
||
$prefix = $livewire->getOwnerRecord()->lot_prefix;
|
||
$set('lot_number', $prefix ? "{$prefix}-{$state}" : $state);
|
||
}),
|
||
|
||
Forms\Components\Hidden::make('lot_number'),
|
||
|
||
Forms\Components\Select::make('size_id')
|
||
->label('Variante (Verpackung)')
|
||
->options(fn () => ProductSize::with('packing')
|
||
->where('product_id', $this->getOwnerRecord()->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 () => Recipe::where('product_id', $this->getOwnerRecord()->id)
|
||
->orderByDesc('version')
|
||
->get()
|
||
->mapWithKeys(fn ($r) => [$r->id => 'v' . $r->version . ' · ' . $r->created_at->format('d.m.Y')])
|
||
)
|
||
->default(fn () => $this->getOwnerRecord()->recipes_id)
|
||
->required()
|
||
->live()
|
||
->afterStateUpdated(function ($state, $set, $get) {
|
||
if (! $state) {
|
||
$set('origins', []);
|
||
return;
|
||
}
|
||
$recipe = Recipe::with('spices')->find($state);
|
||
if (! $recipe) {
|
||
return;
|
||
}
|
||
$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'),
|
||
|
||
Forms\Components\Section::make('Herkunft der Rohstoffe')->schema([
|
||
Forms\Components\Repeater::make('origins')
|
||
->label('')
|
||
->relationship()
|
||
->afterStateHydrated(function ($component, $record, $livewire) {
|
||
if ($record?->id) {
|
||
return;
|
||
}
|
||
$recipeId = $livewire->getOwnerRecord()->recipes_id;
|
||
if (! $recipeId) {
|
||
return;
|
||
}
|
||
$recipe = Recipe::with('spices')->find($recipeId);
|
||
if (! $recipe) {
|
||
return;
|
||
}
|
||
$component->state($recipe->spices->map(fn ($s) => [
|
||
'spice_id' => $s->id,
|
||
'entries' => [['supplier_id' => null, 'origin_country' => null, 'farmer_id' => null, 'notes' => null]],
|
||
])->toArray());
|
||
})
|
||
->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 function table(Table $table): Table
|
||
{
|
||
return $table
|
||
->recordTitleAttribute('lot_number')
|
||
->columns([
|
||
Tables\Columns\TextColumn::make('lot_number')
|
||
->label('LOT-Nummer')
|
||
->searchable()
|
||
->sortable(),
|
||
|
||
Tables\Columns\TextColumn::make('units_produced')
|
||
->label('Einheiten')
|
||
->suffix(fn ($record) => $record->quantity_g
|
||
? ' (' . number_format($record->quantity_g, 0, ',', '.') . 'g)'
|
||
: ''),
|
||
|
||
Tables\Columns\TextColumn::make('recipe.version')
|
||
->label('Rezeptur')
|
||
->formatStateUsing(fn ($state) => $state ? 'v' . $state : '—'),
|
||
|
||
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')
|
||
->actions([
|
||
Tables\Actions\EditAction::make(),
|
||
Tables\Actions\DeleteAction::make(),
|
||
])
|
||
->headerActions([
|
||
Tables\Actions\CreateAction::make(),
|
||
]);
|
||
}
|
||
}
|