This commit is contained in:
Stefan Zwischenbrugger 2026-03-29 22:08:56 +02:00
parent 182b751ced
commit b0feb07a62
4 changed files with 83 additions and 92 deletions

View File

@ -98,11 +98,37 @@ class ShoppingListController extends Controller
$request->user()->id
);
$shoppingItem->update([
'product_name' => $request->string('product_name')->toString(),
'quantity' => $request->filled('quantity') ? $request->string('quantity')->toString() : null,
'store_id' => $storeId,
]);
$isDone = $request->boolean('is_done');
DB::transaction(function () use ($request, $shoppingItem, $storeId, $isDone): void {
$shoppingItem->update([
'product_name' => $request->string('product_name')->toString(),
'quantity' => $request->filled('quantity') ? $request->string('quantity')->toString() : null,
'store_id' => $storeId,
'is_done' => $isDone,
'done_at' => $isDone ? Carbon::now() : null,
]);
if (! $isDone) {
return;
}
$photoPath = $request->file('photo')?->store('price-photos', 'public');
if (! $request->filled('price_decimal') && $photoPath === null) {
return;
}
ItemPriceLog::query()->create([
'shopping_item_id' => $shoppingItem->id,
'store_id' => $storeId,
'price_decimal' => $request->input('price_decimal') ?? 0,
'currency' => 'EUR',
'logged_at' => Carbon::now(),
'photo_path' => $photoPath,
'source' => 'manual',
]);
});
return back()->with('status', 'Eintrag wurde gespeichert.');
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\File;
class UpdateShoppingItemRequest extends FormRequest
{
@ -28,6 +29,13 @@ class UpdateShoppingItemRequest extends FormRequest
'quantity' => ['nullable', 'string', 'max:255'],
'store_id' => ['nullable', 'integer', 'exists:stores,id'],
'new_store_name' => ['nullable', 'string', 'max:255'],
'is_done' => ['sometimes', 'boolean'],
'price_decimal' => ['nullable', 'numeric', 'min:0', 'max:999999.99'],
'photo' => [
'nullable',
File::types(['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'])
->max(15 * 1024),
],
];
}
@ -39,6 +47,10 @@ class UpdateShoppingItemRequest extends FormRequest
'quantity.max' => 'Die Menge darf maximal 255 Zeichen lang sein.',
'store_id.exists' => 'Das ausgewaehlte Geschaeft existiert nicht.',
'new_store_name.max' => 'Der neue Geschaeftsname darf maximal 255 Zeichen lang sein.',
'price_decimal.numeric' => 'Der Preis muss eine Zahl sein.',
'price_decimal.min' => 'Der Preis darf nicht negativ sein.',
'price_decimal.max' => 'Der Preis ist zu gross.',
'photo.max' => 'Das Foto darf maximal 15 MB gross sein.',
];
}
@ -49,6 +61,8 @@ class UpdateShoppingItemRequest extends FormRequest
'quantity' => 'Menge',
'store_id' => 'Geschaeft',
'new_store_name' => 'neues Geschaeft',
'price_decimal' => 'Preis',
'photo' => 'Foto',
];
}
}

View File

@ -101,7 +101,6 @@
@include('shopping-list.partials.item-pencil-panel', [
'item' => $item,
'stores' => $stores,
'showToggleExtras' => true,
])
</div>
</div>
@ -158,7 +157,6 @@
@include('shopping-list.partials.item-pencil-panel', [
'item' => $item,
'stores' => $stores,
'showToggleExtras' => true,
])
</div>
</div>

View File

