1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 10:19:35 +01:00

feat: TUI-Screens für Inventar und Produktion + API-Client Typ-Migration

Neue TUI-Features:
- Inventar: Lageorte auflisten, anlegen, bearbeiten, (de-)aktivieren
- Produktion: Rezepte auflisten, anlegen, Detail-Ansicht
- Navigation erweitert (Hauptmenü, Routing)

API-Client auf generierte OpenAPI-Typen umgestellt:
- 6 neue Alias-Dateien in @effigenix/types (supplier, category, article,
  customer, inventory, production)
- api-client Re-Exports direkt von @effigenix/types statt via Resources
- Backend: @Schema(requiredProperties) auf 16 Response-Records
- Backend: OpenApiCustomizer für application-layer DTOs (UserDTO, RoleDTO)

Hinweis: Backend-Endpoints für GET /api/recipes und
GET /api/inventory/storage-locations/{id} fehlen noch (separate Issues).
This commit is contained in:
Sebastian Frick 2026-02-19 13:45:35 +01:00
parent bee3f28b5f
commit c26d72fbe7
48 changed files with 2090 additions and 474 deletions

View file

@ -1,14 +1,16 @@
package de.effigenix.infrastructure.inventory.web.dto;
import de.effigenix.domain.inventory.StorageLocation;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
@Schema(requiredProperties = {"id", "name", "storageType", "active"})
public record StorageLocationResponse(
String id,
String name,
String storageType,
TemperatureRangeResponse temperatureRange,
@Schema(nullable = true) TemperatureRangeResponse temperatureRange,
boolean active
) {
@ -30,5 +32,6 @@ public record StorageLocationResponse(
);
}
@Schema(requiredProperties = {"minTemperature", "maxTemperature"})
public record TemperatureRangeResponse(BigDecimal minTemperature, BigDecimal maxTemperature) {}
}

View file

