Add recipe versioning: auto-bump on change, history sub-page
Deploy / deploy (push) Successful in 16s
Deploy / deploy (push) Successful in 16s
Each time the recipe name or spices are changed on the product edit
page, a new version is created instead of overwriting the current one.
Unchanged saves are skipped. A 'Rezept-Versionen' button opens a
sub-page (/produkt/{id}/rezept-versionen) listing all versions with
their spices and timestamps; the active version is highlighted in green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
4775cffaae
commit
a7e7375edd
4 files changed
+128
-21
No files matched your search
@@ -193,9 +193,10 @@ class ProductResource extends Resource {
|
|||||||
|
|
||||||
public static function getPages(): array {
|
public static function getPages(): array {
|
||||||
return [
|
return [
|
||||||
'index' => Pages\ListProducts::route('/'),
|
'index' => Pages\ListProducts::route('/'),
|
||||||
'create' => Pages\CreateProduct::route('/create'),
|
'create' => Pages\CreateProduct::route('/create'),
|
||||||
'edit' => Pages\EditProduct::route('/{record}/edit'),
|
'edit' => Pages\EditProduct::route('/{record}/edit'),
|
||||||
|
'recipe-history' => Pages\RecipeHistory::route('/{record}/rezept-versionen'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,23 @@ class EditProduct extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
Actions\Action::make('rezept-versionen')
|
||||||
|
->label('Rezept-Versionen')
|
||||||
|
->icon('heroicon-o-clock')
|
||||||
|
->color('gray')
|
||||||
|
->url(fn () => ProductResource::getUrl('recipe-history', ['record' => $this->record])),
|
||||||
Actions\DeleteAction::make(),
|
Actions\DeleteAction::make(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function afterSave(): void {
|
protected function afterSave(): void
|
||||||
|
{
|
||||||
$this->syncSizes();
|
$this->syncSizes();
|
||||||
$this->syncRecipe();
|
$this->syncRecipe();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function syncSizes(): void {
|
protected function syncSizes(): void
|
||||||
|
{
|
||||||
$product = $this->record;
|
$product = $this->record;
|
||||||
|
|
||||||
$sizes = collect($this->data['sizes'] ?? [])
|
$sizes = collect($this->data['sizes'] ?? [])
|
||||||
@@ -45,29 +52,48 @@ class EditProduct extends EditRecord
|
|||||||
protected function syncRecipe(): void
|
protected function syncRecipe(): void
|
||||||
{
|
{
|
||||||
$product = $this->record;
|
$product = $this->record;
|
||||||
$name = $this->data['recipe_name'] ?? null;
|
$name = $this->data['recipe_name'] ?? null;
|
||||||
$spices = $this->data['recipe_spices'] ?? [];
|
$spices = $this->data['recipe_spices'] ?? [];
|
||||||
|
|
||||||
if (! $name) {
|
if (! $name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$pivotData = collect($spices)->mapWithKeys(fn ($s) => [
|
$newPivot = collect($spices)->mapWithKeys(fn ($s) => [
|
||||||
$s['spice_id'] => ['amount_in_percent' => $s['amount_in_percent']],
|
(int) $s['spice_id'] => (float) $s['amount_in_percent'],
|
||||||
])->toArray();
|
])->toArray();
|
||||||
|
|
||||||
if ($product->recipe) {
|
$current = $product->recipe;
|
||||||
$product->recipe->update(['name' => $name]);
|
|
||||||
$product->recipe->spices()->sync($pivotData);
|
if ($current) {
|
||||||
} else {
|
$current->load('spices');
|
||||||
$nextVersion = Recipe::where('product_id', $product->id)->max('version') + 1;
|
|
||||||
$recipe = Recipe::create([
|
$existingPivot = $current->spices->mapWithKeys(fn ($s) => [
|
||||||
'name' => $name,
|
$s->id => (float) $s->pivot->amount_in_percent,
|
||||||
'product_id' => $product->id,
|
])->toArray();
|
||||||
'version' => $nextVersion,
|
|
||||||
]);
|
$nameChanged = $current->name !== $name;
|
||||||
$recipe->spices()->sync($pivotData);
|
$spicesChanged = $existingPivot != $newPivot;
|
||||||
$product->update(['recipes_id' => $recipe->id]);
|
|
||||||
|
if (! $nameChanged && ! $spicesChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$nextVersion = Recipe::where('product_id', $product->id)->max('version') + 1;
|
||||||
|
|
||||||
|
$recipe = Recipe::create([
|
||||||
|
'name' => $name,
|
||||||
|
'product_id' => $product->id,
|
||||||
|
'version' => $nextVersion,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$recipe->spices()->sync(
|
||||||
|
collect($newPivot)->mapWithKeys(fn ($pct, $id) => [
|
||||||
|
$id => ['amount_in_percent' => $pct],
|
||||||
|
])->toArray()
|
||||||
|
);
|
||||||
|
|
||||||
|
$product->update(['recipes_id' => $recipe->id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\User\Resources\ProductResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\User\Resources\ProductResource;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\Recipe;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Resources\Pages\Page;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Concerns\InteractsWithTable;
|
||||||
|
use Filament\Tables\Contracts\HasTable;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class RecipeHistory extends Page implements HasTable
|
||||||
|
{
|
||||||
|
use InteractsWithTable;
|
||||||
|
|
||||||
|
protected static string $resource = ProductResource::class;
|
||||||
|
protected static string $view = 'filament.user.resources.product-resource.pages.recipe-history';
|
||||||
|
|
||||||
|
public Product $record;
|
||||||
|
|
||||||
|
public function mount(int|string $record): void
|
||||||
|
{
|
||||||
|
$this->record = Product::with('recipe')->findOrFail($record);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return 'Rezept-Versionen · ' . $this->record->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Action::make('back')
|
||||||
|
->label('Zurück zum Produkt')
|
||||||
|
->icon('heroicon-o-arrow-left')
|
||||||
|
->color('gray')
|
||||||
|
->url(ProductResource::getUrl('edit', ['record' => $this->record])),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
$currentRecipeId = $this->record->recipes_id;
|
||||||
|
|
||||||
|
return $table
|
||||||
|
->query(
|
||||||
|
Recipe::query()
|
||||||
|
->where('product_id', $this->record->id)
|
||||||
|
->with('spices')
|
||||||
|
->orderByDesc('version')
|
||||||
|
)
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('version')
|
||||||
|
->label('Version')
|
||||||
|
->formatStateUsing(fn ($state) => 'v' . $state)
|
||||||
|
->badge()
|
||||||
|
->color(fn (Recipe $recipe) => $recipe->id === $currentRecipeId ? 'success' : 'gray'),
|
||||||
|
TextColumn::make('name')
|
||||||
|
->label('Name'),
|
||||||
|
TextColumn::make('spices_summary')
|
||||||
|
->label('Zutaten')
|
||||||
|
->state(fn (Recipe $recipe) =>
|
||||||
|
$recipe->spices->map(fn ($s) => $s->name . ' ' . $s->pivot->amount_in_percent . '%')->implode(' · ')
|
||||||
|
),
|
||||||
|
TextColumn::make('created_at')
|
||||||
|
->label('Erstellt am')
|
||||||
|
->dateTime('d.m.Y H:i')
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->paginated(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
<x-filament-panels::page>
|
||||||
|
{{ $this->table }}
|
||||||
|
</x-filament-panels::page>
|
||||||
Reference in new issue
Block a user