Warenwirtschaft: AP-09 bis AP-13 (Produktbestand, Set-Produkte, Ausschuss, Konzepte)

- AP-09 Produktbestand inkl. Bewegungshistorie (product_stock_movements, ProductStockService)
- AP-10 Rohstoffbestand-Ansicht je Lager (RawMaterialStockController)
- AP-11 Bestandsschwellen / Out-of-Stock-Handling fuer Produkte und Shop
- AP-12 Ausgang/Ausschuss (stock_disposals, StockDisposalController, InventoryService)
- Set-Produkte (product_set_items) inkl. Aufloesung
- Produktentwicklung & Hinweise-Verwaltung (Notices)
- AP-13 Entwicklungskonzept Shop-Bestandsabzug im Plan dokumentiert
- Feature-Tests fuer neue Module + aktualisierter Entwicklungsplan

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Adametz 2026-06-03 11:04:22 +00:00
parent 78679e0c55
commit 3ee2d756e9
63 changed files with 5968 additions and 901 deletions

View file

@ -0,0 +1,44 @@
@extends('layouts.layout-2')
@section('content')
<h4 class="font-weight-bold py-2 mb-2">{{ __('Hinweise') }}</h4>
<div class="card">
<div class="card-body wawi-notices">
{!! $content !!}
</div>
</div>
@endsection
@section('styles')
<style>
.wawi-notices h1 {
font-size: 1.5rem;
}
.wawi-notices h2 {
font-size: 1.25rem;
margin-top: 1.5rem;
}
.wawi-notices h3 {
font-size: 1.05rem;
margin-top: 1rem;
}
.wawi-notices blockquote {
border-left: 3px solid #d4d8dd;
padding: 0.25rem 0 0.25rem 1rem;
color: #6c757d;
}
.wawi-notices hr {
margin: 1.5rem 0;
}
.wawi-notices ul {
padding-left: 1.25rem;
}
</style>
@endsection

View file

@ -0,0 +1,19 @@
@extends('layouts.layout-2')
@section('content')
<h4 class="font-weight-bold py-2 mb-2">{{ __('Produktentwicklung') }}</h4>
<div class="card">
<div class="card-body">
<div class="alert alert-info mb-0">
<h6 class="alert-heading font-weight-bold">{{ __('Briefing ausstehend') }}</h6>
<p class="mb-0">
{{ __('Dieser Bereich ist als Platzhalter angelegt. Zur konkreten Funktionsweise der Produktentwicklung steht noch ein genaues Briefing aus.') }}
</p>
<p class="mb-0 mt-2 text-muted small">
{{ __('Es findet hier aktuell keine Bestandsbuchung und keine Verarbeitungslogik statt.') }}
</p>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,110 @@
@extends('layouts.layout-2')
@php
$months = [
1 => __('Januar'), 2 => __('Februar'), 3 => __('März'), 4 => __('April'),
5 => __('Mai'), 6 => __('Juni'), 7 => __('Juli'), 8 => __('August'),
9 => __('September'), 10 => __('Oktober'), 11 => __('November'), 12 => __('Dezember'),
];
@endphp
@section('content')
@include('admin.inventory.partials.table-actions-style')
<div class="card">
<h6 class="card-header">{{ __('Produktbestand Historie') }}</h6>
<div class="card-body pb-0">
<form method="get" action="{{ route('admin.inventory.product-stock.history') }}">
<div class="form-row">
<div class="form-group col-md-3">
<label class="small text-muted">{{ __('Produkt') }}</label>
<select name="product_id" class="form-control" onchange="this.form.submit()">
<option value="">{{ __('Filter aus') }}</option>
@foreach ($products as $p)
<option value="{{ $p->id }}" @selected($filters['product_id'] === $p->id)>{{ $p->name }}</option>
@endforeach
</select>
</div>
<div class="form-group col-md-2">
<label class="small text-muted">{{ __('Eingang / Ausgang') }}</label>
<select name="direction" class="form-control" onchange="this.form.submit()">
<option value="">{{ __('Filter aus') }}</option>
<option value="in" @selected($filters['direction'] === 'in')>{{ __('Eingang') }}</option>
<option value="out" @selected($filters['direction'] === 'out')>{{ __('Ausgang') }}</option>
</select>
</div>
<div class="form-group col-md-3">
<label class="small text-muted">{{ __('Grund') }}</label>
<select name="reason" class="form-control" onchange="this.form.submit()">
<option value="">{{ __('Filter aus') }}</option>
@foreach ($reasonOptions as $reason)
<option value="{{ $reason }}" @selected($filters['reason'] === $reason)>{{ $reason }}</option>
@endforeach
</select>
</div>
<div class="form-group col-md-2">
<label class="small text-muted">{{ __('Monat') }}</label>
<select name="month" class="form-control" onchange="this.form.submit()">
<option value="">{{ __('Alle') }}</option>
@foreach ($months as $num => $label)
<option value="{{ $num }}" @selected($filters['month'] === $num)>{{ $label }}</option>
@endforeach
</select>
</div>
<div class="form-group col-md-2">
<label class="small text-muted">{{ __('Jahr') }}</label>
<select name="year" class="form-control" onchange="this.form.submit()">
<option value="">{{ __('Alle') }}</option>
@foreach ($years as $y)
<option value="{{ $y }}" @selected($filters['year'] === $y)>{{ $y }}</option>
@endforeach
</select>
</div>
</div>
@if ($filters['product_id'] || $filters['direction'] || $filters['reason'] || $filters['month'] || $filters['year'])
<a href="{{ route('admin.inventory.product-stock.history') }}" class="btn btn-sm btn-outline-secondary mb-3">{{ __('Filter zurücksetzen') }}</a>
@endif
</form>
</div>
<div class="card-datatable table-responsive">
<table class="table table-striped wawi-table mb-0">
<thead>
<tr>
<th>{{ __('Produkt / Material') }}</th>
<th>{{ __('Art') }}</th>
<th class="text-right">{{ __('Stückzahl') }}</th>
<th>{{ __('Datum') }}</th>
<th>{{ __('Grund') }}</th>
<th>{{ __('Hinweis') }}</th>
</tr>
</thead>
<tbody>
@forelse ($movements as $movement)
<tr>
<td>{{ $movement->product?->name ?? '—' }}</td>
<td>
@if ($movement->isIn())
<span class="text-success">{{ __('Eingang') }}</span>
@else
<span class="text-danger">{{ __('Ausgang') }}</span>
@endif
</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($movement->quantity, 0) }} {{ __('Stück') }}</td>
<td>{{ $movement->created_at?->translatedFormat('l, d.m.Y') }}</td>
<td>{{ $movement->reason ?: '—' }}</td>
<td class="text-muted">
{{ $movement->note }}
@if ($movement->user)
<span class="d-block small">{{ __('Mitarbeiter') }}: {{ $movement->user->getFullName(false) ?: $movement->user->email }}</span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center text-muted py-4">{{ __('Keine Bewegungen gefunden.') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endsection

View file

