Warenwirtschaft: AP-44 — Rezeptur-Darstellung in der Produktion aufhübschen

Reine Optik der Chargen-Erfassung (Produktions-Maske):
(a) Phasenkasten leicht hinterlegt (~3,5 %); der innere Kasten um die
    Chargen-Dropdowns entfällt, stattdessen dezente Trennlinie je Rohstoff.
(b) Schriftgrößen vereinheitlicht und schwarz (wie die INCI-Liste) —
    Rohstoffname, Soll-Label und Phasenname einheitlich.
(c) iPad/Touch: Eingabefelder (Charge/Menge) mind. 44 px hoch und 16 px
    Schrift (verhindert iOS-Auto-Zoom) über @media (pointer: coarse).
(d) Phasenbeschreibung formatiert dargestellt wie auf Produktebene eingegeben
    (Zeilenumbrüche erhalten via white-space: pre-wrap; weiterhin .text(),
    also kein HTML-Injection-Risiko).

Umsetzung in _form_fields.blade (scoped <style>) + _scripts.blade
(recipe-item statt border-Kasten, recipe-soll-label, recipe-phase-note).
Test: Render-Smoke der Neu-Maske prüft Buttons (AP-43) + Optik-Hooks (AP-44).
Suite 337 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-08-28 10:01:51 +00:00
parent abd202a02e
commit 2a6daf2e30
3 changed files with 75 additions and 5 deletions

View file

@ -14,6 +14,55 @@
: [];
}
@endphp
{{-- AP-44: Rezeptur-Darstellung — Phasenkasten leicht hinterlegt, kein innerer
Kasten um die Chargen, einheitliche schwarze Schrift wie in der INCI-Liste,
iPad-taugliche (große) Eingabefelder, formatierte Phasenbeschreibung. --}}
<style>
/* (a) Phasenkasten leicht hinterlegt */
#recipe-ingredient-lines .recipe-phase-block {
background-color: rgba(0, 0, 0, .035);
border-color: rgba(0, 0, 0, .075) !important;
}
/* (a) kein innerer Kasten mehr um die Chargen-Zeilen, nur dezenter Abstand */
#recipe-ingredient-lines .recipe-item {
padding: .25rem 0 .5rem;
border-bottom: 1px solid rgba(0, 0, 0, .06);
}
#recipe-ingredient-lines .recipe-item:last-child {
border-bottom: 0;
}
/* (b) Schriftgrößen vereinheitlichen + schwarz (wie INCI-Liste) */
#recipe-ingredient-lines .recipe-item-head strong,
#recipe-ingredient-lines .recipe-soll-label,
#recipe-ingredient-lines .recipe-phase-block > .mb-2 > strong {
font-size: .9375rem;
color: #212529;
font-weight: 600;
}
#recipe-ingredient-lines .recipe-soll-label {
font-weight: 400;
}
/* (d) Phasenbeschreibung wie auf Produktebene eingegeben (Umbrüche erhalten) */
#recipe-ingredient-lines .recipe-phase-note {
white-space: pre-wrap;
color: #212529;
font-size: .9375rem;
}
/* (c) iPad/Touch: Eingabefelder größer und ohne Auto-Zoom (font-size >= 16px) */
@media (pointer: coarse) {
#recipe-ingredient-lines .form-control,
#recipe-ingredient-lines select,
#recipe-ingredient-lines .recipe-qty-input {
min-height: 44px;
font-size: 16px;
}
#recipe-ingredient-lines .input-group-text {
font-size: 16px;
}
}
</style>
<div class="form-row">
<div class="form-group col-12 col-md-6">
<label for="product_id">{{ __('Produkt') }}</label>

View file

@ -237,15 +237,17 @@
function renderIngredientWrap(ing, $container) {
var qty = parseInt($('#quantity').val(), 10) || 1;
var soll = ing.gram * recipeGramScale * (ing.factor || 1) * qty;
var $wrap = $('<div class="border rounded p-2 mb-2" data-ingredient-id="' + ing.id + '"></div>');
// AP-44 (a): kein innerer Kasten mehr um die Chargen-Zeilen.
var $wrap = $('<div class="recipe-item 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>');
var $head = $('<div class="d-flex justify-content-between flex-wrap recipe-item-head"></div>');
var $title = $('<strong></strong>').text(ing.name);
if (ing.alt_name) {
$title.append($('<span class="text-muted font-weight-normal"></span>').text(' · ' + ing.alt_name));
// AP-44 (b): Alternativname mit vereinheitlicht (schwarz), nur normal statt fett.
$title.append($('<span class="font-weight-normal"></span>').text(' · ' + ing.alt_name));
}
$head.append($title);
$head.append($('<span class="text-muted small"></span>')
$head.append($('<span class="recipe-soll-label"></span>')
.html('{{ __('Soll') }}: <span class="recipe-soll">' + fmt(soll) + '</span> g <span class="recipe-status" style="font-size:1rem;">●</span>'));
$wrap.append($head);
$wrap.append('<div class="recipe-rows mt-1"></div>');
@ -319,7 +321,9 @@
// 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));
// AP-44 (d): Phasenbeschreibung formatiert (Zeilenumbrüche erhalten,
// wie auf Produktebene eingegeben) — .text() + CSS white-space:pre-wrap.
$header.append($('<div class="recipe-phase-note"></div>').text(group.note));
}
$block.append($header);
$('#recipe-ingredient-lines').append($block);

View file

@ -169,6 +169,23 @@ test('AP-43: abgeschlossene produktion kann nicht zum entwurf zurück', function
))->toThrow(ValidationException::class);
});
test('AP-43/44: neue-produktion-maske zeigt abschließen + zwischenspeichern und die aufgehübschte rezeptur-optik', function () {
$user = draftUser();
Location::factory()->create();
$this->actingAs($user, 'user');
$this->get(route('admin.inventory.productions.create'))
->assertOk()
// AP-43: umbenannter Abschluss-Button + Entwurf-Button
->assertSee('Produktion abschließen')
->assertSee('Zwischenspeichern (Entwurf)')
->assertSee('name="save_mode"', false)
// AP-44: Optik-Hooks (Phasen-Beschreibung formatiert, kein innerer Kasten, iPad-Felder)
->assertSee('recipe-phase-note', false)
->assertSee('recipe-item', false)
->assertSee('pointer: coarse', false);
});
test('AP-43: HTTP zwischenspeichern ohne ergebnis speichert entwurf; abschließen erzwingt ergebnis', function () {
$user = draftUser();
$location = Location::factory()->create();