@ -1,8 +1,4 @@
{{-- Ein Bleistift oeffnet alle Aenderungsoptionen: Stammdaten (update) + optional Preis/Foto beim Abhaken (nur wenn $showToggleExtras) --}}
@php
$showToggleExtras = $showToggleExtras ?? false;
@endphp
{{-- Ein Bleistift oeffnet ein Formular: Stammdaten, Erledigt-Status, optional Preis/Foto --}}
<details class="group relative shrink-0">
<summary
class="list-none cursor-pointer flex h-11 w-11 items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-600 shadow-sm hover:bg-gray-50 hover:text-gray-900 [&::-webkit-details-marker]:hidden"
@ -14,7 +10,7 @@
</summary>
<div class="absolute right-0 z-20 mt-2 w-[min(calc(100vw-2rem),22rem)] rounded-xl border border-gray-200 bg-white p-4 shadow-lg sm:w-80">
<p class="mb-1 text-sm font-semibold text-gray-900">Eintrag bearbeiten</p>
<p class="mb-3 text-xs text-gray-500">Zwei getrennte Aktionen bitte den passenden Button nutzen.</p>
<p class="mb-3 text-xs text-gray-500">Produkt, Geschaeft, Erledigt-Status sowie optional Preis und Foto in einem Schritt speichern.</p>
@if($item->latestPhotoLog)
<div class="mb-4 rounded-lg border border-emerald-200 bg-emerald-50/80 p-3">
@ -31,11 +27,12 @@
</div>
@endif
<div class="mb-2">
<p class="text-xs font-semibold text-gray-800">1. Produkt & Geschaeft</p>
<p class="text-xs text-gray-500 mt-0.5">Nur Name, Menge und Geschaeft. Der Status (offen/erledigt) bleibt dabei gleich.</p>
</div>
<form method="POST" action="{{ route('shopping-items.update', $item) }}" class="grid grid-cols-1 gap-2">
<form
method="POST"
action="{{ route('shopping-items.update', $item) }}"
enctype="multipart/form-data"
class="grid grid-cols-1 gap-2"
>
@csrf
@method('PATCH')
<label class="text-xs font-medium text-gray-600">Produktname</label>
@ -72,85 +69,41 @@
list="store-options"
class="rounded-md border-gray-300 text-sm min-h-[44px]"
>
<label class="flex items-start gap-2 mt-1 cursor-pointer">
<input
type="checkbox"
name="is_done"
value="1"
@checked($item->is_done)
class="mt-1 h-5 w-5 rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500"
>
<span class="text-xs text-gray-700">Als erledigt markieren</span>
</label>
<p class="text-xs text-gray-500 -mt-1">Preis und Foto werden nur gespeichert, wenn der Eintrag als erledigt markiert ist.</p>
<input
type="number"
step="0.01"
min="0"
name="price_decimal"
placeholder="Preis in EUR (optional)"
class="rounded-md border-gray-300 text-sm min-h-[44px]"
>
<label class="text-xs text-gray-600">Foto Kassenbon (optional)</label>
<input type="file" name="photo" accept="image/*" class="text-sm min-h-[44px]">
@if($item->latestPriceLog)
<div class="text-xs text-gray-500">
Letzter Preis:
{{ number_format((float) $item->latestPriceLog->price_decimal, 2, ',', '.') }} EUR
bei {{ $item->latestPriceLog->store?->name ?: 'ohne Geschaeft' }}
({{ optional($item->latestPriceLog->logged_at)->format('d.m.Y H:i') }})
</div>
@endif
<button
type="submit"
class="mt-1 rounded-lg bg-blue-600 text-white px-4 py-2.5 min-h-[44px] text-sm font-medium hover:bg-blue-700"
>
Nur Produkt speichern
Speichern
</button>
</form>
@if($showToggleExtras)
<div class="my-4 border-t border-gray-200"></div>
<div class="mb-2">
@if($item->is_done)
<p class="text-xs font-semibold text-gray-800">2. Neuer Preis oder Kassenbon</p>
<p class="text-xs text-gray-500 mt-0.5">Fuer die Statistik: weiterer Einkauf desselben Artikels. Alles optional.</p>
@else
<p class="text-xs font-semibold text-gray-800">2. Erledigen & Kasse erfassen</p>
<p class="text-xs text-gray-500 mt-0.5">Hakt die Zeile ab und speichert optional Preis und Foto. Oder nutze nur die Checkbox in der Liste zum Abhaken ohne Kasse.</p>
@endif
</div>
<form
method="POST"
action="{{ route('shopping-items.toggle', $item) }}"
enctype="multipart/form-data"
class="grid grid-cols-1 gap-2"
>
@csrf
@method('PATCH')
<input type="hidden" name="is_done" value="1">
<input
type="text"
name="quantity"
value="{{ $item->quantity }}"
placeholder="Menge (optional)"
class="rounded-md border-gray-300 text-sm min-h-[44px]"
>
<select name="store_id" class="rounded-md border-gray-300 text-sm min-h-[44px]">
<option value="">Geschaeft (optional)</option>
@foreach($stores as $store)
<option value="{{ $store->id }}" @selected($item->store_id === $store->id)>
{{ $store->name }}
</option>
@endforeach
</select>
<input
type="text"
name="new_store_name"
placeholder="Geschaeft frei eingeben (optional)"
list="store-options"
class="rounded-md border-gray-300 text-sm min-h-[44px]"
>
<input
type="number"
step="0.01"
min="0"
name="price_decimal"
placeholder="Preis in EUR (optional)"
class="rounded-md border-gray-300 text-sm min-h-[44px]"
>
<label class="text-xs text-gray-600">Foto Kassenbon (optional)</label>
<input type="file" name="photo" accept="image/*" class="text-sm min-h-[44px]">
@if($item->latestPriceLog)
<div class="text-xs text-gray-500">
Letzter Preis:
{{ number_format((float) $item->latestPriceLog->price_decimal, 2, ',', '.') }} EUR
bei {{ $item->latestPriceLog->store?->name ?: 'ohne Geschaeft' }}
({{ optional($item->latestPriceLog->logged_at)->format('d.m.Y H:i') }})
</div>
@endif
<button
type="submit"
class="rounded-lg bg-emerald-600 text-white px-4 py-2.5 min-h-[44px] text-sm font-medium hover:bg-emerald-700"
>
@if($item->is_done)
Preis & Foto zur Statistik
@else
Abhaken & Kasse speichern
@endif
</button>
</form>
@endif
</div>
</details>