1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 17:39:57 +01:00

feat(production): articleId für Rezepte, TUI-Verbesserungen mit UoM-Carousel, ArticlePicker und Zutaten-Reorder

Backend:
- articleId als Pflichtfeld im Recipe-Aggregate (Domain, Application, Infrastructure)
- Liquibase-Migration 015 mit defaultValue für bestehende Daten
- Alle Tests angepasst (Unit, Integration)

Frontend:
- UoM-Carousel-Selektor in RecipeCreateScreen, AddBatchScreen, AddIngredientScreen
- ArticlePicker-Komponente mit Typeahead-Suche für Artikelauswahl
- Auto-Position bei Zutatenzugabe (kein manuelles Feld mehr)
- Automatische subRecipeId-Erkennung bei Artikelauswahl
- Zutaten-Reorder per Drag im RecipeDetailScreen (Remove + Re-Add)
- Artikelnamen statt UUIDs in der Rezept-Detailansicht
- Navigation-Context: replace()-Methode ergänzt
This commit is contained in:
Sebastian Frick 2026-02-20 01:07:32 +01:00
parent b46495e1aa
commit 6c1e6c24bc
48 changed files with 999 additions and 237 deletions

View file

@ -43,7 +43,7 @@ class ActivateRecipeTest {
private Recipe draftRecipeWithIngredient() {
var recipe = Recipe.create(new RecipeDraft(
"Bratwurst", 1, RecipeType.FINISHED_PRODUCT,
null, 85, 14, "100", "KILOGRAM"
null, 85, 14, "100", "KILOGRAM", "article-123"
)).unsafeGetValue();
recipe.addIngredient(new IngredientDraft(1, "article-123", "5.5", "KILOGRAM", null, false));
return recipe;
@ -52,7 +52,7 @@ class ActivateRecipeTest {
private Recipe draftRecipeWithoutIngredient() {
return Recipe.create(new RecipeDraft(
"Bratwurst", 1, RecipeType.FINISHED_PRODUCT,
null, 85, 14, "100", "KILOGRAM"
null, 85, 14, "100", "KILOGRAM", "article-123"
)).unsafeGetValue();
}
@ -61,7 +61,7 @@ class ActivateRecipeTest {
RecipeId.of("recipe-1"), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(),
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
);
}

View file

@ -39,7 +39,7 @@ class AddRecipeIngredientTest {
private Recipe draftRecipe() {
return Recipe.create(new RecipeDraft(
"Bratwurst", 1, RecipeType.FINISHED_PRODUCT,
null, 85, 14, "100", "KILOGRAM"
null, 85, 14, "100", "KILOGRAM", "article-123"
)).unsafeGetValue();
}

View file

@ -45,7 +45,7 @@ class ArchiveRecipeTest {
RecipeId.of("recipe-1"), new RecipeName("Bratwurst"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(),
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
);
}
@ -53,7 +53,7 @@ class ArchiveRecipeTest {
private Recipe draftRecipe() {
return Recipe.create(new RecipeDraft(
"Bratwurst", 1, RecipeType.FINISHED_PRODUCT,
null, 85, 14, "100", "KILOGRAM"
null, 85, 14, "100", "KILOGRAM", "article-123"
)).unsafeGetValue();
}
@ -118,7 +118,7 @@ class ArchiveRecipeTest {
RecipeId.of("recipe-2"), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ARCHIVED, List.of(), List.of(),
"article-123", RecipeStatus.ARCHIVED, List.of(), List.of(),
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
);
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);

View file

@ -43,7 +43,7 @@ class GetRecipeTest {
RecipeId.of(id), new RecipeName("Bratwurst"), 1, RecipeType.FINISHED_PRODUCT,
"Beschreibung", new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(),
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
);
}

View file

@ -42,7 +42,7 @@ class ListRecipesTest {
RecipeId.of(id), new RecipeName("Rezept-" + id), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
status, List.of(), List.of(),
"article-123", status, List.of(), List.of(),
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
);
}

View file

@ -57,7 +57,7 @@ class PlanBatchTest {
RecipeId.of("recipe-1"), new RecipeName("Bratwurst"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(),
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
);
}
@ -67,7 +67,7 @@ class PlanBatchTest {
RecipeId.of("recipe-1"), new RecipeName("Bratwurst"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.DRAFT, List.of(), List.of(),
"article-123", RecipeStatus.DRAFT, List.of(), List.of(),
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
);
}

View file

@ -48,7 +48,7 @@ class RecipeCycleCheckerTest {
RecipeId.of(id), new RecipeName("Recipe-" + id), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(100), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.DRAFT, ingredients, List.of(),
"article-123", RecipeStatus.DRAFT, ingredients, List.of(),
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC));
}
@ -145,7 +145,7 @@ class RecipeCycleCheckerTest {
RecipeId.of("B"), new RecipeName("Recipe-B"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(100), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.DRAFT, ingredients, List.of(),
"article-123", RecipeStatus.DRAFT, ingredients, List.of(),
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC));
when(recipeRepository.findById(RecipeId.of("B"))).thenReturn(Result.success(Optional.of(recipeB)));

