Preisstaffel fuer Eintraege erfassen

Made-with: Cursor
This commit is contained in:
Stefan Zwischenbrugger 2026-03-30 12:27:53 +02:00
parent a707aadd4f
commit bfc6247322
7 changed files with 97 additions and 8 deletions

View File

@ -111,14 +111,16 @@ class ShoppingListController extends Controller
$photoPath = $request->file('photo')?->store('price-photos', 'public');
if (! $request->filled('price_decimal') && $photoPath === null) {
if (! $request->filled('price_decimal') && ! $request->filled('tier_price_decimal') && $photoPath === null) {
return;
}
ItemPriceLog::query()->create([
'shopping_item_id' => $shoppingItem->id,
'store_id' => $storeId,
'price_decimal' => $request->input('price_decimal') ?? 0,
'price_decimal' => $request->filled('price_decimal') ? $request->input('price_decimal') : null,
'tier_min_qty' => $request->filled('tier_min_qty') ? (int) $request->input('tier_min_qty') : null,
'tier_price_decimal' => $request->filled('tier_price_decimal') ? $request->input('tier_price_decimal') : null,
'currency' => 'EUR',
'logged_at' => Carbon::now(),
'photo_path' => $photoPath,
@ -159,14 +161,16 @@ class ShoppingListController extends Controller
$photoPath = $request->file('photo')?->store('price-photos', 'public');
if (! $request->filled('price_decimal') && $photoPath === null) {
if (! $request->filled('price_decimal') && ! $request->filled('tier_price_decimal') && $photoPath === null) {
return;
}
ItemPriceLog::query()->create([
'shopping_item_id' => $shoppingItem->id,
'store_id' => $storeId,
'price_decimal' => $request->input('price_decimal') ?? 0,
'price_decimal' => $request->filled('price_decimal') ? $request->input('price_decimal') : null,
'tier_min_qty' => $request->filled('tier_min_qty') ? (int) $request->input('tier_min_qty') : null,
'tier_price_decimal' => $request->filled('tier_price_decimal') ? $request->input('tier_price_decimal') : null,
'currency' => 'EUR',
'logged_at' => Carbon::now(),
'photo_path' => $photoPath,

View File

@ -35,6 +35,8 @@ class ToggleShoppingItemRequest extends FormRequest
'store_id' => ['nullable', 'integer', 'exists:stores,id'],
'new_store_name' => ['nullable', 'string', 'max:255'],
'price_decimal' => ['nullable', 'numeric', 'min:0', 'max:999999.99'],
'tier_min_qty' => ['nullable', 'integer', 'min:2', 'max:999999'],
'tier_price_decimal' => ['nullable', 'numeric', 'min:0', 'max:999999.99'],
'photo' => [
'nullable',
File::types(['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'])
@ -54,6 +56,12 @@ class ToggleShoppingItemRequest extends FormRequest
'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.',
'tier_min_qty.integer' => 'Die Mindestmenge (Staffel) muss eine ganze Zahl sein.',
'tier_min_qty.min' => 'Die Mindestmenge (Staffel) muss mindestens 2 sein.',
'tier_min_qty.max' => 'Die Mindestmenge (Staffel) ist zu gross.',
'tier_price_decimal.numeric' => 'Der Staffelpreis muss eine Zahl sein.',
'tier_price_decimal.min' => 'Der Staffelpreis darf nicht negativ sein.',
'tier_price_decimal.max' => 'Der Staffelpreis ist zu gross.',
'photo.max' => 'Das Foto darf maximal 15 MB gross sein.',
'photo.uploaded' => 'Das Foto konnte nicht hochgeladen werden. Haeufig: Datei zu gross fuer die PHP-Grenze auf dem Server (upload_max_filesize / post_max_size). Bitte ein kleineres Bild waehlen oder die Server-Limits erhoehen (siehe scripts/apache-einkauf-debian-setup.sh, Abschnitt PHP-Uploads).',
];
@ -66,6 +74,8 @@ class ToggleShoppingItemRequest extends FormRequest
'store_id' => 'Geschaeft',
'new_store_name' => 'neues Geschaeft',
'price_decimal' => 'Preis',
'tier_min_qty' => 'Mindestmenge (Staffel)',
'tier_price_decimal' => 'Staffelpreis',
'photo' => 'Foto',
];
}

View File

@ -31,6 +31,8 @@ class UpdateShoppingItemRequest extends FormRequest
'new_store_name' => ['nullable', 'string', 'max:255'],
'is_done' => ['sometimes', 'boolean'],
'price_decimal' => ['nullable', 'numeric', 'min:0', 'max:999999.99'],
'tier_min_qty' => ['nullable', 'integer', 'min:2', 'max:999999'],
'tier_price_decimal' => ['nullable', 'numeric', 'min:0', 'max:999999.99'],
'photo' => [
'nullable',
File::types(['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'])
@ -50,6 +52,12 @@ class UpdateShoppingItemRequest extends FormRequest
'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.',
'tier_min_qty.integer' => 'Die Mindestmenge (Staffel) muss eine ganze Zahl sein.',
'tier_min_qty.min' => 'Die Mindestmenge (Staffel) muss mindestens 2 sein.',
'tier_min_qty.max' => 'Die Mindestmenge (Staffel) ist zu gross.',
'tier_price_decimal.numeric' => 'Der Staffelpreis muss eine Zahl sein.',
'tier_price_decimal.min' => 'Der Staffelpreis darf nicht negativ sein.',
'tier_price_decimal.max' => 'Der Staffelpreis ist zu gross.',
'photo.max' => 'Das Foto darf maximal 15 MB gross sein.',
'photo.uploaded' => 'Das Foto konnte nicht hochgeladen werden. Haeufig: Datei zu gross fuer die PHP-Grenze auf dem Server (upload_max_filesize / post_max_size). Bitte ein kleineres Bild waehlen oder die Server-Limits erhoehen (siehe scripts/apache-einkauf-debian-setup.sh, Abschnitt PHP-Uploads).',
];
@ -63,6 +71,8 @@ class UpdateShoppingItemRequest extends FormRequest
'store_id' => 'Geschaeft',
'new_store_name' => 'neues Geschaeft',
'price_decimal' => 'Preis',
'tier_min_qty' => 'Mindestmenge (Staffel)',
'tier_price_decimal' => 'Staffelpreis',
'photo' => 'Foto',
];
}

View File

@ -11,6 +11,8 @@ class ItemPriceLog extends Model
'shopping_item_id',
'store_id',
'price_decimal',
'tier_min_qty',
'tier_price_decimal',
'currency',
'logged_at',
'photo_path',
@ -21,6 +23,7 @@ class ItemPriceLog extends Model
{
return [
'price_decimal' => 'decimal:2',
'tier_price_decimal' => 'decimal:2',
'logged_at' => 'datetime',
];
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('item_price_logs', function (Blueprint $table) {
$table->unsignedInteger('tier_min_qty')->nullable()->after('price_decimal');
$table->decimal('tier_price_decimal', 10, 2)->nullable()->after('tier_min_qty');
});
// MySQL: Spaltenaenderung ohne doctrine/dbal.
DB::statement('ALTER TABLE item_price_logs MODIFY price_decimal DECIMAL(10,2) NULL');
}
public function down(): void
{
Schema::table('item_price_logs', function (Blueprint $table) {
$table->dropColumn(['tier_min_qty', 'tier_price_decimal']);
});
DB::statement('ALTER TABLE item_price_logs MODIFY price_decimal DECIMAL(10,2) NOT NULL');
}
};

View File

@ -136,7 +136,16 @@
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' : '-' }}
Letzter Preis:
@if($item->latestPriceLog?->price_decimal !== null)
{{ number_format((float) $item->latestPriceLog->price_decimal, 2, ',', '.') }} EUR/Stk
@else
-
@endif
@if($item->latestPriceLog?->tier_min_qty !== null && $item->latestPriceLog?->tier_price_decimal !== null)
<span class="mx-1">·</span>
{{ number_format((float) $item->latestPriceLog->tier_price_decimal, 2, ',', '.') }} EUR ab {{ (int) $item->latestPriceLog->tier_min_qty }} Stk
@endif
</div>
@if($item->latestPhotoLog)
<div class="mt-2">
@ -218,7 +227,8 @@
<select
id="list-switch"
class="rounded-md border-gray-300 text-sm min-h-[44px] max-w-md flex-1"
onchange="this.form.action = @js(url('/shopping-lists')) + '/' + this.value + '/switch'; this.form.requestSubmit();"
data-base-url="{{ url('/shopping-lists') }}"
onchange="this.form.action = this.dataset.baseUrl + '/' + this.value + '/switch'; this.form.requestSubmit();"
>
@foreach($accessibleLists as $list)
<option value="{{ $list->id }}" @selected($list->id === $currentList->id)>

View File

@ -86,15 +86,37 @@
step="0.01"
min="0"
name="price_decimal"
placeholder="Preis in EUR (optional)"
placeholder="Preis pro 1 Stueck (optional) z. B. 3.00"
class="rounded-md border-gray-300 text-sm min-h-[44px]"
>
<div class="grid grid-cols-2 gap-2">
<input
type="number"
min="2"
step="1"
name="tier_min_qty"
placeholder="ab Menge (optional) z. B. 2"
class="rounded-md border-gray-300 text-sm min-h-[44px]"
>
<input
type="number"
step="0.01"
min="0"
name="tier_price_decimal"
placeholder="Staffelpreis (optional) z. B. 2.00"
class="rounded-md border-gray-300 text-sm min-h-[44px]"
>
</div>
<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
{{ $item->latestPriceLog->price_decimal !== null ? number_format((float) $item->latestPriceLog->price_decimal, 2, ',', '.') . ' EUR/Stk' : '-' }}
@if($item->latestPriceLog->tier_min_qty !== null && $item->latestPriceLog->tier_price_decimal !== null)
<span class="mx-1">·</span>
{{ number_format((float) $item->latestPriceLog->tier_price_decimal, 2, ',', '.') }} EUR ab {{ (int) $item->latestPriceLog->tier_min_qty }} Stk
@endif
bei {{ $item->latestPriceLog->store?->name ?: 'ohne Geschaeft' }}
({{ optional($item->latestPriceLog->logged_at)->format('d.m.Y H:i') }})
</div>