Warenwirtschaft: AP-40 + AP-41 — Bestand/Differenz-Spalten, Ablage, Verpackung oben, Phasen-Gramm

AP-40: INCI-Tabelle der Produktion um zwei Spalten ergänzt — „Bestand" (aktueller
Rohstoffbestand je INCI, global) und „Differenz" (Bestand − Soll; negativ rot =
Fehlmenge, sonst grün). Spaltenkopf „Regal" → „Ablage". Bestand kommt aus
InventoryService::remainingByIngredient und wird als current_stock in den
recipeJson-Payload gehängt (lazy aufgelöst — InventoryService hängt selbst von
ProductionService ab, Konstruktor-Injektion wäre zirkulär).

AP-41: „Verpackung (Vorschau)" direkt unter die INCI-Tabelle verschoben (man sieht
auf einen Blick den Gesamtbedarf). Je Phase die Gesamt-Gramm hinter dem Namen
(„Phase A (345 g)"), aktualisiert sich mit Stückzahl und Füllgewicht-Skala.

Test für den current_stock-Payload ergänzt. Suite 311 grün.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-08-28 08:27:43 +00:00
parent 744b36cdcb
commit 4c6a57b0f1
4 changed files with 76 additions and 12 deletions

View file

@ -77,7 +77,9 @@
<th>{{ __('Qualität') }}</th>
<th class="text-right">{{ __('Anteil %') }}</th>
<th class="text-right">{{ __('Gramm (gesamt)') }}</th>
<th>{{ __('Regal') }}</th>
<th>{{ __('Ablage') }}</th>
<th class="text-right">{{ __('Bestand') }}</th>
<th class="text-right">{{ __('Differenz') }}</th>
</tr>
</thead>
<tbody id="recipe-inci-body"></tbody>
@ -86,20 +88,22 @@
</div>
</div>
{{-- AP-41: Verpackungs-Vorschau direkt unter die INCI-Tabelle — man sieht auf
einen Blick, was zusätzlich zu den Rohstoffen gebraucht wird. --}}
<h6>{{ __('Verpackung (Vorschau)') }}</h6>
<div class="table-responsive border rounded mb-3">
<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>
<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">

View file

@ -134,6 +134,24 @@
});
renderInciTable();
updateAllStatuses();
updatePhaseTotals();
}
// AP-41: Summe der Soll-Gramm je Phase im Phasenkopf aktualisieren.
function updatePhaseTotals() {
var qty = parseInt($('#quantity').val(), 10) || 1;
$('.recipe-phase-block').each(function () {
var sum = 0;
var any = false;
$(this).find('[data-ingredient-id]').each(function () {
var ing = $(this).data('recipe-ing');
if (ing && ing.gram != null) {
sum += ing.gram * recipeGramScale * (ing.factor || 1) * qty;
any = true;
}
});
$(this).find('.recipe-phase-total').first().text(any ? ' (' + fmt(sum) + ' g)' : '');
});
}
// AP-28: INCI-Liste der Hersteller-Rezeptur (Alternativname, Qualität, Anteil, Gramm, Regal).
@ -155,20 +173,30 @@
if (grams != null) {
totalGrams += grams;
}
$body.append($('<tr></tr>')
var $row = $('<tr></tr>')
.append($('<td></td>').text(ing.name))
.append($('<td></td>').text(ing.alt_name || '—'))
.append($('<td></td>').text(ing.quality || '—'))
.append($('<td class="text-right"></td>').text(ing.gram != null ? fmt(ing.gram) : '—'))
.append($('<td class="text-right"></td>').text(grams != null ? fmt(grams) : '—'))
.append($('<td></td>').text(ing.storage_label || '—')));
.append($('<td></td>').text(ing.storage_label || '—'))
.append($('<td class="text-right"></td>').text(ing.current_stock != null ? fmt(ing.current_stock) + ' g' : '—'));
// AP-40: Differenz = Bestand − Soll (negativ rot = Fehlmenge, sonst grün).
var $diff = $('<td class="text-right"></td>');
if (grams != null && ing.current_stock != null) {
var d = ing.current_stock - grams;
$diff.text((d >= 0 ? '+' : '') + fmt(d) + ' g').css('color', d < 0 ? '#dc3545' : '#28a745');
} else {
$diff.text('—');
}
$body.append($row.append($diff));
});
$foot.append($('<tr class="font-weight-bold"></tr>')
.append($('<td></td>').text('{{ __('Summe') }}'))
.append('<td></td><td></td>')
.append($('<td class="text-right"></td>').text(fmt(totalPercent) + ' %'))
.append($('<td class="text-right"></td>').text(fmt(totalGrams) + ' g'))
.append('<td></td>'));
.append('<td></td><td></td><td></td>'));
$('#recipe-inci').show();
}
@ -281,6 +309,9 @@
var $block = $('<div class="border rounded p-2 mb-3 recipe-phase-block"></div>');
var $header = $('<div class="mb-2"></div>');
$header.append($('<strong></strong>').text(group.name));
// AP-41: Gesamtgramm je Phase hinter dem Namen — zeigt, wie groß
// das Rührgefäß für diese Phase sein muss.
$header.append($('<span class="recipe-phase-total text-muted"></span>'));
if (group.note) {
$header.append($('<div class="text-muted small"></div>').text(group.note));
}
@ -292,6 +323,7 @@
renderIngredientWrap(ing, $target);
});
});
updatePhaseTotals();
if (hintParts.length) {
$('#recipe-hint').text(hintParts.join(' · ')).removeClass('text-muted').addClass('text-danger');