From 78679e0c5550f5c1d9f9154533fb621dbd5aa539 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Tue, 2 Jun 2026 16:30:42 +0000 Subject: [PATCH] Warenwirtschaft: AP-00 bis AP-08 + aktualisierter Entwicklungsplan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Umsetzung der Warenwirtschafts-/Produktmanagement-Erweiterung gemaess Entwicklungsplan V4.0: - AP-00: Regressionsbasis fuer 5.1-Features (ProductPhase51Test) - AP-01: URL-Bugfixes B1/B2 (suppliers/packaging-items, breitere url-Spalten) - AP-04/04.1: iPad-taugliche, vereinheitlichte Tabellen-Aktionen - AP-05: Einstellungen "Allgemein" mit UST-Saetzen (tax_rates) und Lieferzeit-Vorlagen (delivery_times, inkl. Tage-Feld) - AP-06: Lieferanten um Bestellweg, Bestell-Mail/-URL und Lieferzeit erweitert - AP-07/07.1: INCI um Lieferanten-Mehrfachwahl, UST und Lieferzeit erweitert; Lieferanten-Detailansicht im Modal mit pflegbaren INCI-/Verpackungslisten - AP-08: Einkauf um UST-Snapshot, Netto/Brutto-Automatik und Duplizieren erweitert Entwicklungsplan aktualisiert: alle Klaerungspunkte (§5) vom Kunden beantwortet und in die jeweiligen APs eingearbeitet (AP-02/03/09/13/15), neues AP-18 (Hinweise-Doku unter Einstellungen) ergaenzt. Naechster Schritt eindeutig markiert: AP-09 (Produktion auf Hersteller-Rezeptur, kein Fallback, Warnung). --- .../Inventory/DeliveryTimeController.php | 54 +++ .../Inventory/GeneralSettingController.php | 19 + .../Admin/Inventory/StockEntryController.php | 33 ++ .../Admin/Inventory/SupplierController.php | 72 +++ .../Admin/Inventory/TaxRateController.php | 54 +++ app/Http/Controllers/IngredientController.php | 38 +- .../Inventory/StoreDeliveryTimeRequest.php | 35 ++ .../Inventory/StorePackagingItemRequest.php | 2 +- .../Inventory/StoreStockEntryRequest.php | 13 +- .../Inventory/StoreSupplierRequest.php | 6 + .../Inventory/StoreTaxRateRequest.php | 34 ++ .../Inventory/UpdateDeliveryTimeRequest.php | 35 ++ .../Inventory/UpdatePackagingItemRequest.php | 2 +- .../Inventory/UpdateStockEntryRequest.php | 13 +- .../Inventory/UpdateSupplierRequest.php | 6 + .../Inventory/UpdateTaxRateRequest.php | 34 ++ app/Http/Requests/StoreIngredientRequest.php | 61 +++ app/Models/DeliveryTime.php | 41 ++ app/Models/Ingredient.php | 23 + app/Models/StockEntry.php | 13 + app/Models/Supplier.php | 16 + app/Models/TaxRate.php | 41 ++ app/Repositories/StockEntryRepository.php | 53 +++ app/Repositories/SupplierRepository.php | 5 + database/factories/DeliveryTimeFactory.php | 27 ++ database/factories/TaxRateFactory.php | 27 ++ ..._widen_url_columns_in_inventory_tables.php | 30 ++ ...26_06_02_152721_create_tax_rates_table.php | 31 ++ ..._02_153243_create_delivery_times_table.php | 30 ++ ...55_add_order_fields_to_suppliers_table.php | 25 ++ ...60411_add_days_to_delivery_times_table.php | 22 + ..._delivery_time_days_to_suppliers_table.php | 22 + ..._add_order_fields_to_ingredients_table.php | 27 ++ ...61237_create_ingredient_supplier_table.php | 30 ++ ...dd_price_fields_to_stock_entries_table.php | 27 ++ .../seeders/InventoryStammdatenSeeder.php | 26 ++ ...ntwicklungsplan-aktualisiert-02-06-2026.md | 425 ++++++++++++++++++ .../views/admin/ingredient/edit.blade.php | 24 + .../views/admin/ingredient/form.blade.php | 50 +++ .../inventory/delivery-times/form.blade.php | 56 +++ .../admin/inventory/general/index.blade.php | 118 +++++ .../admin/inventory/locations/index.blade.php | 3 +- .../material-qualities/index.blade.php | 3 +- .../inventory/packaging-items/form.blade.php | 2 +- .../inventory/packaging-items/index.blade.php | 3 +- .../packaging-materials/index.blade.php | 3 +- .../partials/table-actions-style.blade.php | 37 ++ .../inventory/productions/index.blade.php | 30 +- .../inventory/stock-entries/_form.blade.php | 53 ++- .../stock-entries/_scripts.blade.php | 47 ++ .../inventory/stock-entries/index.blade.php | 32 +- .../inventory/stock-entries/show.blade.php | 11 +- .../supplier-categories/index.blade.php | 3 +- .../inventory/suppliers/_details.blade.php | 161 +++++++ .../admin/inventory/suppliers/form.blade.php | 158 +++++-- .../admin/inventory/suppliers/index.blade.php | 68 ++- .../admin/inventory/tax-rates/form.blade.php | 60 +++ .../layouts/includes/layout-sidenav.blade.php | 9 + routes/web.php | 13 +- tests/Feature/DeliveryTimeSettingsTest.php | 126 ++++++ tests/Feature/IngredientOrderFieldsTest.php | 135 ++++++ tests/Feature/InventoryUrlFieldsTest.php | 100 +++++ tests/Feature/ProductPhase51Test.php | 263 +++++++++++ tests/Feature/StockEntryPriceTest.php | 203 +++++++++ tests/Feature/SupplierDetailsTest.php | 114 +++++ tests/Feature/SupplierOrderFieldsTest.php | 176 ++++++++ tests/Feature/TaxRateSettingsTest.php | 111 +++++ 67 files changed, 3523 insertions(+), 101 deletions(-) create mode 100644 app/Http/Controllers/Admin/Inventory/DeliveryTimeController.php create mode 100644 app/Http/Controllers/Admin/Inventory/GeneralSettingController.php create mode 100644 app/Http/Controllers/Admin/Inventory/TaxRateController.php create mode 100644 app/Http/Requests/Inventory/StoreDeliveryTimeRequest.php create mode 100644 app/Http/Requests/Inventory/StoreTaxRateRequest.php create mode 100644 app/Http/Requests/Inventory/UpdateDeliveryTimeRequest.php create mode 100644 app/Http/Requests/Inventory/UpdateTaxRateRequest.php create mode 100644 app/Http/Requests/StoreIngredientRequest.php create mode 100644 app/Models/DeliveryTime.php create mode 100644 app/Models/TaxRate.php create mode 100644 database/factories/DeliveryTimeFactory.php create mode 100644 database/factories/TaxRateFactory.php create mode 100644 database/migrations/2026_06_02_145358_widen_url_columns_in_inventory_tables.php create mode 100644 database/migrations/2026_06_02_152721_create_tax_rates_table.php create mode 100644 database/migrations/2026_06_02_153243_create_delivery_times_table.php create mode 100644 database/migrations/2026_06_02_154755_add_order_fields_to_suppliers_table.php create mode 100644 database/migrations/2026_06_02_160411_add_days_to_delivery_times_table.php create mode 100644 database/migrations/2026_06_02_160411_add_delivery_time_days_to_suppliers_table.php create mode 100644 database/migrations/2026_06_02_161237_add_order_fields_to_ingredients_table.php create mode 100644 database/migrations/2026_06_02_161237_create_ingredient_supplier_table.php create mode 100644 database/migrations/2026_06_02_181548_add_price_fields_to_stock_entries_table.php create mode 100644 dev/product management /entwicklungsplan-aktualisiert-02-06-2026.md create mode 100644 resources/views/admin/inventory/delivery-times/form.blade.php create mode 100644 resources/views/admin/inventory/general/index.blade.php create mode 100644 resources/views/admin/inventory/partials/table-actions-style.blade.php create mode 100644 resources/views/admin/inventory/suppliers/_details.blade.php create mode 100644 resources/views/admin/inventory/tax-rates/form.blade.php create mode 100644 tests/Feature/DeliveryTimeSettingsTest.php create mode 100644 tests/Feature/IngredientOrderFieldsTest.php create mode 100644 tests/Feature/InventoryUrlFieldsTest.php create mode 100644 tests/Feature/ProductPhase51Test.php create mode 100644 tests/Feature/StockEntryPriceTest.php create mode 100644 tests/Feature/SupplierDetailsTest.php create mode 100644 tests/Feature/SupplierOrderFieldsTest.php create mode 100644 tests/Feature/TaxRateSettingsTest.php diff --git a/app/Http/Controllers/Admin/Inventory/DeliveryTimeController.php b/app/Http/Controllers/Admin/Inventory/DeliveryTimeController.php new file mode 100644 index 0000000..0d18b37 --- /dev/null +++ b/app/Http/Controllers/Admin/Inventory/DeliveryTimeController.php @@ -0,0 +1,54 @@ + new DeliveryTime(['active' => true, 'pos' => 0]), + ]); + } + + public function store(StoreDeliveryTimeRequest $request): RedirectResponse + { + DeliveryTime::create($request->validated()); + + \Session::flash('alert-save', '1'); + + return redirect()->route('admin.inventory.general'); + } + + public function edit(DeliveryTime $deliveryTime): View + { + return view('admin.inventory.delivery-times.form', [ + 'model' => $deliveryTime, + ]); + } + + public function update(UpdateDeliveryTimeRequest $request, DeliveryTime $deliveryTime): RedirectResponse + { + $deliveryTime->update($request->validated()); + + \Session::flash('alert-save', '1'); + + return redirect()->route('admin.inventory.general'); + } + + public function destroy(DeliveryTime $deliveryTime): RedirectResponse + { + $deliveryTime->delete(); + + \Session::flash('alert-success', __('Eintrag gelöscht')); + + return redirect()->route('admin.inventory.general'); + } +} diff --git a/app/Http/Controllers/Admin/Inventory/GeneralSettingController.php b/app/Http/Controllers/Admin/Inventory/GeneralSettingController.php new file mode 100644 index 0000000..4fbb773 --- /dev/null +++ b/app/Http/Controllers/Admin/Inventory/GeneralSettingController.php @@ -0,0 +1,19 @@ + TaxRate::query()->orderBy('pos')->orderBy('percent')->get(), + 'deliveryTimes' => DeliveryTime::query()->orderBy('pos')->orderBy('label')->get(), + ]); + } +} diff --git a/app/Http/Controllers/Admin/Inventory/StockEntryController.php b/app/Http/Controllers/Admin/Inventory/StockEntryController.php index 44d70e3..4fb5ad7 100644 --- a/app/Http/Controllers/Admin/Inventory/StockEntryController.php +++ b/app/Http/Controllers/Admin/Inventory/StockEntryController.php @@ -12,6 +12,7 @@ use App\Models\MaterialQuality; use App\Models\PackagingItem; use App\Models\StockEntry; use App\Models\Supplier; +use App\Models\TaxRate; use App\Repositories\StockEntryRepository; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; @@ -115,6 +116,37 @@ class StockEntryController extends Controller return redirect()->route('admin.inventory.stock-entries.index'); } + public function copy(StockEntry $stockEntry): RedirectResponse + { + if (! auth()->user()->isAdmin()) { + return redirect()->route('home'); + } + + $data = $stockEntry->only([ + 'entry_type', + 'ingredient_id', + 'packaging_item_id', + 'supplier_id', + 'location_id', + 'ordered_quantity', + 'price_per_kg', + 'price_per_kg_gross', + 'price_total', + 'tax_rate_id', + 'tax_rate_percent', + 'quality_id', + ]); + $data['ordered_at'] = now()->toDateString(); + $data['ordered_by'] = (int) auth()->id(); + $data['status'] = 'pending'; + + $copy = $this->stockEntryRepository->create($data); + + \Session::flash('alert-warning', __('Kopie angelegt – bitte Menge/Charge prüfen und speichern.')); + + return redirect()->route('admin.inventory.stock-entries.edit', $copy); + } + public function destroy(StockEntry $stockEntry): RedirectResponse { if (! auth()->user()->isAdmin()) { @@ -223,6 +255,7 @@ class StockEntryController extends Controller 'suppliers' => Supplier::query()->where('active', true)->orderBy('name')->get(), 'locations' => Location::query()->where('active', true)->orderBy('name')->get(), 'materialQualities' => MaterialQuality::query()->orderBy('pos')->orderBy('name')->get(), + 'taxRates' => TaxRate::query()->active()->orderBy('pos')->orderBy('name')->get(), 'entryTypeLabels' => [ 'ingredient' => __('Rohstoff'), 'packaging' => __('Produktverpackung'), diff --git a/app/Http/Controllers/Admin/Inventory/SupplierController.php b/app/Http/Controllers/Admin/Inventory/SupplierController.php index 68925db..1933621 100644 --- a/app/Http/Controllers/Admin/Inventory/SupplierController.php +++ b/app/Http/Controllers/Admin/Inventory/SupplierController.php @@ -6,9 +6,14 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Inventory\StoreSupplierRequest; use App\Http\Requests\Inventory\UpdateSupplierRequest; use App\Models\Country; +use App\Models\DeliveryTime; +use App\Models\Ingredient; +use App\Models\PackagingItem; use App\Models\Supplier; use App\Models\SupplierCategory; use App\Repositories\SupplierRepository; +use Illuminate\Contracts\View\View; +use Illuminate\Http\Request; class SupplierController extends Controller { @@ -31,6 +36,7 @@ class SupplierController extends Controller 'model' => new Supplier(['active' => true, 'country_id' => $defaultCountryId]), 'countries' => Country::query()->orderBy('de')->get(), 'supplierCategories' => SupplierCategory::query()->orderBy('pos')->orderBy('name')->get(), + 'deliveryTimes' => DeliveryTime::query()->active()->orderBy('pos')->orderBy('label')->get(), ]); } @@ -43,6 +49,71 @@ class SupplierController extends Controller return redirect()->route('admin.inventory.suppliers.index'); } + public function show(Supplier $supplier): View + { + return $this->renderDetails($supplier); + } + + public function attachIngredient(Request $request, Supplier $supplier): View + { + $validated = $request->validate([ + 'ingredient_id' => ['required', 'integer', 'exists:ingredients,id'], + ]); + + $supplier->ingredients()->syncWithoutDetaching([$validated['ingredient_id']]); + + return $this->renderDetails($supplier); + } + + public function detachIngredient(Supplier $supplier, Ingredient $ingredient): View + { + $supplier->ingredients()->detach($ingredient->id); + + return $this->renderDetails($supplier); + } + + public function attachPackagingItem(Request $request, Supplier $supplier): View + { + $validated = $request->validate([ + 'packaging_item_id' => ['required', 'integer', 'exists:packaging_items,id'], + ]); + + PackagingItem::query() + ->whereKey($validated['packaging_item_id']) + ->update(['supplier_id' => $supplier->id]); + + return $this->renderDetails($supplier); + } + + public function detachPackagingItem(Supplier $supplier, PackagingItem $packagingItem): View + { + if ((int) $packagingItem->supplier_id === (int) $supplier->id) { + $packagingItem->update(['supplier_id' => null]); + } + + return $this->renderDetails($supplier); + } + + protected function renderDetails(Supplier $supplier): View + { + $supplier->load(['country', 'supplierCategories', 'ingredients', 'packagingItems']); + + $assignedIngredientIds = $supplier->ingredients->pluck('id')->all(); + $assignedPackagingItemIds = $supplier->packagingItems->pluck('id')->all(); + + return view('admin.inventory.suppliers._details', [ + 'supplier' => $supplier, + 'availableIngredients' => Ingredient::query() + ->whereNotIn('id', $assignedIngredientIds) + ->orderBy('name') + ->get(), + 'availablePackagingItems' => PackagingItem::query() + ->whereNotIn('id', $assignedPackagingItemIds) + ->orderBy('name') + ->get(), + ]); + } + public function edit(Supplier $supplier) { $supplier->load('supplierCategories'); @@ -51,6 +122,7 @@ class SupplierController extends Controller 'model' => $supplier, 'countries' => Country::query()->orderBy('de')->get(), 'supplierCategories' => SupplierCategory::query()->orderBy('pos')->orderBy('name')->get(), + 'deliveryTimes' => DeliveryTime::query()->active()->orderBy('pos')->orderBy('label')->get(), ]); } diff --git a/app/Http/Controllers/Admin/Inventory/TaxRateController.php b/app/Http/Controllers/Admin/Inventory/TaxRateController.php new file mode 100644 index 0000000..1be9b4a --- /dev/null +++ b/app/Http/Controllers/Admin/Inventory/TaxRateController.php @@ -0,0 +1,54 @@ + new TaxRate(['active' => true, 'pos' => 0]), + ]); + } + + public function store(StoreTaxRateRequest $request): RedirectResponse + { + TaxRate::create($request->validated()); + + \Session::flash('alert-save', '1'); + + return redirect()->route('admin.inventory.general'); + } + + public function edit(TaxRate $taxRate): View + { + return view('admin.inventory.tax-rates.form', [ + 'model' => $taxRate, + ]); + } + + public function update(UpdateTaxRateRequest $request, TaxRate $taxRate): RedirectResponse + { + $taxRate->update($request->validated()); + + \Session::flash('alert-save', '1'); + + return redirect()->route('admin.inventory.general'); + } + + public function destroy(TaxRate $taxRate): RedirectResponse + { + $taxRate->delete(); + + \Session::flash('alert-success', __('Eintrag gelöscht')); + + return redirect()->route('admin.inventory.general'); + } +} diff --git a/app/Http/Controllers/IngredientController.php b/app/Http/Controllers/IngredientController.php index 71db4bb..96024f1 100755 --- a/app/Http/Controllers/IngredientController.php +++ b/app/Http/Controllers/IngredientController.php @@ -2,9 +2,12 @@ namespace App\Http\Controllers; +use App\Http\Requests\StoreIngredientRequest; +use App\Models\DeliveryTime; use App\Models\Ingredient; use App\Models\ProductIngredient; -use Request; +use App\Models\Supplier; +use App\Models\TaxRate; class IngredientController extends Controller { @@ -29,40 +32,35 @@ class IngredientController extends Controller $model = new Ingredient; $model->active = true; } else { - $model = Ingredient::findOrFail($id); + $model = Ingredient::with('suppliers')->findOrFail($id); } $data = [ 'model' => $model, - // 'trans' => array_keys(config('localization.supportedLocales')), - + 'taxRates' => TaxRate::query()->active()->orderBy('pos')->orderBy('name')->get(), + 'deliveryTimes' => DeliveryTime::query()->active()->orderBy('pos')->orderBy('label')->get(), + 'suppliers' => Supplier::query()->where('active', true)->orderBy('name')->get(), ]; return view('admin.ingredient.edit', $data); } - public function store() + public function store(StoreIngredientRequest $request) { + $data = $request->validated(); + $data['default_factor'] = $data['default_factor'] ?? 1.10; - $data = Request::all(); - $data['active'] = isset($data['active']) ? true : false; - if (isset($data['default_factor']) && $data['default_factor'] !== '') { - $data['default_factor'] = reFormatNumber($data['default_factor']) ?: 1.10; - } - if (isset($data['min_stock_alert']) && $data['min_stock_alert'] === '') { - $data['min_stock_alert'] = null; - } elseif (isset($data['min_stock_alert']) && $data['min_stock_alert'] !== null) { - $data['min_stock_alert'] = reFormatNumber($data['min_stock_alert']); - } - if (empty($data['material_quality_id'])) { - $data['material_quality_id'] = null; - } - if ($data['id'] === 'new') { + $supplierIds = $request->input('supplier_ids', []); + unset($data['supplier_ids']); + + if ($request->input('id') === 'new') { $model = Ingredient::create($data); } else { - $model = Ingredient::find($data['id']); + $model = Ingredient::findOrFail($request->input('id')); $model->fill($data)->save(); } + $model->suppliers()->sync($supplierIds); + \Session()->flash('alert-save', '1'); return redirect(route('admin_product_ingredients')); diff --git a/app/Http/Requests/Inventory/StoreDeliveryTimeRequest.php b/app/Http/Requests/Inventory/StoreDeliveryTimeRequest.php new file mode 100644 index 0000000..4889339 --- /dev/null +++ b/app/Http/Requests/Inventory/StoreDeliveryTimeRequest.php @@ -0,0 +1,35 @@ +> + */ + public function rules(): array + { + return [ + 'label' => ['required', 'string', 'max:255'], + 'days' => ['nullable', 'integer', 'min:0', 'max:65535'], + 'active' => ['sometimes', 'boolean'], + 'pos' => ['nullable', 'integer', 'min:0', 'max:255'], + ]; + } + + protected function prepareForValidation(): void + { + $this->merge([ + 'active' => $this->boolean('active'), + 'days' => $this->input('days', '') === '' ? null : $this->input('days'), + 'pos' => $this->input('pos', '') === '' ? 0 : $this->input('pos'), + ]); + } +} diff --git a/app/Http/Requests/Inventory/StorePackagingItemRequest.php b/app/Http/Requests/Inventory/StorePackagingItemRequest.php index f483a6d..26f9e32 100644 --- a/app/Http/Requests/Inventory/StorePackagingItemRequest.php +++ b/app/Http/Requests/Inventory/StorePackagingItemRequest.php @@ -24,7 +24,7 @@ class StorePackagingItemRequest extends FormRequest 'category' => ['required', Rule::in(['packaging', 'shipping'])], 'weight_grams' => ['nullable', 'numeric', 'min:0'], 'min_stock_alert' => ['nullable', 'integer', 'min:0'], - 'url' => ['nullable', 'url', 'max:500'], + 'url' => ['nullable', 'string', 'max:2048'], 'product_id' => ['nullable', 'integer', 'exists:products,id'], 'active' => ['sometimes', 'boolean'], ]; diff --git a/app/Http/Requests/Inventory/StoreStockEntryRequest.php b/app/Http/Requests/Inventory/StoreStockEntryRequest.php index 83749ff..716ef9b 100644 --- a/app/Http/Requests/Inventory/StoreStockEntryRequest.php +++ b/app/Http/Requests/Inventory/StoreStockEntryRequest.php @@ -27,7 +27,9 @@ class StoreStockEntryRequest extends FormRequest 'ordered_at' => ['required', 'date'], 'ordered_quantity' => ['required', 'numeric', 'min:0.000001'], 'quality_id' => ['nullable', 'integer', 'exists:material_qualities,id'], + 'tax_rate_id' => ['nullable', 'integer', 'exists:tax_rates,id'], 'price_per_kg' => ['nullable', 'numeric', 'min:0'], + 'price_per_kg_gross' => ['nullable', 'numeric', 'min:0'], 'price_total' => ['nullable', 'numeric', 'min:0'], ]; } @@ -39,7 +41,7 @@ class StoreStockEntryRequest extends FormRequest { return [ 'quality_id.required' => __('Bitte eine Rohstoffqualität wählen.'), - 'price_per_kg.required' => __('Bitte den Netto-Preis pro kg angeben.'), + 'price_per_kg.required' => __('Bitte den Netto- oder Brutto-Preis pro kg angeben.'), 'price_total.required' => __('Bitte den Gesamtpreis netto angeben.'), ]; } @@ -49,6 +51,7 @@ class StoreStockEntryRequest extends FormRequest $this->merge([ 'ordered_quantity' => reFormatNumber($this->input('ordered_quantity')), 'price_per_kg' => $this->filled('price_per_kg') ? reFormatNumber($this->input('price_per_kg')) : null, + 'price_per_kg_gross' => $this->filled('price_per_kg_gross') ? reFormatNumber($this->input('price_per_kg_gross')) : null, 'price_total' => $this->filled('price_total') ? reFormatNumber($this->input('price_total')) : null, ]); } @@ -64,8 +67,12 @@ class StoreStockEntryRequest extends FormRequest if (empty($this->input('quality_id'))) { $validator->errors()->add('quality_id', __('Bitte eine Rohstoffqualität wählen.')); } - if (! is_numeric($this->input('price_per_kg')) || (float) $this->input('price_per_kg') <= 0) { - $validator->errors()->add('price_per_kg', __('Bitte den Netto-Preis pro kg angeben.')); + $net = $this->input('price_per_kg'); + $gross = $this->input('price_per_kg_gross'); + $hasNet = is_numeric($net) && (float) $net > 0; + $hasGross = is_numeric($gross) && (float) $gross > 0; + if (! $hasNet && ! $hasGross) { + $validator->errors()->add('price_per_kg', __('Bitte den Netto- oder Brutto-Preis pro kg angeben.')); } } elseif (! empty($type)) { if (empty($this->input('packaging_item_id'))) { diff --git a/app/Http/Requests/Inventory/StoreSupplierRequest.php b/app/Http/Requests/Inventory/StoreSupplierRequest.php index c06d09b..ef0b408 100644 --- a/app/Http/Requests/Inventory/StoreSupplierRequest.php +++ b/app/Http/Requests/Inventory/StoreSupplierRequest.php @@ -19,6 +19,11 @@ class StoreSupplierRequest extends FormRequest return [ 'name' => ['required', 'string', 'max:255'], 'url' => ['nullable', 'string', 'max:2048'], + 'order_method' => ['nullable', 'in:email,online_shop'], + 'order_email' => ['nullable', 'email', 'max:255'], + 'order_url' => ['nullable', 'string', 'max:2048'], + 'delivery_time' => ['nullable', 'string', 'max:255'], + 'delivery_time_days' => ['nullable', 'integer', 'min:0', 'max:65535'], 'contact_person' => ['nullable', 'string', 'max:255'], 'email' => ['nullable', 'email', 'max:255'], 'phone' => ['nullable', 'string', 'max:100'], @@ -34,6 +39,7 @@ class StoreSupplierRequest extends FormRequest { $this->merge([ 'active' => $this->boolean('active'), + 'delivery_time_days' => $this->input('delivery_time_days', '') === '' ? null : $this->input('delivery_time_days'), ]); } } diff --git a/app/Http/Requests/Inventory/StoreTaxRateRequest.php b/app/Http/Requests/Inventory/StoreTaxRateRequest.php new file mode 100644 index 0000000..c6a29ab --- /dev/null +++ b/app/Http/Requests/Inventory/StoreTaxRateRequest.php @@ -0,0 +1,34 @@ +> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'percent' => ['required', 'numeric', 'min:0', 'max:100'], + 'active' => ['sometimes', 'boolean'], + 'pos' => ['nullable', 'integer', 'min:0', 'max:255'], + ]; + } + + protected function prepareForValidation(): void + { + $this->merge([ + 'active' => $this->boolean('active'), + 'pos' => $this->input('pos', '') === '' ? 0 : $this->input('pos'), + ]); + } +} diff --git a/app/Http/Requests/Inventory/UpdateDeliveryTimeRequest.php b/app/Http/Requests/Inventory/UpdateDeliveryTimeRequest.php new file mode 100644 index 0000000..474f5b1 --- /dev/null +++ b/app/Http/Requests/Inventory/UpdateDeliveryTimeRequest.php @@ -0,0 +1,35 @@ +> + */ + public function rules(): array + { + return [ + 'label' => ['required', 'string', 'max:255'], + 'days' => ['nullable', 'integer', 'min:0', 'max:65535'], + 'active' => ['sometimes', 'boolean'], + 'pos' => ['nullable', 'integer', 'min:0', 'max:255'], + ]; + } + + protected function prepareForValidation(): void + { + $this->merge([ + 'active' => $this->boolean('active'), + 'days' => $this->input('days', '') === '' ? null : $this->input('days'), + 'pos' => $this->input('pos', '') === '' ? 0 : $this->input('pos'), + ]); + } +} diff --git a/app/Http/Requests/Inventory/UpdatePackagingItemRequest.php b/app/Http/Requests/Inventory/UpdatePackagingItemRequest.php index cd3a799..da67a49 100644 --- a/app/Http/Requests/Inventory/UpdatePackagingItemRequest.php +++ b/app/Http/Requests/Inventory/UpdatePackagingItemRequest.php @@ -24,7 +24,7 @@ class UpdatePackagingItemRequest extends FormRequest 'category' => ['required', Rule::in(['packaging', 'shipping'])], 'weight_grams' => ['nullable', 'numeric', 'min:0'], 'min_stock_alert' => ['nullable', 'integer', 'min:0'], - 'url' => ['nullable', 'url', 'max:500'], + 'url' => ['nullable', 'string', 'max:2048'], 'product_id' => ['nullable', 'integer', 'exists:products,id'], 'active' => ['sometimes', 'boolean'], ]; diff --git a/app/Http/Requests/Inventory/UpdateStockEntryRequest.php b/app/Http/Requests/Inventory/UpdateStockEntryRequest.php index 801e304..65435e7 100644 --- a/app/Http/Requests/Inventory/UpdateStockEntryRequest.php +++ b/app/Http/Requests/Inventory/UpdateStockEntryRequest.php @@ -27,7 +27,9 @@ class UpdateStockEntryRequest extends FormRequest 'ordered_at' => ['required', 'date'], 'ordered_quantity' => ['required', 'numeric', 'min:0.000001'], 'quality_id' => ['nullable', 'integer', 'exists:material_qualities,id'], + 'tax_rate_id' => ['nullable', 'integer', 'exists:tax_rates,id'], 'price_per_kg' => ['nullable', 'numeric', 'min:0'], + 'price_per_kg_gross' => ['nullable', 'numeric', 'min:0'], 'price_total' => ['nullable', 'numeric', 'min:0'], ]; } @@ -39,7 +41,7 @@ class UpdateStockEntryRequest extends FormRequest { return [ 'quality_id.required' => __('Bitte eine Rohstoffqualität wählen.'), - 'price_per_kg.required' => __('Bitte den Netto-Preis pro kg angeben.'), + 'price_per_kg.required' => __('Bitte den Netto- oder Brutto-Preis pro kg angeben.'), 'price_total.required' => __('Bitte den Gesamtpreis netto angeben.'), ]; } @@ -49,6 +51,7 @@ class UpdateStockEntryRequest extends FormRequest $this->merge([ 'ordered_quantity' => reFormatNumber($this->input('ordered_quantity')), 'price_per_kg' => $this->filled('price_per_kg') ? reFormatNumber($this->input('price_per_kg')) : null, + 'price_per_kg_gross' => $this->filled('price_per_kg_gross') ? reFormatNumber($this->input('price_per_kg_gross')) : null, 'price_total' => $this->filled('price_total') ? reFormatNumber($this->input('price_total')) : null, ]); } @@ -64,8 +67,12 @@ class UpdateStockEntryRequest extends FormRequest if (empty($this->input('quality_id'))) { $validator->errors()->add('quality_id', __('Bitte eine Rohstoffqualität wählen.')); } - if (! is_numeric($this->input('price_per_kg')) || (float) $this->input('price_per_kg') <= 0) { - $validator->errors()->add('price_per_kg', __('Bitte den Netto-Preis pro kg angeben.')); + $net = $this->input('price_per_kg'); + $gross = $this->input('price_per_kg_gross'); + $hasNet = is_numeric($net) && (float) $net > 0; + $hasGross = is_numeric($gross) && (float) $gross > 0; + if (! $hasNet && ! $hasGross) { + $validator->errors()->add('price_per_kg', __('Bitte den Netto- oder Brutto-Preis pro kg angeben.')); } } elseif (! empty($type)) { if (empty($this->input('packaging_item_id'))) { diff --git a/app/Http/Requests/Inventory/UpdateSupplierRequest.php b/app/Http/Requests/Inventory/UpdateSupplierRequest.php index 7a59e06..271ad44 100644 --- a/app/Http/Requests/Inventory/UpdateSupplierRequest.php +++ b/app/Http/Requests/Inventory/UpdateSupplierRequest.php @@ -19,6 +19,11 @@ class UpdateSupplierRequest extends FormRequest return [ 'name' => ['required', 'string', 'max:255'], 'url' => ['nullable', 'string', 'max:2048'], + 'order_method' => ['nullable', 'in:email,online_shop'], + 'order_email' => ['nullable', 'email', 'max:255'], + 'order_url' => ['nullable', 'string', 'max:2048'], + 'delivery_time' => ['nullable', 'string', 'max:255'], + 'delivery_time_days' => ['nullable', 'integer', 'min:0', 'max:65535'], 'contact_person' => ['nullable', 'string', 'max:255'], 'email' => ['nullable', 'email', 'max:255'], 'phone' => ['nullable', 'string', 'max:100'], @@ -34,6 +39,7 @@ class UpdateSupplierRequest extends FormRequest { $this->merge([ 'active' => $this->boolean('active'), + 'delivery_time_days' => $this->input('delivery_time_days', '') === '' ? null : $this->input('delivery_time_days'), ]); } } diff --git a/app/Http/Requests/Inventory/UpdateTaxRateRequest.php b/app/Http/Requests/Inventory/UpdateTaxRateRequest.php new file mode 100644 index 0000000..5cad11b --- /dev/null +++ b/app/Http/Requests/Inventory/UpdateTaxRateRequest.php @@ -0,0 +1,34 @@ +> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'percent' => ['required', 'numeric', 'min:0', 'max:100'], + 'active' => ['sometimes', 'boolean'], + 'pos' => ['nullable', 'integer', 'min:0', 'max:255'], + ]; + } + + protected function prepareForValidation(): void + { + $this->merge([ + 'active' => $this->boolean('active'), + 'pos' => $this->input('pos', '') === '' ? 0 : $this->input('pos'), + ]); + } +} diff --git a/app/Http/Requests/StoreIngredientRequest.php b/app/Http/Requests/StoreIngredientRequest.php new file mode 100644 index 0000000..416a101 --- /dev/null +++ b/app/Http/Requests/StoreIngredientRequest.php @@ -0,0 +1,61 @@ +> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'inci' => ['nullable', 'string'], + 'effect' => ['nullable', 'string'], + 'pos' => ['nullable', 'integer', 'min:0', 'max:255'], + 'default_factor' => ['nullable', 'numeric', 'min:0'], + 'min_stock_alert' => ['nullable', 'numeric', 'min:0'], + 'material_quality_id' => ['nullable', 'integer', 'exists:material_qualities,id'], + 'tax_rate_id' => ['nullable', 'integer', 'exists:tax_rates,id'], + 'delivery_time' => ['nullable', 'string', 'max:255'], + 'delivery_time_days' => ['nullable', 'integer', 'min:0', 'max:65535'], + 'supplier_ids' => ['nullable', 'array'], + 'supplier_ids.*' => ['integer', 'exists:suppliers,id'], + 'active' => ['sometimes', 'boolean'], + ]; + } + + protected function prepareForValidation(): void + { + $this->merge([ + 'active' => $this->boolean('active'), + 'default_factor' => $this->normalizeDecimal($this->input('default_factor')), + 'min_stock_alert' => $this->normalizeDecimal($this->input('min_stock_alert')), + 'material_quality_id' => $this->nullIfEmpty($this->input('material_quality_id')), + 'tax_rate_id' => $this->nullIfEmpty($this->input('tax_rate_id')), + 'delivery_time_days' => $this->nullIfEmpty($this->input('delivery_time_days')), + ]); + } + + private function normalizeDecimal(mixed $value): ?string + { + if ($value === null || $value === '') { + return null; + } + + return (string) reFormatNumber($value); + } + + private function nullIfEmpty(mixed $value): mixed + { + return $value === '' ? null : $value; + } +} diff --git a/app/Models/DeliveryTime.php b/app/Models/DeliveryTime.php new file mode 100644 index 0000000..223d283 --- /dev/null +++ b/app/Models/DeliveryTime.php @@ -0,0 +1,41 @@ + */ + use HasFactory; + + protected $fillable = [ + 'label', + 'days', + 'active', + 'pos', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'days' => 'integer', + 'active' => 'boolean', + ]; + } + + /** + * @param Builder $query + * @return Builder + */ + public function scopeActive(Builder $query): Builder + { + return $query->where('active', true); + } +} diff --git a/app/Models/Ingredient.php b/app/Models/Ingredient.php index aa870cd..871a32a 100644 --- a/app/Models/Ingredient.php +++ b/app/Models/Ingredient.php @@ -10,6 +10,7 @@ use Carbon\Carbon; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; /** * Class Ingredient @@ -56,6 +57,7 @@ class Ingredient extends Model 'pos' => 'int', 'default_factor' => 'decimal:2', 'min_stock_alert' => 'decimal:2', + 'delivery_time_days' => 'integer', ]; protected $fillable = [ @@ -70,6 +72,9 @@ class Ingredient extends Model 'default_factor', 'min_stock_alert', 'material_quality_id', + 'tax_rate_id', + 'delivery_time', + 'delivery_time_days', ]; /** @@ -80,6 +85,24 @@ class Ingredient extends Model return $this->belongsTo(MaterialQuality::class); } + /** + * @return BelongsTo + */ + public function taxRate(): BelongsTo + { + return $this->belongsTo(TaxRate::class); + } + + /** + * @return BelongsToMany + */ + public function suppliers(): BelongsToMany + { + return $this->belongsToMany(Supplier::class, 'ingredient_supplier') + ->withPivot(['preferred', 'supplier_sku', 'url']) + ->withTimestamps(); + } + public function products() { return $this->belongsToMany(Product::class, 'product_ingredients') diff --git a/app/Models/StockEntry.php b/app/Models/StockEntry.php index a97b6e5..8036640 100644 --- a/app/Models/StockEntry.php +++ b/app/Models/StockEntry.php @@ -24,7 +24,10 @@ class StockEntry extends Model 'ordered_at', 'ordered_quantity', 'price_per_kg', + 'price_per_kg_gross', 'price_total', + 'tax_rate_id', + 'tax_rate_percent', 'received_by', 'received_at', 'received_quantity', @@ -46,7 +49,9 @@ class StockEntry extends Model 'ordered_quantity' => 'decimal:2', 'received_quantity' => 'decimal:2', 'price_per_kg' => 'decimal:4', + 'price_per_kg_gross' => 'decimal:4', 'price_total' => 'decimal:4', + 'tax_rate_percent' => 'decimal:2', ]; } @@ -82,6 +87,14 @@ class StockEntry extends Model return $this->belongsTo(Location::class); } + /** + * @return BelongsTo + */ + public function taxRate(): BelongsTo + { + return $this->belongsTo(TaxRate::class); + } + /** * @return BelongsTo */ diff --git a/app/Models/Supplier.php b/app/Models/Supplier.php index 4c5d6e7..10a09d4 100644 --- a/app/Models/Supplier.php +++ b/app/Models/Supplier.php @@ -18,6 +18,11 @@ class Supplier extends Model protected $fillable = [ 'name', 'url', + 'order_method', + 'order_email', + 'order_url', + 'delivery_time', + 'delivery_time_days', 'contact_person', 'email', 'phone', @@ -33,6 +38,7 @@ class Supplier extends Model { return [ 'active' => 'boolean', + 'delivery_time_days' => 'integer', ]; } @@ -64,4 +70,14 @@ class Supplier extends Model { return $this->hasMany(PackagingItem::class); } + + /** + * @return BelongsToMany + */ + public function ingredients(): BelongsToMany + { + return $this->belongsToMany(Ingredient::class, 'ingredient_supplier') + ->withPivot(['preferred', 'supplier_sku', 'url']) + ->withTimestamps(); + } } diff --git a/app/Models/TaxRate.php b/app/Models/TaxRate.php new file mode 100644 index 0000000..ae99d8c --- /dev/null +++ b/app/Models/TaxRate.php @@ -0,0 +1,41 @@ + */ + use HasFactory; + + protected $fillable = [ + 'name', + 'percent', + 'active', + 'pos', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'percent' => 'decimal:2', + 'active' => 'boolean', + ]; + } + + /** + * @param Builder $query + * @return Builder + */ + public function scopeActive(Builder $query): Builder + { + return $query->where('active', true); + } +} diff --git a/app/Repositories/StockEntryRepository.php b/app/Repositories/StockEntryRepository.php index 17db3fc..750a079 100644 --- a/app/Repositories/StockEntryRepository.php +++ b/app/Repositories/StockEntryRepository.php @@ -3,6 +3,7 @@ namespace App\Repositories; use App\Models\StockEntry; +use App\Models\TaxRate; use Illuminate\Database\Eloquent\Collection; class StockEntryRepository @@ -13,6 +14,7 @@ class StockEntryRepository public function create(array $data): StockEntry { $data['unit'] = ($data['entry_type'] ?? '') === 'ingredient' ? 'gram' : 'piece'; + $data = $this->resolvePrices($data); return StockEntry::query()->create($data); } @@ -26,11 +28,62 @@ class StockEntryRepository $data['unit'] = ($data['entry_type'] ?? '') === 'ingredient' ? 'gram' : 'piece'; } + $data = $this->resolvePrices($data); + $stockEntry->update($data); return $stockEntry->fresh(); } + /** + * Ergänzt Netto-/Brutto-Preis pro kg und den UST-Snapshot. + * Es genügt, Netto oder Brutto anzugeben; der jeweils fehlende Wert wird + * aus dem gewählten Steuersatz berechnet. + * + * @param array $data + * @return array + */ + protected function resolvePrices(array $data): array + { + if (($data['entry_type'] ?? '') !== 'ingredient') { + $data['price_per_kg'] = null; + $data['price_per_kg_gross'] = null; + $data['tax_rate_id'] = null; + $data['tax_rate_percent'] = null; + + return $data; + } + + $percent = null; + if (! empty($data['tax_rate_id'])) { + $percent = TaxRate::query()->whereKey($data['tax_rate_id'])->value('percent'); + } + $data['tax_rate_percent'] = $percent; + + $factor = 1 + ((float) ($percent ?? 0)) / 100; + + $net = isset($data['price_per_kg']) && $data['price_per_kg'] !== null && $data['price_per_kg'] !== '' + ? (float) $data['price_per_kg'] + : null; + $gross = isset($data['price_per_kg_gross']) && $data['price_per_kg_gross'] !== null && $data['price_per_kg_gross'] !== '' + ? (float) $data['price_per_kg_gross'] + : null; + + if ($net !== null && $gross === null) { + $gross = round($net * $factor, 4); + } elseif ($gross !== null && $net === null) { + $net = $factor > 0 ? round($gross / $factor, 4) : $gross; + } elseif ($net !== null && $gross !== null) { + $gross = round($net * $factor, 4); + } + + $data['price_per_kg'] = $net; + $data['price_per_kg_gross'] = $gross; + $data['price_total'] = null; + + return $data; + } + /** * @param array $data */ diff --git a/app/Repositories/SupplierRepository.php b/app/Repositories/SupplierRepository.php index 7eb70e2..627aa2d 100644 --- a/app/Repositories/SupplierRepository.php +++ b/app/Repositories/SupplierRepository.php @@ -47,6 +47,11 @@ class SupplierRepository return collect($data)->only([ 'name', 'url', + 'order_method', + 'order_email', + 'order_url', + 'delivery_time', + 'delivery_time_days', 'contact_person', 'email', 'phone', diff --git a/database/factories/DeliveryTimeFactory.php b/database/factories/DeliveryTimeFactory.php new file mode 100644 index 0000000..0bd6a0f --- /dev/null +++ b/database/factories/DeliveryTimeFactory.php @@ -0,0 +1,27 @@ + + */ +class DeliveryTimeFactory extends Factory +{ + protected $model = DeliveryTime::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'label' => $this->faker->randomElement(['1–3 Werktage', '3–5 Werktage', '1–2 Wochen']), + 'days' => $this->faker->randomElement([3, 5, 14]), + 'active' => true, + 'pos' => 0, + ]; + } +} diff --git a/database/factories/TaxRateFactory.php b/database/factories/TaxRateFactory.php new file mode 100644 index 0000000..8a578da --- /dev/null +++ b/database/factories/TaxRateFactory.php @@ -0,0 +1,27 @@ + + */ +class TaxRateFactory extends Factory +{ + protected $model = TaxRate::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->randomElement(['Standard', 'Ermäßigt', 'Steuerfrei']), + 'percent' => $this->faker->randomElement([19.00, 7.00, 0.00]), + 'active' => true, + 'pos' => 0, + ]; + } +} diff --git a/database/migrations/2026_06_02_145358_widen_url_columns_in_inventory_tables.php b/database/migrations/2026_06_02_145358_widen_url_columns_in_inventory_tables.php new file mode 100644 index 0000000..90492e2 --- /dev/null +++ b/database/migrations/2026_06_02_145358_widen_url_columns_in_inventory_tables.php @@ -0,0 +1,30 @@ +string('url', 2048)->nullable()->change(); + }); + + Schema::table('packaging_items', function (Blueprint $table) { + $table->string('url', 2048)->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('suppliers', function (Blueprint $table) { + $table->string('url')->nullable()->change(); + }); + + Schema::table('packaging_items', function (Blueprint $table) { + $table->string('url', 500)->nullable()->change(); + }); + } +}; diff --git a/database/migrations/2026_06_02_152721_create_tax_rates_table.php b/database/migrations/2026_06_02_152721_create_tax_rates_table.php new file mode 100644 index 0000000..62c7ff5 --- /dev/null +++ b/database/migrations/2026_06_02_152721_create_tax_rates_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->decimal('percent', 5, 2); + $table->boolean('active')->default(true); + $table->unsignedTinyInteger('pos')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tax_rates'); + } +}; diff --git a/database/migrations/2026_06_02_153243_create_delivery_times_table.php b/database/migrations/2026_06_02_153243_create_delivery_times_table.php new file mode 100644 index 0000000..7d4b563 --- /dev/null +++ b/database/migrations/2026_06_02_153243_create_delivery_times_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('label'); + $table->boolean('active')->default(true); + $table->unsignedTinyInteger('pos')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('delivery_times'); + } +}; diff --git a/database/migrations/2026_06_02_154755_add_order_fields_to_suppliers_table.php b/database/migrations/2026_06_02_154755_add_order_fields_to_suppliers_table.php new file mode 100644 index 0000000..be4266d --- /dev/null +++ b/database/migrations/2026_06_02_154755_add_order_fields_to_suppliers_table.php @@ -0,0 +1,25 @@ +enum('order_method', ['email', 'online_shop'])->nullable()->after('url'); + $table->string('order_email')->nullable()->after('order_method'); + $table->string('order_url', 2048)->nullable()->after('order_email'); + $table->string('delivery_time')->nullable()->after('order_url'); + }); + } + + public function down(): void + { + Schema::table('suppliers', function (Blueprint $table) { + $table->dropColumn(['order_method', 'order_email', 'order_url', 'delivery_time']); + }); + } +}; diff --git a/database/migrations/2026_06_02_160411_add_days_to_delivery_times_table.php b/database/migrations/2026_06_02_160411_add_days_to_delivery_times_table.php new file mode 100644 index 0000000..ff419fc --- /dev/null +++ b/database/migrations/2026_06_02_160411_add_days_to_delivery_times_table.php @@ -0,0 +1,22 @@ +unsignedSmallInteger('days')->nullable()->after('label'); + }); + } + + public function down(): void + { + Schema::table('delivery_times', function (Blueprint $table) { + $table->dropColumn('days'); + }); + } +}; diff --git a/database/migrations/2026_06_02_160411_add_delivery_time_days_to_suppliers_table.php b/database/migrations/2026_06_02_160411_add_delivery_time_days_to_suppliers_table.php new file mode 100644 index 0000000..5986b1a --- /dev/null +++ b/database/migrations/2026_06_02_160411_add_delivery_time_days_to_suppliers_table.php @@ -0,0 +1,22 @@ +unsignedSmallInteger('delivery_time_days')->nullable()->after('delivery_time'); + }); + } + + public function down(): void + { + Schema::table('suppliers', function (Blueprint $table) { + $table->dropColumn('delivery_time_days'); + }); + } +}; diff --git a/database/migrations/2026_06_02_161237_add_order_fields_to_ingredients_table.php b/database/migrations/2026_06_02_161237_add_order_fields_to_ingredients_table.php new file mode 100644 index 0000000..b765d6c --- /dev/null +++ b/database/migrations/2026_06_02_161237_add_order_fields_to_ingredients_table.php @@ -0,0 +1,27 @@ +unsignedBigInteger('tax_rate_id')->nullable()->after('material_quality_id'); + $table->string('delivery_time')->nullable()->after('tax_rate_id'); + $table->unsignedSmallInteger('delivery_time_days')->nullable()->after('delivery_time'); + + $table->foreign('tax_rate_id')->references('id')->on('tax_rates')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('ingredients', function (Blueprint $table) { + $table->dropForeign(['tax_rate_id']); + $table->dropColumn(['tax_rate_id', 'delivery_time', 'delivery_time_days']); + }); + } +}; diff --git a/database/migrations/2026_06_02_161237_create_ingredient_supplier_table.php b/database/migrations/2026_06_02_161237_create_ingredient_supplier_table.php new file mode 100644 index 0000000..2e14cc3 --- /dev/null +++ b/database/migrations/2026_06_02_161237_create_ingredient_supplier_table.php @@ -0,0 +1,30 @@ +id(); + $table->unsignedInteger('ingredient_id'); + $table->unsignedBigInteger('supplier_id'); + $table->boolean('preferred')->default(false); + $table->string('supplier_sku')->nullable(); + $table->string('url', 2048)->nullable(); + $table->timestamps(); + + $table->unique(['ingredient_id', 'supplier_id']); + $table->foreign('ingredient_id')->references('id')->on('ingredients')->cascadeOnDelete(); + $table->foreign('supplier_id')->references('id')->on('suppliers')->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists('ingredient_supplier'); + } +}; diff --git a/database/migrations/2026_06_02_181548_add_price_fields_to_stock_entries_table.php b/database/migrations/2026_06_02_181548_add_price_fields_to_stock_entries_table.php new file mode 100644 index 0000000..d0957ae --- /dev/null +++ b/database/migrations/2026_06_02_181548_add_price_fields_to_stock_entries_table.php @@ -0,0 +1,27 @@ +decimal('price_per_kg_gross', 10, 4)->nullable()->after('price_per_kg'); + $table->unsignedBigInteger('tax_rate_id')->nullable()->after('price_total'); + $table->decimal('tax_rate_percent', 5, 2)->nullable()->after('tax_rate_id'); + + $table->foreign('tax_rate_id')->references('id')->on('tax_rates')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('stock_entries', function (Blueprint $table) { + $table->dropForeign(['tax_rate_id']); + $table->dropColumn(['price_per_kg_gross', 'tax_rate_id', 'tax_rate_percent']); + }); + } +}; diff --git a/database/seeders/InventoryStammdatenSeeder.php b/database/seeders/InventoryStammdatenSeeder.php index 891fc7e..4392c73 100644 --- a/database/seeders/InventoryStammdatenSeeder.php +++ b/database/seeders/InventoryStammdatenSeeder.php @@ -2,9 +2,11 @@ namespace Database\Seeders; +use App\Models\DeliveryTime; use App\Models\Location; use App\Models\MaterialQuality; use App\Models\PackagingMaterial; +use App\Models\TaxRate; use Illuminate\Database\Seeder; class InventoryStammdatenSeeder extends Seeder @@ -45,5 +47,29 @@ class InventoryStammdatenSeeder extends Seeder ['pos' => $pos] ); } + + $taxRates = [ + ['name' => 'Standard', 'percent' => 19.00], + ['name' => 'Ermäßigt', 'percent' => 7.00], + ['name' => 'Steuerfrei', 'percent' => 0.00], + ]; + foreach ($taxRates as $pos => $taxRate) { + TaxRate::query()->firstOrCreate( + ['percent' => $taxRate['percent']], + ['name' => $taxRate['name'], 'active' => true, 'pos' => $pos] + ); + } + + $deliveryTimes = [ + ['label' => '1–3 Werktage', 'days' => 3], + ['label' => '3–5 Werktage', 'days' => 5], + ['label' => '1–2 Wochen', 'days' => 14], + ]; + foreach ($deliveryTimes as $pos => $deliveryTime) { + DeliveryTime::query()->firstOrCreate( + ['label' => $deliveryTime['label']], + ['days' => $deliveryTime['days'], 'active' => true, 'pos' => $pos] + ); + } } } diff --git a/dev/product management /entwicklungsplan-aktualisiert-02-06-2026.md b/dev/product management /entwicklungsplan-aktualisiert-02-06-2026.md new file mode 100644 index 0000000..020a4e8 --- /dev/null +++ b/dev/product management /entwicklungsplan-aktualisiert-02-06-2026.md @@ -0,0 +1,425 @@ +# Aktualisierter Entwicklungsplan: Warenwirtschaft, Produktion & Produktbestand + +> **Version:** 4.0 - Stand 02.06.2026 +> **Ersetzt:** `entwicklungsplan-aktualisiert-27-04-2026.md` (V3.0) als operative Arbeitsgrundlage +> **Referenzen:** `entwicklungsplan.md` (V2.0), `briefing-anpassungen-27-04-2026.md`, `feedback.md`, `konzept-final.md`, `docs/Todos.md` +> **Methodik:** Backlog aus kleinen, sequenziell abarbeitbaren Arbeitspaketen (AP). Jedes AP hat Ziel, konkrete Schritte mit Dateipfaden, DB-Änderungen, Akzeptanzkriterien und Tests. Reihenfolge ist so gewählt, dass jedes AP einzeln deploybar ist. + +--- + +## 0. Was dieses Dokument neu macht + +Gegenüber V3.0 wurde der **reale Code-Stand verifiziert** (nicht nur die Protokolltabelle übernommen). Daraus ergeben sich Korrekturen, neu entdeckte Bugs und eine feinere Zerlegung in einzeln umsetzbare Schritte. + +Geprüfte Dateien u. a.: `routes/web.php`, `app/Http/Controllers/Admin/Inventory/*`, `app/Services/ProductionService.php`, `app/Http/Requests/Inventory/*`, `resources/views/admin/inventory/*`, Migrationen unter `database/migrations/`. + +--- + +## 0a. Umsetzungsprotokoll V4.0 (laufend) + +> Jede abgeschlossene Teil-Lieferung wird hier mit Datum, betroffenen Dateien und Test-Status protokolliert. + +| Datum | AP | Kurzbeschreibung | Tests | +|---|---|---|---| +| 02.06.2026 | **AP-01** | URL-Bugfixes B1/B2 umgesetzt: `suppliers/form.blade.php` und `packaging-items/form.blade.php` von `type="url"` auf `type="text"` (placeholder `https://`); `Store/UpdatePackagingItemRequest` URL-Regel `url\|max:500` → `string\|max:2048`; Migration `2026_06_02_145358_widen_url_columns_in_inventory_tables` (suppliers.url + packaging_items.url → varchar(2048)). | `tests/Feature/InventoryUrlFieldsTest.php` (3 grün); Regression Phase 2+3 grün (17) | +| 02.06.2026 | **AP-04** | iPad-taugliche Tabellen-Aktionen (B5): neue Partial `resources/views/admin/inventory/partials/table-actions-style.blade.php` (`@once`-Style für `.wawi-table td .btn`, min. 42px Touch-Target, mehr Abstand); Klasse `wawi-table` + Partial-Include in allen 8 Index-Views (locations, material-qualities, packaging-materials, supplier-categories, suppliers, packaging-items, stock-entries, productions). | Render-Regression Phase 2+3+5 grün (21) | +| 02.06.2026 | **AP-00** | Regressionsbasis für umgesetzte 5.1-Features als Pest-Tests nachgezogen: INCI-Rohstoffqualität-Relation, Hersteller-Rezeptur getrennt von Produkt-Rezeptur, Produkt-Kopie inkl. beider Rezepturen, „nur aktive Produkte" im Produktions-Formular, Produktion edit/copy rendern. | `tests/Feature/ProductPhase51Test.php` (5 grün) | +| 02.06.2026 | **AP-04.1** | Aktionsspalten vereinheitlicht (Kunden-Feedback): einheitliches Schema **Spalte 1 = Ansicht + Bearbeiten** (+ Kopieren bei Produktion), **letzte Spalte = Löschen**. Umgebaut: `stock-entries/index` (Ansicht/Bearbeiten nach vorn, Löschen ans Ende, DataTables `order`/`columnDefs` an verschobene Spalten angepasst) und `productions/index` (Ansicht/Bearbeiten/Kopieren nach vorn). Stammdaten-Tabellen waren bereits konform (kein `show`-Route → keine Ansicht). CSS-Feinjustierung der Button-Abstände durch Kunden in `partials/table-actions-style.blade.php` übernommen. | Produktions-Index-Render + Aktionslinks geprüft; stock-entries Index-Render grün (13) | +| 02.06.2026 | **AP-05 (Teil 1: UST)** | Neuer Unterpunkt **Warenwirtschaft → Einstellungen → „Allgemein"** als erweiterbarer Container für kleinteilige Einstellungen (erstes Modul = Umsatzsteuersätze). Tabelle `tax_rates` (`name`, `percent` DECIMAL(5,2), `active`, `pos`) via Migration `2026_06_02_152721_create_tax_rates_table`; Model `TaxRate` (casts `percent`/`active`, `scopeActive`), `TaxRateFactory`, Seeder-Erweiterung `InventoryStammdatenSeeder` (19/7/0, idempotent per `firstOrCreate`). CRUD: `GeneralSettingController@index` (Allgemein-Seite), `TaxRateController` (create/store/edit/update/destroy, Redirect zurück auf `general`), `Store/UpdateTaxRateRequest`. Views `admin/inventory/general/index.blade.php` (Karte „Umsatzsteuersätze" mit Tabelle + Neu/Bearbeiten/Löschen) und `admin/inventory/tax-rates/form.blade.php`. Routen in `superadmin`-Gruppe (`admin.inventory.general`, Resource `tax-rates` ohne index/show). Sidenav-Eintrag „Allgemein" inkl. `open`/`active`-Logik. Migration + Seed auf DB ausgeführt (3 Default-Sätze vorhanden). | `tests/Feature/TaxRateSettingsTest.php` (7 grün, 24 Assertions): Render, CRUD, Validierung Pflicht/Bereich, `active`-Scope, Zugriffsschutz Nicht-SuperAdmin | +| 02.06.2026 | **AP-05 (Teil 2: Lieferzeiten)** | Zweite Karte „Lieferzeit-Vorlagen" auf der Allgemein-Seite. Tabelle `delivery_times` (`label`, `active`, `pos`) via Migration `2026_06_02_153243_create_delivery_times_table`; Model `DeliveryTime` (cast `active`, `scopeActive`), `DeliveryTimeFactory`, Seeder-Erweiterung (1–3 / 3–5 Werktage / 1–2 Wochen, idempotent). CRUD: `DeliveryTimeController` (create/store/edit/update/destroy → Redirect `general`), `Store/UpdateDeliveryTimeRequest`. `GeneralSettingController` um `deliveryTimes` erweitert. View `admin/inventory/delivery-times/form.blade.php` + zweite Karte in `general/index`. Route Resource `delivery-times` (ohne index/show) in `superadmin`-Gruppe. Sidenav `open`/`active` um `delivery-times` ergänzt. Migration + Seed auf DB ausgeführt. | `tests/Feature/DeliveryTimeSettingsTest.php` (7 grün, 21 Assertions): Render, CRUD, Validierung, `active`-Scope, Zugriffsschutz | +| 02.06.2026 | **AP-06 (Lieferanten erweitern)** | Felder `order_method` ENUM(`email`,`online_shop`), `order_email`, `order_url`, `delivery_time` (Freitext) an `suppliers` via Migration `2026_06_02_154755_add_order_fields_to_suppliers_table`. `Supplier` fillable erweitert; `Store/UpdateSupplierRequest` Regeln (`order_method` in:email,online_shop; `order_email` email; `order_url` string max 2048; `delivery_time` string). `SupplierRepository::extractSupplierAttributes` erweitert. `SupplierController` create/edit übergeben aktive `deliveryTimes` als Vorlagen. `suppliers/form.blade.php`: Bestellweg-Select + bedingte Felder (Bestell-E-Mail / Bestell-URL via JS-Toggle) + Lieferzeit-Textfeld mit `` aus aktiven Lieferzeit-Vorlagen. Migration auf DB ausgeführt. | `tests/Feature/SupplierOrderFieldsTest.php` (6 grün): Formular zeigt nur aktive Vorlagen, Speichern E-Mail-/Shop-Bestellweg, Update, Validierung Bestellweg/Bestell-E-Mail. Regression `InventoryPhase2Test` (9 grün) | +| 02.06.2026 | **AP-06 (Nachtrag: Lieferzeit in Tagen)** | Lieferzeit-Vorlagen erhalten festes Feld `days` (ganze Tage bis Wareneingang, Basis für spätere „rechtzeitig bestellen"-Ableitung). Migration `2026_06_02_160411_add_days_to_delivery_times_table` (`delivery_times.days` unsignedSmallInt nullable) + `2026_06_02_160411_add_delivery_time_days_to_suppliers_table` (`suppliers.delivery_time_days`). `DeliveryTime` (fillable+cast `days`), Factory/Seeder (3/5/14 Tage, Bestandsdaten nachgepflegt). `Store/UpdateDeliveryTimeRequest` + `Store/UpdateSupplierRequest` um `days`/`delivery_time_days` (nullable int) erweitert; `SupplierRepository` + `Supplier` cast. Views: Tage-Feld in `delivery-times/form`, Spalte „Tage" in `general/index`, Tage-Feld im `suppliers/form` + JS-Autofill (`data-days` an Datalist-Optionen setzt Tage bei Vorlagenauswahl, manuell überschreibbar). Migrationen auf DB ausgeführt, Default-Vorlagen mit Tagen befüllt. | `DeliveryTimeSettingsTest` (10 grün): days speichern/optional/Integer-Validierung; `SupplierOrderFieldsTest` (9 grün): `delivery_time_days` speichern, Integer-Validierung, `data-days`-Ausgabe | +| 02.06.2026 | **AP-08 (Einkauf erweitern)** | Einkauf um UST-Satz + Netto/Brutto-Automatik + Duplizieren erweitert. Migration `2026_06_02_181548_add_price_fields_to_stock_entries_table` (`price_per_kg_gross` DECIMAL(10,4), `tax_rate_id` FK→`tax_rates` nullOnDelete, `tax_rate_percent` DECIMAL(5,2) als Snapshot). `price_per_kg` bleibt das bestehende **Netto**-Feld (kein Rename → keine Migration der Bestandsdaten/Tests). `StockEntry`: fillable + casts (`price_per_kg_gross`/`tax_rate_percent`) + `taxRate()` belongsTo. `Store/UpdateStockEntryRequest`: Regeln `tax_rate_id` (exists) + `price_per_kg_gross` (numeric), Reformat dt. Zahl, neue Regel „bei Rohstoff genau eines von Netto/Brutto verpflichtend". **Berechnung zentral im `StockEntryRepository::resolvePrices()`:** UST-Prozent als Snapshot, fehlender Netto-/Brutto-Wert wird aus dem Faktor `(1+%/100)` berechnet (Netto↔Brutto), bei Verpackung Preisfelder/UST genullt (Netto-Gesamt bleibt). View `_form`: UST-Dropdown (aktive `tax_rates`, `data-percent`) + Netto-/Brutto-Felder nebeneinander; `_scripts`: JS rechnet live Netto↔Brutto bei Eingabe und UST-Wechsel (dt. Zahlenformat). `show`: Anzeige Netto/Brutto/USt. **Duplizieren:** Route `stock-entries/{stock_entry}/copy` + `StockEntryController@copy` legt direkt eine `pending`-Kopie der Stufe-1-Felder an (Charge/MHD/Eingangsdaten leer, `ordered_at`=heute, `ordered_by`=aktueller User) und leitet zur Bearbeitung; Kopieren-Button in `index` (Aktionsspalte) + `show`-Header. Migration auf DB ausgeführt. | `tests/Feature/StockEntryPriceTest.php` (6 grün): Netto→Brutto, Brutto→Netto, ohne UST Netto=Brutto, Netto/Brutto-Pflicht, Duplizieren erzeugt pending-Kopie ohne Chargendaten, Copy-Zugriffsschutz. Regression `InventoryPhase3Test` (8 grün) | +| 02.06.2026 | **AP-07.1 (Lieferanten-Detailansicht/Modal)** | Zwischenschritt (Kunde): Lieferanten-Zuordnungen auch von der Lieferantenseite aus sichtbar/pflegbar. `Supplier::ingredients()` belongsToMany (Gegenstück zu `Ingredient::suppliers()`). Resource `suppliers` `show` reaktiviert + neue Routen `suppliers.ingredients.attach/detach` und `suppliers.packaging-items.attach/detach` (admin-Gruppe). `SupplierController`: `show()` + `attach/detachIngredient()` + `attach/detachPackagingItem()` rendern gemeinsames Partial `suppliers/_details.blade.php` (Stammdaten + zwei kleine Listen „Zugeordnete INCIs" / „Zugeordnete Verpackungsartikel" mit Entfernen-Button und Hinzufügen-Auswahl der noch nicht zugeordneten Einträge). Index: Augen-Button (Spalte 1) öffnet Bootstrap-Modal, lädt Details per AJAX; Hinzufügen/Entfernen via delegiertem jQuery-AJAX (X-CSRF-TOKEN-Header) und ersetzt den Modal-Body mit dem neu gerenderten Partial. Verpackungsartikel-Zuordnung = `packaging_items.supplier_id` setzen/leeren. | `tests/Feature/SupplierDetailsTest.php` (7 grün): show zeigt zugeordnete INCIs/Verpackung, INCI attach/detach, Verpackung attach/detach, Validierung, Zugriffsschutz Nicht-Admin | +| 02.06.2026 | **AP-07 (INCI erweitern)** | INCI/Rohstoffe um Lieferanten-Mehrfachwahl, UST-Satz und eigene Lieferzeit (inkl. Tage-Autofill) erweitert. Migration `2026_06_02_161237_add_order_fields_to_ingredients_table` (`ingredients.tax_rate_id` FK→`tax_rates` nullOnDelete, `delivery_time` VARCHAR, `delivery_time_days` unsignedSmallInt) + `2026_06_02_161237_create_ingredient_supplier_table` (Pivot `ingredient_id` (unsignedInt, passend zu altem `increments`) / `supplier_id`, `preferred` bool, `supplier_sku`, `url`(2048), unique-Paar, cascadeOnDelete). `Ingredient`: fillable + cast `delivery_time_days`, Relationen `taxRate()` belongsTo + `suppliers()` belongsToMany (Pivot `preferred`/`supplier_sku`/`url`). **O1 erledigt:** `IngredientController::store()` von `Request::all()` auf neuen `App\Http\Requests\StoreIngredientRequest` umgestellt (validiert + normalisiert deutsche Dezimalzahlen `default_factor`/`min_stock_alert`, leere FKs→null). `edit()` lädt aktive `taxRates`, aktive `deliveryTimes`, aktive `suppliers` + eager-load `suppliers`; nach Speichern `suppliers()->sync()`. Single-Endpoint-Schema (`admin_product_ingredient_store` für Neu+Update) beibehalten → ein FormRequest genügt. View `admin/ingredient/form.blade.php`: UST-Dropdown (aktive `tax_rates`), Select2-Mehrfachwahl Lieferanten, Lieferzeit-Textfeld mit `` (`data-days`) + Tage-Feld; `edit.blade.php` `@section('scripts')` mit Select2-Init + Tage-Autofill (manuell überschreibbar). Lieferzeit-Logik: INCI-Lieferzeit hat Vorrang vor Lieferanten-Lieferzeit (Auswertung erst in AP-10). Migrationen auf DB ausgeführt. | `tests/Feature/IngredientOrderFieldsTest.php` (6 grün): Formular zeigt Lieferanten/UST/aktive Vorlagen+`data-days`, Speichern mit UST/Lieferzeit/Tagen/Lieferanten, Lieferanten-Sync bei Update, Validierung Tage-Integer/UST-Existenz/Name-Pflicht. Regression `SupplierOrderFieldsTest` (8) + `ProductPhase51Test` (5) grün | + +**Status Roadmap:** AP-00, AP-01, AP-04, AP-05, AP-06 (inkl. Nachtrag) erledigt; **AP-07 erledigt** (INCI: Lieferanten-Mehrfachwahl, UST-Satz, eigene Lieferzeit inkl. Tage-Autofill, `ingredient_supplier`-Pivot; O1 `IngredientController` auf FormRequest umgestellt) inkl. **AP-07.1** (Lieferanten-Detailansicht im Modal mit pflegbaren INCI-/Verpackungs-Listen); **AP-08 erledigt** (Einkauf: UST-Snapshot, Netto/Brutto-Automatik, Duplizieren). + +> **Alle Klärungspunkte aus §5 sind beantwortet** (Kunde, 02.06.2026) und in die jeweiligen APs eingearbeitet — keine Blocker mehr offen. +> +> **➡️ NÄCHSTER SCHRITT: AP-09 (Produktion korrigieren).** Konkret: (1) Produktion **ausschließlich** auf Hersteller-Rezeptur umstellen + **Warnung**, wenn keine gepflegt ist (kein Fallback); (2) Chargen-Dropdown-Label + nur Chargen mit Restbestand; (3) B3 „Weitere Charge"-JS-Fix (genau eine Zeile); (4) Soll-Neuberechnung ohne Überschreiben manueller Eingaben; (5) B4 iPad-Layout der Kopfdaten; (6) Produktentwicklung-Platzhalterseite („Briefing ausstehend"). Danach AP-02/AP-03, dann die großen Übersichten. **Neu:** AP-18 (Hinweise-Doku unter Einstellungen) kann jederzeit dazwischengezogen werden. + +--- + +## 1. Verifizierter Ist-Stand (02.06.2026) + +### Umgesetzt und im Code vorhanden + +| Bereich | Status | Belegt durch | +|---|---|---| +| Produktmanagement (Produkte, INCIs, Kategorien, Attribute) | vorhanden | `app/Http/Controllers/ProductController.php`, `IngredientController.php` | +| Rezeptur + Hersteller-Rezeptur (Prozent, Faktor, 100%-Summe) | vorhanden | `product_ingredients.recipe_type`, `Product::p_ingredients()` / `manufacturer_ingredients()` | +| Haltbarkeit am Produkt (PAO / festes MHD) | vorhanden | `products.shelf_life_type`, `shelf_life_months` | +| Stammdaten (Lagerorte, Lieferanten, Kategorien, Rohstoffqualität, Verpackungsmaterial, Produkt-/Versandverpackung) | vorhanden | `app/Http/Controllers/Admin/Inventory/*`, Migrationen `2026_03_27_*` | +| INCI mit Rohstoffqualität | vorhanden | `ingredients.material_quality_id` | +| Verpackung & Material am Produkt (BOM) | vorhanden | `product_packagings`, `Product::packagings()` | +| Einkauf & Wareneingang (zweistufig pending → received, Charge, MHD) | vorhanden | `stock_entries`, `StockEntryController`, `ReceiveStockEntryRequest` | +| Produktion (Chargen-Zuordnung, Soll-Verbrauch, MHD-Warnung, Packaging-Snapshot, edit/copy) | vorhanden | `ProductionService`, `ProductionController`, `production_*`-Tabellen | +| Tests Phase 0–5 | vorhanden | `tests/Feature/ProductPhase0/1/4Test.php`, `InventoryPhase2/3Test.php`, `ProductionPhase5Test.php` | + +### Noch NICHT im Code vorhanden (entgegen Eindruck aus V3.0-Lesart) + +- **Phase 5.2 ist vollständig offen** — keine der dort beschriebenen DB-Strukturen existiert: + - kein `tax_rates` / `tax_rate_id` / `tax_rate_percent` + - kein `is_set`, `main_product_id`, `product_set_items` + - kein `order_method` / `order_email` / `order_url` / `delivery_time` an `suppliers` + - kein `ingredient_supplier`-Pivot, kein `tax_rate`/`delivery_time` an `ingredients` + - kein `price_per_kg_net` / `price_per_kg_gross` an `stock_entries` + - kein `out_of_stock_until` an `products` + - kein `product_stock_movements`, kein `InventoryService`, keine Bestandsseiten +- **Produktion basiert noch auf `p_ingredients` (Produkt-Rezeptur), nicht auf der Hersteller-Rezeptur** (`ProductionService::store()` und `buildRecipePayload()` laden `p_ingredients`). Briefing fordert Hersteller-Rezeptur als Basis → offen. +- **Kein Rohstoffbestand / Produktbestand / Historie**, kein Ausgang/Ausschuss, kein Audit-Trail, kein 2FA, keine blockbasierten Rechte, keine Warenwirtschafts-Einstellungen. + +--- + +## 2. Gefundene Bugs & Optimierungen (sofort vor Feature-Arbeit) + +Diese Punkte sind klein, konkret und blockieren teils die tägliche Nutzung. Sie werden als **Phase 5.1.x Nachzügler** vorgezogen. + +### B1 — Lieferanten-URL: Speichern schlägt fehl, wenn URL ausgefüllt ist (umgekehrtes Verhalten) +- **Symptom (Kunde):** „Neuer Lieferant: sagt ‚URL eingeben‘, obwohl eine drinsteht. Nehme ich sie raus, geht das Abspeichern." +- **Ursache:** `resources/views/admin/inventory/suppliers/form.blade.php` Zeile 56 nutzt ``. Die native Browser-Validierung lehnt eine ausgefüllte, aber nicht streng schema-konforme URL (z. B. ohne `https://` oder mit kodierten Parametern) ab; ein **leeres** Feld ist gültig → exakt das gemeldete „umgekehrte" Verhalten. Die Server-Validierung ist bereits korrekt (`nullable|string|max:2048`). +- **Fix:** `type="url"` → `type="text"` (Server validiert ohnehin als String). Damit werden auch Konfigurator-URLs mit Parametern akzeptiert (siehe B2). +- **Aufwand:** ~15 Min. + +### B2 — URL-Felder müssen Konfigurator-URLs mit Parametern akzeptieren +- **Anforderung (Todos Z. 1–2):** URLs wie + `https://www.kartonsaufmass.de/bestellen?bom_configuration=%7B%2522length%2522:125,...%7D` + müssen gespeichert werden können (Versandverpackungs-Konfiguratoren). +- **Status (verifiziert):** Lieferanten-URL ist bereits `nullable|string|max:2048` (ok, mit B1-Fix vollständig). **`PackagingItem` ist NICHT ok:** `Store/UpdatePackagingItemRequest` haben `['nullable','url','max:500']` — die `url`-Regel lehnt kodierte Konfigurator-URLs ab und `max:500` ist für lange Konfigurator-Links zu kurz. → auf `['nullable','string','max:2048']` ändern und Blade-Input auf `type="text"`. Konsistent für alle URL-Felder im Warenwirtschaftsmodul. +- **Aufwand:** ~30 Min. + +### B3 — „Weitere Charge": es erscheinen zwei Felder statt einem +- **Anforderung (Todos Z. 14, Briefing 5.2.6):** Klick auf „Weitere Charge" soll genau **eine** neue Zeile/Dropdown hinzufügen. +- **Status:** JS-Fehler in der Produktions-Create/Edit-View (Chargen-Splitting). Wird in **AP-09** (Produktionskorrekturen) sauber behoben, da es mit der Soll-Neuberechnung zusammenhängt. + +### B4 — iPad: Produktionsdatum und Stückzahl überlappen grafisch +- **Anforderung (Todos Z. 86, Briefing 5.2.6 D):** Responsive Grid der Kopfdaten in `productions/create.blade.php` / `edit.blade.php` reparieren. → Teil von **AP-09**. + +### B5 — Tabellen-Aktionsicons (Auge/Stift/Mülleimer) zu klein/zu eng (iPad) +- **Anforderung (Todos Z. 36, Briefing 5.2.3 C):** Betrifft **alle** Tabellen im Modul. Eine gemeinsame CSS-Utility-Klasse (z. B. `.wawi-actions` mit größeren Touch-Targets + Abstand) einführen und in allen `index.blade.php` anwenden. → **AP-04** (Querschnitt, früh, weil überall sichtbar). + +### Optimierungen (Konsistenz/Sauberkeit) +- **O1:** `IngredientController` nutzt noch `Request::all()` statt FormRequest → bei INCI-Erweiterung (AP-07) auf `StoreIngredientRequest`/`UpdateIngredientRequest` umstellen. +- **O2:** Offene Tests aus Phase 5.1 (Menü-Labels, INCI-Qualität, Prozent-Rezeptur, 100%-Summe, Hersteller-Rezeptur, Produktion edit/copy, nur aktive Produkte) sind im Plan als `[ ]` markiert, aber Features sind umgesetzt → **AP-00** schreibt diese Tests nach, um eine grüne Regressionsbasis zu haben, bevor 5.2 beginnt. +- **O3:** Steuerart als **Enum, später änderbar** gewünscht (feedback/Briefing INCI B). Lösung: konfigurierbare `tax_rates`-Stammdaten statt Hardcode-Enum (AP-05). + +--- + +## 3. Priorisierte Roadmap (Phasenüberblick) + +| Reihenfolge | AP | Titel | Abhängigkeit | Aufwand | +|---|---|---|---|---| +| 1 | AP-00 | Regressionsbasis: offene 5.1-Tests nachziehen | – | 1 Tag | +| 2 | AP-01 | Quick-Fixes B1/B2 (URL-Felder) | – | 0,5 Tag | +| 3 | AP-04 | Querschnitt: iPad-taugliche Tabellen-Aktionen (B5) | – | 0,5–1 Tag | +| 4 | AP-05 | Einstellungen: UST-Sätze & Lieferzeiten (Stammdaten) | – | 1–2 Tage | +| 5 | AP-06 | Lieferanten erweitern (Bestellweg, Lieferzeit) | AP-05 | 1–2 Tage | +| 6 | AP-07 | INCI erweitern (Lieferanten-Mehrfachwahl, UST, Lieferzeit) | AP-05, AP-06 | 2–3 Tage | +| 7 | AP-08 | Einkauf erweitern (UST, Netto/Brutto, Duplizieren) | AP-05 | 2–3 Tage | +| 8 | AP-09 | Produktion korrigieren (Hersteller-Rezeptur, Charge-JS, iPad, Produktentwicklung-Platzhalter) | – | 2–4 Tage | +| 9 | AP-02 | Produkt-Klassen: Einzelprodukt vs. Set + Hauptprodukt | – | 3–5 Tage | +| 10 | AP-03 | „Nicht vorrätig" mit Zeitangabe | – | 1–2 Tage | +| 11 | AP-10 | Rohstoffbestand (InventoryService + Übersicht) | AP-06, AP-07, AP-09 | 4–6 Tage | +| 12 | AP-11 | Produktbestand + Historie + manuelle Bewegungen | AP-02, AP-10 | 5–8 Tage | +| 13 | AP-12 | Ausgang/Ausschuss (Rohstoffe/Verpackung) | AP-10 | 2–3 Tage | +| 14 | AP-13 | Shop-Anbindung: Bestand bei Verkauf reduzieren (inkl. Sets) | AP-02, AP-11 | 3–5 Tage | +| 15 | AP-14 | Audit-Trail (inventory_logs) | AP-10–13 | 2–3 Tage | +| 16 | AP-15 | Blockbasierte Rechte | AP-05+ | 5–8 Tage | +| 17 | AP-16 | 2FA Google Authenticator für Admins | – | 3–5 Tage | +| 18 | AP-17 | Warenwirtschafts-Einstellungen (Alarm-Mail, Default-Lager, Schwellwerte) | AP-10/11 | 1–2 Tage | +| 19 | AP-18 | Hinweise-/Doku-Seite (Einstellungen → Hinweise, MD-basiert) | – | 0,5 Tag | + +> **Leitplanke:** AP-00 bis AP-09 sind „Korrektur & Datenmodell-Vorbereitung". Erst danach werden die großen Übersichten (Rohstoff-/Produktbestand) gebaut, weil sie auf den neuen Stammdatenfeldern aufsetzen. + +--- + +## 4. Arbeitspakete im Detail + +### AP-00 — Regressionsbasis: offene 5.1-Tests nachziehen +**Ziel:** Grüne Test-Suite als Sicherheitsnetz, bevor 5.2 beginnt. + +**Schritte** +- Pest-Feature-Tests ergänzen (`php artisan make:test --pest `): + - Menü-Labels (Rohstoffqualität, Verpackungsmaterial, Produkt-/Versandverpackung). + - INCI mit `material_quality_id` speichern + Anzeige im Produktformular-Katalog. + - Rezeptur in Prozent (3 Nachkomma) speichern; 100%-Summen-Validierung (grün/rot). + - Hersteller-Rezeptur getrennt speichern (`recipe_type=manufacturer`). + - Produktion `edit`/`update` und `copy`. + - Nur aktive Produkte im Produktions-Dropdown. + +**Akzeptanz:** `php artisan test` läuft vollständig grün; neue Tests decken die genannten Features ab. + +--- + +### AP-01 — Quick-Fixes URL-Felder (B1 + B2) +**Schritte** +- `resources/views/admin/inventory/suppliers/form.blade.php`: `type="url"` → `type="text"` (Zeile ~56). +- URL-Validierung aller Warenwirtschafts-FormRequests prüfen (Supplier ist ok). `Store/UpdatePackagingItemRequest`: falls `url`-Regel → auf `['nullable','string','max:2048']` ändern; zugehöriges Blade-Input auf `type="text"`. +- `vendor/bin/pint --dirty`. + +**Akzeptanz** +- Lieferant mit ausgefüllter URL (auch ohne `https://` und mit kodierten Parametern) speichert ohne Fehler. +- Konfigurator-URL aus Todos Z. 2 wird unverändert gespeichert. + +**Tests:** Feature-Test „Supplier mit Parameter-URL speichern", „PackagingItem mit Parameter-URL speichern". + +--- + +### AP-04 — Querschnitt: iPad-taugliche Tabellen-Aktionen (B5) +**Schritte** +- Gemeinsame CSS-Klasse `.wawi-actions` (größere Buttons, mehr Abstand, Touch-Target ≥ 44px) in vorhandenes Admin-CSS aufnehmen (Laravel Mix; danach `npm run dev`/`prod`). +- In allen `resources/views/admin/inventory/**/index.blade.php` die Aktionsspalte (Auge/Stift/Mülleimer) auf die Klasse umstellen. + +**Akzeptanz:** Aktionen sind auf dem iPad gut und einzeln klickbar; Optik in allen Modul-Tabellen konsistent. + +--- + +### AP-05 — Einstellungen: UST-Sätze & Lieferzeiten +**Ziel:** Konfigurierbare Steuersätze und Lieferzeit-Vorlagen als Stammdaten (Basis für AP-06/07/08). + +**DB** +- `tax_rates`: `name` (z. B. „Standard"), `percent` DECIMAL(5,2), `active` bool, `pos`. Seeder: 19,00 / 7,00 / 0,00. +- `delivery_times`: `label` VARCHAR (Freitext, z. B. „3–5 Werktage"), `days` (ganze Tage bis Wareneingang, optional – Basis für „rechtzeitig bestellen"-Ableitung), `active`, `pos`. + +**Code** +- Models `TaxRate`, `DeliveryTime` (`make:model -mf`), CRUD-Controller unter `Admin/Inventory/`, FormRequests, Views `index`+`form`, Routen unter `admin/inventory` (`superadmin`), Sidenav-Einträge. + +**Akzeptanz:** SuperAdmin pflegt UST-Sätze und Lieferzeiten; nur aktive Sätze sind in Dropdowns wählbar; historische (deaktivierte) Sätze bleiben referenzierbar. + +> **Entscheidung (O3):** UST als Stammdaten-Tabelle statt PHP-Enum, weil „später änderbar" gefordert ist und historische Werte erhalten bleiben müssen. + +> **Umgesetzte Struktur (Kunde, 02.06.2026):** Unter **Warenwirtschaft → Einstellungen** neuer Unterpunkt **„Allgemein"** als Sammelseite für kleinteilige Einstellungen. Sektion 1 = Umsatzsteuersätze, Sektion 2 = Lieferzeit-Vorlagen (jeweils Tabelle, neue Einträge jederzeit ergänzbar). Weitere kleinteilige Einstellungen (Default-Werte etc.) werden später als zusätzliche Karten auf derselben „Allgemein"-Seite ergänzt. +> +> **Status:** Erledigt — Teil 1 (`tax_rates`) und Teil 2 (`delivery_times`) als CRUD unter „Allgemein". + +--- + +### AP-06 — Lieferanten erweitern +**DB (`suppliers`)** +- `order_method` ENUM(`email`,`online_shop`) nullable. +- `order_email` nullable (falls abweichend von `email`). +- `order_url` nullable (falls abweichend von `url`). +- `delivery_time` VARCHAR nullable (Freitext; optional Verknüpfung mit `delivery_times` als Vorlage, aber Freitext bleibt führend). + +**Code** +- Migration + `Supplier` fillable/casts; `Store/UpdateSupplierRequest` erweitern; `suppliers/form.blade.php`: Radio/Select Bestellweg + bedingte Felder + Lieferzeit-Textfeld (mit Vorlagen-Datalist aus `delivery_times`). + +**Akzeptanz:** Pro Lieferant ist Bestellweg + Ziel (Mail/Shop) + Lieferzeit hinterlegt und editierbar; Daten stehen später dem Rohstoffbestand für Bestell-Links zur Verfügung. + +> **Status:** Erledigt (02.06.2026). Migration `2026_06_02_154755_add_order_fields_to_suppliers_table` (`order_method`, `order_email`, `order_url`, `delivery_time`). Formular mit Bestellweg-Select, JS-gesteuerten bedingten Feldern (E-Mail vs. URL) und Lieferzeit-Datalist aus aktiven `delivery_times`. Freitext bleibt führend, Vorlagen sind nur Eingabehilfe. Tests: `tests/Feature/SupplierOrderFieldsTest.php`. +> +> **Nachtrag (02.06.2026, Kunde):** Lieferzeit ist jetzt zusätzlich als fester Tageswert auswertbar. Lieferzeit-Vorlagen haben Feld `days` (ganze Tage). Lieferant hat `delivery_time_days` (`2026_06_02_160411_*`). Beim Auswählen einer Vorlage im Lieferzeit-Feld setzt JS automatisch den Tageswert (manuell überschreibbar). Dieser Tageswert ist die Grundlage, um später Rohstoffe rechtzeitig vor MHD/Bedarf zu bestellen. Gleiche Auto-Befüllung wird in AP-07 (INCI) übernommen. + +--- + +### AP-07 — INCI erweitern +**DB** +- Pivot `ingredient_supplier`: `ingredient_id`, `supplier_id`, optional `preferred` bool, `supplier_sku`, `url`. +- `ingredients`: `tax_rate_id` nullable FK, `delivery_time` VARCHAR nullable. + +**Code** +- `Ingredient`: `suppliers()` belongsToMany, `taxRate()` belongsTo; fillable/casts. +- **O1:** `IngredientController` auf `StoreIngredientRequest`/`UpdateIngredientRequest` umstellen (Ersatz für `Request::all()`). +- `admin/ingredient/form.blade.php`: Select2-Mehrfachauswahl Lieferanten, UST-Dropdown (aktive `tax_rates`), Lieferzeit-Textfeld. + +**Lieferzeit-Logik:** INCI-Lieferzeit überschreibt Lieferanten-Lieferzeit (Auswertung erst im Rohstoffbestand AP-10). + +**Akzeptanz:** INCI kann mehrere Lieferanten, einen UST-Satz und eine eigene Lieferzeit haben; alles wird gespeichert und angezeigt. + +> **Status:** Erledigt (02.06.2026). Migrationen `2026_06_02_161237_add_order_fields_to_ingredients_table` (`tax_rate_id` FK, `delivery_time`, `delivery_time_days`) + `2026_06_02_161237_create_ingredient_supplier_table` (Pivot mit `preferred`/`supplier_sku`/`url`). `Ingredient`: `taxRate()` belongsTo, `suppliers()` belongsToMany (mit Pivot-Feldern), cast `delivery_time_days`. **O1 umgesetzt:** `IngredientController` nutzt jetzt `StoreIngredientRequest` (statt `Request::all()`) und synct Lieferanten via `suppliers()->sync()`. Formular: UST-Dropdown, Select2-Lieferanten-Mehrfachwahl, Lieferzeit-Textfeld mit `data-days`-Datalist + Tage-Feld inkl. JS-Autofill (manuell überschreibbar). Der bestehende Single-Endpoint (`admin_product_ingredient_store` für Neu+Update) wurde beibehalten, daher genügt ein FormRequest. Pivot-Zusatzfelder (`preferred`/`supplier_sku`/`url`) sind im Schema vorbereitet, das Formular synct vorerst nur die Lieferanten-Zuordnung. Tests: `tests/Feature/IngredientOrderFieldsTest.php` (6 grün). + +--- + +### AP-08 — Einkauf erweitern +**DB (`stock_entries`)** +- `tax_rate_id` nullable FK + Snapshot `tax_rate_percent` DECIMAL(5,2) (für historische Korrektheit). +- `price_per_kg_net` DECIMAL(10,4) nullable, `price_per_kg_gross` DECIMAL(10,4) nullable. + +**Code** +- Migration + `StockEntry` fillable/casts; `Store/UpdateStockEntryRequest` erweitern (genau eines von Netto/Brutto verpflichtend bei Rohstoff). +- `stock-entries/_form.blade.php` + `_scripts.blade.php`: UST-Dropdown; JS berechnet Netto↔Brutto gegenseitig beim Eintragen/UST-Wechsel (einheitliche Rundung). +- **Duplizieren:** Route `GET stock-entries/{stock_entry}/copy` + `StockEntryController@copy`: dupliziert Stufe-1-Felder, setzt `status=pending`, lässt Charge/MHD/Eingangsdaten leer. + +**Akzeptanz** +- Einkauf mit Netto **oder** Brutto anlegbar; Gegenfeld wird automatisch korrekt berechnet. +- UST-Wechsel aktualisiert das Gegenfeld. +- Ausgefüllter Einkauf für weitere Kanister/Chargen mit einem Klick duplizierbar. + +**Tests:** Netto→Brutto-Berechnung, Brutto→Netto, Duplizieren erzeugt `pending`-Kopie ohne Chargendaten. + +> **Status:** Erledigt (02.06.2026). Migration `2026_06_02_181548_add_price_fields_to_stock_entries_table` (`price_per_kg_gross`, `tax_rate_id`, `tax_rate_percent`). Das bereits vorhandene `price_per_kg` dient als Netto-Feld (`price_per_kg_net`), ergänzt um `price_per_kg_gross`; bewusst kein Rename, um Bestandsdaten/Factory/Tests stabil zu halten. Netto/Brutto-Umrechnung zentral in `StockEntryRepository::resolvePrices()` (UST-Prozent-Snapshot, fehlender Wert wird berechnet), live im Formular via JS. Duplizieren über `stock-entries/{id}/copy` legt direkt eine `pending`-Kopie der Stufe-1-Felder an. Verpackungspreis bleibt Netto-Gesamt ohne UST/Brutto (außerhalb des Plan-Scopes, kann später ergänzt werden). Tests: `tests/Feature/StockEntryPriceTest.php` (6 grün). + +--- + +### AP-09 — Produktion korrigieren +**Ziel:** Produktion auf Hersteller-Rezeptur stellen, JS-/iPad-Fehler beheben, Platzhalter Produktentwicklung. + +**Code** +- **Basis Hersteller-Rezeptur:** `ProductionService::store()`, `updateProduction()`, `requiredGramsByIngredient()`, `buildRecipePayload()` von `p_ingredients` auf `manufacturer_ingredients` umstellen (Pivot `gram`/`factor` analog). `ProductionController::recipeJson()` entsprechend. + > **Entscheidung (§5.1, geklärt):** Produktion nutzt **ausschließlich** die Hersteller-Rezeptur. **Kein Fallback** auf die Produkt-Rezeptur. Ist für das gewählte Produkt **keine Hersteller-Rezeptur** gepflegt, muss im Produktions-Formular direkt eine **deutliche Warnung** erscheinen (kein stilles Laden der Produkt-Rezeptur, Produktion ohne Hersteller-Rezeptur blockieren bzw. unmissverständlich warnen). +- **Chargen-Dropdown-Label:** `Lieferant - Chargennr. - dd.mm.yyyy` (kein „MHD"-Text). Nur Chargen mit **Restbestand > 0** anzeigen (Restbestand = `received_quantity` − bereits in `production_ingredients` verbrauchte Menge dieser Charge). Erfordert Verbrauchsabfrage je `stock_entry_id`. +- **B3 JS-Fix:** „Weitere Charge" fügt genau **eine** Zeile/ein Dropdown hinzu. +- **Soll-Neuberechnung stabil:** Ändert sich oben die Stückzahl, bleiben bereits eingetragene Chargen/Ist-Mengen erhalten; nur Soll-Gramm werden neu berechnet (keine Überschreibung manueller Eingaben). +- **UI vereinfachen:** Spaltenüberschriften „Charge"/„Menge" pro Rohstoffzeile entfernen; `g` hinter Mengen; weniger Linien. +- **B4 iPad-Fix:** Bootstrap-Grid der Kopfdaten (Produktionsdatum / Stückzahl) responsive ohne Überlappung. +- **Produktentwicklung-Platzhalter (§5.5, geklärt):** Sidenav-Unterpunkt unter „Produktion"; Route + simple View mit Hinweistext, dass hier **noch ein genaues Briefing aussteht** (keine Bestandsbuchung, keine Logik). + +**Akzeptanz:** Produktion rechnet auf Basis Hersteller-Rezeptur; **fehlt diese, erscheint eine Warnung** (kein Fallback); Chargenliste zeigt nur verfügbare Chargen im geforderten Label; „Weitere Charge" erzeugt eine Zeile; Stückzahländerung zerstört keine Eingaben; iPad-Layout sauber; Menüpunkt Produktentwicklung mit „Briefing ausstehend"-Hinweis sichtbar. + +**Tests:** Soll-Verbrauch aus Hersteller-Rezeptur; **Warnung bei fehlender Hersteller-Rezeptur**; Charge ohne Restbestand erscheint nicht; Service-Berechnung bei Stückzahländerung. + +--- + +### AP-02 — Produkt-Klassen: Einzelprodukt vs. Set + Hauptprodukt +> **Entscheidung (§5.6, geklärt):** Echte Sets via Pivot (`product_set_items`) — mehrere Einzelprodukte bündelbar, nicht nur „genau ein Hauptprodukt". + +**DB (`products`)** +- `is_set` bool default 0. +- `main_product_id` nullable FK auf `products` (Child→Hauptprodukt). +- `main_product_quantity` UINT nullable (z. B. 50 für „50 × 15 ml"). +- Pivot `product_set_items`: `set_product_id`, `component_product_id`, `quantity`. + +**Code** +- `Product`: `setItems()`, `mainProduct()`, Scopes `mainProducts()` / `singleProducts()`. +- Produktformular: Checkbox „Ist Set"; bei aktiv Karten Rezeptur/Verpackung/Warenwirtschaft ausblenden, Karte „Set-Bestandteile" einblenden (Modal wie Rezeptur, nur Einzelprodukte wählbar, mit Menge). +- Validierung: Set enthält nur Einzelprodukte (keine Sets), mind. 1 Bestandteil; Einzelprodukt darf Rezeptur/Packaging/Warenwirtschaft pflegen. + +**Akzeptanz:** Sets bestehen aus Einzelprodukten mit Menge; Sets sind nicht produzierbar; Produktbestand (AP-11) zeigt nur Haupt-/Einzelprodukte; Set-Verkauf reduziert später die enthaltenen Einzelprodukte (AP-13). + +--- + +### AP-03 — „Nicht vorrätig" mit Zeitangabe +**DB (`products`)** +- `out_of_stock_until` DATE nullable (Empfehlung: aus Tagen berechnet, sauber für Resttage). +- `out_of_stock_indefinite` bool default 0 (zweites Kästchen „auf unbestimmte Zeit vergriffen", ohne Tagefeld). + +**Code** +- Produktformular: Checkbox „Nicht vorrätig" + Tagefeld → `out_of_stock_until = now()->addDays($tage)`; zweite Checkbox „unbestimmt". +- Shop-/Bestellansicht: bei `out_of_stock_until` in der Zukunft Hinweis „In ca. X Tagen wieder da!" (Resttage dynamisch); bei `indefinite` entsprechender Dauerhinweis. + +**Entscheidung (§5.3, geklärt):** Vorerst **nur Hinweis**, der Kauf bleibt möglich. In der Hinweise-Doku (AP-18) ist zu dokumentieren, dass **künftig optional eine Kauf-Sperre** ergänzt werden kann/muss. + +**Akzeptanz:** Produkt zeitweise/unbefristet als nicht vorrätig markierbar; Resttage zählen automatisch herunter; nach Ablauf verschwindet der Hinweis ohne manuelles Zutun. + +--- + +### AP-10 — Rohstoffbestand (InventoryService + Übersicht) +**Code** +- `app/Services/InventoryService.php`: Restbestand je Rohstoff/Charge/Lagerort = `SUM(received_quantity)` − `SUM(production_ingredients.quantity_used)` − `SUM(stock_disposals.quantity)` (Ausgang ab AP-12). +- Controller + View „Rohstoffbestand" (Sidenav-Menüpunkt). Spalten: INCI/Rohstoff, Qualität, Gesamtbestand, Bestand je Lagerort (dynamisch aus `locations`), verbraucht/Produktion, Meldebestand/Bedarf, Status, Lieferanten, Lieferzeit (INCI vor Lieferant), Bestellaktion (`mailto:`/Shop-Link je `order_method`). +- Nur Chargen mit Restbestand > 0 einbeziehen; kritische Rohstoffe visuell markieren. + +**Akzeptanz:** Reale Restbestände sichtbar; Bestellweg direkt aus der Übersicht erreichbar; kritische Rohstoffe hervorgehoben. + +--- + +### AP-11 — Produktbestand + Historie +**DB** +- `product_stock_movements`: `product_id`, `direction` ENUM(`in`,`out`), `quantity`, `reason`, `source` (produktion/verkauf/manuell/set), `user_id`, `created_at`, `reference_type`/`reference_id` (polymorph, nullable). +- Schwellwerte: Felder an `products` (`min_product_stock`, `critical_product_stock`) oder eigene Tabelle. +- **Initialisierung (Briefing):** Lagerbestand einmalig einpflegbar (Anfangsbestand als `in`-Bewegung mit Grund „Initialbestand"). + +**Code** +- Bestand = `SUM(in)` − `SUM(out)`. Manuelle Bewegung: Menge + Grund + Richtung Pflicht. +- Hauptmenü „Produktbestand" (nur Hauptprodukte, Suche, Checkbox „nur kritische", Buttons `+`/`−`/`Produzieren`, rot/gelb-Markierung) + Untermenü „Historie" (filterbar Produkt/Quelle/Zeitraum/User; revisionssicher, Korrektur nur per Gegenbuchung). + +**Akzeptanz:** Bestand schnell pflegbar; jede Bewegung in der Historie; nur Hauptprodukte sichtbar; Kritisch-Filter funktioniert. + +--- + +### AP-12 — Ausgang / Ausschuss (Rohstoffe/Verpackung) +- `stock_disposals` (Typ, Artikel, Charge optional, Lagerort, Menge, Einheit, Grund Pflicht, User, Datum) + Controller/Views; Integration in `InventoryService`. +- **Akzeptanz:** Ausgang reduziert Rohstoff-/Verpackungsbestand; Grund ist Pflicht. + +--- + +### AP-13 — Shop-Anbindung: Bestand bei Verkauf reduzieren +- **Entscheidung (§5.2, geklärt):** Bestandsabzug erfolgt **beim Versand** (erst wenn der Versand gebucht ist, wurde das Produkt real „aus dem Regal" genommen). Dieser Hinweis ist auch in der Hinweise-Doku (AP-18) zu hinterlegen. +- Beim Statuswechsel auf **versendet** `product_stock_movements`-`out`-Buchung; bei Sets die enthaltenen Einzelprodukte (× Menge) reduzieren. Stornos/Retouren als Gegenbuchung (Detailregel bei Umsetzung festzurren). +- **Akzeptanz:** Versand reduziert Produktbestand; Set-Versand reduziert Einzelprodukte; jede Buchung in der Historie. + +--- + +### AP-14 — Audit-Trail +- `inventory_logs` (polymorph) + Observer auf `StockEntry`/`Production`/`StockDisposal`/`ProductStockMovement`. +- **Akzeptanz:** Jede Bestandsbewegung wird mit User/Zeit/Änderungen protokolliert. + +--- + +### AP-15 — Blockbasierte Rechte +- **Entscheidung (§5.4, geklärt):** Blockrechte gelten **nur für Warenwirtschaft und Produktmanagement**, nicht für alle Admin-Bereiche. In der Hinweise-Doku (AP-18) dokumentieren, dass die Rechte **bei Bedarf später ausgebaut** werden können/müssen, falls sie nicht ausreichen. +- `admin_permission_blocks` + `admin_permission_user` (view/edit pro Block: Produkte, Einkauf, Rohstoffbestand, Produktbestand, Produktion, Lieferanten, Einstellungen, Historie); Middleware/Gates; Sidenav zeigt nur erlaubte Blöcke. +- Bestehende Level (`copyreader`/`admin`/`superadmin`) bleiben als Grundschutz. +- **Akzeptanz:** SuperAdmin vergibt pro Mitarbeiter view/edit je Block (Warenwirtschaft + Produktmanagement); Leserecht ohne Schreibrecht greift; gesperrte Blöcke unsichtbar. + +--- + +### AP-16 — 2FA Google Authenticator (Admins) +- TOTP-Secret am `App\User` (Guard `user`), Setup-Flow, Login-Zwischenschritt; Recovery-Codes. +- **Akzeptanz:** Bei aktivem 2FA kein Zugriff auf geschützte Bereiche ohne Code. + +--- + +### AP-17 — Warenwirtschafts-Einstellungen +- Über bestehendes `Setting`-Model: `inventory_alert_email`, `inventory_alert_enabled`, `inventory_default_location`, optional Produktbestands-Schwellwerte, Standardtexte „Nicht vorrätig". SuperAdmin-only. + +--- + +### AP-18 — Hinweise-/Doku-Seite (Einstellungen → Hinweise) +> **Anforderung (§5, Kunde):** Eine als MD gepflegte Doku, die unter **Warenwirtschaft → Einstellungen → „Hinweise"** im Admin sichtbar ist, damit auch der Kunde Einsicht hat. + +**Code** +- Markdown-Datei im Repo (z. B. `docs/hinweise.md` oder `resources/docs/hinweise.md`) als Pflege-Quelle. +- Route + View unter `admin/inventory` (Einstellungen-Gruppe), die das MD gerendert anzeigt (Parsedown o. Ä.); Sidenav-Eintrag „Hinweise". + +**Inhalt (laufend zu pflegen):** +- Kurzer **Entwicklungsstand / Überblick** (was fertig ist, was offen ist). +- Wichtige Hinweise & **noch nötige Schritte** verständlich für den Kunden. +- Festgehaltene **offene/spätere Entscheidungen**, u. a.: + - „Nicht vorrätig" kann künftig optional zur **Kauf-Sperre** ausgebaut werden (§5.3). + - **Blockrechte** ggf. später über Warenwirtschaft/Produktmanagement hinaus ausbauen (§5.4). + - Shop-Bestandsabzug erfolgt **bei Versand** (§5.2). +- **Akzeptanz:** Kunde sieht unter Einstellungen → Hinweise eine lesbare, gepflegte Statusseite. + +> **Empfehlung:** Früh als Platzhalter anlegen und mit jedem AP fortschreiben, damit der Kunde jederzeit den Stand sieht. + +--- + +## 5. Klärungspunkte — ALLE GEKLÄRT (Kunde, 02.06.2026) + +> Alle Punkte sind beantwortet und in die jeweiligen Arbeitspakete eingearbeitet. Keine Blocker mehr offen. + +1. **Produktion-Basis** → **Ausschließlich Hersteller-Rezeptur.** Kein Fallback. Ist keine angelegt, erscheint direkt eine Warnung. → eingearbeitet in **AP-09**. + +2. **Shop-Bestandsabzug** → **Beim Versand** (erst mit gebuchtem Versand ist das Produkt real „aus dem Regal"). Als Hinweis dokumentieren. → eingearbeitet in **AP-13** + Hinweis in **AP-18**. + +3. **„Nicht vorrätig"** → Vorerst **nur Hinweis**. Dokumentieren, dass künftig optional eine **Kauf-Sperre** ergänzt werden kann. → eingearbeitet in **AP-03** + Hinweis in **AP-18**. + +4. **Blockrechte-Geltung** → **Nur Warenwirtschaft und Produktmanagement.** Dokumentieren, dass die Rechte bei Bedarf später ausgebaut werden können. → eingearbeitet in **AP-15** + Hinweis in **AP-18**. + +5. **Produktentwicklung** → **Platzhalter-Seite** mit Hinweis, dass ein genaues Briefing noch aussteht. → eingearbeitet in **AP-09**. + +6. **Child-Produkt / Sets** → **Echte Sets via Pivot** (`product_set_items`). → eingearbeitet in **AP-02**. + +7. **Hinweise-Doku (neu):** MD-basierte Doku-Seite unter **Einstellungen → Hinweise** mit Entwicklungsstand, wichtigen Hinweisen und noch nötigen Schritten, einsehbar auch für den Kunden. → neues **AP-18**. + +--- + +## 6. Empfohlene Sofort-Reihenfolge (nächste Schritte) + +✅ **Erledigt:** AP-00, AP-01, AP-04 (+ AP-04.1), AP-05, AP-06 (+ Nachtrag), AP-07 (+ AP-07.1), AP-08. + +**➡️ Hier geht es weiter:** +1. **AP-09** Produktionskorrekturen: ausschließlich Hersteller-Rezeptur (+ Warnung bei fehlender Rezeptur, kein Fallback), Chargen-Label + Restbestandsfilter, B3 „Weitere Charge"-Fix, stabile Soll-Neuberechnung, B4 iPad-Layout, Produktentwicklung-Platzhalter. +2. **AP-18** Hinweise-Doku (Einstellungen → Hinweise) — kann parallel/früh als Platzhalter angelegt und laufend gepflegt werden. +3. Datenmodell **AP-02** (Sets via Pivot) / **AP-03** („Nicht vorrätig", nur Hinweis). +4. Große Übersichten **AP-10/AP-11** und Folge-APs (AP-12–AP-17). + +--- + +## 7. Pflege dieses Dokuments + +- Jedes abgeschlossene AP hier mit Datum + Kurzbeschreibung + Test-Status protokollieren (analog Umsetzungsprotokoll in `entwicklungsplan.md`). +- Bei DB-Änderungen: Migration-Dateinamen referenzieren; bei Modellen Casts in `casts()`-Methode pflegen (L11-Konvention). +- Vor jedem Commit: `vendor/bin/pint --dirty` und betroffene Tests (`php artisan test --filter=...`). diff --git a/resources/views/admin/ingredient/edit.blade.php b/resources/views/admin/ingredient/edit.blade.php index 1fb8a0c..0f0116f 100755 --- a/resources/views/admin/ingredient/edit.blade.php +++ b/resources/views/admin/ingredient/edit.blade.php @@ -35,3 +35,27 @@ {!! Form::close() !!} @endsection + +@section('scripts') + +@endsection diff --git a/resources/views/admin/ingredient/form.blade.php b/resources/views/admin/ingredient/form.blade.php index 66b027e..8f6fe4b 100755 --- a/resources/views/admin/ingredient/form.blade.php +++ b/resources/views/admin/ingredient/form.blade.php @@ -42,6 +42,56 @@ @endforeach +
+ + +
+ + + @php + $selectedSupplierIds = old( + 'supplier_ids', + $model->exists ? $model->suppliers->pluck('id')->all() : [], + ); + @endphp +
+
+ +
+ +
+
+
+ +
+
+ + + + @foreach($deliveryTimes as $deliveryTime) + + @endforeach + + {{ __('Lieferzeit des Rohstoffs (hat Vorrang vor der Lieferanten-Lieferzeit).') }} +
+
+ + +
diff --git a/resources/views/admin/inventory/delivery-times/form.blade.php b/resources/views/admin/inventory/delivery-times/form.blade.php new file mode 100644 index 0000000..c65063c --- /dev/null +++ b/resources/views/admin/inventory/delivery-times/form.blade.php @@ -0,0 +1,56 @@ +@extends('layouts.layout-2') + +@section('content') +
+
{{ $model->exists ? __('Lieferzeit-Vorlage bearbeiten') : __('Lieferzeit-Vorlage anlegen') }}
+
+
+ @csrf + @if($model->exists) + @method('PUT') + @endif + +
+ + + @error('label') +
{{ $message }}
+ @enderror +
+ +
+ + + {{ __('Ganze Tage bis zum Wareneingang. Wird später für „rechtzeitig bestellen"-Hinweise verwendet.') }} + @error('days') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('pos') +
{{ $message }}
+ @enderror +
+ +
+ +
+ + + {{ __('Zurück') }} +
+
+
+@endsection diff --git a/resources/views/admin/inventory/general/index.blade.php b/resources/views/admin/inventory/general/index.blade.php new file mode 100644 index 0000000..3bf455a --- /dev/null +++ b/resources/views/admin/inventory/general/index.blade.php @@ -0,0 +1,118 @@ +@extends('layouts.layout-2') + +@section('content') + @include('admin.inventory.partials.table-actions-style') + +
+
+ {{ __('Umsatzsteuersätze') }} + {{ __('Neu anlegen') }} +
+
+ + + + + + + + + + + + @forelse($taxRates as $taxRate) + + + + + + + + @empty + + + + @endforelse + +
 {{ __('Name') }}{{ __('Satz') }}{{ __('Status') }}
+ + + + {{ $taxRate->name }}{{ number_format($taxRate->percent, 2, ',', '.') }} % + @if ($taxRate->active) + + @else + + @endif + +
+ @csrf + @method('DELETE') + +
+
+ {{ __('Noch keine Umsatzsteuersätze angelegt.') }}
+
+
+ +
+
+ {{ __('Lieferzeit-Vorlagen') }} + {{ __('Neu anlegen') }} +
+
+ + + + + + + + + + + + @forelse($deliveryTimes as $deliveryTime) + + + + + + + + @empty + + + + @endforelse + +
 {{ __('Bezeichnung') }}{{ __('Tage') }}{{ __('Status') }}
+ + + + {{ $deliveryTime->label }}{{ $deliveryTime->days !== null ? $deliveryTime->days : '–' }} + @if ($deliveryTime->active) + + @else + + @endif + +
+ @csrf + @method('DELETE') + +
+
+ {{ __('Noch keine Lieferzeit-Vorlagen angelegt.') }}
+
+
+@endsection diff --git a/resources/views/admin/inventory/locations/index.blade.php b/resources/views/admin/inventory/locations/index.blade.php index 04d599d..1a400d3 100644 --- a/resources/views/admin/inventory/locations/index.blade.php +++ b/resources/views/admin/inventory/locations/index.blade.php @@ -1,13 +1,14 @@ @extends('layouts.layout-2') @section('content') + @include('admin.inventory.partials.table-actions-style')
{{ __('Lagerorte') }} {{ __('Neu anlegen') }}
- +
diff --git a/resources/views/admin/inventory/material-qualities/index.blade.php b/resources/views/admin/inventory/material-qualities/index.blade.php index f226ec5..f7874d8 100644 --- a/resources/views/admin/inventory/material-qualities/index.blade.php +++ b/resources/views/admin/inventory/material-qualities/index.blade.php @@ -1,13 +1,14 @@ @extends('layouts.layout-2') @section('content') + @include('admin.inventory.partials.table-actions-style')
{{ __('Rohstoffqualität') }} {{ __('Neu anlegen') }}
-
 
+
diff --git a/resources/views/admin/inventory/packaging-items/form.blade.php b/resources/views/admin/inventory/packaging-items/form.blade.php index 21d8beb..6169aa1 100644 --- a/resources/views/admin/inventory/packaging-items/form.blade.php +++ b/resources/views/admin/inventory/packaging-items/form.blade.php @@ -77,7 +77,7 @@
- @error('url')
{{ $message }}
diff --git a/resources/views/admin/inventory/packaging-items/index.blade.php b/resources/views/admin/inventory/packaging-items/index.blade.php index 5213805..e267106 100644 --- a/resources/views/admin/inventory/packaging-items/index.blade.php +++ b/resources/views/admin/inventory/packaging-items/index.blade.php @@ -1,13 +1,14 @@ @extends('layouts.layout-2') @section('content') + @include('admin.inventory.partials.table-actions-style')
{{ $pageTitle }} {{ __('Neu anlegen') }}
-
 
+
diff --git a/resources/views/admin/inventory/packaging-materials/index.blade.php b/resources/views/admin/inventory/packaging-materials/index.blade.php index ce19f64..4f59ef0 100644 --- a/resources/views/admin/inventory/packaging-materials/index.blade.php +++ b/resources/views/admin/inventory/packaging-materials/index.blade.php @@ -1,13 +1,14 @@ @extends('layouts.layout-2') @section('content') + @include('admin.inventory.partials.table-actions-style')
{{ __('Verpackungsmaterial') }} {{ __('Neu anlegen') }}
-
 
+
diff --git a/resources/views/admin/inventory/partials/table-actions-style.blade.php b/resources/views/admin/inventory/partials/table-actions-style.blade.php new file mode 100644 index 0000000..11856b4 --- /dev/null +++ b/resources/views/admin/inventory/partials/table-actions-style.blade.php @@ -0,0 +1,37 @@ +@once + +@endonce diff --git a/resources/views/admin/inventory/productions/index.blade.php b/resources/views/admin/inventory/productions/index.blade.php index 2d62b30..137c7be 100644 --- a/resources/views/admin/inventory/productions/index.blade.php +++ b/resources/views/admin/inventory/productions/index.blade.php @@ -1,26 +1,38 @@ @extends('layouts.layout-2') @section('content') + @include('admin.inventory.partials.table-actions-style')
{{ __('Produktionen') }} {{ __('Neue Produktion') }}
-
 
+
+ - @foreach($values as $row) + @@ -32,17 +44,6 @@ @endif - @endforeach @@ -54,7 +55,8 @@ $('.datatables-style').dataTable({ "bLengthChange": false, "iDisplayLength": 100, - "order": [[0, "desc"]], + "order": [[1, "desc"]], + "columnDefs": [{"orderable": false, "targets": [0]}], "language": {"url": "/js/German.json"} }); }); diff --git a/resources/views/admin/inventory/stock-entries/_form.blade.php b/resources/views/admin/inventory/stock-entries/_form.blade.php index b9bda08..bdb75c5 100644 --- a/resources/views/admin/inventory/stock-entries/_form.blade.php +++ b/resources/views/admin/inventory/stock-entries/_form.blade.php @@ -115,14 +115,51 @@ @enderror -
{{ __('Datum') }} {{ __('Produkt') }} {{ __('Stück') }} {{ __('Standort') }} {{ __('MHD-Hinweis') }}
+ + + + + + + + + + {{ $row->produced_at?->format('d.m.Y') }} {{ $row->product?->name ?? '—' }} {{ $row->quantity }} - - - - - - - - - -
+
+ - + @foreach($values as $row) + @@ -77,7 +88,8 @@ $('.datatables-style').dataTable({ "bLengthChange": false, "iDisplayLength": 100, - "order": [[0, "asc"], [1, "desc"]], + "order": [[1, "asc"], [2, "desc"]], + "columnDefs": [{"orderable": false, "targets": [0, 7]}], "language": {"url": "/js/German.json"} }); }); diff --git a/resources/views/admin/inventory/stock-entries/show.blade.php b/resources/views/admin/inventory/stock-entries/show.blade.php index 9cf0036..4d5971a 100644 --- a/resources/views/admin/inventory/stock-entries/show.blade.php +++ b/resources/views/admin/inventory/stock-entries/show.blade.php @@ -18,6 +18,9 @@ {{ __('Zurück zur Liste') }} + @if(Auth::user()->isAdmin()) + {{ __('Duplizieren') }} + @endif @if(Auth::user()->isAdmin() && $model->isPending()) {{ __('Bearbeiten') }} @endif @@ -70,10 +73,16 @@
@if($model->entry_type === 'ingredient') @if($model->price_per_kg !== null) - {{ \App\Services\Util::formatNumber($model->price_per_kg) }} € / kg + {{ \App\Services\Util::formatNumber($model->price_per_kg) }} € / kg {{ __('netto') }} @else — @endif + @if($model->price_per_kg_gross !== null) + · {{ \App\Services\Util::formatNumber($model->price_per_kg_gross) }} € / kg {{ __('brutto') }} + @endif + @if($model->tax_rate_percent !== null) + · {{ __('USt.') }} {{ \App\Services\Util::formatNumber($model->tax_rate_percent) }} % + @endif @else @if($model->price_total !== null) {{ \App\Services\Util::formatNumber($model->price_total) }} € {{ __('netto') }} diff --git a/resources/views/admin/inventory/supplier-categories/index.blade.php b/resources/views/admin/inventory/supplier-categories/index.blade.php index be0ee8f..47fa4a7 100644 --- a/resources/views/admin/inventory/supplier-categories/index.blade.php +++ b/resources/views/admin/inventory/supplier-categories/index.blade.php @@ -1,13 +1,14 @@ @extends('layouts.layout-2') @section('content') + @include('admin.inventory.partials.table-actions-style')
{{ __('Lieferanten-Kategorien') }} {{ __('Neu anlegen') }}
-
{{ __('Status') }} {{ __('Bestellt') }} {{ __('Art') }} {{ __('Artikel') }} {{ __('Lieferant') }} {{ __('Menge') }}
+ + + + @if(Auth::user()->isAdmin() && $row->status === 'pending') + + + + @endif + @if(Auth::user()->isAdmin()) + + + + @endif + @if($row->status === 'pending') {{ __('Offen') }} @@ -51,18 +68,12 @@ @endif - - - @if(Auth::user()->isAdmin() && $row->status === 'pending') - - -
@csrf @method('DELETE') - +
@endif
+
diff --git a/resources/views/admin/inventory/suppliers/_details.blade.php b/resources/views/admin/inventory/suppliers/_details.blade.php new file mode 100644 index 0000000..b44d39b --- /dev/null +++ b/resources/views/admin/inventory/suppliers/_details.blade.php @@ -0,0 +1,161 @@ +@php + $orderMethodLabels = [ + 'email' => __('Per E-Mail'), + 'online_shop' => __('Online-Shop'), + ]; +@endphp +
+
+
+
+
{{ __('Name') }}
+
{{ $supplier->name }}
+ +
{{ __('Land') }}
+
{{ $supplier->country?->de ?? '—' }}
+ +
{{ __('Kategorien') }}
+
+ @forelse($supplier->supplierCategories as $cat) + {{ $cat->name }} + @empty + — + @endforelse +
+ +
{{ __('Webseite') }}
+
+ @if($supplier->url) + {{ $supplier->url }} + @else + — + @endif +
+
+
+
+
+
{{ __('Bestellweg') }}
+
{{ $orderMethodLabels[$supplier->order_method] ?? '—' }}
+ +
{{ __('Bestell-E-Mail') }}
+
{{ $supplier->order_email ?: '—' }}
+ +
{{ __('Bestell-URL') }}
+
+ @if($supplier->order_url) + {{ $supplier->order_url }} + @else + — + @endif +
+ +
{{ __('Lieferzeit') }}
+
+ {{ $supplier->delivery_time ?: '—' }} + @if($supplier->delivery_time_days !== null) + ({{ $supplier->delivery_time_days }} {{ __('Tage') }}) + @endif +
+ +
{{ __('Ansprechpartner') }}
+
{{ $supplier->contact_person ?: '—' }}
+ +
{{ __('E-Mail') }}
+
{{ $supplier->email ?: '—' }}
+ +
{{ __('Telefon') }}
+
{{ $supplier->phone ?: '—' }}
+
+
+
+ + @if($supplier->notes) +
+ {{ __('Notizen') }}: +
{!! nl2br(e($supplier->notes)) !!}
+
+ @endif + +
+ +
+
+
{{ __('Zugeordnete INCIs') }}
+
    + @forelse($supplier->ingredients as $ingredient) +
  • + {{ $ingredient->name }} + + + + + + +
  • + @empty +
  • {{ __('Noch keine INCIs zugeordnet.') }}
  • + @endforelse +
