Einkaufsliste-UI, Prompts, Apache-Setup ignorieren

This commit is contained in:
Stefan Zwischenbrugger 2026-03-29 18:10:02 +02:00
parent 2c48f68190
commit b2a518e349
8 changed files with 384 additions and 80 deletions

3
.gitignore vendored
View File

@ -22,3 +22,6 @@
Homestead.json
Homestead.yaml
Thumbs.db
# Versehentlicher Nested-Clone desselben Repos (ohne gueltigen Checkout)
/Einkaufsliste

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Http\Requests\StoreShoppingItemRequest;
use App\Http\Requests\ToggleShoppingItemRequest;
use App\Http\Requests\UpdateShoppingItemRequest;
use App\Models\ItemPriceLog;
use App\Models\ShoppingItem;
use App\Models\Store;
@ -68,6 +69,27 @@ class ShoppingListController extends Controller
return back()->with('status', 'Eintrag wurde hinzugefuegt.');
}
public function update(UpdateShoppingItemRequest $request, ShoppingItem $shoppingItem): RedirectResponse
{
if ($shoppingItem->user_id !== $request->user()->id) {
abort(403);
}
$storeId = $this->resolveStoreId(
$request->integer('store_id') ?: null,
$request->input('new_store_name'),
$request->user()->id
);
$shoppingItem->update([
'product_name' => $request->string('product_name')->toString(),
'quantity' => $request->filled('quantity') ? $request->string('quantity')->toString() : null,
'store_id' => $storeId,
]);
return back()->with('status', 'Eintrag wurde gespeichert.');
}
public function toggle(ToggleShoppingItemRequest $request, ShoppingItem $shoppingItem): RedirectResponse
{
if ($shoppingItem->user_id !== $request->user()->id) {
@ -82,27 +104,30 @@ class ShoppingListController extends Controller
);
DB::transaction(function () use ($request, $shoppingItem, $isDone, $storeId): void {
$shoppingItem->update([
$payload = [
'is_done' => $isDone,
'done_at' => $isDone ? Carbon::now() : null,
'store_id' => $storeId,
]);
];
if ($request->filled('quantity')) {
$payload['quantity'] = $request->string('quantity')->toString();
}
$shoppingItem->update($payload);
if (!$isDone) {
return;
}
$photoPath = $request->file('photo')?->store('price-photos', 'public');
$price = $request->input('price_decimal');
if ($price === null && $photoPath === null) {
if (!$request->filled('price_decimal') && $photoPath === null) {
return;
}
ItemPriceLog::query()->create([
'shopping_item_id' => $shoppingItem->id,
'store_id' => $storeId,
'price_decimal' => $price ?? 0,
'price_decimal' => $request->input('price_decimal') ?? 0,
'currency' => 'EUR',
'logged_at' => Carbon::now(),
'photo_path' => $photoPath,

View File

@ -14,6 +14,13 @@ class ToggleShoppingItemRequest extends FormRequest
return $this->user() !== null;
}
protected function prepareForValidation(): void
{
if ($this->has('store_id') && $this->input('store_id') === '') {
$this->merge(['store_id' => null]);
}
}
/**
* Get the validation rules that apply to the request.
*
@ -23,6 +30,7 @@ class ToggleShoppingItemRequest extends FormRequest
{
return [
'is_done' => ['required', 'boolean'],
'quantity' => ['nullable', 'string', 'max:255'],
'store_id' => ['nullable', 'integer', 'exists:stores,id'],
'new_store_name' => ['nullable', 'string', 'max:255'],
'price_decimal' => ['nullable', 'numeric', 'min:0', 'max:999999.99'],
@ -36,6 +44,7 @@ class ToggleShoppingItemRequest extends FormRequest
'is_done.required' => 'Statusfeld fehlt.',
'is_done.boolean' => 'Ungueltiger Statuswert.',
'store_id.exists' => 'Das ausgewaehlte Geschaeft existiert nicht.',
'quantity.max' => 'Die Menge darf maximal 255 Zeichen lang sein.',
'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.',
@ -48,6 +57,7 @@ class ToggleShoppingItemRequest extends FormRequest
public function attributes(): array
{
return [
'quantity' => 'Menge',
'store_id' => 'Geschaeft',
'new_store_name' => 'neues Geschaeft',
'price_decimal' => 'Preis',

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateShoppingItemRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
protected function prepareForValidation(): void
{
if ($this->has('store_id') && $this->input('store_id') === '') {
$this->merge(['store_id' => null]);
}
}
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'product_name' => ['required', 'string', 'max:255'],
'quantity' => ['nullable', 'string', 'max:255'],
'store_id' => ['nullable', 'integer', 'exists:stores,id'],
'new_store_name' => ['nullable', 'string', 'max:255'],
];
}
public function messages(): array
{
return [
'product_name.required' => 'Bitte gib einen Produktnamen ein.',
'product_name.max' => 'Der Produktname darf maximal 255 Zeichen lang sein.',
'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.',
];
}
public function attributes(): array
{
return [
'product_name' => 'Produktname',
'quantity' => 'Menge',
'store_id' => 'Geschaeft',
'new_store_name' => 'neues Geschaeft',
];
}
}

76
prompt_einkaufsliste.txt Normal file
View File

@ -0,0 +1,76 @@
Einkaufsliste Produktspezifikation (Gesamtprompt)
==================================================
Ziel-URL: https://einkauf.pauker.at
Lokal: http://pauker/einkauf
Dieses Dokument ist die verbindliche Sammel-Spezifikation fuer dieses Projekt (Laravel-App „einkauf“).
Technische Details der Umsetzung: Code im Repo; bei Abweichungen Prompt aktualisieren.
Plattform & Darstellung
-----------------------
- Desktop: gut bedienbar (grosse Klickflächen, übersichtlich).
- Smartphone: wie eine „App“ nutzbar (responsive UI).
- Offline: **soll möglich sein** → PWA mit Web-App-Manifest und Service-Worker; statische Shell und gecachte Assets; bei Bedarf später Strategie für Listendaten offline (Cache/API, Sync nach Reconnect schrittweise ausarbeiten).
Authentifizierung & Sicherheit
------------------------------
- Registrierung / Login mit E-Mail und Passwort; Session merken („eingeloggt bleiben“ über Laravel-Session/Cookie wie üblich).
- Passwort nur als Hash speichern (bcrypt/Argon2 Laravel-Standard).
- Passwort vergessen: Link per E-Mail, mit dem ein neues Passwort gesetzt werden kann (Password-Reset-Flow).
Listenfunktionalität Schnellfluss (Pflicht-UX)
-----------------------------------------------
- Neuer Eintrag: zuerst **nur der Produktname**; Absenden mit **Enter** (zusaetzlicher Button „Hinzufuegen“ ist ok).
- Eintrag erscheint in der **offenen** Liste.
- **Erledigen wie in Microsoft To Do:** in der offenen Liste **Checkbox** links; **Ankreuzen** sendet den Eintrag als erledigt (**ohne** Pflicht zu Geschaeft, Menge, Preis, Foto). Klick auf den Produktnamen toggelt die Checkbox (Label).
- **Erledigte Eintraege:** weiterhin sichtbar; **angekreuzte Checkbox**; **Abwaehlen** setzt den Eintrag wieder auf **offen**.
Bearbeiten & Zusatzangaben (ein Einstieg)
-----------------------------------------
- Rechts neben jeder Zeile ein **Bleistift-Icon**; ein Klick oeffnet **ein** Panel mit allen Aenderungsmoeglichkeiten (kein getrenntes „Bearbeiten“ und „Details“ mehr).
- **Stammdaten:** Produktname, Menge, Geschaeft (Auswahl und/oder freier Name) **„Aenderungen speichern“** (PATCH update), **ohne** Erledigt-Status zu aendern.
- **Nur bei offenen Eintraegen** zusaetzlich im selben Panel: optional **Preis**, **Foto**, ggf. Menge/Geschaeft beim Abhaken **„Erledigt mit Angaben“** (PATCH toggle); alles optional; schnelles Abhaken per **Checkbox** bleibt unabhaengig.
- Beim Anlegen weiterhin nur Name im Eingabefeld oben; Stueck/Geschaeft ueber das Bleistift-Panel.
Preise & Historie
-----------------
- Preis **manuell** eintragbar; optional **Foto**-Upload; **kein OCR-/Scan-Zwang** zuerst reicht manuelle Eingabe; OCR hoechstens **viel spaeter** optional.
- Erfasster Preis mit **Datum**, **Geschaeft**, **Produkt** speichern (Preishistorie / Logs wo im Backend vorgesehen).
- **Auswertung:** Anzeige, wie viel der Einkauf (nach erfassten Preisen) **pro Geschaeft** und **gesamt** kostet.
Geschäfte (Stores)
------------------
- Mehrere Geschäfte anlegbar/benutzbar (z. B. Spar, Lidl, Obi …), frei erweiterbar.
Externe / Online-Preise
-----------------------
- Automatisch Online-Preise zu Listeneinträgen beziehen: **noch nicht** fuer **spaeter** vormerken; keine Umsetzung bis Quellen/API geklärt sind.
Leitplanken fuer Code & Texte
------------------------------
- UI-Sprache: Deutsch; im Code bei fehlenden Umlauten konsistent **ae / oe / ue** wie im bestehenden Projekt.
- Backend: **Laravel** (dieses Repo).
- Aenderungen fokussiert; keine unnötigen Grossrefactors.
Festgelegte Produktentscheidungen
---------------------------------
1. Preis: **zuerst manuell** (+ optional Foto); **kein** OCR-Meilenstein in naher Planung.
2. Online-Preise automatisch: **spaeter**.
3. **Offline:** mit PWA/SW **anstreben** (Umsetzung iterativ).
Implementierungs-Stand (Referenz, bei Features nachpflegen)
-----------------------------------------------------------
- Routen u. a.: `shopping-items.store`, `shopping-items.update` (PATCH), `shopping-items.toggle` (PATCH, erledigt/offen, optionale Preis-/Foto-Daten).
- Views: Einkaufsliste-Dashboard mit offenen/erledigten Listen, Checkboxen, **Bleistift-Panel** (`item-pencil-panel`) pro Eintrag, Summen pro Geschaeft.
- PWA (Manifest/Service-Worker): laut obigen Zielen noch **auszubauen**, sofern nicht bereits erledigt.

View File

@ -29,109 +29,119 @@
@endif
<div class="bg-white overflow-hidden shadow-sm sm:rounded-xl p-4 sm:p-6">
<h3 class="text-lg font-semibold mb-4">Neuen Eintrag erfassen</h3>
<form method="POST" action="{{ route('shopping-items.store') }}" class="grid grid-cols-1 md:grid-cols-5 gap-3">
<h3 class="text-lg font-semibold mb-3">Neuer Eintrag</h3>
<p class="text-sm text-gray-600 mb-3">Nur Namen eingeben, mit <kbd class="px-1 bg-gray-100 rounded text-xs">Enter</kbd> speichern.</p>
<form method="POST" action="{{ route('shopping-items.store') }}" class="flex flex-col sm:flex-row gap-2">
@csrf
<input
type="text"
name="product_name"
id="new-item-name"
value="{{ old('product_name') }}"
placeholder="Produktname"
class="rounded border-gray-300 min-h-[44px]"
placeholder="z. B. Milch"
class="flex-1 rounded-md border-gray-300 shadow-sm min-h-[44px] text-base"
required
autocomplete="off"
autofocus
>
<input
type="text"
name="quantity"
value="{{ old('quantity') }}"
placeholder="Menge (optional)"
class="rounded border-gray-300 min-h-[44px]"
<button
type="submit"
class="shrink-0 rounded-md bg-blue-600 text-white px-5 min-h-[44px] text-base font-medium hover:bg-blue-700"
>
<select name="store_id" class="rounded border-gray-300 min-h-[44px]">
<option value="">Geschaeft (optional)</option>
@foreach($stores as $store)
<option value="{{ $store->id }}" @selected(old('store_id') == $store->id)>
{{ $store->name }}
</option>
@endforeach
</select>
<input
type="text"
name="new_store_name"
value="{{ old('new_store_name') }}"
placeholder="Geschaeft frei eingeben (optional)"
list="store-options"
class="rounded border-gray-300 min-h-[44px]"
>
<button class="rounded bg-blue-600 text-white px-4 py-2 min-h-[44px] text-base hover:bg-blue-700">
Hinzufuegen
</button>
</form>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-xl p-4 sm:p-6">
<div class="bg-white overflow-visible shadow-sm sm:rounded-xl p-4 sm:p-6">
<h3 class="text-lg font-semibold mb-4">Offene Eintraege</h3>
<div class="space-y-3">
@forelse($openItems as $item)
<form method="POST" action="{{ route('shopping-items.toggle', $item) }}" enctype="multipart/form-data" class="border rounded-lg p-3">
@csrf
@method('PATCH')
<input type="hidden" name="is_done" value="1">
<div class="font-medium">{{ $item->product_name }}</div>
<div class="text-sm text-gray-600 mb-2">
Menge: {{ $item->quantity ?: '-' }} | Geschaeft: {{ $item->store?->name ?: '-' }}
<div class="border border-gray-200 rounded-xl p-3 sm:p-4">
<div class="flex gap-2 items-start">
<form
method="POST"
action="{{ route('shopping-items.toggle', $item) }}"
class="flex min-w-0 flex-1 gap-3"
>
@csrf
@method('PATCH')
<input
type="checkbox"
name="is_done"
value="1"
id="open-item-{{ $item->id }}"
class="mt-1 h-6 w-6 shrink-0 rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500 cursor-pointer"
aria-label="Als erledigt markieren"
onchange="this.form.requestSubmit()"
>
<div class="min-w-0 flex-1 pt-0.5">
<label for="open-item-{{ $item->id }}" class="cursor-pointer text-base font-medium leading-snug text-gray-900">
{{ $item->product_name }}
</label>
@if($item->quantity || $item->store)
<div class="mt-0.5 text-sm text-gray-500">
@if($item->quantity)
<span>Menge: {{ $item->quantity }}</span>
@endif
@if($item->quantity && $item->store)
<span class="mx-1">·</span>
@endif
@if($item->store)
<span>{{ $item->store->name }}</span>
@endif
</div>
@endif
</div>
</form>
@include('shopping-list.partials.item-pencil-panel', [
'item' => $item,
'stores' => $stores,
'showToggleExtras' => true,
])
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<select name="store_id" class="rounded border-gray-300 text-sm min-h-[44px]">
<option value="">Geschaeft waehlen</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="Oder Geschaeft eingeben" list="store-options" class="rounded 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 border-gray-300 text-sm min-h-[44px]">
</div>
<input type="file" name="photo" accept="image/*" class="rounded border-gray-300 text-sm mt-2 min-h-[44px]">
@if($item->latestPriceLog)
<div class="text-xs text-gray-500 mt-2">
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 class="mt-2 rounded bg-green-600 text-white px-3 py-2 text-sm min-h-[44px] hover:bg-green-700">
Als erledigt markieren
</button>
</form>
</div>
@empty
<p class="text-gray-600">Keine offenen Eintraege.</p>
@endforelse
</div>
</div>
<div class="bg-white overflow-hidden shadow-sm sm:rounded-xl p-4 sm:p-6">
<div class="bg-white overflow-visible shadow-sm sm:rounded-xl p-4 sm:p-6">
<h3 class="text-lg font-semibold mb-4">Erledigte Eintraege</h3>
<div class="space-y-3">
@forelse($doneItems as $item)
<form method="POST" action="{{ route('shopping-items.toggle', $item) }}" class="border rounded-lg p-3">
@csrf
@method('PATCH')
<input type="hidden" name="is_done" value="0">
<div class="font-medium line-through text-gray-700">{{ $item->product_name }}</div>
<div class="text-sm text-gray-600">
Erledigt: {{ optional($item->done_at)->format('d.m.Y H:i') ?: '-' }}
<div class="border rounded-lg p-3">
<div class="flex gap-2 items-start">
<form method="POST" action="{{ route('shopping-items.toggle', $item) }}" class="flex min-w-0 flex-1 gap-3">
@csrf
@method('PATCH')
<input type="hidden" name="is_done" value="1" class="js-toggle-done-flag">
<input
type="checkbox"
checked
class="mt-1 h-6 w-6 shrink-0 rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500 cursor-pointer"
aria-label="Erledigt. Abwaehlen, um wieder in die offene Liste zu setzen."
onchange="this.form.querySelector('.js-toggle-done-flag').value = this.checked ? '1' : '0'; this.form.requestSubmit();"
>
<div class="min-w-0 flex-1">
<div class="font-medium text-gray-700 line-through">{{ $item->product_name }}</div>
<div class="text-sm text-gray-600">
Erledigt: {{ optional($item->done_at)->format('d.m.Y H:i') ?: '-' }}
</div>
<div class="text-sm text-gray-600">
Letzter Preis: {{ $item->latestPriceLog?->price_decimal !== null ? number_format((float) $item->latestPriceLog->price_decimal, 2, ',', '.') . ' EUR' : '-' }}
</div>
</div>
</form>
@include('shopping-list.partials.item-pencil-panel', [
'item' => $item,
'stores' => $stores,
'showToggleExtras' => false,
])
</div>
<div class="text-sm text-gray-600 mb-2">
Letzter Preis: {{ $item->latestPriceLog?->price_decimal !== null ? number_format((float) $item->latestPriceLog->price_decimal, 2, ',', '.') . ' EUR' : '-' }}
</div>
<button class="rounded bg-gray-600 text-white px-3 py-2 text-sm min-h-[44px] hover:bg-gray-700">
Wieder offen setzen
</button>
</form>
</div>
@empty
<p class="text-gray-600">Noch keine erledigten Eintraege.</p>
@endforelse

View File

@ -0,0 +1,125 @@
{{-- Ein Bleistift oeffnet alle Aenderungsoptionen: Stammdaten (update) + optional Preis/Foto beim Abhaken (nur wenn $showToggleExtras) --}}
@php
$showToggleExtras = $showToggleExtras ?? false;
@endphp
<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"
aria-label="Eintrag bearbeiten"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</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-3 text-sm font-medium text-gray-900">Eintrag anpassen</p>
<form method="POST" action="{{ route('shopping-items.update', $item) }}" class="grid grid-cols-1 gap-2">
@csrf
@method('PATCH')
<label class="text-xs font-medium text-gray-600">Produktname</label>
<input
type="text"
name="product_name"
value="{{ $item->product_name }}"
required
class="rounded-md border-gray-300 text-sm min-h-[44px]"
autocomplete="off"
>
<label class="text-xs font-medium text-gray-600">Menge (optional)</label>
<input
type="text"
name="quantity"
value="{{ $item->quantity }}"
placeholder="z. B. 2 Stueck"
class="rounded-md border-gray-300 text-sm min-h-[44px]"
>
<label class="text-xs font-medium text-gray-600">Geschaeft (optional)</label>
<select name="store_id" class="rounded-md border-gray-300 text-sm min-h-[44px]">
<option value=""></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"
value=""
placeholder="Neues Geschaeft (optional)"
list="store-options"
class="rounded-md border-gray-300 text-sm min-h-[44px]"
>
<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"
>
Aenderungen speichern
</button>
</form>
@if($showToggleExtras)
<div class="my-4 border-t border-gray-200"></div>
<p class="mb-2 text-xs font-medium text-gray-700">Beim Abhaken (optional)</p>
<p class="mb-3 text-xs text-gray-500">Preis und Foto nur noetig, wenn du sie gleich mit erfassen willst reicht auch die Checkbox in der Liste.</p>
<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"
>
Erledigt mit Angaben
</button>
</form>
@endif
</div>
</details>

View File

@ -11,6 +11,7 @@ Route::get('/', function () {
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', [ShoppingListController::class, 'index'])->name('dashboard');
Route::post('/shopping-items', [ShoppingListController::class, 'store'])->name('shopping-items.store');
Route::patch('/shopping-items/{shoppingItem}', [ShoppingListController::class, 'update'])->name('shopping-items.update');
Route::patch('/shopping-items/{shoppingItem}/toggle', [ShoppingListController::class, 'toggle'])->name('shopping-items.toggle');
});