Warenwirtschaft: AP-36 — Rezeptur-Textphasen ohne Rohstoffe anzeigen

Phasen ohne Rohstoffzeile (reine Hinweisphasen wie „Vorbereitung"/„Nachbereitung")
verschwanden in der Produktionsmaske, weil buildPhaseGroups() leere Gruppen
wegfilterte. Filter so angepasst, dass benannte Phasen erhalten bleiben und nur
der namenlose „lose" Sammeltopf verworfen wird. Die Phasen kommen serverseitig
bereits in pos-Reihenfolge (recipePhases orderBy pos), sodass „Vorbereitung"
korrekt über „Phase A" steht. Test sichert den recipeJson-Datenvertrag ab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-08-27 13:59:51 +00:00
parent 00be848604
commit d60947c34d
2 changed files with 55 additions and 1 deletions

View file

@ -181,7 +181,10 @@
if (loose.ingredients.length) {
groups.push(loose);
}
return groups.filter(function (g) { return g.ingredients.length > 0; });
// AP-36: benannte Phasen ohne Rohstoffe (reine Text-/Hinweisphasen wie
// „Vorbereitung"/„Nachbereitung") bleiben erhalten — nur der namenlose
// „lose" Sammeltopf wird verworfen, wenn er leer ist.
return groups.filter(function (g) { return g.ingredients.length > 0 || g.name != null; });
}
function renderIngredientWrap(ing, $container) {

View file

@ -1,10 +1,12 @@
<?php
use App\Models\Ingredient;
use App\Models\Location;
use App\Models\Product;
use App\Models\RecipePhase;
use App\Repositories\ProductRepository;
use App\Services\ProductionService;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
@ -190,3 +192,52 @@ test('produktion rechnet unveraendert ueber alle phasen', function () {
expect($required[$a->id])->toBe(300.0)
->and($required[$b->id])->toBe(200.0);
});
test('AP-36: recipeJson liefert auch phasen ohne rohstoffe (textphasen)', function () {
// Die eigentliche Anzeige-Korrektur sitzt im JS (buildPhaseGroups rendert leere
// benannte Phasen). Dieser Test sichert den Datenvertrag ab, auf den das JS baut:
// eine Phase ohne Rohstoffzeile muss im recipeJson-Payload erhalten bleiben.
$product = ap23MakeProduct('AP36-Textphase');
$a = ap23MakeIngredient('AP36-Sheabutter');
ap23SaveManufacturerRecipe($product, [
'mfg_phase_name' => ['Vorbereitung', 'Phase A'],
'mfg_phase_note' => ['Arbeitsplatz desinfizieren', ''],
// nur die zweite Phase trägt einen Rohstoff — „Vorbereitung" bleibt leer
'mfg_ingredient_id' => [(string) $a->id],
'mfg_row_phase' => ['1'],
'mfg_gram' => ['100'],
'mfg_factor' => ['1,00'],
]);
$user = phaseJsonUser();
$this->actingAs($user, 'user');
$location = Location::factory()->create();
$response = $this->getJson(route('admin.inventory.api.products.recipe', [
'product' => $product->id,
'location_id' => $location->id,
'quantity' => 1,
]))->assertSuccessful();
$phases = collect($response->json('phases'));
expect($phases)->toHaveCount(2);
$vorbereitung = $phases->firstWhere('name', 'Vorbereitung');
expect($vorbereitung)->not->toBeNull()
->and($vorbereitung['note'])->toBe('Arbeitsplatz desinfizieren');
// Vorbereitung steht vor Phase A (pos-Reihenfolge)
expect($phases[0]['name'])->toBe('Vorbereitung');
});
function phaseJsonUser(): User
{
$user = User::query()->create([
'email' => uniqid('ap36_', true).'@test.example',
'password' => bcrypt('password'),
]);
$user->forceFill([
'admin' => 7, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false,
])->save();
return $user->fresh();
}