+
+ +
+ +
+
+
+ +
+
{{ __('Zugeordnete Verpackungsartikel') }}
+
    + @forelse($supplier->packagingItems as $item) +
  • + {{ $item->name }} + + + + + + +
  • + @empty +
  • {{ __('Noch keine Verpackungsartikel zugeordnet.') }}
  • + @endforelse +
+
+ +
+ +
+
+
+
+
diff --git a/resources/views/admin/inventory/suppliers/form.blade.php b/resources/views/admin/inventory/suppliers/form.blade.php index 0868d98..a942962 100644 --- a/resources/views/admin/inventory/suppliers/form.blade.php +++ b/resources/views/admin/inventory/suppliers/form.blade.php @@ -2,89 +2,166 @@ @section('content') @php - $selectedCategoryIds = old('supplier_category_ids', $model->exists ? $model->supplierCategories->pluck('id')->all() : []); + $selectedCategoryIds = old( + 'supplier_category_ids', + $model->exists ? $model->supplierCategories->pluck('id')->all() : [], + ); @endphp
{{ $model->exists ? __('Lieferant bearbeiten') : __('Lieferant anlegen') }}
-
+ @csrf - @if($model->exists) + @if ($model->exists) @method('PUT') @endif
- + @error('name') -
{{ $message }}
+
{{ $message }}
@enderror
- + @foreach ($supplierCategories as $cat) + @endforeach
@error('supplier_category_ids') -
{{ $message }}
+
{{ $message }}
@enderror
- - @foreach($countries as $country) - @endforeach @error('country_id') -
{{ $message }}
+
{{ $message }}
@enderror
- + @error('url') -
{{ $message }}
+
{{ $message }}
+ @enderror +
+ + @php + $orderMethod = old('order_method', $model->order_method); + @endphp +
+
+ + + @error('order_method') +
{{ $message }}
+ @enderror +
+
+ + + + @foreach ($deliveryTimes as $deliveryTime) + + @endforeach + + @error('delivery_time') +
{{ $message }}
+ @enderror +
+
+ + + @error('delivery_time_days') +
{{ $message }}
+ @enderror +
+
+ + + +
- + @error('contact_person') -
{{ $message }}
+
{{ $message }}
@enderror
- + @error('email') -
{{ $message }}
+
{{ $message }}
@enderror
- + @error('phone') -
{{ $message }}
+
{{ $message }}
@enderror
@@ -92,20 +169,21 @@ @error('notes') -
{{ $message }}
+
{{ $message }}
@enderror
- {{ __('Zurück') }} + {{ __('Zurück') }}
@@ -113,13 +191,33 @@ @section('scripts') @endsection diff --git a/resources/views/admin/inventory/suppliers/index.blade.php b/resources/views/admin/inventory/suppliers/index.blade.php index 8d45215..8b49ddd 100644 --- a/resources/views/admin/inventory/suppliers/index.blade.php +++ b/resources/views/admin/inventory/suppliers/index.blade.php @@ -1,13 +1,14 @@ @extends('layouts.layout-2') @section('content') + @include('admin.inventory.partials.table-actions-style')
{{ __('Lieferanten') }} {{ __('Neu anlegen') }}
-
 
