191 lines
10 KiB
PHP
191 lines
10 KiB
PHP
@extends('layouts.layout-2')
|
|
|
|
@section('content')
|
|
<h4 class="font-weight-bold py-2 mb-2">{{ __('Neue Produktion') }}</h4>
|
|
|
|
<div class="card">
|
|
<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>
|
|
|
|
<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>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
@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>
|
|
@endsection
|