@ -1,7 +1,9 @@
package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.shared.common.Address;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(requiredProperties = {"street", "houseNumber", "postalCode", "city", "country"})
public record AddressResponse(
String street,
String houseNumber,

View file

@ -2,10 +2,12 @@ package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.Article;
import de.effigenix.domain.masterdata.SupplierId;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(requiredProperties = {"id", "name", "articleNumber", "categoryId", "salesUnits", "status", "supplierIds", "createdAt", "updatedAt"})
public record ArticleResponse(
String id,
String name,

View file

@ -1,7 +1,9 @@
package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.shared.common.ContactInfo;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(requiredProperties = {"phone", "email", "contactPerson"})
public record ContactInfoResponse(
String phone,
String email,

View file

@ -1,9 +1,11 @@
package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.ContractLineItem;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
@Schema(requiredProperties = {"articleId", "agreedPrice", "agreedQuantity", "unit"})
public record ContractLineItemResponse(
String articleId,
BigDecimal agreedPrice,

View file

@ -2,19 +2,21 @@ package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.Customer;
import de.effigenix.domain.masterdata.CustomerPreference;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(requiredProperties = {"id", "name", "type", "billingAddress", "contactInfo", "deliveryAddresses", "preferences", "status", "createdAt", "updatedAt"})
public record CustomerResponse(
String id,
String name,
String type,
AddressResponse billingAddress,
ContactInfoResponse contactInfo,
PaymentTermsResponse paymentTerms,
@Schema(nullable = true) PaymentTermsResponse paymentTerms,
List<DeliveryAddressResponse> deliveryAddresses,
FrameContractResponse frameContract,
@Schema(nullable = true) FrameContractResponse frameContract,
List<String> preferences,
String status,
LocalDateTime createdAt,

View file

@ -1,7 +1,9 @@
package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.DeliveryAddress;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(requiredProperties = {"label", "address", "contactPerson", "deliveryNotes"})
public record DeliveryAddressResponse(
String label,
AddressResponse address,

View file

@ -1,10 +1,12 @@
package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.FrameContract;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import java.util.List;
@Schema(requiredProperties = {"id", "validFrom", "validUntil", "deliveryRhythm", "lineItems"})
public record FrameContractResponse(
String id,
LocalDate validFrom,

View file

@ -1,7 +1,9 @@
package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.shared.common.PaymentTerms;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(requiredProperties = {"paymentDueDays", "paymentDescription"})
public record PaymentTermsResponse(
int paymentDueDays,
String paymentDescription

View file

@ -1,7 +1,9 @@
package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.ProductCategory;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(requiredProperties = {"id", "name", "description"})
public record ProductCategoryResponse(
String id,
String name,

View file

@ -1,9 +1,11 @@
package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.QualityCertificate;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
@Schema(requiredProperties = {"certificateType", "issuer", "validFrom", "validUntil"})
public record QualityCertificateResponse(
String certificateType,
String issuer,

View file

@ -1,9 +1,11 @@
package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.SalesUnit;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
@Schema(requiredProperties = {"id", "unit", "priceModel", "price"})
public record SalesUnitResponse(
String id,
String unit,

View file

@ -1,7 +1,9 @@
package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.SupplierRating;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(requiredProperties = {"qualityScore", "deliveryScore", "priceScore"})
public record SupplierRatingResponse(
int qualityScore,
int deliveryScore,

View file

@ -1,18 +1,20 @@
package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.Supplier;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(requiredProperties = {"id", "name", "contactInfo", "certificates", "status", "createdAt", "updatedAt"})
public record SupplierResponse(
String id,
String name,
AddressResponse address,
@Schema(nullable = true) AddressResponse address,
ContactInfoResponse contactInfo,
PaymentTermsResponse paymentTerms,
@Schema(nullable = true) PaymentTermsResponse paymentTerms,
List<QualityCertificateResponse> certificates,
SupplierRatingResponse rating,
@Schema(nullable = true) SupplierRatingResponse rating,
String status,
LocalDateTime createdAt,
LocalDateTime updatedAt

View file

@ -1,14 +1,16 @@
package de.effigenix.infrastructure.production.web.dto;
import de.effigenix.domain.production.Ingredient;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(requiredProperties = {"id", "position", "articleId", "quantity", "uom", "substitutable"})
public record IngredientResponse(
String id,
int position,
String articleId,
String quantity,
String uom,
String subRecipeId,
@Schema(nullable = true) String subRecipeId,
boolean substitutable
) {
public static IngredientResponse from(Ingredient ingredient) {

View file

@ -1,10 +1,12 @@
package de.effigenix.infrastructure.production.web.dto;
import de.effigenix.domain.production.Recipe;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(requiredProperties = {"id", "name", "version", "type", "description", "yieldPercentage", "outputQuantity", "outputUom", "status", "ingredients", "createdAt", "updatedAt"})
public record RecipeResponse(
String id,
String name,
@ -12,7 +14,7 @@ public record RecipeResponse(
String type,
String description,
int yieldPercentage,
Integer shelfLifeDays,
@Schema(nullable = true) Integer shelfLifeDays,
String outputQuantity,
String outputUom,
String status,

View file

@ -12,7 +12,8 @@ import java.time.LocalDateTime;
* Client should store the access token and send it in Authorization header
* for subsequent requests.
*/
@Schema(description = "Login response with JWT tokens")
@Schema(description = "Login response with JWT tokens",
requiredProperties = {"accessToken", "tokenType", "expiresIn", "expiresAt", "refreshToken"})
public record LoginResponse(
@Schema(description = "JWT access token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
String accessToken,

View file

@ -7,8 +7,15 @@ import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.servers.Server;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* OpenAPI/Swagger Configuration.
*
@ -111,6 +118,35 @@ import org.springframework.context.annotation.Configuration;
"""
)
public class OpenApiConfig {
// Configuration is done via annotations
// No additional beans needed
/**
* Marks fields as required for application-layer DTOs that cannot carry
* {@code @Schema} annotations (to preserve the clean architecture boundary).
*/
@Bean
public OpenApiCustomizer applicationDtoRequiredFieldsCustomizer() {
// Application-layer DTOs all required except explicitly nullable
Map<String, Set<String>> nullableFields = Map.of(
"UserDTO", Set.of("branchId", "lastLogin"),
"RoleDTO", Set.of()
);
return openApi -> {
var schemas = openApi.getComponents().getSchemas();
if (schemas == null) return;
nullableFields.forEach((schemaName, nullable) -> {
var schema = schemas.get(schemaName);
if (schema == null || schema.getProperties() == null) return;
List<String> required = new ArrayList<>();
schema.getProperties().keySet().forEach(prop -> {
if (!nullable.contains(prop)) {
required.add((String) prop);
}
});
schema.setRequired(required);
});
};
}
}