1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 19:30:16 +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

@ -26,7 +26,7 @@ public class CreateRecipe {
var draft = new RecipeDraft(
cmd.name(), cmd.version(), cmd.type(), cmd.description(),
cmd.yieldPercentage(), cmd.shelfLifeDays(),
cmd.outputQuantity(), cmd.outputUom()
cmd.outputQuantity(), cmd.outputUom(), cmd.articleId()
);
Recipe recipe;

View file

@ -10,5 +10,6 @@ public record CreateRecipeCommand(
int yieldPercentage,
Integer shelfLifeDays,
String outputQuantity,
String outputUom
String outputUom,
String articleId
) {}

View file

@ -31,6 +31,7 @@ import static de.effigenix.shared.common.Result.*;
* 11. Recipe can only be activated when it has at least one ingredient
* 12. Recipe can only be activated from DRAFT status
* 13. Recipe can only be archived from ACTIVE status
* 14. ArticleId must not be blank (references the output article in Master Data)
*/
public class Recipe {
@ -42,6 +43,7 @@ public class Recipe {
private YieldPercentage yieldPercentage;
private Integer shelfLifeDays;
private Quantity outputQuantity;
private String articleId;
private RecipeStatus status;
private final List<Ingredient> ingredients;
private final List<ProductionStep> productionSteps;
@ -57,6 +59,7 @@ public class Recipe {
YieldPercentage yieldPercentage,
Integer shelfLifeDays,
Quantity outputQuantity,
String articleId,
RecipeStatus status,
List<Ingredient> ingredients,
List<ProductionStep> productionSteps,
@ -71,6 +74,7 @@ public class Recipe {
this.yieldPercentage = yieldPercentage;
this.shelfLifeDays = shelfLifeDays;
this.outputQuantity = outputQuantity;
this.articleId = articleId;
this.status = status;
this.ingredients = new ArrayList<>(ingredients);
this.productionSteps = new ArrayList<>(productionSteps);
@ -111,7 +115,12 @@ public class Recipe {
}
}
// 5. Output Quantity (required, positive)
// 5. ArticleId (required)
if (draft.articleId() == null || draft.articleId().isBlank()) {
return Result.failure(new RecipeError.ValidationFailure("ArticleId must not be blank"));
}
// 6. Output Quantity (required, positive)
Quantity outputQuantity;
try {
BigDecimal amount = new BigDecimal(draft.outputQuantity());
@ -128,7 +137,7 @@ public class Recipe {
return Result.success(new Recipe(
RecipeId.generate(), name, draft.version(), draft.type(),
draft.description(), yieldPercentage, shelfLifeDays, outputQuantity,
RecipeStatus.DRAFT, List.of(), List.of(), now, now
draft.articleId(), RecipeStatus.DRAFT, List.of(), List.of(), now, now
));
}
@ -144,6 +153,7 @@ public class Recipe {
YieldPercentage yieldPercentage,
Integer shelfLifeDays,
Quantity outputQuantity,
String articleId,
RecipeStatus status,
List<Ingredient> ingredients,
List<ProductionStep> productionSteps,
@ -151,7 +161,7 @@ public class Recipe {
OffsetDateTime updatedAt
) {
return new Recipe(id, name, version, type, description,
yieldPercentage, shelfLifeDays, outputQuantity, status, ingredients, productionSteps, createdAt, updatedAt);
yieldPercentage, shelfLifeDays, outputQuantity, articleId, status, ingredients, productionSteps, createdAt, updatedAt);
}
// ==================== Ingredient Management ====================
@ -253,6 +263,7 @@ public class Recipe {
public YieldPercentage yieldPercentage() { return yieldPercentage; }
public Integer shelfLifeDays() { return shelfLifeDays; }
public Quantity outputQuantity() { return outputQuantity; }
public String articleId() { return articleId; }
public RecipeStatus status() { return status; }
public List<Ingredient> ingredients() { return Collections.unmodifiableList(ingredients); }
public List<ProductionStep> productionSteps() { return Collections.unmodifiableList(productionSteps); }

View file

@ -12,6 +12,7 @@ package de.effigenix.domain.production;
* @param shelfLifeDays Shelf life in days (nullable; required for FINISHED_PRODUCT and INTERMEDIATE)
* @param outputQuantity Expected output quantity amount (required)
* @param outputUom Expected output unit of measure (required)
* @param articleId Article ID of the output product (optional; null if not linked)
*/
public record RecipeDraft(
String name,
@ -21,5 +22,6 @@ public record RecipeDraft(
int yieldPercentage,
Integer shelfLifeDays,
String outputQuantity,
String outputUom
String outputUom,
String articleId
) {}

View file

@ -40,6 +40,9 @@ public class RecipeEntity {
@Column(name = "output_uom", nullable = false, length = 20)
private String outputUom;
@Column(name = "article_id", nullable = false, length = 36)
private String articleId;
@Column(name = "status", nullable = false, length = 20)
private String status;
@ -61,7 +64,7 @@ public class RecipeEntity {
public RecipeEntity(String id, String name, int version, String type, String description,
int yieldPercentage, Integer shelfLifeDays, BigDecimal outputQuantity,
String outputUom, String status, OffsetDateTime createdAt, OffsetDateTime updatedAt) {
String outputUom, String articleId, String status, OffsetDateTime createdAt, OffsetDateTime updatedAt) {
this.id = id;
this.name = name;
this.version = version;
@ -71,6 +74,7 @@ public class RecipeEntity {
this.shelfLifeDays = shelfLifeDays;
this.outputQuantity = outputQuantity;
this.outputUom = outputUom;
this.articleId = articleId;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
@ -85,6 +89,7 @@ public class RecipeEntity {
public Integer getShelfLifeDays() { return shelfLifeDays; }
public BigDecimal getOutputQuantity() { return outputQuantity; }
public String getOutputUom() { return outputUom; }
public String getArticleId() { return articleId; }
public String getStatus() { return status; }
public OffsetDateTime getCreatedAt() { return createdAt; }
public OffsetDateTime getUpdatedAt() { return updatedAt; }
@ -100,6 +105,7 @@ public class RecipeEntity {
public void setShelfLifeDays(Integer shelfLifeDays) { this.shelfLifeDays = shelfLifeDays; }
public void setOutputQuantity(BigDecimal outputQuantity) { this.outputQuantity = outputQuantity; }
public void setOutputUom(String outputUom) { this.outputUom = outputUom; }
public void setArticleId(String articleId) { this.articleId = articleId; }
public void setStatus(String status) { this.status = status; }
public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }

View file

@ -25,6 +25,7 @@ public class RecipeMapper {
recipe.shelfLifeDays(),
recipe.outputQuantity().amount(),
recipe.outputQuantity().uom().name(),
recipe.articleId(),
recipe.status().name(),
recipe.createdAt(),
recipe.updatedAt()
@ -64,6 +65,7 @@ public class RecipeMapper {
entity.getOutputQuantity(),
UnitOfMeasure.valueOf(entity.getOutputUom())
),
entity.getArticleId(),
RecipeStatus.valueOf(entity.getStatus()),
ingredients,
productionSteps,

View file

@ -136,7 +136,7 @@ public class RecipeController {
var cmd = new CreateRecipeCommand(
request.name(), request.version(), request.type(), request.description(),
request.yieldPercentage(), request.shelfLifeDays(),
request.outputQuantity(), request.outputUom()
request.outputQuantity(), request.outputUom(), request.articleId()
);
var result = createRecipe.execute(cmd, actorId);

View file

@ -12,5 +12,6 @@ public record CreateRecipeRequest(
int yieldPercentage,
Integer shelfLifeDays,
@NotBlank String outputQuantity,
@NotBlank String outputUom
@NotBlank String outputUom,
@NotBlank String articleId
) {}

View file

@ -6,7 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import java.time.OffsetDateTime;
import java.util.List;
@Schema(requiredProperties = {"id", "name", "version", "type", "description", "yieldPercentage", "outputQuantity", "outputUom", "status", "ingredients", "productionSteps", "createdAt", "updatedAt"})
@Schema(requiredProperties = {"id", "name", "version", "type", "description", "yieldPercentage", "outputQuantity", "outputUom", "articleId", "status", "ingredients", "productionSteps", "createdAt", "updatedAt"})
public record RecipeResponse(
String id,
String name,
@ -17,6 +17,7 @@ public record RecipeResponse(
@Schema(nullable = true) Integer shelfLifeDays,
String outputQuantity,
String outputUom,
String articleId,
String status,
List<IngredientResponse> ingredients,
List<ProductionStepResponse> productionSteps,
@ -34,6 +35,7 @@ public record RecipeResponse(
recipe.shelfLifeDays(),
recipe.outputQuantity().amount().toPlainString(),
recipe.outputQuantity().uom().name(),
recipe.articleId(),
recipe.status().name(),
recipe.ingredients().stream().map(IngredientResponse::from).toList(),
recipe.productionSteps().stream().map(ProductionStepResponse::from).toList(),

View file

@ -5,7 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import java.time.OffsetDateTime;
@Schema(requiredProperties = {"id", "name", "version", "type", "description", "yieldPercentage", "outputQuantity", "outputUom", "status", "ingredientCount", "stepCount", "createdAt", "updatedAt"})
@Schema(requiredProperties = {"id", "name", "version", "type", "description", "yieldPercentage", "outputQuantity", "outputUom", "articleId", "status", "ingredientCount", "stepCount", "createdAt", "updatedAt"})
public record RecipeSummaryResponse(
String id,
String name,
@ -16,6 +16,7 @@ public record RecipeSummaryResponse(
@Schema(nullable = true) Integer shelfLifeDays,
String outputQuantity,
String outputUom,
String articleId,
String status,
int ingredientCount,
int stepCount,
@ -33,6 +34,7 @@ public record RecipeSummaryResponse(
recipe.shelfLifeDays(),
recipe.outputQuantity().amount().toPlainString(),
recipe.outputQuantity().uom().name(),
recipe.articleId(),
recipe.status().name(),
recipe.ingredients().size(),
recipe.productionSteps().size(),

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="018-add-article-id-to-recipes" author="effigenix">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="recipes" columnName="article_id"/>
</not>
</preConditions>
<addColumn tableName="recipes">
<column name="article_id" type="VARCHAR(36)" defaultValue="UNKNOWN">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>

View file

@ -22,5 +22,6 @@
<include file="db/changelog/changes/015-create-batches-table.xml"/>
<include file="db/changelog/changes/016-create-batch-number-sequences-table.xml"/>
<include file="db/changelog/changes/017-timestamps-to-timestamptz.xml"/>
<include file="db/changelog/changes/018-add-article-id-to-recipes.xml"/>
</databaseChangeLog>