@ -0,0 +1,144 @@
@extends('layouts.layout-2')
@section('content')
@include('admin.inventory.partials.table-actions-style')
<div class="card">
<div class="card-header">
<div class="d-flex flex-wrap justify-content-between align-items-center">
<h6 class="mb-0">{{ __('Produktbestand') }}</h6>
<label class="form-check d-inline-flex align-items-center mb-0">
<input type="checkbox" id="ps-only-critical" class="form-check-input mr-2">
<span>{{ __('nur kritische anzeigen') }}</span>
</label>
</div>
<div class="form-group row mt-3 mb-0 align-items-center">
<label for="ps-search" class="col-sm-1 col-form-label">{{ __('Suchen') }}</label>
<div class="col-sm-5">
<input type="text" id="ps-search" class="form-control" autocomplete="off">
</div>
</div>
</div>
<div class="card-datatable table-responsive">
<table class="table table-striped wawi-table mb-0" id="ps-table">
<thead>
<tr>
<th style="width:4rem"></th>
<th>{{ __('Name') }}</th>
<th class="text-right">{{ __('Bestand') }}</th>
<th style="width:18rem">{{ __('Aktion') }}</th>
</tr>
</thead>
<tbody>
@forelse($rows as $row)
@php
$product = $row['product'];
$rowClass = $row['status'] === 'critical' ? 'table-danger' : ($row['status'] === 'warning' ? 'table-warning' : '');
$stockClass = $row['status'] === 'critical' ? 'text-danger font-weight-bold' : ($row['status'] === 'warning' ? 'text-warning font-weight-bold' : '');
@endphp
<tr class="ps-row {{ $rowClass }}" data-name="{{ Str::lower($product->name) }} {{ Str::lower($product->number ?? '') }}" data-status="{{ $row['status'] }}">
<td>
@if ($product->images->count())
<img class="img-fluid" alt="" style="max-height: 42px"
src="{{ route('product_image', [$product->images->first()->slug]) }}">
@endif
</td>
<td>{{ $product->name }}</td>
<td class="text-right {{ $stockClass }}">{{ \App\Services\Util::formatNumber($row['stock'], 0) }} {{ __('Stück') }}</td>
<td>
@if (Auth::user()->isAdmin())
<button type="button" class="btn icon-btn btn-sm btn-success js-ps-move"
data-product="{{ $product->id }}" data-name="{{ $product->name }}" data-direction="in"
title="{{ __('Eingang buchen') }}"><span class="fas fa-plus"></span></button>
<button type="button" class="btn icon-btn btn-sm btn-outline-danger js-ps-move"
data-product="{{ $product->id }}" data-name="{{ $product->name }}" data-direction="out"
title="{{ __('Ausgang buchen') }}"><span class="fas fa-minus"></span></button>
@endif
<a href="{{ route('admin.inventory.productions.create', ['product_id' => $product->id]) }}"
class="btn btn-sm btn-dark">{{ __('Produzieren') }}</a>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="text-center text-muted py-4">{{ __('Keine Produkte vorhanden.') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@if (Auth::user()->isAdmin())
<div class="modal fade" id="ps-move-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<form method="post" id="ps-move-form">
@csrf
<input type="hidden" name="direction" id="ps-move-direction">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ps-move-title">{{ __('Bestandsbewegung') }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<p class="text-muted mb-3" id="ps-move-product"></p>
<div class="form-group">
<label for="ps-move-quantity">{{ __('Stückzahl') }} <span class="text-danger">*</span></label>
<input type="number" min="1" step="1" name="quantity" id="ps-move-quantity" class="form-control" required>
</div>
<div class="form-group">
<label for="ps-move-reason">{{ __('Grund') }} <span class="text-danger">*</span></label>
<select name="reason" id="ps-move-reason" class="form-control" required>
@foreach ($reasons as $reason)
<option value="{{ $reason }}">{{ $reason }}</option>
@endforeach
</select>
</div>
<div class="form-group mb-0">
<label for="ps-move-note">{{ __('Hinweis') }}</label>
<input type="text" name="note" id="ps-move-note" class="form-control" maxlength="255" autocomplete="off">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{ __('Abbrechen') }}</button>
<button type="submit" class="btn btn-primary">{{ __('Buchen') }}</button>
</div>
</div>
</form>
</div>
</div>
@endif
<script>
$(function () {
var $rows = $('#ps-table tbody tr.ps-row');
function applyFilter() {
var term = ($('#ps-search').val() || '').toLowerCase().trim();
var onlyCritical = $('#ps-only-critical').is(':checked');
$rows.each(function () {
var $row = $(this);
var matchesTerm = term === '' || ($row.data('name') || '').toString().indexOf(term) !== -1;
var status = $row.data('status');
var matchesCritical = !onlyCritical || (status === 'critical' || status === 'warning');
$row.toggle(matchesTerm && matchesCritical);
});
}
$('#ps-search').on('keyup', applyFilter);
$('#ps-only-critical').on('change', applyFilter);
var moveTemplate = "{{ route('admin.inventory.product-stock.movement', ['product' => '__ID__']) }}";
$('.js-ps-move').on('click', function () {
var id = $(this).data('product');
var name = $(this).data('name');
var direction = $(this).data('direction');
$('#ps-move-form').attr('action', moveTemplate.replace('__ID__', id));
$('#ps-move-direction').val(direction);
$('#ps-move-product').text(name);
$('#ps-move-title').text(direction === 'in' ? '{{ __('Eingang buchen') }}' : '{{ __('Ausgang buchen') }}');
$('#ps-move-quantity').val('');
$('#ps-move-note').val('');
$('#ps-move-modal').modal('show');
});
});
</script>
@endsection

View file

@ -0,0 +1,71 @@
@php($isEdit = $isEdit ?? false)
<div class="form-row">
<div class="form-group col-12 col-md-6">
<label for="product_id">{{ __('Produkt') }}</label>
<select name="product_id" id="product_id" class="form-control @error('product_id') is-invalid @enderror" required>
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($products as $p)
<option value="{{ $p->id }}" @selected(old('product_id', $model?->product_id ?? ($defaultProductId ?? null)) == $p->id)>{{ $p->name }}</option>
@endforeach
</select>
@error('product_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group col-12 col-md-6">
<label for="location_id">{{ __('Lagerort / Produktionsstandort') }}</label>
<select name="location_id" id="location_id" class="form-control @error('location_id') is-invalid @enderror" required>
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($locations as $loc)
<option value="{{ $loc->id }}" @selected(old('location_id', $model?->location_id ?? $defaultLocationId) == $loc->id)>{{ $loc->name }}</option>
@endforeach
</select>
@error('location_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="form-row">
<div class="form-group col-12 col-sm-6">
<label for="produced_at">{{ __('Produktionsdatum') }}</label>
<input type="text" name="produced_at" id="produced_at" autocomplete="off"
class="form-control datepicker-base @error('produced_at') is-invalid @enderror"
value="{{ old('produced_at', $isEdit ? $model?->produced_at?->format('d.m.Y') : now()->format('d.m.Y')) }}" required>
@error('produced_at')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group col-12 col-sm-6">
<label for="quantity">{{ __('Produzierte Stückzahl') }}</label>
<input type="number" name="quantity" id="quantity" min="1" step="1" class="form-control @error('quantity') is-invalid @enderror"
value="{{ old('quantity', $model?->quantity ?? 1) }}" required>
@error('quantity')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div id="recipe-warning" class="alert alert-warning" style="display:none;"></div>
<div id="recipe-area" class="mb-3" style="display:none;">
<hr>
<h6>{{ __('Chargen zuordnen') }}</h6>
<p class="text-muted small" id="recipe-hint"></p>
<div id="recipe-ingredient-lines"></div>
@error('ingredient_lines')
<div class="text-danger small">{{ $message }}</div>
@enderror
<h6 class="mt-3">{{ __('Verpackung (Vorschau)') }}</h6>
<div class="table-responsive border rounded">
<table class="table table-sm mb-0">
<thead><tr><th>{{ __('Artikel') }}</th><th>{{ __('Stück gesamt') }}</th></tr></thead>
<tbody id="recipe-packaging-preview"></tbody>
</table>
</div>
</div>
<div class="form-group">
<label for="notes">{{ __('Notizen') }}</label>
<textarea name="notes" id="notes" class="form-control" rows="2">{{ old('notes', $model?->notes) }}</textarea>
</div>

View file

@ -0,0 +1,213 @@
<script>
(function ($) {
var recipeBase = @json(url('/admin/inventory/api/products'));
var existingLines = @json($existingLines ?? (object) []);
var excludeProductionId = @json($excludeProductionId ?? null);
var lineIndex = 0;
var recipeIngredients = [];
var loadedKey = null;
function currentKey() {
return ($('#product_id').val() || '') + '|' + ($('#location_id').val() || '');
}
function recipeUrl() {
var pid = $('#product_id').val();
var lid = $('#location_id').val();
var qty = $('#quantity').val() || 1;
if (!pid || !lid) {
return null;
}
var url = recipeBase + '/' + pid + '/recipe?location_id=' + encodeURIComponent(lid) + '&quantity=' + encodeURIComponent(qty);
if (excludeProductionId) {
url += '&exclude_production=' + encodeURIComponent(excludeProductionId);
}
return url;
}
function fmt(value) {
return Number(value).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
function buildStockSelect(idx, stockEntries, preselectedEntryId) {
var select = $('<select class="form-control form-control-sm" name="ingredient_lines[' + idx + '][stock_entry_id]" required></select>');
select.append('<option value="">{{ __('Charge wählen') }}</option>');
(stockEntries || []).forEach(function (se) {
var label = se.label;
if (se.remaining != null) {
label += ' — {{ __('Rest') }} ' + fmt(se.remaining) + ' g';
}
var opt = $('<option></option>').attr('value', se.id).text(label);
if (preselectedEntryId && String(se.id) === String(preselectedEntryId)) {
opt.prop('selected', true);
}
select.append(opt);
});
return select;
}
function addIngredientRow($wrap, ing, preselectedEntryId, preselectedQty) {
var idx = lineIndex++;
var $row = $('<div class="form-row align-items-center mb-1 recipe-row"></div>');
var $colSelect = $('<div class="col-12 col-sm-7 mb-1"></div>');
$colSelect.append($('<input type="hidden" name="ingredient_lines[' + idx + '][ingredient_id]">').val(ing.id));
$colSelect.append(buildStockSelect(idx, ing.stock_entries, preselectedEntryId));
$row.append($colSelect);
var $colQty = $('<div class="col-12 col-sm-5 mb-1"></div>');
var $group = $('<div class="input-group input-group-sm"></div>');
$group.append($('<input type="text" class="form-control" required placeholder="0" name="ingredient_lines[' + idx + '][quantity_used]">').val(preselectedQty || ''));
$group.append('<div class="input-group-append"><span class="input-group-text">g</span></div>');
$colQty.append($group);
$row.append($colQty);
$wrap.find('.recipe-rows').first().append($row);
}
function recomputeSoll() {
var qty = parseInt($('#quantity').val(), 10) || 1;
$('#recipe-ingredient-lines [data-ingredient-id]').each(function () {
var $wrap = $(this);
var ing = $wrap.data('recipe-ing');
if (!ing || ing.gram == null) {
return;
}
var soll = ing.gram * (ing.factor || 1) * qty;
$wrap.find('.recipe-soll').text(fmt(soll));
});
}
function renderRecipe(data) {
recipeIngredients = data.ingredients || [];
$('#recipe-ingredient-lines').empty();
lineIndex = 0;
if (data.recipe_required === false) {
$('#recipe-warning').hide();
$('#btn-submit-production').prop('disabled', false);
$('#recipe-area').show();
$('#recipe-hint')
.text('{{ __('Dieses Produkt benötigt keine Rezeptur. Es wird ohne Rohstoffverbrauch produziert.') }}')
.removeClass('text-danger').addClass('text-muted');
var $pkOnly = $('#recipe-packaging-preview').empty();
(data.packagings || []).forEach(function (pk) {
$pkOnly.append($('<tr></tr>')
.append($('<td></td>').text(pk.name))
.append($('<td></td>').text(pk.total_pieces)));
});
return;
}
if (data.has_recipe === false) {
$('#recipe-area').hide();
$('#recipe-warning')
.text('{{ __('Für dieses Produkt ist keine Hersteller-Rezeptur gepflegt. Eine Produktion ist erst möglich, wenn eine Hersteller-Rezeptur hinterlegt wurde.') }}')
.show();
$('#btn-submit-production').prop('disabled', true);
return;
}
$('#recipe-warning').hide();
$('#btn-submit-production').prop('disabled', false);
$('#recipe-area').show();
var qty = parseInt($('#quantity').val(), 10) || 1;
var hintParts = [];
var hasMissingGram = false;
recipeIngredients.forEach(function (ing) {
if (ing.gram == null) {
hasMissingGram = true;
hintParts.push(ing.name + ': {{ __('Anteil in der Hersteller-Rezeptur fehlt') }}');
return;
}
var soll = ing.gram * (ing.factor || 1) * qty;
var $wrap = $('<div class="border rounded p-2 mb-2" data-ingredient-id="' + ing.id + '"></div>');
$wrap.data('recipe-ing', ing);
var $head = $('<div class="d-flex justify-content-between flex-wrap"></div>');
$head.append($('<strong></strong>').text(ing.name));
$head.append($('<span class="text-muted small"></span>').html('{{ __('Soll') }}: <span class="recipe-soll">' + fmt(soll) + '</span> g'));
$wrap.append($head);
$wrap.append('<div class="recipe-rows mt-1"></div>');
$wrap.append('<button type="button" class="btn btn-sm btn-outline-secondary btn-add-split mt-1">{{ __('Weitere Charge') }}</button>');
$('#recipe-ingredient-lines').append($wrap);
var existing = existingLines[String(ing.id)] || [];
if (existing.length > 0) {
existing.forEach(function (ex) {
addIngredientRow($wrap, ing, ex.stock_entry_id, ex.quantity_used);
});
} else {
addIngredientRow($wrap, ing);
}
});
if (hasMissingGram) {
$('#recipe-hint').text(hintParts.join(' · ')).removeClass('text-muted').addClass('text-danger');
} else {
$('#recipe-hint')
.text('{{ __('Pro Charge die entnommene Menge in Gramm eintragen. Summe je Inhaltsstoff muss dem Soll entsprechen.') }}')
.removeClass('text-danger').addClass('text-muted');
}
var $pk = $('#recipe-packaging-preview').empty();
(data.packagings || []).forEach(function (pk) {
$pk.append($('<tr></tr>')
.append($('<td></td>').text(pk.name))
.append($('<td></td>').text(pk.total_pieces)));
});
}
$(document).on('click', '.btn-add-split', function (e) {
e.preventDefault();
var $wrap = $(this).closest('[data-ingredient-id]');
var ing = $wrap.data('recipe-ing');
if (!ing) {
return;
}
addIngredientRow($wrap, ing);
});
function loadRecipe() {
var url = recipeUrl();
if (!url) {
$('#recipe-area').hide();
$('#recipe-warning').hide();
return;
}
$('#recipe-warning').hide();
$('#recipe-area').show();
$('#recipe-hint').text('{{ __('Lade ') }}');
$.getJSON(url)
.done(function (data) {
loadedKey = currentKey();
renderRecipe(data);
})
.fail(function () {
$('#recipe-area').show();
$('#recipe-hint').text('{{ __('Rezept konnte nicht geladen werden.') }}').addClass('text-danger');
});
}
$('#product_id, #location_id').on('change', function () {
existingLines = {};
loadRecipe();
});
$('#quantity').on('change input', function () {
if (loadedKey && loadedKey === currentKey() && recipeIngredients.length > 0) {
recomputeSoll();
} else {
loadRecipe();
}
});
$(document).ready(function () {
if ($('#product_id').val() && $('#location_id').val()) {
loadRecipe();
}
});
})(jQuery);
</script>

View file

@ -7,73 +7,7 @@
<div class="card-body">
<form method="post" action="{{ route('admin.inventory.productions.store') }}" id="form-production">
@csrf
<div class="form-row">
<div class="form-group col-md-6">
<label for="product_id">{{ __('Produkt') }}</label>
<select name="product_id" id="product_id" class="form-control @error('product_id') is-invalid @enderror" required>
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($products as $p)
<option value="{{ $p->id }}" @selected(old('product_id', $model?->product_id) == $p->id)>{{ $p->name }}</option>
@endforeach
</select>
@error('product_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group col-md-6">
<label for="location_id">{{ __('Lagerort / Produktionsstandort') }}</label>
<select name="location_id" id="location_id" class="form-control @error('location_id') is-invalid @enderror" required>
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($locations as $loc)
<option value="{{ $loc->id }}" @selected(old('location_id', $model?->location_id ?? $defaultLocationId) == $loc->id)>{{ $loc->name }}</option>
@endforeach
</select>
@error('location_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<label for="produced_at">{{ __('Produktionsdatum') }}</label>
<input type="date" name="produced_at" id="produced_at" class="form-control @error('produced_at') is-invalid @enderror"
value="{{ old('produced_at', now()->toDateString()) }}" required>
@error('produced_at')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group col-md-4">
<label for="quantity">{{ __('Produzierte Stückzahl') }}</label>
<input type="number" name="quantity" id="quantity" min="1" step="1" class="form-control @error('quantity') is-invalid @enderror"
value="{{ old('quantity', $model?->quantity ?? 1) }}" required>
@error('quantity')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div id="recipe-area" class="mb-3" style="display:none;">
<hr>
<h6>{{ __('Chargen zuordnen') }}</h6>
<p class="text-muted small" id="recipe-hint"></p>
<div id="recipe-ingredient-lines"></div>
@error('ingredient_lines')
<div class="text-danger small">{{ $message }}</div>
@enderror
<h6 class="mt-3">{{ __('Verpackung (Vorschau)') }}</h6>
<div class="table-responsive border rounded">
<table class="table table-sm mb-0">
<thead><tr><th>{{ __('Artikel') }}</th><th>{{ __('Stück gesamt') }}</th></tr></thead>
<tbody id="recipe-packaging-preview"></tbody>
</table>
</div>
</div>
<div class="form-group">
<label for="notes">{{ __('Notizen') }}</label>
<textarea name="notes" id="notes" class="form-control" rows="2">{{ old('notes') }}</textarea>
</div>
@include('admin.inventory.productions._form_fields', ['isEdit' => false])
<button type="submit" class="btn btn-primary" id="btn-submit-production">{{ __('Produktion speichern') }}</button>
<a href="{{ route('admin.inventory.productions.index') }}" class="btn btn-outline-secondary">{{ __('Abbrechen') }}</a>
@ -83,109 +17,12 @@
@endsection
@section('scripts')
<script>
(function ($) {
var recipeBase = @json(url('/admin/inventory/api/products'));
var lineIndex = 0;
function recipeUrl() {
var pid = $('#product_id').val();
var lid = $('#location_id').val();
var qty = $('#quantity').val() || 1;
if (!pid || !lid) return null;
return recipeBase + '/' + pid + '/recipe?location_id=' + encodeURIComponent(lid) + '&quantity=' + encodeURIComponent(qty);
}
function addIngredientRow($tbody, ing, stockEntries) {
var idx = lineIndex;
lineIndex++;
var tr = $('<tr></tr>');
var select = $('<select class="form-control form-control-sm" name="ingredient_lines[' + idx + '][stock_entry_id]" required></select>');
select.append('<option value="">{{ __('Charge wählen') }}</option>');
(stockEntries || []).forEach(function (se) {
var label = '#' + se.id;
if (se.batch_number) label += ' — ' + se.batch_number;
if (se.best_before) label += ' (MHD ' + se.best_before + ')';
select.append($('<option></option>').attr('value', se.id).text(label));
});
var td1 = $('<td></td>');
td1.append($('<input type="hidden" name="ingredient_lines[' + idx + '][ingredient_id]" value="' + ing.id + '">'));
td1.append(select);
tr.append(td1);
tr.append($('<td></td>').append(
$('<input type="text" class="form-control form-control-sm" required name="ingredient_lines[' + idx + '][quantity_used]" placeholder="0">')
));
$tbody.append(tr);
}
function renderRecipe(data) {
$('#recipe-ingredient-lines').empty();
lineIndex = 0;
var hintParts = [];
var hasMissingGram = false;
(data.ingredients || []).forEach(function (ing) {
if (ing.required_grams_total === null) {
hasMissingGram = true;
hintParts.push(ing.name + ': {{ __('Gramm in der Rezeptur fehlt') }}');
return;
}
var wrap = $('<div class="border rounded p-2 mb-2" data-ingredient-id="' + ing.id + '"></div>');
wrap.data('recipe-ing', ing);
var soll = '<strong>' + $('<div/>').text(ing.name).html() + '</strong> — {{ __('Soll') }}: ' +
ing.required_grams_total.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' g';
wrap.append(soll);
var tbody = $('<tbody></tbody>');
var tbl = $('<table class="table table-sm table-bordered mb-1"><thead><tr><th>{{ __('Charge') }}</th><th>{{ __('Menge (g)') }}</th></tr></thead></table>');
tbl.append(tbody);
wrap.append(tbl);
wrap.append('<button type="button" class="btn btn-sm btn-outline-secondary btn-add-split">{{ __('Weitere Charge') }}</button>');
$('#recipe-ingredient-lines').append(wrap);
addIngredientRow(tbody, ing, ing.stock_entries || []);
});
if (hasMissingGram) {
$('#recipe-hint').text(hintParts.join(' · ')).removeClass('text-muted').addClass('text-danger');
} else {
$('#recipe-hint').text('{{ __('Pro Charge die entnommene Menge in Gramm eintragen. Summe je Inhaltsstoff muss dem Soll entsprechen.') }}').removeClass('text-danger').addClass('text-muted');
}
var $pk = $('#recipe-packaging-preview').empty();
(data.packagings || []).forEach(function (pk) {
$pk.append('<tr><td>' + $('<div/>').text(pk.name).html() + '</td><td>' + pk.total_pieces + '</td></tr>');
});
}
$(document).on('click', '.btn-add-split', function () {
var $wrap = $(this).closest('[data-ingredient-id]');
var ing = $wrap.data('recipe-ing');
if (!ing) return;
var $tbody = $wrap.find('tbody').first();
addIngredientRow($tbody, ing, ing.stock_entries || []);
});
function loadRecipe() {
var url = recipeUrl();
if (!url) {
$('#recipe-area').hide();
return;
}
$('#recipe-hint').text('{{ __('Lade ') }}');
$.getJSON(url)
.done(function (data) {
$('#recipe-area').show();
renderRecipe(data);
})
.fail(function () {
$('#recipe-area').show();
$('#recipe-hint').text('{{ __('Rezept konnte nicht geladen werden.') }}').addClass('text-danger');
});
}
$('#product_id, #location_id, #quantity').on('change', loadRecipe);
$(document).ready(function () {
if ($('#product_id').val() && $('#location_id').val()) {
loadRecipe();
}
});
})(jQuery);
</script>
@php
$existingLines = $model
? $model->productionIngredients->groupBy('ingredient_id')->map(function ($lines) {
return $lines->map(fn ($l) => ['stock_entry_id' => $l->stock_entry_id, 'quantity_used' => (float) $l->quantity_used])->values();
})
: [];
@endphp
@include('admin.inventory.productions._scripts', ['existingLines' => $existingLines, 'excludeProductionId' => null])
@endsection

View file

@ -8,73 +8,7 @@
<form method="post" action="{{ route('admin.inventory.productions.update', $model) }}" id="form-production">
@csrf
@method('PUT')
<div class="form-row">
<div class="form-group col-md-6">
<label for="product_id">{{ __('Produkt') }}</label>
<select name="product_id" id="product_id" class="form-control @error('product_id') is-invalid @enderror" required>
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($products as $p)
<option value="{{ $p->id }}" @selected(old('product_id', $model->product_id) == $p->id)>{{ $p->name }}</option>
@endforeach
</select>
@error('product_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group col-md-6">
<label for="location_id">{{ __('Lagerort / Produktionsstandort') }}</label>
<select name="location_id" id="location_id" class="form-control @error('location_id') is-invalid @enderror" required>
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($locations as $loc)
<option value="{{ $loc->id }}" @selected(old('location_id', $model->location_id) == $loc->id)>{{ $loc->name }}</option>
@endforeach
</select>
@error('location_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<label for="produced_at">{{ __('Produktionsdatum') }}</label>
<input type="date" name="produced_at" id="produced_at" class="form-control @error('produced_at') is-invalid @enderror"
value="{{ old('produced_at', $model->produced_at?->toDateString()) }}" required>
@error('produced_at')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group col-md-4">
<label for="quantity">{{ __('Produzierte Stückzahl') }}</label>
<input type="number" name="quantity" id="quantity" min="1" step="1" class="form-control @error('quantity') is-invalid @enderror"
value="{{ old('quantity', $model->quantity) }}" required>
@error('quantity')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div id="recipe-area" class="mb-3" style="display:none;">
<hr>
<h6>{{ __('Chargen zuordnen') }}</h6>
<p class="text-muted small" id="recipe-hint"></p>
<div id="recipe-ingredient-lines"></div>
@error('ingredient_lines')
<div class="text-danger small">{{ $message }}</div>
@enderror
<h6 class="mt-3">{{ __('Verpackung (Vorschau)') }}</h6>
<div class="table-responsive border rounded">
<table class="table table-sm mb-0">
<thead><tr><th>{{ __('Artikel') }}</th><th>{{ __('Stück gesamt') }}</th></tr></thead>
<tbody id="recipe-packaging-preview"></tbody>
</table>
</div>
</div>
<div class="form-group">
<label for="notes">{{ __('Notizen') }}</label>
<textarea name="notes" id="notes" class="form-control" rows="2">{{ old('notes', $model->notes) }}</textarea>
</div>
@include('admin.inventory.productions._form_fields', ['isEdit' => true])
<button type="submit" class="btn btn-primary" id="btn-submit-production">{{ __('Produktion speichern') }}</button>
<a href="{{ route('admin.inventory.productions.show', $model) }}" class="btn btn-outline-secondary">{{ __('Abbrechen') }}</a>
@ -84,120 +18,10 @@
@endsection
@section('scripts')
<script>
(function ($) {
var recipeBase = @json(url('/admin/inventory/api/products'));
var lineIndex = 0;
function recipeUrl() {
var pid = $('#product_id').val();
var lid = $('#location_id').val();
var qty = $('#quantity').val() || 1;
if (!pid || !lid) return null;
return recipeBase + '/' + pid + '/recipe?location_id=' + encodeURIComponent(lid) + '&quantity=' + encodeURIComponent(qty);
}
function addIngredientRow($tbody, ing, stockEntries, preselectedEntryId, preselectedQty) {
var idx = lineIndex;
lineIndex++;
var tr = $('<tr></tr>');
var select = $('<select class="form-control form-control-sm" name="ingredient_lines[' + idx + '][stock_entry_id]" required></select>');
select.append('<option value="">{{ __('Charge wählen') }}</option>');
(stockEntries || []).forEach(function (se) {
var label = '#' + se.id;
if (se.batch_number) label += ' — ' + se.batch_number;
if (se.best_before) label += ' (MHD ' + se.best_before + ')';
var opt = $('<option></option>').attr('value', se.id).text(label);
if (preselectedEntryId && String(se.id) === String(preselectedEntryId)) opt.prop('selected', true);
select.append(opt);
});
var td1 = $('<td></td>');
td1.append($('<input type="hidden" name="ingredient_lines[' + idx + '][ingredient_id]" value="' + ing.id + '">'));
td1.append(select);
tr.append(td1);
tr.append($('<td></td>').append(
$('<input type="text" class="form-control form-control-sm" required name="ingredient_lines[' + idx + '][quantity_used]" placeholder="0">').val(preselectedQty || '')
));
$tbody.append(tr);
}
function renderRecipe(data) {
$('#recipe-ingredient-lines').empty();
lineIndex = 0;
var existingLines = @json($model->productionIngredients->groupBy('ingredient_id')->map(function($lines) {
return $lines->map(function($l) {
return ['stock_entry_id' => $l->stock_entry_id, 'quantity_used' => (float) $l->quantity_used];
})->values();
}));
var hintParts = [];
var hasMissingGram = false;
(data.ingredients || []).forEach(function (ing) {
if (ing.required_grams_total === null) {
hasMissingGram = true;
hintParts.push(ing.name + ': {{ __('Gramm in der Rezeptur fehlt') }}');
return;
}
var wrap = $('<div class="border rounded p-2 mb-2" data-ingredient-id="' + ing.id + '"></div>');
wrap.data('recipe-ing', ing);
var soll = '<strong>' + $('<div/>').text(ing.name).html() + '</strong> — {{ __('Soll') }}: ' +
ing.required_grams_total.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' g';
wrap.append(soll);
var tbody = $('<tbody></tbody>');
var tbl = $('<table class="table table-sm table-bordered mb-1"><thead><tr><th>{{ __('Charge') }}</th><th>{{ __('Menge (g)') }}</th></tr></thead></table>');
tbl.append(tbody);
wrap.append(tbl);
wrap.append('<button type="button" class="btn btn-sm btn-outline-secondary btn-add-split">{{ __('Weitere Charge') }}</button>');
$('#recipe-ingredient-lines').append(wrap);
var existing = existingLines[String(ing.id)] || [];
if (existing.length > 0) {
existing.forEach(function (ex) {
addIngredientRow(tbody, ing, ing.stock_entries || [], ex.stock_entry_id, ex.quantity_used);
});
} else {
addIngredientRow(tbody, ing, ing.stock_entries || []);
}
});
if (hasMissingGram) {
$('#recipe-hint').text(hintParts.join(' · ')).removeClass('text-muted').addClass('text-danger');
} else {
$('#recipe-hint').text('{{ __('Pro Charge die entnommene Menge in Gramm eintragen.') }}').removeClass('text-danger').addClass('text-muted');
}
var $pk = $('#recipe-packaging-preview').empty();
(data.packagings || []).forEach(function (pk) {
$pk.append('<tr><td>' + $('<div/>').text(pk.name).html() + '</td><td>' + pk.total_pieces + '</td></tr>');
});
}
$(document).on('click', '.btn-add-split', function () {
var $wrap = $(this).closest('[data-ingredient-id]');
var ing = $wrap.data('recipe-ing');
if (!ing) return;
var $tbody = $wrap.find('tbody').first();
addIngredientRow($tbody, ing, ing.stock_entries || []);
});
function loadRecipe() {
var url = recipeUrl();
if (!url) { $('#recipe-area').hide(); return; }
$('#recipe-hint').text('{{ __('Lade ') }}');
$.getJSON(url)
.done(function (data) { $('#recipe-area').show(); renderRecipe(data); })
.fail(function () {
$('#recipe-area').show();
$('#recipe-hint').text('{{ __('Rezept konnte nicht geladen werden.') }}').addClass('text-danger');
});
}
$('#product_id, #location_id, #quantity').on('change', loadRecipe);
$(document).ready(function () {
if ($('#product_id').val() && $('#location_id').val()) {
loadRecipe();
}
});
})(jQuery);
</script>
@php
$existingLines = $model->productionIngredients->groupBy('ingredient_id')->map(function ($lines) {
return $lines->map(fn ($l) => ['stock_entry_id' => $l->stock_entry_id, 'quantity_used' => (float) $l->quantity_used])->values();
});
@endphp
@include('admin.inventory.productions._scripts', ['existingLines' => $existingLines, 'excludeProductionId' => $model->id])
@endsection

View file

@ -0,0 +1,156 @@
@extends('layouts.layout-2')
@section('content')
@include('admin.inventory.partials.table-actions-style')
<style>
#rms-table tbody tr.rms-row {
cursor: pointer;
}
#rms-table tbody tr.rms-row:hover {
background-color: rgba(0, 0, 0, 0.05);
}
#rms-table tbody tr.rms-row:hover .fa-edit {
color: #26b4ff !important;
}
</style>
<div class="card">
<div class="card-header">
<div class="d-flex flex-wrap justify-content-between align-items-center">
<h6 class="mb-0">{{ __('Rohstoffbestand') }}</h6>
<label class="form-check d-inline-flex align-items-center mb-0">
<input type="checkbox" id="rms-only-critical" class="form-check-input mr-2">
<span>{{ __('nur kritische anzeigen') }}</span>
</label>
</div>
<div class="form-group row mt-3 mb-0 align-items-center">
<label for="rms-search" class="col-sm-1 col-form-label">{{ __('Suchen') }}</label>
<div class="col-sm-5">
<input type="text" id="rms-search" class="form-control" autocomplete="off">
</div>
</div>
</div>
<div class="card-datatable table-responsive">
<table class="table table-striped wawi-table mb-0" id="rms-table">
<thead>
<tr>
<th>{{ __('Name') }}</th>
<th>{{ __('Qualität') }}</th>
<th class="text-right">{{ __('Bestand') }}</th>
<th class="text-right">{{ __('Offen bestellt') }}</th>
<th class="text-right">{{ __('Verbrauch / Tag') }}</th>
<th>{{ __('Voraussichtlich auf Null') }}</th>
<th class="text-right" style="min-width: 13rem;">
<select id="rms-horizon" class="form-control form-control-sm">
@foreach($horizonOptions as $days => $label)
<option value="{{ $days }}" @selected($days === $defaultHorizon)>{{ $label }}</option>
@endforeach
</select>
</th>
</tr>
</thead>
<tbody>
@forelse($rows as $row)
@php
$ingredient = $row['ingredient'];
$rowClass = $row['status'] === 'critical'
? 'table-danger'
: (in_array($row['status'], ['warning', 'critical_ordered'], true) ? 'table-warning' : '');
@endphp
<tr class="rms-row {{ $rowClass }}"
data-name="{{ Str::lower($ingredient->name) }} {{ Str::lower($ingredient->inci ?? '') }}"
data-status="{{ $row['status'] }}"
data-href="{{ route('admin.inventory.raw-material-stock.show', $ingredient) }}">
<td>
<i class="far fa-edit text-muted mr-2" title="{{ __('Bestellung öffnen') }}"></i>
{{ $ingredient->name }}
</td>
<td class="text-muted">{{ $ingredient->materialQuality?->name ?? '—' }}</td>
<td class="text-right {{ in_array($row['status'], ['critical', 'critical_ordered'], true) ? 'text-danger font-weight-bold' : '' }}">
{{ \App\Services\Util::formatNumber($row['remaining'], 0) }} g
@if($row['status'] === 'critical_ordered')
<span class="badge badge-info ml-1">{{ __('bestellt') }}</span>
@endif
</td>
<td class="text-right">
@if($row['open_order'] > 0)
{{ \App\Services\Util::formatNumber($row['open_order'], 0) }} g
@else
<span class="text-muted"></span>
@endif
</td>
<td class="text-right">
@if($row['daily'] !== null && $row['daily'] > 0)
{{ \App\Services\Util::formatNumber($row['daily'], 0) }} g
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if($row['expected_empty'] !== null)
{{ $row['expected_empty']->format('d.m.Y') }}
<span class="text-muted">({{ $row['days_until_empty'] }} {{ trans_choice('Tag|Tagen', $row['days_until_empty']) }})</span>
@else
<span class="text-muted"></span>
@endif
</td>
<td class="text-right rms-forecast" data-daily="{{ $row['daily'] !== null ? $row['daily'] : 0 }}">
@if($row['daily'] !== null && $row['daily'] > 0)
{{ \App\Services\Util::formatNumber($row['daily'] * $defaultHorizon, 0) }} g
@else
<span class="text-muted"></span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center text-muted py-4">{{ __('Keine aktiven Rohstoffe vorhanden.') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<script>
$(function () {
var $rows = $('#rms-table tbody tr.rms-row');
function applyFilter() {
var term = ($('#rms-search').val() || '').toLowerCase().trim();
var onlyCritical = $('#rms-only-critical').is(':checked');
$rows.each(function () {
var $row = $(this);
var matchesTerm = term === '' || ($row.data('name') || '').toString().indexOf(term) !== -1;
var status = $row.data('status');
var matchesCritical = !onlyCritical || (status === 'critical' || status === 'warning');
$row.toggle(matchesTerm && matchesCritical);
});
}
function applyForecast() {
var days = parseInt($('#rms-horizon').val(), 10) || 0;
$('.rms-forecast').each(function () {
var daily = parseFloat($(this).data('daily')) || 0;
if (daily > 0) {
var total = Math.round(daily * days);
$(this).text(total.toLocaleString('de-DE') + ' g');
} else {
$(this).html('<span class="text-muted">—</span>');
}
});
}
$('#rms-search').on('keyup', applyFilter);
$('#rms-only-critical').on('change', applyFilter);
$('#rms-horizon').on('change', applyForecast);
$rows.on('click', function () {
var href = $(this).data('href');
if (href) {
window.location = href;
}
});
});
</script>
@endsection

View file

@ -0,0 +1,256 @@
@extends('layouts.layout-2')
@section('content')
@include('admin.inventory.partials.table-actions-style')
@php
$statusBadge = match ($status) {
'critical' => '<span class="badge badge-danger">' . __('Kritisch') . '</span>',
'critical_ordered' => '<span class="badge badge-warning">' . __('Kritisch · bereits bestellt') . '</span>',
'warning' => '<span class="badge badge-warning">' . __('Bald nachbestellen') . '</span>',
default => '<span class="badge badge-success">' . __('Ausreichend') . '</span>',
};
@endphp
<div class="card mb-3">
<h6 class="card-header d-flex justify-content-between align-items-center">
<span>{{ __('Rohstoffbestellung') }}: {{ $ingredient->name }} {!! $statusBadge !!}</span>
<a href="{{ route('admin.inventory.raw-material-stock.index') }}" class="btn btn-sm btn-outline-secondary">{{ __('Zurück') }}</a>
</h6>
<div class="card-body">
<div class="row">
<div class="col-md-3 col-6 mb-2">
<div class="text-muted small">{{ __('Qualität') }}</div>
<div>{{ $ingredient->materialQuality?->name ?? '—' }}</div>
</div>
<div class="col-md-3 col-6 mb-2">
<div class="text-muted small">{{ __('Gesamtbestand') }}</div>
<div class="{{ in_array($status, ['critical', 'critical_ordered'], true) ? 'text-danger font-weight-bold' : 'font-weight-bold' }}">
{{ \App\Services\Util::formatNumber($remaining, 0) }} g
</div>
</div>
<div class="col-md-3 col-6 mb-2">
<div class="text-muted small">{{ __('Offen bestellt') }}</div>
<div>
@if($openTotal > 0)
{{ \App\Services\Util::formatNumber($openTotal, 0) }} g
@else
@endif
</div>
</div>
<div class="col-md-3 col-6 mb-2">
<div class="text-muted small">{{ __('Verbrauch / Tag') }}</div>
<div>
@if($daily !== null && $daily > 0)
{{ \App\Services\Util::formatNumber($daily, 0) }} g
@else
@endif
</div>
</div>
<div class="col-md-3 col-6 mb-2">
<div class="text-muted small">{{ __('Voraussichtlich auf Null') }}</div>
<div>
@if($expectedEmpty !== null)
{{ $expectedEmpty->format('d.m.Y') }}
<span class="text-muted">({{ $daysUntilEmpty }} {{ trans_choice('Tag|Tagen', $daysUntilEmpty) }})</span>
@else
@endif
</div>
</div>
</div>
@if($ingredient->min_stock_alert !== null)
<div class="text-muted small mt-2">
{{ __('Meldebestand') }}: {{ \App\Services\Util::formatNumber($ingredient->min_stock_alert, 0) }} g
</div>
@endif
</div>
</div>
<div class="card mb-3">
<h6 class="card-header">{{ __('Enthalten in') }}</h6>
<div class="card-datatable table-responsive">
<table class="table table-striped wawi-table mb-0">
<thead>
<tr>
<th>{{ __('Produkt') }}</th>
<th class="text-right">{{ __('Produktbestand') }}</th>
<th class="text-right">{{ __('Rezeptur-Anteil (g/Stück)') }}</th>
</tr>
</thead>
<tbody>
@forelse($ingredient->products as $product)
<tr>
<td>{{ $product->name }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($productStock[$product->id] ?? 0, 0) }} {{ __('Stück') }}</td>
<td class="text-right">
@if($product->pivot->gram !== null && $product->pivot->gram !== '')
{{ \App\Services\Util::formatNumber($product->pivot->gram, 2) }} g
@else
<span class="text-muted"></span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="3" class="text-center text-muted py-3">{{ __('Dieser Rohstoff wird in keiner aktiven Rezeptur verwendet.') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="card mb-3">
<h6 class="card-header">{{ __('Lieferanten & Bestellung') }}</h6>
<div class="card-datatable table-responsive">
<table class="table table-striped wawi-table mb-0">
<thead>
<tr>
<th>{{ __('Lieferant') }}</th>
<th>{{ __('Lieferzeit') }}</th>
<th class="text-right">{{ __('Letzter Einkauf (Netto/kg)') }}</th>
<th class="text-right" style="width: 11rem;"></th>
</tr>
</thead>
<tbody>
@forelse($ingredient->suppliers as $supplier)
@php
$deliveryTime = $ingredient->delivery_time ?: $supplier->delivery_time;
$lastPrice = $lastPriceBySupplier[$supplier->id] ?? null;
$orderUrl = $supplier->order_url ?: $supplier->url;
$orderEmail = $supplier->order_email ?: $supplier->email;
@endphp
<tr>
<td>
{{ $supplier->name }}
@if($supplier->pivot->preferred)
<span class="badge badge-success ml-1">{{ __('bevorzugt') }}</span>
@endif
@if($supplier->pivot->supplier_sku)
<div class="text-muted small">{{ __('Art.-Nr.') }}: {{ $supplier->pivot->supplier_sku }}</div>
@endif
</td>
<td>{{ $deliveryTime ?: '—' }}</td>
<td class="text-right">
@if($lastPrice !== null)
{{ \App\Services\Util::formatNumber($lastPrice, 2) }}
@else
<span class="text-muted"></span>
@endif
</td>
<td class="text-right">
@if($supplier->order_method === 'online_shop' && $orderUrl)
<a href="{{ $orderUrl }}" target="_blank" rel="noopener" class="btn btn-sm btn-dark">{{ __('Zum Shop') }}</a>
@elseif($orderEmail)
<a href="mailto:{{ $orderEmail }}?subject={{ rawurlencode(__('Bestellung') . ': ' . $ingredient->name) }}" class="btn btn-sm btn-dark">{{ __('Per Mail') }}</a>
@elseif($orderUrl)
<a href="{{ $orderUrl }}" target="_blank" rel="noopener" class="btn btn-sm btn-dark">{{ __('Zum Shop') }}</a>
@else
<span class="text-muted"></span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="4" class="text-center text-muted py-3">{{ __('Diesem Rohstoff ist kein Lieferant zugeordnet.') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if(Auth::user()->isAdmin())
<div class="card-footer">
<a href="{{ route('admin.inventory.stock-entries.create', ['ingredient_id' => $ingredient->id]) }}" class="btn btn-sm btn-primary">{{ __('Einkauf erfassen') }}</a>
<a href="{{ route('admin.inventory.stock-disposals.create', ['ingredient_id' => $ingredient->id]) }}" class="btn btn-sm btn-outline-danger">{{ __('Ausschuss erfassen') }}</a>
</div>
@endif
</div>
<div class="card mb-3">
<h6 class="card-header">{{ __('Offene Bestellungen / unterwegs') }}</h6>
<div class="card-datatable table-responsive">
<table class="table table-striped wawi-table mb-0">
<thead>
<tr>
<th>{{ __('Bestellt am') }}</th>
<th>{{ __('Lieferant') }}</th>
<th>{{ __('Lagerort') }}</th>
<th class="text-right">{{ __('Bestellte Menge') }}</th>
<th class="text-right" style="width: 8rem;"></th>
</tr>
</thead>
<tbody>
@forelse($openOrders as $order)
<tr>
<td>{{ $order->ordered_at?->format('d.m.Y') ?? '—' }}</td>
<td>{{ $order->supplier?->name ?? '—' }}</td>
<td>{{ $order->location?->name ?? '—' }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($order->ordered_quantity, 0) }} g</td>
<td class="text-right">
<a href="{{ route('admin.inventory.stock-entries.show', $order) }}" class="btn icon-btn btn-sm btn-primary" title="{{ __('Wareneingang buchen') }}">
<span class="far fa-eye"></span>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="text-center text-muted py-3">{{ __('Keine offenen Bestellungen.') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($openOrders->isNotEmpty())
<div class="card-footer text-muted small">
{{ __('Offene Bestellungen zählen erst nach gebuchtem Wareneingang zum Bestand.') }}
</div>
@endif
</div>
<div class="card mb-3">
<h6 class="card-header">{{ __('Verfügbare Chargen') }}</h6>
<div class="card-datatable table-responsive">
<table class="table table-striped wawi-table mb-0">
<thead>
<tr>
<th>{{ __('Charge') }}</th>
<th>{{ __('Lieferant') }}</th>
<th>{{ __('Lagerort') }}</th>
<th>{{ __('MHD') }}</th>
<th class="text-right">{{ __('Restbestand') }}</th>
</tr>
</thead>
<tbody>
@forelse($charges as $charge)
<tr>
<td>{{ $charge->batch_number ?: '#' . $charge->id }}</td>
<td>{{ $charge->supplier?->name ?? '—' }}</td>
<td>{{ $charge->location?->name ?? '—' }}</td>
<td>{{ $charge->best_before?->format('d.m.Y') ?? '—' }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($charge->getAttribute('remaining_quantity'), 0) }} g</td>
</tr>
@empty
<tr>
<td colspan="5" class="text-center text-muted py-3">{{ __('Kein Restbestand vorhanden.') }}</td>
</tr>
@endforelse
</tbody>
@if($charges->isNotEmpty() && count($remainingByLocation) > 0)
<tfoot>
@foreach($locations as $location)
@if(isset($remainingByLocation[$location->id]) && $remainingByLocation[$location->id] > 0)
<tr>
<td colspan="4" class="text-right text-muted">{{ __('Bestand') }} {{ $location->name }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($remainingByLocation[$location->id], 0) }} g</td>
</tr>
@endif
@endforeach
</tfoot>
@endif
</table>
</div>
</div>
@endsection

View file

@ -0,0 +1,194 @@
@extends('layouts.layout-2')
@section('content')
<h4 class="font-weight-bold py-2 mb-2">{{ __('Ausgang / Ausschuss erfassen') }}</h4>
<div class="card">
<div class="card-body">
<p class="text-muted small">{{ __('Reduziert den Bestand des gewählten Rohstoffs bzw. Verpackungsartikels. Der Grund ist Pflicht und erscheint in der Ausgangsliste.') }}</p>
<form method="post" action="{{ route('admin.inventory.stock-disposals.store') }}">
@csrf
<div class="form-group">
<label for="disposal_type">{{ __('Art') }} <span class="text-danger">*</span></label>
<select name="disposal_type" id="disposal_type" class="form-control" required>
<option value="ingredient" @selected(old('disposal_type', $prefill['disposal_type']) === 'ingredient')>{{ __('Rohstoff') }}</option>
<option value="packaging" @selected(old('disposal_type', $prefill['disposal_type']) === 'packaging')>{{ __('Verpackung') }}</option>
</select>
</div>
<div id="disposal-ingredient-block" class="form-group" style="display:none;">
<label for="ingredient_id">{{ __('Rohstoff') }} <span class="text-danger">*</span></label>
<div class="light-style">
<select name="ingredient_id" id="ingredient_id" class="w-100"
data-search-url="{{ route('admin.inventory.api.ingredients.search') }}"
data-charges-url="{{ route('admin.inventory.api.disposals.ingredient-charges', ['ingredient' => '__ID__']) }}">
@if ($prefill['ingredient_id'])
<option value="{{ $prefill['ingredient_id'] }}" selected>{{ $prefill['ingredient_label'] }}</option>
@elseif(old('ingredient_id'))
<option value="{{ old('ingredient_id') }}" selected>{{ old('ingredient_id') }}</option>
@endif
</select>
</div>
@error('ingredient_id')
<div class="text-danger small">{{ $message }}</div>
@enderror
</div>
<div id="disposal-charge-block" class="form-group" style="display:none;">
<label for="stock_entry_id">{{ __('Charge') }} <span class="text-muted small">({{ __('optional') }})</span></label>
<select name="stock_entry_id" id="stock_entry_id" class="form-control">
<option value="">{{ __('— keine bestimmte Charge —') }}</option>
</select>
<small class="text-muted">{{ __('Bei Auswahl wird der Lagerort automatisch gesetzt.') }}</small>
</div>
<div id="disposal-packaging-block" class="form-group" style="display:none;">
<label for="packaging_item_id">{{ __('Verpackungsartikel') }} <span class="text-danger">*</span></label>
<div class="light-style">
<select name="packaging_item_id" id="packaging_item_id" class="w-100"
data-search-url="{{ route('admin.inventory.api.packaging-items.search') }}">
@if (old('packaging_item_id'))
<option value="{{ old('packaging_item_id') }}" selected>{{ old('packaging_item_id') }}</option>
@endif
</select>
</div>
@error('packaging_item_id')
<div class="text-danger small">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="location_id">{{ __('Lagerort') }} <span class="text-danger">*</span></label>
<select name="location_id" id="location_id" class="form-control @error('location_id') is-invalid @enderror" required>
<option value="">{{ __('Bitte wählen') }}</option>
@foreach ($locations as $loc)
<option value="{{ $loc->id }}" @selected((string) old('location_id') === (string) $loc->id)>{{ $loc->name }}</option>
@endforeach
</select>
@error('location_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="quantity">{{ __('Menge') }} <span class="text-danger">*</span></label>
<input type="text" name="quantity" id="quantity" autocomplete="off"
class="form-control @error('quantity') is-invalid @enderror" value="{{ old('quantity') }}" required>
<small class="text-muted" id="quantity-hint">{{ __('Bei Rohstoff in Gramm, bei Verpackung in Stück.') }}</small>
@error('quantity')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="reason">{{ __('Grund') }} <span class="text-danger">*</span></label>
<select name="reason" id="reason" class="form-control @error('reason') is-invalid @enderror" required>
@foreach ($reasons as $reason)
<option value="{{ $reason }}" @selected(old('reason') === $reason)>{{ $reason }}</option>
@endforeach
</select>
@error('reason')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="note">{{ __('Hinweis') }}</label>
<input type="text" name="note" id="note" maxlength="255" autocomplete="off"
class="form-control @error('note') is-invalid @enderror" value="{{ old('note') }}">
@error('note')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="disposed_at">{{ __('Datum') }} <span class="text-danger">*</span></label>
<input type="text" name="disposed_at" id="disposed_at" autocomplete="off"
class="form-control datepicker-base @error('disposed_at') is-invalid @enderror"
value="{{ old('disposed_at', now()->format('d.m.Y')) }}" required>
@error('disposed_at')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<button type="submit" class="btn btn-primary">{{ __('Buchen') }}</button>
<a href="{{ route('admin.inventory.stock-disposals.index') }}" class="btn btn-outline-secondary">{{ __('Zurück') }}</a>
</form>
</div>
</div>
@endsection
@section('scripts')
<script>
(function ($) {
function toggleBlocks() {
var isIng = $('#disposal_type').val() === 'ingredient';
$('#disposal-ingredient-block').toggle(isIng);
$('#disposal-charge-block').toggle(isIng);
$('#disposal-packaging-block').toggle(!isIng);
$('#quantity-hint').text(isIng
? '{{ __('Menge in Gramm.') }}'
: '{{ __('Menge in Stück.') }}');
}
function initSearchSelect2(id, placeholder) {
var $el = $('#' + id);
if ($el.data('select2')) {
$el.select2('destroy');
}
$el.select2({
theme: 'default',
width: '100%',
placeholder: placeholder,
allowClear: true,
ajax: {
url: $el.data('search-url'),
dataType: 'json',
delay: 250,
data: function (params) { return {q: params.term || ''}; },
processResults: function (data) { return {results: data.results || []}; },
cache: true
},
minimumInputLength: 1
});
}
function loadCharges(ingredientId) {
var $charge = $('#stock_entry_id');
$charge.empty().append($('<option>').val('').text('{{ __(' keine bestimmte Charge ') }}'));
if (!ingredientId) {
return;
}
var url = $('#ingredient_id').data('charges-url').replace('__ID__', ingredientId);
$.getJSON(url, function (data) {
(data.charges || []).forEach(function (c) {
$charge.append($('<option>').val(c.id).text(c.text).attr('data-location', c.location_id));
});
});
}
$(document).ready(function () {
toggleBlocks();
initSearchSelect2('ingredient_id', '{{ __('Rohstoff suchen…') }}');
initSearchSelect2('packaging_item_id', '{{ __('Verpackungsartikel suchen…') }}');
$('#disposal_type').on('change', toggleBlocks);
$('#ingredient_id').on('change', function () {
loadCharges($(this).val());
});
$('#stock_entry_id').on('change', function () {
var loc = $(this).find('option:selected').data('location');
if (loc) {
$('#location_id').val(String(loc));
}
});
@if ($prefill['ingredient_id'])
loadCharges('{{ $prefill['ingredient_id'] }}');
@endif
});
})(jQuery);
</script>
@endsection

View file

@ -0,0 +1,76 @@
@extends('layouts.layout-2')
@section('content')
@include('admin.inventory.partials.table-actions-style')
<div class="card">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center">
<h6 class="mb-0">{{ __('Ausgang / Ausschuss') }}</h6>
<div>
<form method="get" class="d-inline-block mr-2">
<select name="type" class="form-control form-control-sm d-inline-block" style="width:auto"
onchange="this.form.submit()">
<option value="">{{ __('Alle Arten') }}</option>
<option value="ingredient" @selected($typeFilter === 'ingredient')>{{ __('Rohstoff') }}</option>
<option value="packaging" @selected($typeFilter === 'packaging')>{{ __('Verpackung') }}</option>
</select>
</form>
@if (Auth::user()->isAdmin())
<a href="{{ route('admin.inventory.stock-disposals.create') }}" class="btn btn-sm btn-primary">
{{ __('Ausschuss erfassen') }}
</a>
@endif
</div>
</div>
<div class="card-datatable table-responsive">
<table class="table table-striped wawi-table mb-0">
<thead>
<tr>
<th>{{ __('Datum') }}</th>
<th>{{ __('Art') }}</th>
<th>{{ __('Artikel') }}</th>
<th>{{ __('Charge') }}</th>
<th>{{ __('Lagerort') }}</th>
<th class="text-right">{{ __('Menge') }}</th>
<th>{{ __('Grund') }}</th>
<th>{{ __('Hinweis') }}</th>
<th>{{ __('Mitarbeiter') }}</th>
</tr>
</thead>
<tbody>
@forelse($values as $disposal)
<tr>
<td>{{ $disposal->disposed_at?->format('d.m.Y') }}</td>
<td>
@if ($disposal->isIngredient())
<span class="badge badge-info">{{ __('Rohstoff') }}</span>
@else
<span class="badge badge-secondary">{{ __('Verpackung') }}</span>
@endif
</td>
<td>{{ $disposal->articleName() }}</td>
<td>
@if ($disposal->stockEntry)
{{ $disposal->stockEntry->batch_number ?: '#'.$disposal->stockEntry->id }}
@else
<span class="text-muted"></span>
@endif
</td>
<td>{{ $disposal->location?->name ?? '—' }}</td>
<td class="text-right text-danger font-weight-bold">
{{ \App\Services\Util::formatNumber($disposal->quantity, $disposal->unit === 'piece' ? 0 : 2) }}
{{ $disposal->unit === 'piece' ? __('Stück') : 'g' }}
</td>
<td>{{ $disposal->reason }}</td>
<td class="text-muted">{{ $disposal->note }}</td>
<td>{{ $disposal->user?->getFullName(false) ?: ($disposal->user?->email ?? '—') }}</td>
</tr>
@empty
<tr>
<td colspan="9" class="text-center text-muted py-4">{{ __('Noch keine Ausgänge erfasst.') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endsection

View file

@ -95,9 +95,9 @@
<div class="form-group">
<label for="ordered_at">{{ __('Bestelldatum') }} <span class="text-danger">*</span></label>
<input type="date" name="ordered_at" id="ordered_at"
class="form-control @error('ordered_at') is-invalid @enderror"
value="{{ old('ordered_at', $model->ordered_at ? $model->ordered_at->format('Y-m-d') : '') }}" required>
<input type="text" name="ordered_at" id="ordered_at" autocomplete="off"
class="form-control datepicker-base @error('ordered_at') is-invalid @enderror"
value="{{ old('ordered_at', $model->ordered_at ? $model->ordered_at->format('d.m.Y') : '') }}" required>
@error('ordered_at')
<div class="invalid-feedback">{{ $message }}</div>
@enderror

View file

@ -137,9 +137,9 @@
<div class="form-group">
<label for="received_at">{{ __('Eingangsdatum') }} <span class="text-danger">*</span></label>
<input type="date" name="received_at" id="received_at" required
class="form-control @error('received_at') is-invalid @enderror"
value="{{ old('received_at', now()->toDateString()) }}">
<input type="text" name="received_at" id="received_at" required autocomplete="off"
class="form-control datepicker-base @error('received_at') is-invalid @enderror"
value="{{ old('received_at', now()->format('d.m.Y')) }}">
@error('received_at')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
@ -175,8 +175,8 @@
<div class="form-group">
<label for="best_before">{{ __('Mindesthaltbarkeit') }} <span class="text-danger">*</span></label>
<input type="date" name="best_before" id="best_before"
class="form-control @error('best_before') is-invalid @enderror"
<input type="text" name="best_before" id="best_before" autocomplete="off"
class="form-control datepicker-base @error('best_before') is-invalid @enderror"
value="{{ old('best_before') }}">
@error('best_before')
<div class="invalid-feedback">{{ $message }}</div>

View file

@ -21,6 +21,11 @@
<h4 class="mb-2">
<a href="#" class="text-body">{{ $product->name }}</a>
</h4>
@if ($product->isOutOfStock())
<div class="alert alert-danger py-2 my-3 font-weight-bold">
<i class="fa fa-clock-o"></i> {{ $product->outOfStockNotice() }}
</div>
@endif
{!! $product->copy !!}
<table class="table my-4">

View file

@ -62,12 +62,21 @@
'material_name' => $item->packagingMaterial?->name ?? '',
];
});
$set_product_catalog_for_js = $set_product_catalog->keyBy('id')->map(function ($item) {
return [
'name' => $item->name,
'number' => $item->number,
'weight' => $item->weight ? $item->weight . ' g' : '—',
'price' => $item->getFormattedPrice() !== '' ? $item->getFormattedPrice() . ' €' : '—',
];
});
@endphp
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
<script>
(function ($) {
var catalog = @json($ingredient_catalog_for_js);
var packagingCatalog = @json($packaging_catalog_for_js);
var setCatalog = @json($set_product_catalog_for_js);
function parseDeNumber(val) {
if (val === undefined || val === null || val === '') return NaN;
@ -449,6 +458,124 @@
}
$(document).on('change', '.js-shelf-life-type', toggleShelfMonths);
toggleShelfMonths();
function toggleRecipeFields() {
var noRecipe = $('#no_recipe_required').is(':checked');
$('.js-recipe-fields').toggle(!noRecipe);
}
$(document).on('change', '#no_recipe_required', toggleRecipeFields);
toggleRecipeFields();
// === Set-Bestandteile ===
var $setTbody = document.getElementById('set-sortable-rows');
if ($setTbody && typeof Sortable !== 'undefined') {
new Sortable($setTbody, {
handle: '.set-drag-handle',
animation: 150,
});
}
function appendSetRow(id) {
id = String(id);
var sp = setCatalog[id];
if (!sp) {
return;
}
var row = $('<tr data-set-product-id="' + id + '">' +
'<td class="text-muted align-middle set-drag-handle" style="cursor:grab">&#9776;</td>' +
'<td class="align-middle"></td><td class="align-middle small text-muted"></td>' +
'<td class="align-middle text-right small text-muted"></td>' +
'<td class="align-middle text-right small text-muted"></td>' +
'<td><input type="hidden" name="set_component_id[]" value="' + id + '">' +
'<input type="number" min="1" step="1" name="set_quantity[]" class="form-control form-control-sm set-quantity" value="1" autocomplete="off"></td>' +
'<td class="align-middle"><a class="text-danger set-row-remove" href="#" title="{{ __('Entfernen') }}"><i class="far fa-trash-alt"></i></a></td></tr>');
row.find('td').eq(1).text(sp.name || '');
row.find('td').eq(2).text(sp.number || '—');
row.find('td').eq(3).text(sp.weight || '—');
row.find('td').eq(4).text(sp.price || '—');
$('#set-sortable-rows').append(row);
}
function syncSetModalRows() {
var usedIds = {};
$('#set-sortable-rows tr').each(function () {
var sid = $(this).data('set-product-id');
if (sid) {
usedIds[String(sid)] = true;
}
});
$('#modal-set-pick .set-modal-row').each(function () {
var sid = String($(this).data('set-product-id'));
var inUse = !!usedIds[sid];
var $cb = $(this).find('.js-set-modal-cb');
$cb.prop('disabled', inUse);
$cb.attr('title', inUse ? '{{ __('Bereits im Set') }}' : '');
if (inUse) {
$cb.prop('checked', false);
}
$(this).css('opacity', inUse ? 0.65 : 1);
$(this).find('.js-set-modal-name').toggleClass('text-muted', inUse);
$(this).find('.js-set-modal-number').toggleClass('text-muted', inUse);
});
}
$('#set-modal-search').on('input', function () {
var q = $(this).val().trim().toLowerCase();
var visible = 0;
$('#set-modal-tbody .set-modal-row').each(function () {
var hay = $(this).attr('data-set-search') || '';
var match = !q || hay.indexOf(q) !== -1;
$(this).toggle(match);
if (match) {
visible++;
}
});
$('#set-modal-no-results').toggleClass('d-none', !(q.length > 0 && visible === 0));
});
$('#modal-set-pick').on('show.bs.modal', function () {
$('#set-modal-search').val('');
$('#set-modal-tbody .set-modal-row').show();
$('#set-modal-no-results').addClass('d-none');
syncSetModalRows();
});
$('#btn-set-modal-add').on('click', function () {
var added = 0;
$('#modal-set-pick .js-set-modal-cb:checked:not(:disabled)').each(function () {
appendSetRow(String($(this).val()));
$(this).prop('checked', false);
added++;
});
if (added > 0) {
syncSetModalRows();
$('#modal-set-pick').modal('hide');
}
});
$(document).on('click', '.set-row-remove', function (ev) {
ev.preventDefault();
$(this).closest('tr').remove();
syncSetModalRows();
});
function toggleSetMode() {
var isSet = $('#is_set').is(':checked');
$('.js-set-fields').toggle(isSet);
$('#product-section-rezeptur, #product-section-herstellerrezeptur, #product-section-verpackung, #product-section-warenwirtschaft').toggle(!isSet);
$('.js-nav-recipe').toggleClass('d-none', isSet);
}
$(document).on('change', '#is_set', toggleSetMode);
toggleSetMode();
function toggleOutOfStock() {
var indefinite = $('#out_of_stock_indefinite').is(':checked');
var active = $('#out_of_stock_active').is(':checked');
$('#out_of_stock_active').prop('disabled', indefinite);
$('.js-out-of-stock-days').toggle(active && !indefinite);
}
$(document).on('change', '#out_of_stock_active, #out_of_stock_indefinite', toggleOutOfStock);
toggleOutOfStock();
});
})(jQuery);
</script>

View file

@ -6,13 +6,16 @@
id="product-form-section-nav" aria-label="{{ __('Sprungmarken Produktformular') }}">
<span class="navbar-text small font-weight-bold text-muted mr-2 mb-1">{{ __('Bereiche') }}:</span>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-produkt">{{ __('Produkt') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-verfuegbarkeit">{{ __('Verfügbarkeit') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-set">{{ __('Set') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-preise">{{ __('Preise in EUR') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-laenderpreise">{{ __('Landesspezifische Preise') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-whitelabel">{{ __('White-Label') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-details">{{ __('Details') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-rezeptur">{{ __('Inhaltsstoffe') }} / {{ __('Rezeptur') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-herstellerrezeptur">{{ __('Hersteller Rezeptur') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-warenwirtschaft">{{ __('Warenwirtschaft') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1 js-nav-recipe" href="#product-section-rezeptur">{{ __('Inhaltsstoffe') }} / {{ __('Rezeptur') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1 js-nav-recipe" href="#product-section-herstellerrezeptur">{{ __('Hersteller Rezeptur') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1 js-nav-recipe" href="#product-section-verpackung">{{ __('Verpackung') }}</a>
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1 js-nav-recipe" href="#product-section-warenwirtschaft">{{ __('Warenwirtschaft') }}</a>
@if (Auth::user()->isSySAdmin())
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-sysadmin">{{ __('SySAdmin Einstellungen') }}</a>
@endif
@ -97,6 +100,171 @@
</div>
</div>
<div class="card mb-2" id="product-section-verfuegbarkeit">
<h5 class="card-header">{{ __('Verfügbarkeit') }}</h5>
<div class="card-body">
@php($outOfStockDays = $product->outOfStockRemainingDays())
<div class="form-group">
<label class="custom-control custom-checkbox">
{!! Form::checkbox('out_of_stock_active', 1, old('out_of_stock_active', $outOfStockDays !== null), ['class' => 'custom-control-input', 'id' => 'out_of_stock_active']) !!}
<span class="custom-control-label">{{ __('Vorübergehend nicht vorrätig (mit Zeitangabe)') }}</span>
</label>
<p class="text-muted small mb-0">{{ __('Zeigt im Shop den Hinweis „In ca. X Tagen wieder da!". Der Kauf bleibt weiterhin möglich.') }}</p>
</div>
<div class="form-row js-out-of-stock-days" style="display:none;">
<div class="form-group col-sm-4">
<label class="form-label" for="out_of_stock_days">{{ __('Wieder verfügbar in (Tagen)') }}</label>
{{ Form::number('out_of_stock_days', old('out_of_stock_days', $outOfStockDays), ['placeholder' => __('z. B. 14'), 'class' => 'form-control', 'id' => 'out_of_stock_days', 'min' => 0, 'step' => 1]) }}
</div>
</div>
<hr>
<div class="form-group">
<label class="custom-control custom-checkbox">
{!! Form::checkbox('out_of_stock_indefinite', 1, old('out_of_stock_indefinite', $product->out_of_stock_indefinite), ['class' => 'custom-control-input', 'id' => 'out_of_stock_indefinite']) !!}
<span class="custom-control-label">{{ __('Auf unbestimmte Zeit nicht vorrätig') }}</span>
</label>
<p class="text-muted small mb-0">{{ __('Daueranzeige ohne Zeitangabe (hat Vorrang vor der Tagesangabe). Der Kauf bleibt weiterhin möglich.') }}</p>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<hr>
<button type="submit" class=" float-right btn btn-sm btn-submit">{{ __('save') }}</button>&nbsp;
</div>
</div>
</div>
</div>
<div class="card mb-2" id="product-section-set">
<h5 class="card-header">{{ __('Set / Produktart') }}</h5>
<div class="card-body">
<div class="form-group">
<label class="custom-control custom-checkbox">
{!! Form::checkbox('is_set', 1, old('is_set', $product->is_set), ['class' => 'custom-control-input', 'id' => 'is_set']) !!}
<span class="custom-control-label">{{ __('Dieses Produkt ist ein Set (Bündel mehrerer Einzelprodukte)') }}</span>
</label>
<p class="text-muted small mb-0">{{ __('Bei aktivem Set werden Rezeptur, Verpackung und Warenwirtschaft ausgeblendet. Ein Set wird nicht produziert; beim Verkauf werden später die enthaltenen Einzelprodukte abgebucht.') }}</p>
</div>
<div class="js-set-fields" style="display:none;">
<p class="text-muted small mb-2">{{ __('Set-Bestandteile (nur Einzelprodukte). Reihenfolge per Drag & Drop ändern, danach speichern.') }}</p>
<div class="table-responsive">
<table class="table table-striped table-bordered mb-3">
<thead>
<tr>
<th style="width:2rem"></th>
<th>{{ __('Name') }}</th>
<th>{{ __('Artikelnr.') }}</th>
<th class="text-right" style="width:8rem">{{ __('Gewicht') }}</th>
<th class="text-right" style="width:10rem">{{ __('Preis VK in EUR (Brutto)') }}</th>
<th style="width:8rem">{{ __('Menge') }}</th>
<th style="width:3rem"></th>
</tr>
</thead>
<tbody id="set-sortable-rows">
@foreach ($product->setItems as $component)
<tr data-set-product-id="{{ $component->id }}">
<td class="text-muted align-middle set-drag-handle" style="cursor:grab"
title="{{ __('Verschieben') }}">&#9776;</td>
<td class="align-middle">{{ $component->name }}</td>
<td class="align-middle small text-muted">{{ $component->number ?? '—' }}</td>
<td class="align-middle text-right small text-muted">{{ $component->weight ? $component->weight . ' g' : '—' }}</td>
<td class="align-middle text-right small text-muted">{{ $component->getFormattedPrice() !== '' ? $component->getFormattedPrice() . ' €' : '—' }}</td>
<td>
<input type="hidden" name="set_component_id[]" value="{{ $component->id }}">
<input type="number" min="1" step="1" name="set_quantity[]"
class="form-control form-control-sm set-quantity"
value="{{ (int) ($component->pivot->quantity ?? 1) }}" autocomplete="off">
</td>
<td class="align-middle">
<a class="text-danger set-row-remove" href="#"
title="{{ __('Entfernen') }}"><i class="far fa-trash-alt"></i></a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="form-group">
<button type="button" class="btn btn-default" data-toggle="modal"
data-target="#modal-set-pick">{{ __('Einzelprodukte hinzufügen') }}</button>
</div>
<div class="modal fade" id="modal-set-pick" tabindex="-1" role="dialog"
aria-labelledby="modal-set-pick-title" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modal-set-pick-title">{{ __('Einzelprodukte auswählen') }}</h5>
<button type="button" class="close" data-dismiss="modal"
aria-label="{{ __('Schließen') }}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group mb-2">
<input type="search" id="set-modal-search" class="form-control" autocomplete="off"
placeholder="{{ __('Name oder Artikelnr. filtern …') }}"
aria-label="{{ __('Name oder Artikelnr. filtern …') }}">
</div>
<p class="small text-muted mb-2 d-none" id="set-modal-no-results">{{ __('Keine Treffer.') }}
</p>
<div class="table-responsive border rounded"
style="max-height: min(60vh, 480px); overflow-y: auto;">
<table class="table table-sm table-striped mb-0">
<thead class="thead-light sticky-top">
<tr style="background-color: #f8f9fa;">
<th style="width:2.5rem"></th>
<th>{{ __('Name') }}</th>
<th>{{ __('Artikelnr.') }}</th>
<th class="text-right">{{ __('Gewicht') }}</th>
<th class="text-right">{{ __('Preis VK in EUR (Brutto)') }}</th>
</tr>
</thead>
<tbody id="set-modal-tbody">
@foreach ($set_product_catalog as $sp)
<tr class="set-modal-row" data-set-product-id="{{ $sp->id }}"
data-set-search="{{ e(mb_strtolower(trim(($sp->name ?? '') . ' ' . ($sp->number ?? '')), 'UTF-8')) }}">
<td class="align-middle">
<label class="custom-control custom-checkbox mb-0">
<input type="checkbox" class="custom-control-input js-set-modal-cb"
value="{{ $sp->id }}"
@if ($product->setItems->contains('id', $sp->id)) disabled @endif>
<span class="custom-control-label"></span>
</label>
</td>
<td class="align-middle js-set-modal-name">{{ $sp->name }}</td>
<td class="align-middle small text-muted js-set-modal-number">{{ $sp->number ?? '—' }}</td>
<td class="align-middle small text-muted text-right">{{ $sp->weight ? $sp->weight . ' g' : '—' }}</td>
<td class="align-middle small text-muted text-right">{{ $sp->getFormattedPrice() !== '' ? $sp->getFormattedPrice() . ' €' : '—' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default"
data-dismiss="modal">{{ __('Abbrechen') }}</button>
<button type="button" class="btn btn-primary"
id="btn-set-modal-add">{{ __('Hinzufügen') }}</button>
</div>
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<hr>
<button type="submit" class=" float-right btn btn-sm btn-submit">{{ __('save') }}</button>&nbsp;
</div>
</div>
</div>
</div>
<div class="card mb-2" id="product-section-preise">
<h5 class="card-header">
{{ __('Preise in EUR') }}
@ -419,6 +587,16 @@
</h5>
<div class="card-body">
<input type="hidden" name="product_inci_sync_sent" value="1">
<div class="form-group">
<label class="custom-control custom-checkbox">
{!! Form::checkbox('no_recipe_required', 1, old('no_recipe_required', $product->no_recipe_required), ['class' => 'custom-control-input', 'id' => 'no_recipe_required']) !!}
<span class="custom-control-label">{{ __('Dieses Produkt benötigt keine Rezeptur (Eigenprodukt, z. B. Broschüre, Etikett)') }}</span>
</label>
<p class="text-muted small mb-0">{{ __('Bei aktiver Option werden die Rezeptur-Felder ausgeblendet und in der Produktion wird keine Rezeptur abgefragt.') }}</p>
</div>
<div class="js-recipe-fields">
<p class="text-muted small mb-2">{{ __('Reihenfolge per Drag & Drop ändern, danach speichern.') }}</p>
<div class="table-responsive">
<table class="table table-striped table-bordered mb-3" id="recipe-table-product">
@ -553,6 +731,7 @@
</div>
</div>
</div>
</div>{{-- /.js-recipe-fields --}}
<div class="form-row">
<div class="form-group col-md-12">
<hr>
@ -568,6 +747,7 @@
</h5>
<div class="card-body">
<input type="hidden" name="manufacturer_inci_sync_sent" value="1">
<div class="js-recipe-fields">
<p class="text-muted small mb-2">{{ __('Eigene Hersteller-Rezeptur (separate INCI-Liste). Reihenfolge per Drag & Drop ändern, danach speichern.') }}</p>
<div class="table-responsive">
<table class="table table-striped table-bordered mb-3" id="recipe-table-manufacturer">
@ -683,6 +863,7 @@
</div>
</div>
</div>
</div>{{-- /.js-recipe-fields --}}
<div class="form-row">
<div class="form-group col-md-12">
<hr>
@ -846,6 +1027,40 @@
@endforeach
</select>
</div>
<hr>
<p class="text-muted small mb-2">{{ __('Produktbestand-Schwellwerte (Stück). Bei Erreichen wird das Produkt im Produktbestand farblich markiert.') }}</p>
<div class="form-row">
<div class="form-group col-sm-6">
<label class="form-label" for="min_product_stock">{{ __('Meldebestand (gelb)') }}</label>
{{ Form::number('min_product_stock', old('min_product_stock', $product->min_product_stock), ['placeholder' => __('z. B. 20'), 'class' => 'form-control', 'id' => 'min_product_stock', 'min' => 0, 'step' => 1]) }}
</div>
<div class="form-group col-sm-6">
<label class="form-label" for="critical_product_stock">{{ __('Kritischer Bestand (rot)') }}</label>
{{ Form::number('critical_product_stock', old('critical_product_stock', $product->critical_product_stock), ['placeholder' => __('z. B. 10'), 'class' => 'form-control', 'id' => 'critical_product_stock', 'min' => 0, 'step' => 1]) }}
</div>
</div>
<hr>
<p class="text-muted small mb-2">{{ __('Optional: dieses Einzelprodukt einem Hauptprodukt zuordnen (z. B. „50 × 15 ml"). Der Produktbestand führt nur Haupt-/Einzelprodukte.') }}</p>
<div class="form-row">
<div class="form-group col-sm-8">
<label class="form-label" for="main_product_id">{{ __('Hauptprodukt') }}</label>
<select name="main_product_id" id="main_product_id" class="form-control">
<option value="">{{ __('— kein Hauptprodukt —') }}</option>
@foreach ($set_product_catalog as $mp)
<option value="{{ $mp->id }}" @selected((int) old('main_product_id', $product->main_product_id) === (int) $mp->id)>
{{ $mp->name }}@if ($mp->number) ({{ $mp->number }})@endif
</option>
@endforeach
</select>
</div>
<div class="form-group col-sm-4">
<label class="form-label" for="main_product_quantity">{{ __('Menge im Hauptprodukt') }}</label>
{{ Form::number('main_product_quantity', old('main_product_quantity', $product->main_product_quantity), ['placeholder' => __('z. B. 50'), 'class' => 'form-control', 'id' => 'main_product_quantity', 'min' => 1, 'step' => 1]) }}
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<hr>

View file

@ -4,87 +4,117 @@
<div class="card">
<h6 class="card-header">
{{__('Produkte')}}
{{ __('Produkte') }}
<label class="custom-control custom-checkbox float-right mb-0">
<input type="checkbox" class="custom-control-input show-active-products" name="show_active_products" @if(get_user_attr('show_active_products') === "true") checked @endif>
<input type="checkbox" class="custom-control-input show-active-products" name="show_active_products"
@if (get_user_attr('show_active_products') === 'true') checked @endif>
<span class="custom-control-label font-style-normal font-weight-normal">nur aktive anzeigen</span>
</label>
</h6>
<div class="card-datatable table-responsive">
<table class="datatables-product table table-striped table-bordered">
<thead>
<tr>
<th style="max-width: 60px;">&nbsp;</th>
<th>{{__('Pos')}}</th>
<th>{{__('Bild')}}</th>
<th>{{__('Name')}}</th>
<th>{{__('Artikelnummer')}}</th>
<th>{{__('Kategorie')}}</th>
<th>{{__('Preis')}}</th>
<th>{{__('Inhalt')}}</th>
<th>{{__('Einheit')}}</th>
<th>{{__('Grundpreis')}}</th>
<th>{{__('Gewicht')}}</th>
<th>{{__('sichbar')}}</th>
<th><div data-toggle="tooltip" title data-original-title="White Label">{{__('WL')}}</div></th>
<th><div data-toggle="tooltip" title data-original-title="Kompensationsprodukt">{{__('KP')}}</div></th>
<th><div data-toggle="tooltip" title data-original-title="Maximaler Kauf pro Berater">{{__('MK')}}</div></th>
<th><div data-toggle="tooltip" title data-original-title="Einzelrabatt">{{__('ER')}}</div></th>
<th><div data-toggle="tooltip" title data-original-title="Auswertung Absatzmengen ausschließen ">{{__('AA')}}</div></th>
<tr>
<th style="max-width: 60px;">&nbsp;</th>
<th>{{ __('Pos') }}</th>
<th>{{ __('Bild') }}</th>
<th>{{ __('Name') }}</th>
<th>{{ __('Artikelnummer') }}</th>
<th>{{ __('Kategorie') }}</th>
<th>{{ __('Preis') }}</th>
<th>{{ __('Inhalt') }}</th>
<th>{{ __('Einheit') }}</th>
<th>{{ __('Grundpreis') }}</th>
<th>{{ __('Gewicht') }}</th>
<th>{{ __('Verfügbarkeit') }}</th>
<th>{{ __('sichbar') }}</th>
<th>
<div data-toggle="tooltip" title data-original-title="White Label">{{ __('WL') }}</div>
</th>
<th>
<div data-toggle="tooltip" title data-original-title="Kompensationsprodukt">{{ __('KP') }}
</div>
</th>
<th>
<div data-toggle="tooltip" title data-original-title="Maximaler Kauf pro Berater">
{{ __('MK') }}</div>
</th>
<th>
<div data-toggle="tooltip" title data-original-title="Einzelrabatt">{{ __('ER') }}</div>
</th>
<th>
<div data-toggle="tooltip" title data-original-title="Auswertung Absatzmengen ausschließen ">
{{ __('AA') }}</div>
</th>
<th>{{__('Status')}}</th>
<th></th>
</tr>
<th>{{ __('Status') }}</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach($values as $value)
<tr>
<td>
<a href="{{route('admin_product_edit', [$value->id])}}" class="btn icon-btn btn-sm btn-primary">
<span class="far fa-edit"></span>
</a>
</td>
<td>{{ $value->pos }}</td>
<td>
@if(count($value->images))
<img class="img-fluid" alt="" style="max-height: 80px" src="{{ route('product_image', [$value->images->first()->slug]) }}">
@endif
</td>
<td>{{ $value->name }}</td>
<td>{{ $value->number }}</td>
<td>
@foreach($value->categories as $category)
<div style="white-space: nowrap">{{ $category->category->name }}</div>
@endforeach
</td>
<td>{{ $value->getFormattedPrice() }}</td>
<td>{{ $value->contents_total }}</td>
<td>{{ $value->getUnitType() }}</td>
<td>{{ $value->getBasePriceFormatted() }}</td>
<td>{{ $value->weight }}</td>
<td>{!! $value->getShowOnTypes('<br>') !!}</td>
<td data-sort="{{ $value->whitelabel }}">{!! get_active_badge($value->whitelabel, $value->whitelabel_name) !!}</td>
<td data-sort="{{ $value->shipping_addon }}">{!! get_active_badge($value->shipping_addon) !!}</td>
<td data-sort="{{ $value->max_buy }}">{!! get_active_badge($value->max_buy) !!}</td>
<td data-sort="{{ $value->single_commission }}">{!! get_active_badge($value->single_commission) !!}</td>
<td data-sort="{{ $value->exclude_stats_sales }}">{!! get_active_badge($value->exclude_stats_sales) !!}</td>
<td data-sort="{{ $value->active }}">{!! get_active_badge($value->active) !!}</td>
<td><a class="text-info" href="{{ route('admin_product_copy', [$value->id]) }}" onclick="return confirm('{{__('Eintrag kopieren?')}}');"><i class="far fa-copy"></i></a> &nbsp;
<a class="text-danger" href="{{ route('admin_product_delete', [$value->id]) }}" onclick="return confirm('{{__('Really delete entry?')}}');"><i class="far fa-trash-alt"></i></a></td>
</tr>
@endforeach
@foreach ($values as $value)
<tr>
<td>
<a href="{{ route('admin_product_edit', [$value->id]) }}"
class="btn icon-btn btn-sm btn-primary">
<span class="far fa-edit"></span>
</a>
</td>
<td>{{ $value->pos }}</td>
<td>
@if (count($value->images))
<img class="img-fluid" alt="" style="max-height: 80px"
src="{{ route('product_image', [$value->images->first()->slug]) }}">
@endif
</td>
<td>{{ $value->name }}</td>
<td>{{ $value->number }}</td>
<td>
@foreach ($value->categories as $category)
<div style="white-space: nowrap">{{ $category->category->name }}</div>
@endforeach
</td>
<td>{{ $value->getFormattedPrice() }}</td>
<td>{{ $value->contents_total }}</td>
<td>{{ $value->getUnitType() }}</td>
<td>{{ $value->getBasePriceFormatted() }}</td>
<td>{{ $value->weight }}</td>
<td data-sort="{{ $value->isOutOfStock() ? 1 : 0 }}">
@if ($value->isOutOfStock())
<span class="badge badge-danger"
style="white-space: normal;">{{ $value->outOfStockNotice() }}</span>
@else
<span class="text-muted small">{{ __('vorrätig') }}</span>
@endif
</td>
<td>{!! $value->getShowOnTypes('<br>') !!}</td>
<td data-sort="{{ $value->whitelabel }}">{!! get_active_badge($value->whitelabel, $value->whitelabel_name) !!}</td>
<td data-sort="{{ $value->shipping_addon }}">{!! get_active_badge($value->shipping_addon) !!}</td>
<td data-sort="{{ $value->max_buy }}">{!! get_active_badge($value->max_buy) !!}</td>
<td data-sort="{{ $value->single_commission }}">{!! get_active_badge($value->single_commission) !!}</td>
<td data-sort="{{ $value->exclude_stats_sales }}">{!! get_active_badge($value->exclude_stats_sales) !!}</td>
<td data-sort="{{ $value->active }}">{!! get_active_badge($value->active) !!}</td>
<td><a class="text-info" href="{{ route('admin_product_copy', [$value->id]) }}"
onclick="return confirm('{{ __('Eintrag kopieren?') }}');"><i
class="far fa-copy"></i></a> &nbsp;
<a class="text-danger" href="{{ route('admin_product_delete', [$value->id]) }}"
onclick="return confirm('{{ __('Really delete entry?') }}');"><i
class="far fa-trash-alt"></i></a>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="mt-4 ml-4">
<a href="{{route('admin_product_edit', ['new'])}}" class="btn btn-sm btn-primary">
{{__('Neues Produkt erstellen')}}
<a href="{{ route('admin_product_edit', ['new']) }}" class="btn btn-sm btn-primary">
{{ __('Neues Produkt erstellen') }}
</a>
</div>
</div>
</div>
<script>
$( document ).ready(function() {
$(document).ready(function() {
$('.datatables-product').dataTable({
"bLengthChange": false,
"iDisplayLength": 50,
@ -92,15 +122,13 @@
"url": "/js/German.json"
}
});
$('.show-active-products').on('change', function () {
$('.show-active-products').on('change', function() {
console.log($(this));
console.log($(this).prop('checked'));
document.location.search = $(this).attr('name')+"="+$(this).prop('checked');
document.location.search = $(this).attr('name') + "=" + $(this).prop('checked');
// window.location.reload(true);
})
});
</script>
@endsection

View file

@ -211,17 +211,70 @@
<li class="sidenav-header small font-weight-semibold">WARENWIRTSCHAFT</li>
@if (Auth::user()->isCopyReader())
<li class="sidenav-item @if (Request::is('admin/inventory/product-stock*')) open @endif">
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
<i class="sidenav-icon ion ion-md-cube"></i>
<div>{{ __('Produktbestand') }}
@if (($criticalProductCount ?? 0) > 0)
<span class="badge badge-danger ml-1">{{ $criticalProductCount }}</span>
@endif
</div>
</a>
<ul class="sidenav-menu">
<li class="sidenav-item{{ Request::is('admin/inventory/product-stock') ? ' active' : '' }}">
<a href="{{ route('admin.inventory.product-stock.index') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-list"></i>
<div>{{ __('Übersicht') }}</div>
</a>
</li>
<li class="sidenav-item{{ Request::is('admin/inventory/product-stock/history') ? ' active' : '' }}">
<a href="{{ route('admin.inventory.product-stock.history') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-time"></i>
<div>{{ __('Historie') }}</div>
</a>
</li>
</ul>
</li>
<li class="sidenav-item{{ Request::is('admin/inventory/raw-material-stock*') ? ' active' : '' }}">
<a href="{{ route('admin.inventory.raw-material-stock.index') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-beaker"></i>
<div>{{ __('Rohstoffbestand') }}</div>
@if (($criticalIngredientCount ?? 0) > 0)
<span class="badge badge-danger ml-1">{{ $criticalIngredientCount }}</span>
@endif
</a>
</li>
<li class="sidenav-item{{ Request::is('admin/inventory/stock-entries*') ? ' active' : '' }}">
<a href="{{ route('admin.inventory.stock-entries.index') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-download"></i>
<div>{{ __('Einkauf & Wareneingang') }}</div>
</a>
</li>
<li class="sidenav-item{{ Request::is('admin/inventory/productions*') ? ' active' : '' }}">
<a href="{{ route('admin.inventory.productions.index') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-construct"></i>
<li class="sidenav-item{{ Request::is('admin/inventory/stock-disposals*') ? ' active' : '' }}">
<a href="{{ route('admin.inventory.stock-disposals.index') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-trash"></i>
<div>{{ __('Ausgang / Ausschuss') }}</div>
</a>
</li>
<li class="sidenav-item @if (Request::is('admin/inventory/productions*', 'admin/inventory/product-development*')) open @endif">
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
<i class="sidenav-icon ion ion-md-construct"></i>
<div>{{ __('Produktion') }}</div>
</a>
<ul class="sidenav-menu">
<li class="sidenav-item{{ Request::is('admin/inventory/productions*') ? ' active' : '' }}">
<a href="{{ route('admin.inventory.productions.index') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-list"></i>
<div>{{ __('Produktionen') }}</div>
</a>
</li>
<li class="sidenav-item{{ Request::is('admin/inventory/product-development*') ? ' active' : '' }}">
<a href="{{ route('admin.inventory.product-development') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-flask"></i>
<div>{{ __('Produktentwicklung') }}</div>
</a>
</li>
</ul>
</li>
@endif
@if (Auth::user()->isAdmin())
@ -268,7 +321,8 @@
'admin/inventory/delivery-times*',
'admin/inventory/locations*',
'admin/inventory/material-qualities*',
'admin/inventory/packaging-materials*')) open @endif">
'admin/inventory/packaging-materials*',
'admin/inventory/notices*')) open @endif">
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
<i class="sidenav-icon ion ion-md-cog"></i>
<div>Einstellungen</div>
@ -300,6 +354,12 @@
<div>{{ __('Verpackungsmaterial') }}</div>
</a>
</li>
<li class="sidenav-item{{ Request::is('admin/inventory/notices*') ? ' active' : '' }}">
<a href="{{ route('admin.inventory.notices') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-information-circle"></i>
<div>{{ __('Hinweise') }}</div>
</a>
</li>
</ul>
</li>
@endif

View file

@ -1,72 +1,90 @@
@extends('layouts.layout-2')
@section('content')
<style>
.btn-md-extra {
padding: 0.3rem 0.6rem;
font-size: 0.8rem;
line-height: 1.5;
border-radius: 0.25rem;
}
.md-btn-extra {
width: calc(1.7rem + 2px) !important;
line-height: 1.5rem;
}
.form-control.input-extra {
padding: 0.28rem 0.6rem;
font-size: 0.8rem;
font-weight: 500;
min-height: calc(1.8rem + 2px);
height: calc(1.8rem + 2px);
width: 44px;
}
.input-group-min-w {
min-width: 102px;
}
.img-extra {
min-width:55px;
max-height: 160px;
}
@media (max-width: 767px) {
.default-style:not([dir=rtl]) div.card-datatable table.dataTable thead th:first-child,
.default-style:not([dir=rtl]) div.card-datatable table.dataTable tbody td:first-child,
.default-style:not([dir=rtl]) div.card-datatable table.dataTable tfoot th:first-child {
padding-left: 0.6rem !important;
<style>
.btn-md-extra {
padding: 0.3rem 0.6rem;
font-size: 0.8rem;
line-height: 1.5;
border-radius: 0.25rem;
}
.md-btn-extra {
width: calc(1.7rem + 2px) !important;
line-height: 1.5rem;
}
.form-control.input-extra {
padding: 0.28rem 0.6rem;
font-size: 0.8rem;
font-weight: 500;
min-height: calc(1.8rem + 2px);
height: calc(1.8rem + 2px);
width: 44px;
}
.input-group-min-w {
min-width: 102px;
}
.img-extra {
min-width:35px;
min-width: 55px;
max-height: 160px;
}
}
.product-stock-hint {
display: inline-block;
background-color: #c81031;
color: #fff;
font-weight: 700;
padding: 0.5rem 0.85rem;
border-radius: 0.25rem;
white-space: normal;
line-height: 1.3;
}
@media (max-width: 767px) {
.default-style:not([dir=rtl]) div.card-datatable table.dataTable thead th:first-child,
.default-style:not([dir=rtl]) div.card-datatable table.dataTable tbody td:first-child,
.default-style:not([dir=rtl]) div.card-datatable table.dataTable tfoot th:first-child {
padding-left: 0.6rem !important;
}
.img-extra {
min-width: 35px;
max-height: 160px;
}
}
</style>
</style>
@if($for === 'cr')
<h4 class="font-weight-bold py-2 mb-2">
{{ __('navigation.my_orders') }} / Mein Guthaben aufladen
<a href="{{ route('user_order_my_delivery', [$for, $delivery_id]) }}" class="btn btn-sm btn-default float-right">zurück</a>
<div class="clearfix"></div>
</h4>
@if ($for === 'cr')
<h4 class="font-weight-bold py-2 mb-2">
{{ __('navigation.my_orders') }} / Mein Guthaben aufladen
<a href="{{ route('user_order_my_delivery', [$for, $delivery_id]) }}"
class="btn btn-sm btn-default float-right">zurück</a>
<div class="clearfix"></div>
</h4>
@else
<h4 class="font-weight-bold py-2 mb-2">
{{ __('navigation.my_orders') }} / {{ __('navigation.do_order') }}
<a href="{{ route('user_order_my_delivery', [$for, $delivery_id]) }}" class="btn btn-sm btn-default float-right">zurück</a>
<div class="clearfix"></div>
</h4>
@if($user->user_level)
<p>Die Produktpreise werden entsprechend Deiner Rolle: <strong>{{$user->user_level->name}}</strong> angezeigt.<br>
<h4 class="font-weight-bold py-2 mb-2">
{{ __('navigation.my_orders') }} / {{ __('navigation.do_order') }}
<a href="{{ route('user_order_my_delivery', [$for, $delivery_id]) }}"
class="btn btn-sm btn-default float-right">zurück</a>
<div class="clearfix"></div>
</h4>
@if ($user->user_level)
<p>Die Produktpreise werden entsprechend Deiner Rolle: <strong>{{ $user->user_level->name }}</strong>
angezeigt.<br>
Hinweis: Wenn Du den Warenkorb verlässt, gehen alle Einstellungen verloren.</p>
@else
<p>Hinweis: Dir wurde noch keine Rolle zugewisen. Bitte wende dich an serivce@gruene-seele.bio</p>
@endif
@endif
@if($errors->has('switchers-comp-product'))
@if ($errors->has('switchers-comp-product'))
<div class="row">
<div class="col-sm-12">
<div class="alert alert-danger" id="gotocomp">
@ -86,46 +104,50 @@
<div class="row">
<div class="col-md-3 mb-3">
<div class="text-muted small">{{ __('payment.ordering_country') }}</div>
{{ App\Services\UserService::getOrderInfo('billing_state') }}
{{ App\Services\UserService::getOrderInfo('billing_state') }}
</div>
<div class="col-md-3 mb-3">
<div class="text-muted small">{{ __('payment.country_of_delivery') }}</div>
{{ App\Services\UserService::getOrderInfo('shipping_state') }}
{{ App\Services\UserService::getOrderInfo('shipping_state') }}
</div>
<div class="col-md-3 mb-3">
<div class="text-muted small">{{ __('payment.VAT') }}</div>
{{ App\Services\UserService::getOrderInfo('tax_free') }}
{{ App\Services\UserService::getOrderInfo('tax_free') }}
</div>
<div class="col-md-3 mb-3">
<div class="text-muted small">{{ __('payment.reverse_charge_procedure') }}</div>
{{ App\Services\UserService::getOrderInfo('user_reverse_charge') }}
{{ App\Services\UserService::getOrderInfo('user_reverse_charge') }}
</div>
</div>
<i>{!! __('order.delivery_country_changed_info', ['link'=> route('user_edit')]) !!}</i>
<hr>
@if($user->user_level)
<p>{!! __('order.product_prices_career_level_info', ['user_level_name'=>$user->user_level->getLang('name'), 'user_level_margin'=>$user->user_level->getFormattedMargin()]) !!}</p>
@else
<p>{{ __('order.no_career_level_info') }}</p>
@endif
<i>{!! __('order.delivery_country_changed_info', ['link' => route('user_edit')]) !!}</i>
<hr>
@if ($user->user_level)
<p>{!! __('order.product_prices_career_level_info', [
'user_level_name' => $user->user_level->getLang('name'),
'user_level_margin' => $user->user_level->getFormattedMargin(),
]) !!}</p>
@else
<p>{{ __('order.no_career_level_info') }}</p>
@endif
</div>
</div>
<div class="card">
<div class="card-datatable table-responsive">
<table class="datatables-order-list table table-striped table-bordered" id="datatables-order-list" data-url="{{route('user_order_my_perform_request')}}">
<table class="datatables-order-list table table-striped table-bordered" id="datatables-order-list"
data-url="{{ route('user_order_my_perform_request') }}">
<thead>
<tr>
<th>{{__('Bild')}}</th>
<th>{{__('Produkt')}}</th>
<th>{{__('Kategorie')}}</th>
<th>{{__('Preis netto')}}</th>
<th>{{__('Preis brutto')}}</th>
<th>{{__('Provision')}}</th>
<th>{{__('Gewicht')}}</th>
<th><span class="no-line-break">{{__('Inhalt (ml)')}}</span></th>
<th>{{__('Artikelnummer')}}</th>
</tr>
<tr>
<th>{{ __('Bild') }}</th>
<th>{{ __('Produkt') }}</th>
<th>{{ __('Kategorie') }}</th>
<th>{{ __('Preis netto') }}</th>
<th>{{ __('Preis brutto') }}</th>
<th>{{ __('Provision') }}</th>
<th>{{ __('Gewicht') }}</th>
<th><span class="no-line-break">{{ __('Inhalt (ml)') }}</span></th>
<th>{{ __('Artikelnummer') }}</th>
</tr>
</thead>
<tbody>
</tbody>
@ -134,30 +156,29 @@
</div>
{!! Form::open(['action' => route('user_order_my_payment', [$for, $delivery_id]), 'class' => 'form-horizontal']) !!}
<input type="hidden" name="shipping_is_for" value="{{$for}}">
<input type="hidden" name="shipping_is_for" value="{{ $for }}">
@if($for === 'cr')
@if ($for === 'cr')
@include('user.order.shipping_credit')
@else
<div class="card mt-4">
<div class="card-body">
@if($for === 'ot')
<h4>Lieferland des Kunden</h4>
@include('user.order.shipping_ot')
@endif
@if($for === 'me' || $for === 'mp')
<h4>Mein Lieferland</h4>
@include('user.order.shipping_me')
@endif
@if ($for === 'ot')
<h4>Lieferland des Kunden</h4>
@include('user.order.shipping_ot')
@endif
@if ($for === 'me' || $for === 'mp')
<h4>Mein Lieferland</h4>
@include('user.order.shipping_me')
@endif
</div>
</div>
@endif
@if($comp_products)
@if ($comp_products)
<div id="holder_html_view_comp_product">
@include('user.order.comp_product')
</div>
@endif
<div class="card mt-4">
@ -169,13 +190,14 @@
</div>
</div>
<div class="mt-2">
<a href="{{ route('user_order_my_delivery', [$for, $delivery_id]) }}" class="btn btn-sm btn-default float-right">zurück</a>
<a href="{{ route('user_order_my_delivery', [$for, $delivery_id]) }}"
class="btn btn-sm btn-default float-right">zurück</a>
</div>
{!! Form::close() !!}
<script>
$( document ).ready(function() {
$(document).ready(function() {
var iqShoppingCart = IqShoppingCart.init();
@ -183,23 +205,61 @@
"processing": true,
"serverSide": true,
ajax: {
url: '{!! route( 'user_order_my_datatable') !!}',
url: '{!! route('user_order_my_datatable') !!}',
data: function(d) {
d.shipping_is_for = $('input[name=shipping_is_for]').val();
// d.filter_customer_member = $('select[name=filter_customer_member]').val();
// d.filter_customer_member = $('select[name=filter_customer_member]').val();
}
},
"order": [[8, "asc" ]],
"columns": [
{ data: 'picture', name: 'picture', searchable: false, width: 35 },
{ data: 'product', name: 'product' },
{ data: 'category', name: 'category', orderable: true },
{ data: 'price_net', name: 'price_net', searchable: false, orderable: false },
{ data: 'price_gross', name: 'price_gross', searchable: false, orderable: false },
{ data: 'single_commission', name: 'single_commission', searchable: false },
{ data: 'weight', name: 'weight', searchable: false },
{ data: 'contents_total', name: 'contents_total', searchable: false },
{ data: 'number', name: 'number' },
"order": [
[8, "asc"]
],
"columns": [{
data: 'picture',
name: 'picture',
searchable: false,
width: 35
},
{
data: 'product',
name: 'product'
},
{
data: 'category',
name: 'category',
orderable: true
},
{
data: 'price_net',
name: 'price_net',
searchable: false,
orderable: false
},
{
data: 'price_gross',
name: 'price_gross',
searchable: false,
orderable: false
},
{
data: 'single_commission',
name: 'single_commission',
searchable: false
},
{
data: 'weight',
name: 'weight',
searchable: false
},
{
data: 'contents_total',
name: 'contents_total',
searchable: false
},
{
data: 'number',
name: 'number'
},
],
"bLengthChange": false,
"iDisplayLength": 1000,
@ -207,7 +267,7 @@
"language": {
"url": "/js/German.json"
},
drawCallback: function (settings) {
drawCallback: function(settings) {
iqShoppingCart.reInit();
}
});

View file

@ -55,7 +55,11 @@
<div class="product-item-price mt-2 mt-2 pb-3">
{{ $product->getFormattedPrice() }} &euro;*
<br><span class="small text-muted">@if($product->unit) {{ $product->getBasePriceFormattedFull() }} &euro; @else &nbsp; @endif</span>
@if ($product->isOutOfStock())
<div class="small text-warning font-weight-bold mt-1">
<i class="fa fa-clock-o"></i> {{ $product->outOfStockNotice() }}
</div>
@endif
</div>
</div>
</div>

View file

@ -16,6 +16,11 @@
<div class="media-body py-4 px-3 px-md-4">
{!! $product->copy !!}
@if ($product->isOutOfStock())
<div class="alert alert-warning py-2 my-3">
<i class="fa fa-clock-o"></i> <strong>{{ $product->outOfStockNotice() }}</strong>
</div>
@endif
<table class="table table-striped my-4">
<tbody>
<tr>