Warenwirtschaft: F-07 — Verpackungsvorschau aus dem Produktionsergebnis

Stückliste je Ergebnisprodukt wird ins Formular gegeben; die Vorschau rechnet
Outputs × Stückliste (wie die Buchung) und reagiert auf Hinzufügen/Entfernen,
Produkt- und Mengenwechsel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-08 10:59:48 +00:00
parent 122d28bc4f
commit fd1572df35
3 changed files with 49 additions and 14 deletions

View file

@ -285,12 +285,23 @@ class ProductionController extends Controller
$existingOutputIds = $production?->outputs?->pluck('product_id')->map(fn ($id) => (int) $id)->all() ?? [];
$outputProducts = Product::query()
->with('packagings:id,name')
->where(fn ($q) => $q->where('active', 1)->where('is_set', false))
->when($existingOutputIds !== [], fn ($q) => $q->orWhereIn('id', $existingOutputIds))
->orderBy('name')
->get($columns);
return ['products' => $products, 'outputProducts' => $outputProducts];
// F-07: Verpackungs-Stückliste je Ergebnisprodukt für die Vorschau im Browser —
// dieselbe Basis wie die Buchung (syncPackagingSnapshot: Outputs × Stückliste).
$packagingBom = $outputProducts
->mapWithKeys(fn (Product $p) => [(int) $p->id => $p->packagings->map(fn ($pk) => [
'id' => (int) $pk->id,
'name' => $pk->name,
'per_unit' => (float) ($pk->pivot->quantity ?? 1),
])->values()->all()])
->all();
return ['products' => $products, 'outputProducts' => $outputProducts, 'packagingBom' => $packagingBom];
}
/**

View file

@ -139,7 +139,7 @@
{{-- 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>
<h6>{{ __('Verpackung (Vorschau)') }} <small class="text-muted font-weight-normal">{{ __('aus dem Produktionsergebnis') }}</small></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>
@ -175,7 +175,7 @@
<div class="col-sm-3">{{ __('Chargen-Nr.') }}</div>
<div class="col-sm-1"></div>
</div>
<div id="production-outputs" data-initial='@json($initialOutputs)'></div>
<div id="production-outputs" data-initial='@json($initialOutputs)' data-packaging-bom='@json(($packagingBom ?? []) === [] ? (object) [] : $packagingBom)'></div>
<button type="button" class="btn btn-sm btn-outline-secondary mb-3" id="btn-add-output">{{ __('+ weitere Ergebniszeile') }}</button>
<script type="text/template" id="output-row-template">

View file

@ -301,12 +301,7 @@
$('#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)));
});
renderPackagingPreview();
return;
}
@ -365,12 +360,35 @@
.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)));
renderPackagingPreview();
}
// F-07: Verpackungs-Vorschau aus dem Produktionsergebnis (Outputs × Stückliste),
// dieselbe Basis wie die Buchung beim Abschluss — nicht aus Planungsprodukt/-menge.
var packagingBom = $('#production-outputs').data('packaging-bom') || {};
function renderPackagingPreview() {
var totals = {};
var order = [];
$('#production-outputs .output-row').each(function () {
var pid = String($(this).find('.output-product').val() || '');
var qty = parseInt($(this).find('.output-quantity').val(), 10) || 0;
(packagingBom[pid] || []).forEach(function (pk) {
if (!totals[pk.id]) {
totals[pk.id] = {name: pk.name, pieces: 0};
order.push(pk.id);
}
totals[pk.id].pieces += pk.per_unit * qty;
});
});
var $pk = $('#recipe-packaging-preview').empty();
order.forEach(function (id) {
$pk.append($('<tr></tr>')
.append($('<td></td>').text(totals[id].name))
.append($('<td></td>').text(Math.round(totals[id].pieces))));
});
if (!order.length) {
$pk.append('<tr><td colspan="2" class="text-muted">{{ __('Keine Verpackung hinterlegt.') }}</td></tr>');
}
}
$(document).on('click', '.btn-add-split', function (e) {
@ -482,6 +500,7 @@
if (!data || !data.batch_number) {
refreshBatch($row);
}
renderPackagingPreview();
}
$('#btn-add-output').on('click', function (e) {
@ -495,16 +514,19 @@
return;
}
$(this).closest('.output-row').remove();
renderPackagingPreview();
});
$(document).on('change', '.output-product', function () {
var $row = $(this).closest('.output-row');
$row.data('auto', false);
refreshBatch($row);
renderPackagingPreview();
});
$(document).on('input change', '.output-quantity', function () {
$(this).closest('.output-row').data('auto', false);
renderPackagingPreview();
});
$(document).on('input', '.output-batch', function () {
@ -521,6 +543,7 @@
refreshBatch($row);
}
});
renderPackagingPreview();
});
$('#quantity').on('change input', function () {
@ -531,6 +554,7 @@
$row.find('.output-quantity').val(val || 1);
}
});
renderPackagingPreview();
});
$('#produced_at').on('change', function () {