gruene-seele/resources/views/admin/inventory/stock-entries/_scripts.blade.php
Kevin Adametz be1d8fdc39 Warenwirtschaft: Einheit (g/ml) als Pflichtfeld über der bestellten Menge
Die Einheit hing bisher als Anhang rechts am Mengenfeld — auf breiten
Monitoren außer Sicht, und „g" war stiller Default. Jetzt:

- Eigenes Pflichtfeld „Einheit der bestellten Menge" direkt VOR der Menge
  mit „Bitte wählen"; beim Neuanlegen muss bewusst Gramm oder Milliliter
  gewählt werden, beim Bearbeiten ist die gespeicherte Einheit vorbelegt.
- Hinweis unter der Einheit sagt, ob ml möglich ist (Dichte hinterlegt,
  Wert wird genannt) oder erst gepflegt werden muss; ml bleibt ohne Dichte
  gesperrt. Mengen-Hinweis und ml→g-Live-Vorschau folgen der Auswahl.
- Server: Store-/UpdateStockEntryRequest weisen Rohstoff-Einkäufe ohne
  Einheit ab (Verpackung bleibt ohne Einheit). Bestehende Tests ergänzt,
  neuer Test für die Pflicht.
- In-App-Doku (Hinweise, Testleitfaden) nachgezogen.

Suite: 390 grün.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 14:09:34 +00:00