View file

@ -17,7 +17,7 @@ class RecipeTest {
return new RecipeDraft(
"Bratwurst Grob", 1, RecipeType.FINISHED_PRODUCT,
"Grobe Bratwurst nach Hausrezept", 85,
14, "100", "KILOGRAM"
14, "100", "KILOGRAM", "article-123"
);
}
@ -56,7 +56,7 @@ class RecipeTest {
@DisplayName("should fail when name is blank")
void should_Fail_When_NameIsBlank() {
var draft = new RecipeDraft(
"", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM"
"", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM", "article-123"
);
var result = Recipe.create(draft);
@ -69,7 +69,7 @@ class RecipeTest {
@DisplayName("should fail when name is null")
void should_Fail_When_NameIsNull() {
var draft = new RecipeDraft(
null, 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM"
null, 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM", "article-123"
);
var result = Recipe.create(draft);
@ -82,7 +82,7 @@ class RecipeTest {
@DisplayName("should fail when version is less than 1")
void should_Fail_When_VersionLessThanOne() {
var draft = new RecipeDraft(
"Test", 0, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM"
"Test", 0, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM", "article-123"
);
var result = Recipe.create(draft);
@ -95,7 +95,7 @@ class RecipeTest {
@DisplayName("should fail when yield percentage is 0")
void should_Fail_When_YieldPercentageIsZero() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 0, 14, "100", "KILOGRAM"
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 0, 14, "100", "KILOGRAM", "article-123"
);
var result = Recipe.create(draft);
@ -108,7 +108,7 @@ class RecipeTest {
@DisplayName("should fail when yield percentage is 201")
void should_Fail_When_YieldPercentageIs201() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 201, 14, "100", "KILOGRAM"
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 201, 14, "100", "KILOGRAM", "article-123"
);
var result = Recipe.create(draft);
@ -121,7 +121,7 @@ class RecipeTest {
@DisplayName("should fail when shelf life is 0 for FINISHED_PRODUCT")
void should_Fail_When_ShelfLifeZeroForFinishedProduct() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 0, "100", "KILOGRAM"
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 0, "100", "KILOGRAM", "article-123"
);
var result = Recipe.create(draft);
@ -134,7 +134,7 @@ class RecipeTest {
@DisplayName("should fail when shelf life is null for FINISHED_PRODUCT")
void should_Fail_When_ShelfLifeNullForFinishedProduct() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, null, "100", "KILOGRAM"
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, null, "100", "KILOGRAM", "article-123"
);
var result = Recipe.create(draft);
@ -147,7 +147,7 @@ class RecipeTest {
@DisplayName("should fail when shelf life is 0 for INTERMEDIATE")
void should_Fail_When_ShelfLifeZeroForIntermediate() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.INTERMEDIATE, null, 85, 0, "100", "KILOGRAM"
"Test", 1, RecipeType.INTERMEDIATE, null, 85, 0, "100", "KILOGRAM", "article-123"
);
var result = Recipe.create(draft);
@ -160,7 +160,7 @@ class RecipeTest {
@DisplayName("should allow null shelf life for RAW_MATERIAL")
void should_AllowNullShelfLife_When_RawMaterial() {
var draft = new RecipeDraft(
"Schweinefleisch", 1, RecipeType.RAW_MATERIAL, null, 100, null, "50", "KILOGRAM"
"Schweinefleisch", 1, RecipeType.RAW_MATERIAL, null, 100, null, "50", "KILOGRAM", "article-123"
);
var result = Recipe.create(draft);
@ -173,7 +173,7 @@ class RecipeTest {
@DisplayName("should fail when output quantity is zero")
void should_Fail_When_OutputQuantityZero() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "0", "KILOGRAM"
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "0", "KILOGRAM", "article-123"
);
var result = Recipe.create(draft);
@ -186,7 +186,7 @@ class RecipeTest {
@DisplayName("should fail when output UoM is invalid")
void should_Fail_When_OutputUomInvalid() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "INVALID"
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "INVALID", "article-123"
);
var result = Recipe.create(draft);
@ -199,7 +199,7 @@ class RecipeTest {
@DisplayName("should allow description to be null")
void should_AllowNullDescription() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM"
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM", "article-123"
);
var result = Recipe.create(draft);
@ -238,7 +238,7 @@ class RecipeTest {
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(),
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
);
@ -321,7 +321,7 @@ class RecipeTest {
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(),
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
);
@ -375,7 +375,7 @@ class RecipeTest {
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(),
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
);
@ -444,7 +444,7 @@ class RecipeTest {
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(),
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
);
@ -506,7 +506,7 @@ class RecipeTest {
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(),
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
);
@ -527,7 +527,7 @@ class RecipeTest {
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ARCHIVED, List.of(), List.of(),
"article-123", RecipeStatus.ARCHIVED, List.of(), List.of(),
java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
);
@ -553,7 +553,7 @@ class RecipeTest {
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(),
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
);
var updatedBefore = recipe.updatedAt();
@ -587,7 +587,7 @@ class RecipeTest {
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ARCHIVED, List.of(), List.of(),
"article-123", RecipeStatus.ARCHIVED, List.of(), List.of(),
java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
);
@ -614,7 +614,7 @@ class RecipeTest {
recipe1.id(), new RecipeName("Other"), 2, RecipeType.RAW_MATERIAL,
null, new YieldPercentage(100), null,
Quantity.of(new java.math.BigDecimal("50"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(), recipe1.createdAt(), recipe1.updatedAt()
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(), recipe1.createdAt(), recipe1.updatedAt()
);
assertThat(recipe1).isEqualTo(recipe2);

View file

@ -312,7 +312,8 @@ class BatchControllerIntegrationTest extends AbstractIntegrationTest {
"yieldPercentage": 85,
"shelfLifeDays": 14,
"outputQuantity": "100",
"outputUom": "KILOGRAM"
"outputUom": "KILOGRAM",
"articleId": "article-123"
}
""".formatted(UUID.randomUUID().toString().substring(0, 8));

View file

@ -82,7 +82,8 @@ class GetRecipeIntegrationTest extends AbstractIntegrationTest {
"yieldPercentage": 85,
"shelfLifeDays": 14,
"outputQuantity": "100",
"outputUom": "KILOGRAM"
"outputUom": "KILOGRAM",
"articleId": "article-123"
}
""";

View file

@ -113,7 +113,8 @@ class ListRecipesIntegrationTest extends AbstractIntegrationTest {
"yieldPercentage": 85,
"shelfLifeDays": 14,
"outputQuantity": "100",
"outputUom": "KILOGRAM"
"outputUom": "KILOGRAM",
"articleId": "article-123"
}
""".formatted(name, version);