+
@@ -21,8 +22,13 @@ @foreach($values as $value) - @@ -54,6 +60,22 @@
 
- + + +
+ + @endsection diff --git a/resources/views/admin/inventory/tax-rates/form.blade.php b/resources/views/admin/inventory/tax-rates/form.blade.php new file mode 100644 index 0000000..f1738cb --- /dev/null +++ b/resources/views/admin/inventory/tax-rates/form.blade.php @@ -0,0 +1,60 @@ +@extends('layouts.layout-2') + +@section('content') +
+
{{ $model->exists ? __('Umsatzsteuersatz bearbeiten') : __('Umsatzsteuersatz anlegen') }}
+
+
+ @csrf + @if($model->exists) + @method('PUT') + @endif + +
+ + + @error('name') +
{{ $message }}
+ @enderror +
+ +
+ +
+ +
+ % +
+ @error('percent') +
{{ $message }}
+ @enderror +
+
+ +
+ + + @error('pos') +
{{ $message }}
+ @enderror +
+ +
+ +
+ + + {{ __('Zurück') }} +
+
+
+@endsection diff --git a/resources/views/layouts/includes/layout-sidenav.blade.php b/resources/views/layouts/includes/layout-sidenav.blade.php index 0a7d2af..aa18e05 100644 --- a/resources/views/layouts/includes/layout-sidenav.blade.php +++ b/resources/views/layouts/includes/layout-sidenav.blade.php @@ -263,6 +263,9 @@ @endif @if (Auth::user()->isSuperAdmin())
  • @@ -271,6 +274,12 @@
    Einstellungen