201 lines
8.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script>
(function ($) {
function toggleBlocks() {
var t = $('#entry_type').val();
var isIng = t === 'ingredient';
$('#stock-entry-ingredient-block').toggle(isIng);
$('#stock-entry-quality-block').toggle(isIng);
$('#stock-entry-packaging-block').toggle(!isIng);
$('#price-per-kg-block').toggle(isIng);
$('#price-total-block').toggle(!isIng);
}
function initIngredientSelect2() {
var $el = $('#ingredient_id');
if ($el.data('select2')) {
$el.select2('destroy');
}
$el.select2({
theme: 'default',
width: '100%',
placeholder: '{{ __('Inhaltsstoff suchen…') }}',
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 initPackagingSelect2() {
var $el = $('#packaging_item_id');
if ($el.data('select2')) {
$el.select2('destroy');
}
$el.select2({
theme: 'default',
width: '100%',
placeholder: '{{ __('Verpackungsartikel suchen…') }}',
allowClear: true,
ajax: {
url: $el.data('search-url'),
dataType: 'json',
delay: 250,
data: function (params) {
return {
q: params.term || '',
entry_type: $('#entry_type').val()
};
},
processResults: function (data) {
return {results: data.results || []};
},
cache: true
},
minimumInputLength: 0
});
}
// AP-42: aktuelle Dichte des gewählten Rohstoffs (aus dem AJAX-Ergebnis oder
// — bei vorbelegtem Rohstoff — aus der data-density-Option).
function currentDensity() {
var sel = $('#ingredient_id').select2('data');
if (sel && sel[0] && sel[0].density != null && sel[0].density !== '') {
return parseFloat(sel[0].density);
}
var d = $('#ingredient_id option:selected').data('density');
return (d != null && d !== '') ? parseFloat(d) : null;
}
function applyOrderedUnit() {
var isIng = $('#entry_type').val() === 'ingredient';
var $unit = $('#ordered_unit');
// Einheit ist eigenes Pflichtfeld vor der Menge — nur bei Rohstoffen sichtbar
// und nur dann required (ein verstecktes required-Feld blockiert das Absenden).
$('#ordered-unit-wrap').toggle(isIng);
$unit.prop('required', isIng);
var density = isIng ? currentDensity() : null;
var hasDensity = density != null && density > 0;
$unit.find('option[value="ml"]').prop('disabled', !hasDensity);
if (!hasDensity && $unit.val() === 'ml') {
$unit.val('');
}
var $unitHint = $('#ordered-unit-hint');
var $hint = $('#ordered-quantity-hint');
var $preview = $('#ordered-quantity-preview');
$preview.hide().text('');
if (!isIng) {
$hint.text('{{ __('Bei Verpackung in Stück.') }}');
return;
}
if (!$('#ingredient_id').val()) {
$unitHint.text('{{ __('Zuerst den Inhaltsstoff wählen — davon hängt ab, ob ml möglich ist.') }}');
} else if (hasDensity) {
$unitHint.text('{{ __('Dichte hinterlegt') }} (' + formatDensity(density) + ' g/ml) — {{ __('Erfassung in Gramm oder Milliliter möglich.') }}');
} else {
$unitHint.text('{{ __('Keine Dichte am Rohstoff hinterlegt — nur Gramm möglich. Für ml zuerst die Dichte pflegen.') }}');
}
var unit = $unit.val();
if (unit === 'ml' && hasDensity) {
$hint.text('{{ __('Erfassung in ml — der Bestand wird über die Dichte in Gramm gebucht.') }}');
var ml = parseNumber($('#ordered_quantity').val());
if (ml != null && ml > 0) {
// AP-42: Live-Vorschau der ml→g-Umrechnung direkt am Mengenfeld.
$preview.text(formatQuantity(ml) + ' ml × ' + formatDensity(density) + ' g/ml = ' + formatQuantity(ml * density) + ' g').show();
}
} else if (unit === 'gram') {
$hint.text('{{ __('Erfassung in Gramm.') }}');
} else {
$hint.text('{{ __('Bitte zuerst oben die Einheit wählen.') }}');
}
}
function parseNumber(value) {
if (value === null || value === undefined) {
return null;
}
// AP-53: gleiche Regel wie reFormatWawiNumber — Punkt mit 1–2 Stellen oder "0.xxx" = Komma.
var raw = String(value).trim();
var normalized = /^-?(\d+\.\d{1,2}|0\.\d{3})$/.test(raw) ? raw : raw.replace(/\./g, '').replace(',', '.');
if (normalized === '' || isNaN(normalized)) {
return null;
}
return parseFloat(normalized);
}
// Prüfung AP-53 (4.3): Mengen (3 Stellen), Preise (2 Stellen) und Dichte (4 Stellen) getrennt formatieren.
function formatQuantity(value) {
return Number(value).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 3, useGrouping: false});
}
function formatPrice(value) {
return Number(value).toFixed(2).replace('.', ',');
}
function formatDensity(value) {
return Number(value).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 4, useGrouping: false});
}
function currentFactor() {
var percent = parseFloat($('#tax_rate_id option:selected').data('percent')) || 0;
return 1 + percent / 100;
}
function recalcFromNet() {
var net = parseNumber($('#price_per_kg').val());
if (net === null) {
return;
}
$('#price_per_kg_gross').val(formatPrice(net * currentFactor()));
}
function recalcFromGross() {
var gross = parseNumber($('#price_per_kg_gross').val());
if (gross === null) {
return;
}
var factor = currentFactor();
$('#price_per_kg').val(formatPrice(factor > 0 ? gross / factor : gross));
}
$(document).ready(function () {
toggleBlocks();
initIngredientSelect2();
initPackagingSelect2();
applyOrderedUnit();
$('#entry_type').on('change', function () {
toggleBlocks();
applyOrderedUnit();
$('#packaging_item_id').val(null).trigger('change');
initPackagingSelect2();
});
$('#ingredient_id').on('change', applyOrderedUnit);
$('#ordered_unit').on('change', applyOrderedUnit);
$('#ordered_quantity').on('input', applyOrderedUnit);
$('#price_per_kg').on('input', recalcFromNet);
$('#price_per_kg_gross').on('input', recalcFromGross);
$('#tax_rate_id').on('change', function () {
if (parseNumber($('#price_per_kg').val()) !== null) {
recalcFromNet();
} else {
recalcFromGross();
}
});
});
})(jQuery);
</script>