mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 13:49:36 +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:
parent
bee3f28b5f
commit
c26d72fbe7
48 changed files with 2090 additions and 474 deletions
|
|
@ -1,14 +1,16 @@
|
||||||
package de.effigenix.infrastructure.inventory.web.dto;
|
package de.effigenix.infrastructure.inventory.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.inventory.StorageLocation;
|
import de.effigenix.domain.inventory.StorageLocation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"id", "name", "storageType", "active"})
|
||||||
public record StorageLocationResponse(
|
public record StorageLocationResponse(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
String storageType,
|
String storageType,
|
||||||
TemperatureRangeResponse temperatureRange,
|
@Schema(nullable = true) TemperatureRangeResponse temperatureRange,
|
||||||
boolean active
|
boolean active
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
@ -30,5 +32,6 @@ public record StorageLocationResponse(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"minTemperature", "maxTemperature"})
|
||||||
public record TemperatureRangeResponse(BigDecimal minTemperature, BigDecimal maxTemperature) {}
|
public record TemperatureRangeResponse(BigDecimal minTemperature, BigDecimal maxTemperature) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package de.effigenix.infrastructure.masterdata.web.dto;
|
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.shared.common.Address;
|
import de.effigenix.shared.common.Address;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"street", "houseNumber", "postalCode", "city", "country"})
|
||||||
public record AddressResponse(
|
public record AddressResponse(
|
||||||
String street,
|
String street,
|
||||||
String houseNumber,
|
String houseNumber,
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.masterdata.Article;
|
import de.effigenix.domain.masterdata.Article;
|
||||||
import de.effigenix.domain.masterdata.SupplierId;
|
import de.effigenix.domain.masterdata.SupplierId;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"id", "name", "articleNumber", "categoryId", "salesUnits", "status", "supplierIds", "createdAt", "updatedAt"})
|
||||||
public record ArticleResponse(
|
public record ArticleResponse(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package de.effigenix.infrastructure.masterdata.web.dto;
|
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.shared.common.ContactInfo;
|
import de.effigenix.shared.common.ContactInfo;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"phone", "email", "contactPerson"})
|
||||||
public record ContactInfoResponse(
|
public record ContactInfoResponse(
|
||||||
String phone,
|
String phone,
|
||||||
String email,
|
String email,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package de.effigenix.infrastructure.masterdata.web.dto;
|
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.masterdata.ContractLineItem;
|
import de.effigenix.domain.masterdata.ContractLineItem;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"articleId", "agreedPrice", "agreedQuantity", "unit"})
|
||||||
public record ContractLineItemResponse(
|
public record ContractLineItemResponse(
|
||||||
String articleId,
|
String articleId,
|
||||||
BigDecimal agreedPrice,
|
BigDecimal agreedPrice,
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,21 @@ package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.masterdata.Customer;
|
import de.effigenix.domain.masterdata.Customer;
|
||||||
import de.effigenix.domain.masterdata.CustomerPreference;
|
import de.effigenix.domain.masterdata.CustomerPreference;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"id", "name", "type", "billingAddress", "contactInfo", "deliveryAddresses", "preferences", "status", "createdAt", "updatedAt"})
|
||||||
public record CustomerResponse(
|
public record CustomerResponse(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
String type,
|
String type,
|
||||||
AddressResponse billingAddress,
|
AddressResponse billingAddress,
|
||||||
ContactInfoResponse contactInfo,
|
ContactInfoResponse contactInfo,
|
||||||
PaymentTermsResponse paymentTerms,
|
@Schema(nullable = true) PaymentTermsResponse paymentTerms,
|
||||||
List<DeliveryAddressResponse> deliveryAddresses,
|
List<DeliveryAddressResponse> deliveryAddresses,
|
||||||
FrameContractResponse frameContract,
|
@Schema(nullable = true) FrameContractResponse frameContract,
|
||||||
List<String> preferences,
|
List<String> preferences,
|
||||||
String status,
|
String status,
|
||||||
LocalDateTime createdAt,
|
LocalDateTime createdAt,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package de.effigenix.infrastructure.masterdata.web.dto;
|
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.masterdata.DeliveryAddress;
|
import de.effigenix.domain.masterdata.DeliveryAddress;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"label", "address", "contactPerson", "deliveryNotes"})
|
||||||
public record DeliveryAddressResponse(
|
public record DeliveryAddressResponse(
|
||||||
String label,
|
String label,
|
||||||
AddressResponse address,
|
AddressResponse address,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package de.effigenix.infrastructure.masterdata.web.dto;
|
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.masterdata.FrameContract;
|
import de.effigenix.domain.masterdata.FrameContract;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"id", "validFrom", "validUntil", "deliveryRhythm", "lineItems"})
|
||||||
public record FrameContractResponse(
|
public record FrameContractResponse(
|
||||||
String id,
|
String id,
|
||||||
LocalDate validFrom,
|
LocalDate validFrom,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package de.effigenix.infrastructure.masterdata.web.dto;
|
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.shared.common.PaymentTerms;
|
import de.effigenix.shared.common.PaymentTerms;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"paymentDueDays", "paymentDescription"})
|
||||||
public record PaymentTermsResponse(
|
public record PaymentTermsResponse(
|
||||||
int paymentDueDays,
|
int paymentDueDays,
|
||||||
String paymentDescription
|
String paymentDescription
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package de.effigenix.infrastructure.masterdata.web.dto;
|
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.masterdata.ProductCategory;
|
import de.effigenix.domain.masterdata.ProductCategory;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"id", "name", "description"})
|
||||||
public record ProductCategoryResponse(
|
public record ProductCategoryResponse(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package de.effigenix.infrastructure.masterdata.web.dto;
|
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.masterdata.QualityCertificate;
|
import de.effigenix.domain.masterdata.QualityCertificate;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"certificateType", "issuer", "validFrom", "validUntil"})
|
||||||
public record QualityCertificateResponse(
|
public record QualityCertificateResponse(
|
||||||
String certificateType,
|
String certificateType,
|
||||||
String issuer,
|
String issuer,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package de.effigenix.infrastructure.masterdata.web.dto;
|
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.masterdata.SalesUnit;
|
import de.effigenix.domain.masterdata.SalesUnit;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"id", "unit", "priceModel", "price"})
|
||||||
public record SalesUnitResponse(
|
public record SalesUnitResponse(
|
||||||
String id,
|
String id,
|
||||||
String unit,
|
String unit,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package de.effigenix.infrastructure.masterdata.web.dto;
|
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.masterdata.SupplierRating;
|
import de.effigenix.domain.masterdata.SupplierRating;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"qualityScore", "deliveryScore", "priceScore"})
|
||||||
public record SupplierRatingResponse(
|
public record SupplierRatingResponse(
|
||||||
int qualityScore,
|
int qualityScore,
|
||||||
int deliveryScore,
|
int deliveryScore,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
package de.effigenix.infrastructure.masterdata.web.dto;
|
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.masterdata.Supplier;
|
import de.effigenix.domain.masterdata.Supplier;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"id", "name", "contactInfo", "certificates", "status", "createdAt", "updatedAt"})
|
||||||
public record SupplierResponse(
|
public record SupplierResponse(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
AddressResponse address,
|
@Schema(nullable = true) AddressResponse address,
|
||||||
ContactInfoResponse contactInfo,
|
ContactInfoResponse contactInfo,
|
||||||
PaymentTermsResponse paymentTerms,
|
@Schema(nullable = true) PaymentTermsResponse paymentTerms,
|
||||||
List<QualityCertificateResponse> certificates,
|
List<QualityCertificateResponse> certificates,
|
||||||
SupplierRatingResponse rating,
|
@Schema(nullable = true) SupplierRatingResponse rating,
|
||||||
String status,
|
String status,
|
||||||
LocalDateTime createdAt,
|
LocalDateTime createdAt,
|
||||||
LocalDateTime updatedAt
|
LocalDateTime updatedAt
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
package de.effigenix.infrastructure.production.web.dto;
|
package de.effigenix.infrastructure.production.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.production.Ingredient;
|
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(
|
public record IngredientResponse(
|
||||||
String id,
|
String id,
|
||||||
int position,
|
int position,
|
||||||
String articleId,
|
String articleId,
|
||||||
String quantity,
|
String quantity,
|
||||||
String uom,
|
String uom,
|
||||||
String subRecipeId,
|
@Schema(nullable = true) String subRecipeId,
|
||||||
boolean substitutable
|
boolean substitutable
|
||||||
) {
|
) {
|
||||||
public static IngredientResponse from(Ingredient ingredient) {
|
public static IngredientResponse from(Ingredient ingredient) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package de.effigenix.infrastructure.production.web.dto;
|
package de.effigenix.infrastructure.production.web.dto;
|
||||||
|
|
||||||
import de.effigenix.domain.production.Recipe;
|
import de.effigenix.domain.production.Recipe;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(requiredProperties = {"id", "name", "version", "type", "description", "yieldPercentage", "outputQuantity", "outputUom", "status", "ingredients", "createdAt", "updatedAt"})
|
||||||
public record RecipeResponse(
|
public record RecipeResponse(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
|
|
@ -12,7 +14,7 @@ public record RecipeResponse(
|
||||||
String type,
|
String type,
|
||||||
String description,
|
String description,
|
||||||
int yieldPercentage,
|
int yieldPercentage,
|
||||||
Integer shelfLifeDays,
|
@Schema(nullable = true) Integer shelfLifeDays,
|
||||||
String outputQuantity,
|
String outputQuantity,
|
||||||
String outputUom,
|
String outputUom,
|
||||||
String status,
|
String status,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ import java.time.LocalDateTime;
|
||||||
* Client should store the access token and send it in Authorization header
|
* Client should store the access token and send it in Authorization header
|
||||||
* for subsequent requests.
|
* 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(
|
public record LoginResponse(
|
||||||
@Schema(description = "JWT access token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
@Schema(description = "JWT access token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
||||||
String accessToken,
|
String accessToken,
|
||||||
|
|
|
||||||
|
|
@ -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.info.License;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||||
import io.swagger.v3.oas.annotations.servers.Server;
|
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 org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenAPI/Swagger Configuration.
|
* OpenAPI/Swagger Configuration.
|
||||||
*
|
*
|
||||||
|
|
@ -111,6 +118,35 @@ import org.springframework.context.annotation.Configuration;
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
public class OpenApiConfig {
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,16 @@ import { CustomerDetailScreen } from './components/masterdata/customers/Customer
|
||||||
import { CustomerCreateScreen } from './components/masterdata/customers/CustomerCreateScreen.js';
|
import { CustomerCreateScreen } from './components/masterdata/customers/CustomerCreateScreen.js';
|
||||||
import { AddDeliveryAddressScreen } from './components/masterdata/customers/AddDeliveryAddressScreen.js';
|
import { AddDeliveryAddressScreen } from './components/masterdata/customers/AddDeliveryAddressScreen.js';
|
||||||
import { SetPreferencesScreen } from './components/masterdata/customers/SetPreferencesScreen.js';
|
import { SetPreferencesScreen } from './components/masterdata/customers/SetPreferencesScreen.js';
|
||||||
|
// Lagerverwaltung
|
||||||
|
import { InventoryMenu } from './components/inventory/InventoryMenu.js';
|
||||||
|
import { StorageLocationListScreen } from './components/inventory/StorageLocationListScreen.js';
|
||||||
|
import { StorageLocationCreateScreen } from './components/inventory/StorageLocationCreateScreen.js';
|
||||||
|
import { StorageLocationDetailScreen } from './components/inventory/StorageLocationDetailScreen.js';
|
||||||
|
// Produktion
|
||||||
|
import { ProductionMenu } from './components/production/ProductionMenu.js';
|
||||||
|
import { RecipeListScreen } from './components/production/RecipeListScreen.js';
|
||||||
|
import { RecipeCreateScreen } from './components/production/RecipeCreateScreen.js';
|
||||||
|
import { RecipeDetailScreen } from './components/production/RecipeDetailScreen.js';
|
||||||
|
|
||||||
function ScreenRouter() {
|
function ScreenRouter() {
|
||||||
const { isAuthenticated, loading } = useAuth();
|
const { isAuthenticated, loading } = useAuth();
|
||||||
|
|
@ -87,6 +97,16 @@ function ScreenRouter() {
|
||||||
{current === 'customer-create' && <CustomerCreateScreen />}
|
{current === 'customer-create' && <CustomerCreateScreen />}
|
||||||
{current === 'customer-add-delivery-address' && <AddDeliveryAddressScreen />}
|
{current === 'customer-add-delivery-address' && <AddDeliveryAddressScreen />}
|
||||||
{current === 'customer-set-preferences' && <SetPreferencesScreen />}
|
{current === 'customer-set-preferences' && <SetPreferencesScreen />}
|
||||||
|
{/* Lagerverwaltung */}
|
||||||
|
{current === 'inventory-menu' && <InventoryMenu />}
|
||||||
|
{current === 'storage-location-list' && <StorageLocationListScreen />}
|
||||||
|
{current === 'storage-location-create' && <StorageLocationCreateScreen />}
|
||||||
|
{current === 'storage-location-detail' && <StorageLocationDetailScreen />}
|
||||||
|
{/* Produktion */}
|
||||||
|
{current === 'production-menu' && <ProductionMenu />}
|
||||||
|
{current === 'recipe-list' && <RecipeListScreen />}
|
||||||
|
{current === 'recipe-create' && <RecipeCreateScreen />}
|
||||||
|
{current === 'recipe-detail' && <RecipeDetailScreen />}
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ export function MainMenu() {
|
||||||
|
|
||||||
const items: MenuItem[] = [
|
const items: MenuItem[] = [
|
||||||
{ label: 'Stammdaten', screen: 'masterdata-menu' },
|
{ label: 'Stammdaten', screen: 'masterdata-menu' },
|
||||||
|
{ label: 'Lagerverwaltung', screen: 'inventory-menu' },
|
||||||
|
{ label: 'Produktion', screen: 'production-menu' },
|
||||||
{ label: 'Benutzer verwalten', screen: 'user-list' },
|
{ label: 'Benutzer verwalten', screen: 'user-list' },
|
||||||
{ label: 'Rollen anzeigen', screen: 'role-list' },
|
{ label: 'Rollen anzeigen', screen: 'role-list' },
|
||||||
{ label: 'Abmelden', action: () => void logout() },
|
{ label: 'Abmelden', action: () => void logout() },
|
||||||
|
|
|
||||||
64
frontend/apps/cli/src/components/inventory/InventoryMenu.tsx
Normal file
64
frontend/apps/cli/src/components/inventory/InventoryMenu.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Box, Text, useInput } from 'ink';
|
||||||
|
import { useNavigation } from '../../state/navigation-context.js';
|
||||||
|
import type { Screen } from '../../state/navigation-context.js';
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
label: string;
|
||||||
|
screen: Screen;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MENU_ITEMS: MenuItem[] = [
|
||||||
|
{ label: 'Lagerorte', screen: 'storage-location-list', description: 'Lagerorte verwalten (Kühlräume, Trockenlager, …)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function InventoryMenu() {
|
||||||
|
const { navigate, back } = useNavigation();
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
useInput((_input, key) => {
|
||||||
|
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
||||||
|
if (key.downArrow) setSelectedIndex((i) => Math.min(MENU_ITEMS.length - 1, i + 1));
|
||||||
|
if (key.return) {
|
||||||
|
const item = MENU_ITEMS[selectedIndex];
|
||||||
|
if (item) navigate(item.screen);
|
||||||
|
}
|
||||||
|
if (key.backspace || key.escape) back();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" paddingY={1}>
|
||||||
|
<Box marginBottom={1}>
|
||||||
|
<Text color="cyan" bold>Lagerverwaltung</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
borderStyle="round"
|
||||||
|
borderColor="gray"
|
||||||
|
paddingX={2}
|
||||||
|
paddingY={1}
|
||||||
|
width={50}
|
||||||
|
>
|
||||||
|
{MENU_ITEMS.map((item, index) => (
|
||||||
|
<Box key={item.screen} flexDirection="column">
|
||||||
|
<Text color={index === selectedIndex ? 'cyan' : 'white'}>
|
||||||
|
{index === selectedIndex ? '▶ ' : ' '}
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
{index === selectedIndex && (
|
||||||
|
<Box paddingLeft={4}>
|
||||||
|
<Text color="gray" dimColor>{item.description}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text color="gray" dimColor>↑↓ navigieren · Enter auswählen · Backspace Zurück</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Box, Text, useInput } from 'ink';
|
||||||
|
import { useNavigation } from '../../state/navigation-context.js';
|
||||||
|
import { useStorageLocations } from '../../hooks/useStorageLocations.js';
|
||||||
|
import { FormInput } from '../shared/FormInput.js';
|
||||||
|
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||||
|
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||||
|
import { STORAGE_TYPE_LABELS } from '@effigenix/api-client';
|
||||||
|
import type { StorageType } from '@effigenix/api-client';
|
||||||
|
|
||||||
|
type Field = 'name' | 'storageType' | 'minTemperature' | 'maxTemperature';
|
||||||
|
const FIELDS: Field[] = ['name', 'storageType', 'minTemperature', 'maxTemperature'];
|
||||||
|
|
||||||
|
const FIELD_LABELS: Record<Field, string> = {
|
||||||
|
name: 'Name *',
|
||||||
|
storageType: 'Lagertyp * (←→ wechseln)',
|
||||||
|
minTemperature: 'Min. Temperatur (°C)',
|
||||||
|
maxTemperature: 'Max. Temperatur (°C)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const STORAGE_TYPES: StorageType[] = ['COLD_ROOM', 'FREEZER', 'DRY_STORAGE', 'DISPLAY_COUNTER', 'PRODUCTION_AREA'];
|
||||||
|
|
||||||
|
export function StorageLocationCreateScreen() {
|
||||||
|
const { navigate, back } = useNavigation();
|
||||||
|
const { createStorageLocation, loading, error, clearError } = useStorageLocations();
|
||||||
|
|
||||||
|
const [values, setValues] = useState<Record<Field, string>>({
|
||||||
|
name: '',
|
||||||
|
storageType: 'DRY_STORAGE',
|
||||||
|
minTemperature: '',
|
||||||
|
maxTemperature: '',
|
||||||
|
});
|
||||||
|
const [activeField, setActiveField] = useState<Field>('name');
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<Partial<Record<Field, string>>>({});
|
||||||
|
|
||||||
|
const setField = (field: Field) => (value: string) => {
|
||||||
|
setValues((v) => ({ ...v, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
useInput((input, key) => {
|
||||||
|
if (loading) return;
|
||||||
|
|
||||||
|
if (activeField === 'storageType') {
|
||||||
|
if (key.leftArrow || key.rightArrow) {
|
||||||
|
const idx = STORAGE_TYPES.indexOf(values.storageType as StorageType);
|
||||||
|
const dir = key.rightArrow ? 1 : -1;
|
||||||
|
const next = STORAGE_TYPES[(idx + dir + STORAGE_TYPES.length) % STORAGE_TYPES.length];
|
||||||
|
if (next) setValues((v) => ({ ...v, storageType: next }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.tab || key.downArrow) {
|
||||||
|
setActiveField((f) => {
|
||||||
|
const idx = FIELDS.indexOf(f);
|
||||||
|
return FIELDS[(idx + 1) % FIELDS.length] ?? f;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (key.upArrow) {
|
||||||
|
setActiveField((f) => {
|
||||||
|
const idx = FIELDS.indexOf(f);
|
||||||
|
return FIELDS[(idx - 1 + FIELDS.length) % FIELDS.length] ?? f;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (key.escape) back();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const errors: Partial<Record<Field, string>> = {};
|
||||||
|
if (!values.name.trim()) errors.name = 'Name ist erforderlich.';
|
||||||
|
if (!values.storageType) errors.storageType = 'Lagertyp ist erforderlich.';
|
||||||
|
setFieldErrors(errors);
|
||||||
|
if (Object.keys(errors).length > 0) return;
|
||||||
|
|
||||||
|
const result = await createStorageLocation({
|
||||||
|
name: values.name.trim(),
|
||||||
|
storageType: values.storageType,
|
||||||
|
...(values.minTemperature.trim() ? { minTemperature: values.minTemperature.trim() } : {}),
|
||||||
|
...(values.maxTemperature.trim() ? { maxTemperature: values.maxTemperature.trim() } : {}),
|
||||||
|
});
|
||||||
|
if (result) navigate('storage-location-list');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFieldSubmit = (field: Field) => (_value: string) => {
|
||||||
|
const idx = FIELDS.indexOf(field);
|
||||||
|
if (idx < FIELDS.length - 1) {
|
||||||
|
setActiveField(FIELDS[idx + 1] ?? field);
|
||||||
|
} else {
|
||||||
|
void handleSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" alignItems="center" paddingY={2}>
|
||||||
|
<LoadingSpinner label="Lagerort wird angelegt..." />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageTypeLabel = STORAGE_TYPE_LABELS[values.storageType as StorageType] ?? values.storageType;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Text color="cyan" bold>Neuer Lagerort</Text>
|
||||||
|
{error && <ErrorDisplay message={error} onDismiss={clearError} />}
|
||||||
|
|
||||||
|
<Box flexDirection="column" gap={1} width={60}>
|
||||||
|
{FIELDS.map((field) => {
|
||||||
|
if (field === 'storageType') {
|
||||||
|
return (
|
||||||
|
<Box key={field} flexDirection="column">
|
||||||
|
<Text color={activeField === field ? 'cyan' : 'gray'}>
|
||||||
|
{FIELD_LABELS[field]}: <Text bold color="white">{storageTypeLabel}</Text>
|
||||||
|
</Text>
|
||||||
|
{fieldErrors[field] && <Text color="red">{fieldErrors[field]}</Text>}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormInput
|
||||||
|
key={field}
|
||||||
|
label={FIELD_LABELS[field]}
|
||||||
|
value={values[field]}
|
||||||
|
onChange={setField(field)}
|
||||||
|
onSubmit={handleFieldSubmit(field)}
|
||||||
|
focus={activeField === field}
|
||||||
|
{...(fieldErrors[field] ? { error: fieldErrors[field] } : {})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text color="gray" dimColor>
|
||||||
|
Tab/↑↓ Feld wechseln · ←→ Lagertyp · Enter auf letztem Feld speichern · Escape Abbrechen
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Box, Text, useInput } from 'ink';
|
||||||
|
import type { StorageLocationDTO, StorageType } from '@effigenix/api-client';
|
||||||
|
import { STORAGE_TYPE_LABELS } from '@effigenix/api-client';
|
||||||
|
import { useNavigation } from '../../state/navigation-context.js';
|
||||||
|
import { useStorageLocations } from '../../hooks/useStorageLocations.js';
|
||||||
|
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||||
|
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||||
|
import { SuccessDisplay } from '../shared/SuccessDisplay.js';
|
||||||
|
import { ConfirmDialog } from '../shared/ConfirmDialog.js';
|
||||||
|
import { client } from '../../utils/api-client.js';
|
||||||
|
|
||||||
|
type MenuAction = 'toggle-status' | 'back';
|
||||||
|
type Mode = 'menu' | 'confirm-status';
|
||||||
|
|
||||||
|
const MENU_ITEMS: { id: MenuAction; label: (loc: StorageLocationDTO) => string }[] = [
|
||||||
|
{ id: 'toggle-status', label: (loc) => loc.active ? '[Deaktivieren]' : '[Aktivieren]' },
|
||||||
|
{ id: 'back', label: () => '[Zurück]' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function errorMessage(err: unknown): string {
|
||||||
|
return err instanceof Error ? err.message : 'Unbekannter Fehler';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StorageLocationDetailScreen() {
|
||||||
|
const { params, back } = useNavigation();
|
||||||
|
const storageLocationId = params['storageLocationId'] ?? '';
|
||||||
|
const { activateStorageLocation, deactivateStorageLocation } = useStorageLocations();
|
||||||
|
|
||||||
|
const [location, setLocation] = useState<StorageLocationDTO | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectedAction, setSelectedAction] = useState(0);
|
||||||
|
const [mode, setMode] = useState<Mode>('menu');
|
||||||
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadLocation = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
client.storageLocations.getById(storageLocationId)
|
||||||
|
.then((loc) => { setLocation(loc); setLoading(false); })
|
||||||
|
.catch((err: unknown) => { setError(errorMessage(err)); setLoading(false); });
|
||||||
|
}, [storageLocationId]);
|
||||||
|
|
||||||
|
useEffect(() => { if (storageLocationId) loadLocation(); }, [loadLocation, storageLocationId]);
|
||||||
|
|
||||||
|
useInput((_input, key) => {
|
||||||
|
if (loading || actionLoading) return;
|
||||||
|
|
||||||
|
if (mode !== 'menu') return;
|
||||||
|
if (key.upArrow) setSelectedAction((i) => Math.max(0, i - 1));
|
||||||
|
if (key.downArrow) setSelectedAction((i) => Math.min(MENU_ITEMS.length - 1, i + 1));
|
||||||
|
if (key.return) void handleAction();
|
||||||
|
if (key.backspace || key.escape) back();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAction = async () => {
|
||||||
|
if (!location) return;
|
||||||
|
const item = MENU_ITEMS[selectedAction];
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
switch (item.id) {
|
||||||
|
case 'toggle-status':
|
||||||
|
setMode('confirm-status');
|
||||||
|
break;
|
||||||
|
case 'back':
|
||||||
|
back();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleStatus = useCallback(async () => {
|
||||||
|
if (!location) return;
|
||||||
|
setMode('menu');
|
||||||
|
setActionLoading(true);
|
||||||
|
const fn = location.active ? deactivateStorageLocation : activateStorageLocation;
|
||||||
|
const updated = await fn(location.id);
|
||||||
|
setActionLoading(false);
|
||||||
|
if (updated) {
|
||||||
|
setLocation(updated);
|
||||||
|
setSuccessMessage(location.active ? 'Lagerort deaktiviert.' : 'Lagerort aktiviert.');
|
||||||
|
}
|
||||||
|
}, [location, activateStorageLocation, deactivateStorageLocation]);
|
||||||
|
|
||||||
|
if (loading) return <LoadingSpinner label="Lade Lagerort..." />;
|
||||||
|
if (error && !location) return <ErrorDisplay message={error} onDismiss={back} />;
|
||||||
|
if (!location) return <Text color="red">Lagerort nicht gefunden.</Text>;
|
||||||
|
|
||||||
|
const statusColor = location.active ? 'green' : 'red';
|
||||||
|
const typeName = STORAGE_TYPE_LABELS[location.storageType as StorageType] ?? location.storageType;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Text color="cyan" bold>Lagerort: {location.name}</Text>
|
||||||
|
|
||||||
|
{error && <ErrorDisplay message={error} onDismiss={() => setError(null)} />}
|
||||||
|
{successMessage && <SuccessDisplay message={successMessage} onDismiss={() => setSuccessMessage(null)} />}
|
||||||
|
|
||||||
|
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={2} paddingY={1}>
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="gray">Status:</Text>
|
||||||
|
<Text color={statusColor} bold>{location.active ? 'AKTIV' : 'INAKTIV'}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="gray">Lagertyp:</Text>
|
||||||
|
<Text>{typeName}</Text>
|
||||||
|
</Box>
|
||||||
|
{location.temperatureRange && (
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="gray">Temperatur:</Text>
|
||||||
|
<Text>{location.temperatureRange.minTemperature}°C – {location.temperatureRange.maxTemperature}°C</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{mode === 'confirm-status' && (
|
||||||
|
<ConfirmDialog
|
||||||
|
message={location.active ? 'Lagerort deaktivieren?' : 'Lagerort aktivieren?'}
|
||||||
|
onConfirm={() => void handleToggleStatus()}
|
||||||
|
onCancel={() => setMode('menu')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mode === 'menu' && (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text color="gray" bold>Aktionen:</Text>
|
||||||
|
{actionLoading && <LoadingSpinner label="Aktion wird ausgeführt..." />}
|
||||||
|
{!actionLoading && MENU_ITEMS.map((item, index) => (
|
||||||
|
<Box key={item.id}>
|
||||||
|
<Text color={index === selectedAction ? 'cyan' : 'white'}>
|
||||||
|
{index === selectedAction ? '▶ ' : ' '}{item.label(location)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text color="gray" dimColor>↑↓ navigieren · Enter ausführen · Backspace Zurück</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Box, Text, useInput } from 'ink';
|
||||||
|
import { useNavigation } from '../../state/navigation-context.js';
|
||||||
|
import { useStorageLocations } from '../../hooks/useStorageLocations.js';
|
||||||
|
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||||
|
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||||
|
import { STORAGE_TYPE_LABELS } from '@effigenix/api-client';
|
||||||
|
import type { StorageType, StorageLocationFilter } from '@effigenix/api-client';
|
||||||
|
|
||||||
|
type Filter = 'ALL' | 'ACTIVE' | 'INACTIVE';
|
||||||
|
|
||||||
|
const STORAGE_TYPES: StorageType[] = ['COLD_ROOM', 'FREEZER', 'DRY_STORAGE', 'DISPLAY_COUNTER', 'PRODUCTION_AREA'];
|
||||||
|
|
||||||
|
export function StorageLocationListScreen() {
|
||||||
|
const { navigate, back } = useNavigation();
|
||||||
|
const { storageLocations, loading, error, fetchStorageLocations, clearError } = useStorageLocations();
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
const [statusFilter, setStatusFilter] = useState<Filter>('ALL');
|
||||||
|
const [typeFilter, setTypeFilter] = useState<StorageType | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filter: StorageLocationFilter = {};
|
||||||
|
if (typeFilter) filter.storageType = typeFilter;
|
||||||
|
if (statusFilter !== 'ALL') filter.active = statusFilter === 'ACTIVE';
|
||||||
|
void fetchStorageLocations(filter);
|
||||||
|
}, [fetchStorageLocations, statusFilter, typeFilter]);
|
||||||
|
|
||||||
|
useInput((input, key) => {
|
||||||
|
if (loading) return;
|
||||||
|
|
||||||
|
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
||||||
|
if (key.downArrow) setSelectedIndex((i) => Math.min(storageLocations.length - 1, i + 1));
|
||||||
|
|
||||||
|
if (key.return && storageLocations.length > 0) {
|
||||||
|
const loc = storageLocations[selectedIndex];
|
||||||
|
if (loc) navigate('storage-location-detail', { storageLocationId: loc.id });
|
||||||
|
}
|
||||||
|
if (input === 'n') navigate('storage-location-create');
|
||||||
|
if (input === 'a') { setStatusFilter('ALL'); setSelectedIndex(0); }
|
||||||
|
if (input === 'A') { setStatusFilter('ACTIVE'); setSelectedIndex(0); }
|
||||||
|
if (input === 'I') { setStatusFilter('INACTIVE'); setSelectedIndex(0); }
|
||||||
|
if (input === 't') {
|
||||||
|
setTypeFilter((current) => {
|
||||||
|
if (!current) return STORAGE_TYPES[0] ?? null;
|
||||||
|
const idx = STORAGE_TYPES.indexOf(current);
|
||||||
|
if (idx >= STORAGE_TYPES.length - 1) return null;
|
||||||
|
return STORAGE_TYPES[idx + 1] ?? null;
|
||||||
|
});
|
||||||
|
setSelectedIndex(0);
|
||||||
|
}
|
||||||
|
if (key.backspace || key.escape) back();
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterLabel: Record<Filter, string> = { ALL: 'Alle', ACTIVE: 'Aktiv', INACTIVE: 'Inaktiv' };
|
||||||
|
const typeLabel = typeFilter ? STORAGE_TYPE_LABELS[typeFilter] : 'Alle';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="cyan" bold>Lagerorte</Text>
|
||||||
|
<Text color="gray" dimColor>
|
||||||
|
Status: <Text color="yellow">{filterLabel[statusFilter]}</Text>
|
||||||
|
{' · '}Typ: <Text color="yellow">{typeLabel}</Text>
|
||||||
|
{' '}({storageLocations.length})
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{loading && <LoadingSpinner label="Lade Lagerorte..." />}
|
||||||
|
{error && !loading && <ErrorDisplay message={error} onDismiss={clearError} />}
|
||||||
|
|
||||||
|
{!loading && !error && (
|
||||||
|
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
|
||||||
|
<Box paddingX={1}>
|
||||||
|
<Text color="gray" bold>{' Status Name'.padEnd(30)}</Text>
|
||||||
|
<Text color="gray" bold>{'Typ'.padEnd(20)}</Text>
|
||||||
|
<Text color="gray" bold>Temperatur</Text>
|
||||||
|
</Box>
|
||||||
|
{storageLocations.length === 0 && (
|
||||||
|
<Box paddingX={1} paddingY={1}>
|
||||||
|
<Text color="gray" dimColor>Keine Lagerorte gefunden.</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{storageLocations.map((loc, index) => {
|
||||||
|
const isSelected = index === selectedIndex;
|
||||||
|
const statusColor = loc.active ? 'green' : 'red';
|
||||||
|
const textColor = isSelected ? 'cyan' : 'white';
|
||||||
|
const typeName = STORAGE_TYPE_LABELS[loc.storageType as StorageType] ?? loc.storageType;
|
||||||
|
const tempRange = loc.temperatureRange
|
||||||
|
? `${loc.temperatureRange.minTemperature}°C – ${loc.temperatureRange.maxTemperature}°C`
|
||||||
|
: '–';
|
||||||
|
return (
|
||||||
|
<Box key={loc.id} paddingX={1}>
|
||||||
|
<Text color={textColor}>{isSelected ? '▶ ' : ' '}</Text>
|
||||||
|
<Text color={statusColor}>{loc.active ? '● ' : '○ '}</Text>
|
||||||
|
<Text color={textColor}>{loc.name.substring(0, 24).padEnd(25)}</Text>
|
||||||
|
<Text color={isSelected ? 'cyan' : 'gray'}>{typeName.padEnd(20)}</Text>
|
||||||
|
<Text color={isSelected ? 'cyan' : 'gray'}>{tempRange}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text color="gray" dimColor>
|
||||||
|
↑↓ nav · Enter Details · [n] Neu · [a] Alle · [A] Aktiv · [I] Inaktiv · [t] Typ · Backspace Zurück
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Box, Text, useInput } from 'ink';
|
||||||
|
import { useNavigation } from '../../state/navigation-context.js';
|
||||||
|
import type { Screen } from '../../state/navigation-context.js';
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
label: string;
|
||||||
|
screen: Screen;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MENU_ITEMS: MenuItem[] = [
|
||||||
|
{ label: 'Rezepte', screen: 'recipe-list', description: 'Rezepte anlegen und verwalten' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ProductionMenu() {
|
||||||
|
const { navigate, back } = useNavigation();
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
useInput((_input, key) => {
|
||||||
|
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
||||||
|
if (key.downArrow) setSelectedIndex((i) => Math.min(MENU_ITEMS.length - 1, i + 1));
|
||||||
|
if (key.return) {
|
||||||
|
const item = MENU_ITEMS[selectedIndex];
|
||||||
|
if (item) navigate(item.screen);
|
||||||
|
}
|
||||||
|
if (key.backspace || key.escape) back();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" paddingY={1}>
|
||||||
|
<Box marginBottom={1}>
|
||||||
|
<Text color="cyan" bold>Produktion</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
borderStyle="round"
|
||||||
|
borderColor="gray"
|
||||||
|
paddingX={2}
|
||||||
|
paddingY={1}
|
||||||
|
width={50}
|
||||||
|
>
|
||||||
|
{MENU_ITEMS.map((item, index) => (
|
||||||
|
<Box key={item.screen} flexDirection="column">
|
||||||
|
<Text color={index === selectedIndex ? 'cyan' : 'white'}>
|
||||||
|
{index === selectedIndex ? '▶ ' : ' '}
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
{index === selectedIndex && (
|
||||||
|
<Box paddingLeft={4}>
|
||||||
|
<Text color="gray" dimColor>{item.description}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text color="gray" dimColor>↑↓ navigieren · Enter auswählen · Backspace Zurück</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Box, Text, useInput } from 'ink';
|
||||||
|
import { useNavigation } from '../../state/navigation-context.js';
|
||||||
|
import { useRecipes } from '../../hooks/useRecipes.js';
|
||||||
|
import { FormInput } from '../shared/FormInput.js';
|
||||||
|
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||||
|
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||||
|
import { RECIPE_TYPE_LABELS } from '@effigenix/api-client';
|
||||||
|
import type { RecipeType } from '@effigenix/api-client';
|
||||||
|
|
||||||
|
type Field = 'name' | 'version' | 'type' | 'description' | 'yieldPercentage' | 'shelfLifeDays' | 'outputQuantity' | 'outputUom';
|
||||||
|
const FIELDS: Field[] = ['name', 'version', 'type', 'description', 'yieldPercentage', 'shelfLifeDays', 'outputQuantity', 'outputUom'];
|
||||||
|
|
||||||
|
const FIELD_LABELS: Record<Field, string> = {
|
||||||
|
name: 'Name *',
|
||||||
|
version: 'Version *',
|
||||||
|
type: 'Rezepttyp * (←→ wechseln)',
|
||||||
|
description: 'Beschreibung',
|
||||||
|
yieldPercentage: 'Ausbeute (%) *',
|
||||||
|
shelfLifeDays: 'Haltbarkeit (Tage)',
|
||||||
|
outputQuantity: 'Ausgabemenge *',
|
||||||
|
outputUom: 'Mengeneinheit *',
|
||||||
|
};
|
||||||
|
|
||||||
|
const RECIPE_TYPES: RecipeType[] = ['RAW_MATERIAL', 'INTERMEDIATE', 'FINISHED_PRODUCT'];
|
||||||
|
|
||||||
|
export function RecipeCreateScreen() {
|
||||||
|
const { navigate, back } = useNavigation();
|
||||||
|
const { createRecipe, loading, error, clearError } = useRecipes();
|
||||||
|
|
||||||
|
const [values, setValues] = useState<Record<Field, string>>({
|
||||||
|
name: '',
|
||||||
|
version: '1',
|
||||||
|
type: 'FINISHED_PRODUCT',
|
||||||
|
description: '',
|
||||||
|
yieldPercentage: '100',
|
||||||
|
shelfLifeDays: '',
|
||||||
|
outputQuantity: '',
|
||||||
|
outputUom: '',
|
||||||
|
});
|
||||||
|
const [activeField, setActiveField] = useState<Field>('name');
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<Partial<Record<Field, string>>>({});
|
||||||
|
|
||||||
|
const setField = (field: Field) => (value: string) => {
|
||||||
|
setValues((v) => ({ ...v, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
useInput((input, key) => {
|
||||||
|
if (loading) return;
|
||||||
|
|
||||||
|
if (activeField === 'type') {
|
||||||
|
if (key.leftArrow || key.rightArrow) {
|
||||||
|
const idx = RECIPE_TYPES.indexOf(values.type as RecipeType);
|
||||||
|
const dir = key.rightArrow ? 1 : -1;
|
||||||
|
const next = RECIPE_TYPES[(idx + dir + RECIPE_TYPES.length) % RECIPE_TYPES.length];
|
||||||
|
if (next) setValues((v) => ({ ...v, type: next }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.tab || key.downArrow) {
|
||||||
|
setActiveField((f) => {
|
||||||
|
const idx = FIELDS.indexOf(f);
|
||||||
|
return FIELDS[(idx + 1) % FIELDS.length] ?? f;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (key.upArrow) {
|
||||||
|
setActiveField((f) => {
|
||||||
|
const idx = FIELDS.indexOf(f);
|
||||||
|
return FIELDS[(idx - 1 + FIELDS.length) % FIELDS.length] ?? f;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (key.escape) back();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const errors: Partial<Record<Field, string>> = {};
|
||||||
|
if (!values.name.trim()) errors.name = 'Name ist erforderlich.';
|
||||||
|
if (!values.version.trim() || isNaN(parseInt(values.version, 10))) errors.version = 'Version muss eine Zahl sein.';
|
||||||
|
if (!values.type) errors.type = 'Rezepttyp ist erforderlich.';
|
||||||
|
if (!values.yieldPercentage.trim() || isNaN(parseInt(values.yieldPercentage, 10))) errors.yieldPercentage = 'Ausbeute muss eine Zahl sein.';
|
||||||
|
if (!values.outputQuantity.trim()) errors.outputQuantity = 'Ausgabemenge ist erforderlich.';
|
||||||
|
if (!values.outputUom.trim()) errors.outputUom = 'Mengeneinheit ist erforderlich.';
|
||||||
|
setFieldErrors(errors);
|
||||||
|
if (Object.keys(errors).length > 0) return;
|
||||||
|
|
||||||
|
const result = await createRecipe({
|
||||||
|
name: values.name.trim(),
|
||||||
|
version: parseInt(values.version, 10),
|
||||||
|
type: values.type as RecipeType,
|
||||||
|
...(values.description.trim() ? { description: values.description.trim() } : {}),
|
||||||
|
yieldPercentage: parseInt(values.yieldPercentage, 10),
|
||||||
|
...(values.shelfLifeDays.trim() ? { shelfLifeDays: parseInt(values.shelfLifeDays, 10) } : {}),
|
||||||
|
outputQuantity: values.outputQuantity.trim(),
|
||||||
|
outputUom: values.outputUom.trim(),
|
||||||
|
});
|
||||||
|
if (result) navigate('recipe-list');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFieldSubmit = (field: Field) => (_value: string) => {
|
||||||
|
const idx = FIELDS.indexOf(field);
|
||||||
|
if (idx < FIELDS.length - 1) {
|
||||||
|
setActiveField(FIELDS[idx + 1] ?? field);
|
||||||
|
} else {
|
||||||
|
void handleSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" alignItems="center" paddingY={2}>
|
||||||
|
<LoadingSpinner label="Rezept wird angelegt..." />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeLabel = RECIPE_TYPE_LABELS[values.type as RecipeType] ?? values.type;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Text color="cyan" bold>Neues Rezept</Text>
|
||||||
|
{error && <ErrorDisplay message={error} onDismiss={clearError} />}
|
||||||
|
|
||||||
|
<Box flexDirection="column" gap={1} width={60}>
|
||||||
|
{FIELDS.map((field) => {
|
||||||
|
if (field === 'type') {
|
||||||
|
return (
|
||||||
|
<Box key={field} flexDirection="column">
|
||||||
|
<Text color={activeField === field ? 'cyan' : 'gray'}>
|
||||||
|
{FIELD_LABELS[field]}: <Text bold color="white">{typeLabel}</Text>
|
||||||
|
</Text>
|
||||||
|
{fieldErrors[field] && <Text color="red">{fieldErrors[field]}</Text>}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormInput
|
||||||
|
key={field}
|
||||||
|
label={FIELD_LABELS[field]}
|
||||||
|
value={values[field]}
|
||||||
|
onChange={setField(field)}
|
||||||
|
onSubmit={handleFieldSubmit(field)}
|
||||||
|
focus={activeField === field}
|
||||||
|
{...(fieldErrors[field] ? { error: fieldErrors[field] } : {})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text color="gray" dimColor>
|
||||||
|
Tab/↑↓ Feld wechseln · ←→ Rezepttyp · Enter auf letztem Feld speichern · Escape Abbrechen
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Box, Text, useInput } from 'ink';
|
||||||
|
import type { RecipeDTO, RecipeType } from '@effigenix/api-client';
|
||||||
|
import { RECIPE_TYPE_LABELS } from '@effigenix/api-client';
|
||||||
|
import { useNavigation } from '../../state/navigation-context.js';
|
||||||
|
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||||
|
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||||
|
import { client } from '../../utils/api-client.js';
|
||||||
|
|
||||||
|
function errorMessage(err: unknown): string {
|
||||||
|
return err instanceof Error ? err.message : 'Unbekannter Fehler';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecipeDetailScreen() {
|
||||||
|
const { params, back } = useNavigation();
|
||||||
|
const recipeId = params['recipeId'] ?? '';
|
||||||
|
|
||||||
|
const [recipe, setRecipe] = useState<RecipeDTO | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadRecipe = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
client.recipes.getById(recipeId)
|
||||||
|
.then((r) => { setRecipe(r); setLoading(false); })
|
||||||
|
.catch((err: unknown) => { setError(errorMessage(err)); setLoading(false); });
|
||||||
|
}, [recipeId]);
|
||||||
|
|
||||||
|
useEffect(() => { if (recipeId) loadRecipe(); }, [loadRecipe, recipeId]);
|
||||||
|
|
||||||
|
useInput((_input, key) => {
|
||||||
|
if (loading) return;
|
||||||
|
if (key.backspace || key.escape) back();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) return <LoadingSpinner label="Lade Rezept..." />;
|
||||||
|
if (error && !recipe) return <ErrorDisplay message={error} onDismiss={back} />;
|
||||||
|
if (!recipe) return <Text color="red">Rezept nicht gefunden.</Text>;
|
||||||
|
|
||||||
|
const typeName = RECIPE_TYPE_LABELS[recipe.type as RecipeType] ?? recipe.type;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Text color="cyan" bold>Rezept: {recipe.name}</Text>
|
||||||
|
|
||||||
|
{error && <ErrorDisplay message={error} onDismiss={() => setError(null)} />}
|
||||||
|
|
||||||
|
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={2} paddingY={1}>
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="gray">Status:</Text>
|
||||||
|
<Text bold>{recipe.status}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="gray">Typ:</Text>
|
||||||
|
<Text>{typeName}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="gray">Version:</Text>
|
||||||
|
<Text>{recipe.version}</Text>
|
||||||
|
</Box>
|
||||||
|
{recipe.description && (
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="gray">Beschreibung:</Text>
|
||||||
|
<Text>{recipe.description}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="gray">Ausbeute:</Text>
|
||||||
|
<Text>{recipe.yieldPercentage}%</Text>
|
||||||
|
</Box>
|
||||||
|
{recipe.shelfLifeDays !== null && (
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="gray">Haltbarkeit:</Text>
|
||||||
|
<Text>{recipe.shelfLifeDays} Tage</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="gray">Ausgabemenge:</Text>
|
||||||
|
<Text>{recipe.outputQuantity} {recipe.outputUom}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{recipe.ingredients.length > 0 && (
|
||||||
|
<Box flexDirection="column" marginTop={1}>
|
||||||
|
<Text color="gray">Zutaten:</Text>
|
||||||
|
{recipe.ingredients.map((ing) => (
|
||||||
|
<Box key={ing.id} paddingLeft={2} gap={1}>
|
||||||
|
<Text color="yellow">{ing.position}.</Text>
|
||||||
|
<Text>{ing.quantity} {ing.uom}</Text>
|
||||||
|
<Text color="gray">(Artikel: {ing.articleId})</Text>
|
||||||
|
{ing.substitutable && <Text color="green">[austauschbar]</Text>}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text color="gray" dimColor>Backspace Zurück</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Box, Text, useInput } from 'ink';
|
||||||
|
import { useNavigation } from '../../state/navigation-context.js';
|
||||||
|
import { useRecipes } from '../../hooks/useRecipes.js';
|
||||||
|
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||||
|
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||||
|
import { RECIPE_TYPE_LABELS } from '@effigenix/api-client';
|
||||||
|
import type { RecipeType } from '@effigenix/api-client';
|
||||||
|
|
||||||
|
export function RecipeListScreen() {
|
||||||
|
const { navigate, back } = useNavigation();
|
||||||
|
const { recipes, loading, error, fetchRecipes, clearError } = useRecipes();
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void fetchRecipes();
|
||||||
|
}, [fetchRecipes]);
|
||||||
|
|
||||||
|
useInput((input, key) => {
|
||||||
|
if (loading) return;
|
||||||
|
|
||||||
|
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
||||||
|
if (key.downArrow) setSelectedIndex((i) => Math.min(recipes.length - 1, i + 1));
|
||||||
|
|
||||||
|
if (key.return && recipes.length > 0) {
|
||||||
|
const recipe = recipes[selectedIndex];
|
||||||
|
if (recipe) navigate('recipe-detail', { recipeId: recipe.id });
|
||||||
|
}
|
||||||
|
if (input === 'n') navigate('recipe-create');
|
||||||
|
if (key.backspace || key.escape) back();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Box gap={2}>
|
||||||
|
<Text color="cyan" bold>Rezepte</Text>
|
||||||
|
<Text color="gray" dimColor>({recipes.length})</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{loading && <LoadingSpinner label="Lade Rezepte..." />}
|
||||||
|
{error && !loading && <ErrorDisplay message={error} onDismiss={clearError} />}
|
||||||
|
|
||||||
|
{!loading && !error && (
|
||||||
|
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
|
||||||
|
<Box paddingX={1}>
|
||||||
|
<Text color="gray" bold>{' Name'.padEnd(30)}</Text>
|
||||||
|
<Text color="gray" bold>{'Typ'.padEnd(18)}</Text>
|
||||||
|
<Text color="gray" bold>{'V.'.padEnd(5)}</Text>
|
||||||
|
<Text color="gray" bold>Status</Text>
|
||||||
|
</Box>
|
||||||
|
{recipes.length === 0 && (
|
||||||
|
<Box paddingX={1} paddingY={1}>
|
||||||
|
<Text color="gray" dimColor>Keine Rezepte gefunden.</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{recipes.map((recipe, index) => {
|
||||||
|
const isSelected = index === selectedIndex;
|
||||||
|
const textColor = isSelected ? 'cyan' : 'white';
|
||||||
|
const typeName = RECIPE_TYPE_LABELS[recipe.type as RecipeType] ?? recipe.type;
|
||||||
|
return (
|
||||||
|
<Box key={recipe.id} paddingX={1}>
|
||||||
|
<Text color={textColor}>{isSelected ? '▶ ' : ' '}</Text>
|
||||||
|
<Text color={textColor}>{recipe.name.substring(0, 26).padEnd(27)}</Text>
|
||||||
|
<Text color={isSelected ? 'cyan' : 'gray'}>{typeName.padEnd(18)}</Text>
|
||||||
|
<Text color={isSelected ? 'cyan' : 'gray'}>{String(recipe.version).padEnd(5)}</Text>
|
||||||
|
<Text color={isSelected ? 'cyan' : 'gray'}>{recipe.status}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text color="gray" dimColor>
|
||||||
|
↑↓ nav · Enter Details · [n] Neu · Backspace Zurück
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
frontend/apps/cli/src/hooks/useRecipes.ts
Normal file
54
frontend/apps/cli/src/hooks/useRecipes.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import type { RecipeDTO, CreateRecipeRequest } from '@effigenix/api-client';
|
||||||
|
import { client } from '../utils/api-client.js';
|
||||||
|
|
||||||
|
interface RecipesState {
|
||||||
|
recipes: RecipeDTO[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorMessage(err: unknown): string {
|
||||||
|
return err instanceof Error ? err.message : 'Unbekannter Fehler';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRecipes() {
|
||||||
|
const [state, setState] = useState<RecipesState>({
|
||||||
|
recipes: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchRecipes = useCallback(async () => {
|
||||||
|
setState((s) => ({ ...s, loading: true, error: null }));
|
||||||
|
try {
|
||||||
|
const recipes = await client.recipes.list();
|
||||||
|
setState({ recipes, loading: false, error: null });
|
||||||
|
} catch (err) {
|
||||||
|
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const createRecipe = useCallback(async (request: CreateRecipeRequest) => {
|
||||||
|
setState((s) => ({ ...s, loading: true, error: null }));
|
||||||
|
try {
|
||||||
|
const recipe = await client.recipes.create(request);
|
||||||
|
setState((s) => ({ recipes: [...s.recipes, recipe], loading: false, error: null }));
|
||||||
|
return recipe;
|
||||||
|
} catch (err) {
|
||||||
|
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearError = useCallback(() => {
|
||||||
|
setState((s) => ({ ...s, error: null }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
fetchRecipes,
|
||||||
|
createRecipe,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
}
|
||||||
104
frontend/apps/cli/src/hooks/useStorageLocations.ts
Normal file
104
frontend/apps/cli/src/hooks/useStorageLocations.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import type {
|
||||||
|
StorageLocationDTO,
|
||||||
|
CreateStorageLocationRequest,
|
||||||
|
UpdateStorageLocationRequest,
|
||||||
|
StorageLocationFilter,
|
||||||
|
} from '@effigenix/api-client';
|
||||||
|
import { client } from '../utils/api-client.js';
|
||||||
|
|
||||||
|
interface StorageLocationsState {
|
||||||
|
storageLocations: StorageLocationDTO[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorMessage(err: unknown): string {
|
||||||
|
return err instanceof Error ? err.message : 'Unbekannter Fehler';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStorageLocations() {
|
||||||
|
const [state, setState] = useState<StorageLocationsState>({
|
||||||
|
storageLocations: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchStorageLocations = useCallback(async (filter?: StorageLocationFilter) => {
|
||||||
|
setState((s) => ({ ...s, loading: true, error: null }));
|
||||||
|
try {
|
||||||
|
const storageLocations = await client.storageLocations.list(filter);
|
||||||
|
setState({ storageLocations, loading: false, error: null });
|
||||||
|
} catch (err) {
|
||||||
|
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const createStorageLocation = useCallback(async (request: CreateStorageLocationRequest) => {
|
||||||
|
setState((s) => ({ ...s, loading: true, error: null }));
|
||||||
|
try {
|
||||||
|
const location = await client.storageLocations.create(request);
|
||||||
|
setState((s) => ({ storageLocations: [...s.storageLocations, location], loading: false, error: null }));
|
||||||
|
return location;
|
||||||
|
} catch (err) {
|
||||||
|
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateStorageLocation = useCallback(async (id: string, request: UpdateStorageLocationRequest) => {
|
||||||
|
try {
|
||||||
|
const updated = await client.storageLocations.update(id, request);
|
||||||
|
setState((s) => ({
|
||||||
|
...s,
|
||||||
|
storageLocations: s.storageLocations.map((loc) => (loc.id === id ? updated : loc)),
|
||||||
|
}));
|
||||||
|
return updated;
|
||||||
|
} catch (err) {
|
||||||
|
setState((s) => ({ ...s, error: errorMessage(err) }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const activateStorageLocation = useCallback(async (id: string) => {
|
||||||
|
try {
|
||||||
|
const updated = await client.storageLocations.activate(id);
|
||||||
|
setState((s) => ({
|
||||||
|
...s,
|
||||||
|
storageLocations: s.storageLocations.map((loc) => (loc.id === id ? updated : loc)),
|
||||||
|
}));
|
||||||
|
return updated;
|
||||||
|
} catch (err) {
|
||||||
|
setState((s) => ({ ...s, error: errorMessage(err) }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deactivateStorageLocation = useCallback(async (id: string) => {
|
||||||
|
try {
|
||||||
|
const updated = await client.storageLocations.deactivate(id);
|
||||||
|
setState((s) => ({
|
||||||
|
...s,
|
||||||
|
storageLocations: s.storageLocations.map((loc) => (loc.id === id ? updated : loc)),
|
||||||
|
}));
|
||||||
|
return updated;
|
||||||
|
} catch (err) {
|
||||||
|
setState((s) => ({ ...s, error: errorMessage(err) }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearError = useCallback(() => {
|
||||||
|
setState((s) => ({ ...s, error: null }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
fetchStorageLocations,
|
||||||
|
createStorageLocation,
|
||||||
|
updateStorageLocation,
|
||||||
|
activateStorageLocation,
|
||||||
|
deactivateStorageLocation,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -27,7 +27,17 @@ export type Screen =
|
||||||
| 'customer-detail'
|
| 'customer-detail'
|
||||||
| 'customer-create'
|
| 'customer-create'
|
||||||
| 'customer-add-delivery-address'
|
| 'customer-add-delivery-address'
|
||||||
| 'customer-set-preferences';
|
| 'customer-set-preferences'
|
||||||
|
// Lagerverwaltung
|
||||||
|
| 'inventory-menu'
|
||||||
|
| 'storage-location-list'
|
||||||
|
| 'storage-location-create'
|
||||||
|
| 'storage-location-detail'
|
||||||
|
// Produktion
|
||||||
|
| 'production-menu'
|
||||||
|
| 'recipe-list'
|
||||||
|
| 'recipe-create'
|
||||||
|
| 'recipe-detail';
|
||||||
|
|
||||||
interface NavigationState {
|
interface NavigationState {
|
||||||
current: Screen;
|
current: Screen;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -22,6 +22,8 @@ export { createCategoriesResource } from './resources/categories.js';
|
||||||
export { createSuppliersResource } from './resources/suppliers.js';
|
export { createSuppliersResource } from './resources/suppliers.js';
|
||||||
export { createArticlesResource } from './resources/articles.js';
|
export { createArticlesResource } from './resources/articles.js';
|
||||||
export { createCustomersResource } from './resources/customers.js';
|
export { createCustomersResource } from './resources/customers.js';
|
||||||
|
export { createStorageLocationsResource } from './resources/storage-locations.js';
|
||||||
|
export { createRecipesResource } from './resources/recipes.js';
|
||||||
export {
|
export {
|
||||||
ApiError,
|
ApiError,
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
|
|
@ -29,12 +31,15 @@ export {
|
||||||
RefreshTokenExpiredError,
|
RefreshTokenExpiredError,
|
||||||
} from './errors.js';
|
} from './errors.js';
|
||||||
export type { ValidationErrorDetail } from './errors.js';
|
export type { ValidationErrorDetail } from './errors.js';
|
||||||
|
// Auth types (no generated alias, stay in resource)
|
||||||
export type {
|
export type {
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
RefreshTokenRequest,
|
RefreshTokenRequest,
|
||||||
AuthResource,
|
AuthResource,
|
||||||
} from './resources/auth.js';
|
} from './resources/auth.js';
|
||||||
|
|
||||||
|
// Types from @effigenix/types (generated OpenAPI aliases)
|
||||||
export type {
|
export type {
|
||||||
UserDTO,
|
UserDTO,
|
||||||
RoleDTO,
|
RoleDTO,
|
||||||
|
|
@ -42,18 +47,7 @@ export type {
|
||||||
UpdateUserRequest,
|
UpdateUserRequest,
|
||||||
ChangePasswordRequest,
|
ChangePasswordRequest,
|
||||||
AssignRoleRequest,
|
AssignRoleRequest,
|
||||||
UsersResource,
|
|
||||||
} from './resources/users.js';
|
|
||||||
export type { RolesResource } from './resources/roles.js';
|
|
||||||
export type {
|
|
||||||
ProductCategoryDTO,
|
|
||||||
CreateCategoryRequest,
|
|
||||||
UpdateCategoryRequest,
|
|
||||||
CategoriesResource,
|
|
||||||
} from './resources/categories.js';
|
|
||||||
export type {
|
|
||||||
SupplierDTO,
|
SupplierDTO,
|
||||||
SupplierStatus,
|
|
||||||
AddressDTO,
|
AddressDTO,
|
||||||
ContactInfoDTO,
|
ContactInfoDTO,
|
||||||
PaymentTermsDTO,
|
PaymentTermsDTO,
|
||||||
|
|
@ -64,36 +58,57 @@ export type {
|
||||||
RateSupplierRequest,
|
RateSupplierRequest,
|
||||||
AddCertificateRequest,
|
AddCertificateRequest,
|
||||||
RemoveCertificateRequest,
|
RemoveCertificateRequest,
|
||||||
SuppliersResource,
|
ProductCategoryDTO,
|
||||||
} from './resources/suppliers.js';
|
CreateCategoryRequest,
|
||||||
export type {
|
UpdateCategoryRequest,
|
||||||
ArticleDTO,
|
ArticleDTO,
|
||||||
ArticleStatus,
|
|
||||||
SalesUnitDTO,
|
SalesUnitDTO,
|
||||||
Unit,
|
|
||||||
PriceModel,
|
|
||||||
CreateArticleRequest,
|
CreateArticleRequest,
|
||||||
UpdateArticleRequest,
|
UpdateArticleRequest,
|
||||||
AddSalesUnitRequest,
|
AddSalesUnitRequest,
|
||||||
UpdateSalesUnitPriceRequest,
|
UpdateSalesUnitPriceRequest,
|
||||||
ArticlesResource,
|
|
||||||
} from './resources/articles.js';
|
|
||||||
export { UNIT_LABELS, PRICE_MODEL_LABELS } from './resources/articles.js';
|
|
||||||
export type {
|
|
||||||
CustomerDTO,
|
CustomerDTO,
|
||||||
CustomerType,
|
|
||||||
CustomerStatus,
|
|
||||||
CustomerPreference,
|
|
||||||
DeliveryRhythm,
|
|
||||||
DeliveryAddressDTO,
|
DeliveryAddressDTO,
|
||||||
FrameContractDTO,
|
FrameContractDTO,
|
||||||
ContractLineItemDTO,
|
ContractLineItemDTO,
|
||||||
CreateCustomerRequest,
|
CreateCustomerRequest,
|
||||||
UpdateCustomerRequest,
|
UpdateCustomerRequest,
|
||||||
AddDeliveryAddressRequest,
|
AddDeliveryAddressRequest,
|
||||||
|
SetFrameContractLineItem,
|
||||||
|
SetFrameContractRequest,
|
||||||
|
StorageLocationDTO,
|
||||||
|
TemperatureRangeDTO,
|
||||||
|
CreateStorageLocationRequest,
|
||||||
|
UpdateStorageLocationRequest,
|
||||||
|
RecipeDTO,
|
||||||
|
IngredientDTO,
|
||||||
|
CreateRecipeRequest,
|
||||||
|
AddRecipeIngredientRequest,
|
||||||
|
} from '@effigenix/types';
|
||||||
|
|
||||||
|
// Resource types (runtime, stay in resource files)
|
||||||
|
export type { UsersResource } from './resources/users.js';
|
||||||
|
export type { RolesResource } from './resources/roles.js';
|
||||||
|
export type { CategoriesResource } from './resources/categories.js';
|
||||||
|
export type { SuppliersResource, SupplierStatus } from './resources/suppliers.js';
|
||||||
|
export type { ArticlesResource, ArticleStatus, Unit, PriceModel } from './resources/articles.js';
|
||||||
|
export { UNIT_LABELS, PRICE_MODEL_LABELS } from './resources/articles.js';
|
||||||
|
export type {
|
||||||
CustomersResource,
|
CustomersResource,
|
||||||
|
CustomerType,
|
||||||
|
CustomerStatus,
|
||||||
|
CustomerPreference,
|
||||||
|
DeliveryRhythm,
|
||||||
} from './resources/customers.js';
|
} from './resources/customers.js';
|
||||||
export { CUSTOMER_PREFERENCE_LABELS, DELIVERY_RHYTHM_LABELS } from './resources/customers.js';
|
export { CUSTOMER_PREFERENCE_LABELS, DELIVERY_RHYTHM_LABELS } from './resources/customers.js';
|
||||||
|
export type {
|
||||||
|
StorageLocationsResource,
|
||||||
|
StorageType,
|
||||||
|
StorageLocationFilter,
|
||||||
|
} from './resources/storage-locations.js';
|
||||||
|
export { STORAGE_TYPE_LABELS } from './resources/storage-locations.js';
|
||||||
|
export type { RecipesResource, RecipeType, RecipeStatus } from './resources/recipes.js';
|
||||||
|
export { RECIPE_TYPE_LABELS } from './resources/recipes.js';
|
||||||
|
|
||||||
import { createApiClient } from './client.js';
|
import { createApiClient } from './client.js';
|
||||||
import { createAuthResource } from './resources/auth.js';
|
import { createAuthResource } from './resources/auth.js';
|
||||||
|
|
@ -103,6 +118,8 @@ import { createCategoriesResource } from './resources/categories.js';
|
||||||
import { createSuppliersResource } from './resources/suppliers.js';
|
import { createSuppliersResource } from './resources/suppliers.js';
|
||||||
import { createArticlesResource } from './resources/articles.js';
|
import { createArticlesResource } from './resources/articles.js';
|
||||||
import { createCustomersResource } from './resources/customers.js';
|
import { createCustomersResource } from './resources/customers.js';
|
||||||
|
import { createStorageLocationsResource } from './resources/storage-locations.js';
|
||||||
|
import { createRecipesResource } from './resources/recipes.js';
|
||||||
import type { TokenProvider } from './token-provider.js';
|
import type { TokenProvider } from './token-provider.js';
|
||||||
import type { ApiConfig } from '@effigenix/config';
|
import type { ApiConfig } from '@effigenix/config';
|
||||||
|
|
||||||
|
|
@ -124,6 +141,8 @@ export function createEffigenixClient(
|
||||||
suppliers: createSuppliersResource(axiosClient),
|
suppliers: createSuppliersResource(axiosClient),
|
||||||
articles: createArticlesResource(axiosClient),
|
articles: createArticlesResource(axiosClient),
|
||||||
customers: createCustomersResource(axiosClient),
|
customers: createCustomersResource(axiosClient),
|
||||||
|
storageLocations: createStorageLocationsResource(axiosClient),
|
||||||
|
recipes: createRecipesResource(axiosClient),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AxiosInstance } from 'axios';
|
import type { AxiosInstance } from 'axios';
|
||||||
|
import type {
|
||||||
|
ArticleDTO,
|
||||||
|
SalesUnitDTO,
|
||||||
|
CreateArticleRequest,
|
||||||
|
UpdateArticleRequest,
|
||||||
|
AddSalesUnitRequest,
|
||||||
|
UpdateSalesUnitPriceRequest,
|
||||||
|
} from '@effigenix/types';
|
||||||
|
|
||||||
export type Unit = 'PIECE_FIXED' | 'KG' | 'HUNDRED_GRAM' | 'PIECE_VARIABLE';
|
export type Unit = 'PIECE_FIXED' | 'KG' | 'HUNDRED_GRAM' | 'PIECE_VARIABLE';
|
||||||
export type PriceModel = 'FIXED' | 'WEIGHT_BASED';
|
export type PriceModel = 'FIXED' | 'WEIGHT_BASED';
|
||||||
|
|
@ -27,48 +35,14 @@ export const PRICE_MODEL_LABELS: Record<PriceModel, string> = {
|
||||||
WEIGHT_BASED: 'Gewichtsbasiert',
|
WEIGHT_BASED: 'Gewichtsbasiert',
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SalesUnitDTO {
|
export type {
|
||||||
id: string;
|
ArticleDTO,
|
||||||
unit: Unit;
|
SalesUnitDTO,
|
||||||
priceModel: PriceModel;
|
CreateArticleRequest,
|
||||||
price: number;
|
UpdateArticleRequest,
|
||||||
}
|
AddSalesUnitRequest,
|
||||||
|
UpdateSalesUnitPriceRequest,
|
||||||
export interface ArticleDTO {
|
};
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
articleNumber: string;
|
|
||||||
categoryId: string;
|
|
||||||
salesUnits: SalesUnitDTO[];
|
|
||||||
status: ArticleStatus;
|
|
||||||
supplierIds: string[];
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateArticleRequest {
|
|
||||||
name: string;
|
|
||||||
articleNumber: string;
|
|
||||||
categoryId: string;
|
|
||||||
unit: Unit;
|
|
||||||
priceModel: PriceModel;
|
|
||||||
price: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateArticleRequest {
|
|
||||||
name?: string;
|
|
||||||
categoryId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddSalesUnitRequest {
|
|
||||||
unit: Unit;
|
|
||||||
priceModel: PriceModel;
|
|
||||||
price: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateSalesUnitPriceRequest {
|
|
||||||
price: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Resource factory ─────────────────────────────────────────────────────────
|
// ── Resource factory ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,22 +7,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AxiosInstance } from 'axios';
|
import type { AxiosInstance } from 'axios';
|
||||||
|
import type {
|
||||||
|
ProductCategoryDTO,
|
||||||
|
CreateCategoryRequest,
|
||||||
|
UpdateCategoryRequest,
|
||||||
|
} from '@effigenix/types';
|
||||||
|
|
||||||
export interface ProductCategoryDTO {
|
export type {
|
||||||
id: string;
|
ProductCategoryDTO,
|
||||||
name: string;
|
CreateCategoryRequest,
|
||||||
description: string | null;
|
UpdateCategoryRequest,
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface CreateCategoryRequest {
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateCategoryRequest {
|
|
||||||
name?: string;
|
|
||||||
description?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Resource factory ─────────────────────────────────────────────────────────
|
// ── Resource factory ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AxiosInstance } from 'axios';
|
import type { AxiosInstance } from 'axios';
|
||||||
import type { AddressDTO, ContactInfoDTO, PaymentTermsDTO } from './suppliers.js';
|
import type {
|
||||||
|
CustomerDTO,
|
||||||
|
DeliveryAddressDTO,
|
||||||
|
FrameContractDTO,
|
||||||
|
ContractLineItemDTO,
|
||||||
|
CreateCustomerRequest,
|
||||||
|
UpdateCustomerRequest,
|
||||||
|
AddDeliveryAddressRequest,
|
||||||
|
SetFrameContractRequest,
|
||||||
|
} from '@effigenix/types';
|
||||||
|
|
||||||
export type CustomerType = 'B2B' | 'B2C';
|
export type CustomerType = 'B2B' | 'B2C';
|
||||||
export type CustomerStatus = 'ACTIVE' | 'INACTIVE';
|
export type CustomerStatus = 'ACTIVE' | 'INACTIVE';
|
||||||
|
|
@ -44,96 +53,16 @@ export const DELIVERY_RHYTHM_LABELS: Record<DeliveryRhythm, string> = {
|
||||||
ON_DEMAND: 'Nach Bedarf',
|
ON_DEMAND: 'Nach Bedarf',
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface DeliveryAddressDTO {
|
export type {
|
||||||
label: string;
|
CustomerDTO,
|
||||||
address: AddressDTO;
|
DeliveryAddressDTO,
|
||||||
contactPerson: string | null;
|
FrameContractDTO,
|
||||||
deliveryNotes: string | null;
|
ContractLineItemDTO,
|
||||||
}
|
CreateCustomerRequest,
|
||||||
|
UpdateCustomerRequest,
|
||||||
export interface ContractLineItemDTO {
|
AddDeliveryAddressRequest,
|
||||||
articleId: string;
|
SetFrameContractRequest,
|
||||||
agreedPrice: number;
|
};
|
||||||
agreedQuantity: number | null;
|
|
||||||
unit: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FrameContractDTO {
|
|
||||||
id: string;
|
|
||||||
validFrom: string | null;
|
|
||||||
validUntil: string | null;
|
|
||||||
deliveryRhythm: DeliveryRhythm;
|
|
||||||
lineItems: ContractLineItemDTO[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CustomerDTO {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: CustomerType;
|
|
||||||
status: CustomerStatus;
|
|
||||||
billingAddress: AddressDTO;
|
|
||||||
contactInfo: ContactInfoDTO;
|
|
||||||
paymentTerms: PaymentTermsDTO | null;
|
|
||||||
deliveryAddresses: DeliveryAddressDTO[];
|
|
||||||
frameContract: FrameContractDTO | null;
|
|
||||||
preferences: CustomerPreference[];
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateCustomerRequest {
|
|
||||||
name: string;
|
|
||||||
type: CustomerType;
|
|
||||||
phone: string;
|
|
||||||
street: string;
|
|
||||||
houseNumber: string;
|
|
||||||
postalCode: string;
|
|
||||||
city: string;
|
|
||||||
country: string;
|
|
||||||
email?: string;
|
|
||||||
contactPerson?: string;
|
|
||||||
paymentDueDays?: number;
|
|
||||||
paymentDescription?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateCustomerRequest {
|
|
||||||
name?: string;
|
|
||||||
phone?: string;
|
|
||||||
email?: string | null;
|
|
||||||
contactPerson?: string | null;
|
|
||||||
street?: string;
|
|
||||||
houseNumber?: string;
|
|
||||||
postalCode?: string;
|
|
||||||
city?: string;
|
|
||||||
country?: string;
|
|
||||||
paymentDueDays?: number | null;
|
|
||||||
paymentDescription?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddDeliveryAddressRequest {
|
|
||||||
label: string;
|
|
||||||
street: string;
|
|
||||||
houseNumber: string;
|
|
||||||
postalCode: string;
|
|
||||||
city: string;
|
|
||||||
country: string;
|
|
||||||
contactPerson?: string;
|
|
||||||
deliveryNotes?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SetFrameContractLineItem {
|
|
||||||
articleId: string;
|
|
||||||
agreedPrice: number;
|
|
||||||
agreedQuantity?: number;
|
|
||||||
unit?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SetFrameContractRequest {
|
|
||||||
validFrom?: string;
|
|
||||||
validUntil?: string;
|
|
||||||
rhythm: DeliveryRhythm;
|
|
||||||
lineItems: SetFrameContractLineItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Resource factory ─────────────────────────────────────────────────────────
|
// ── Resource factory ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
66
frontend/packages/api-client/src/resources/recipes.ts
Normal file
66
frontend/packages/api-client/src/resources/recipes.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* Recipes resource – Production BC.
|
||||||
|
* Endpoints: POST /api/recipes,
|
||||||
|
* POST /api/recipes/{id}/ingredients,
|
||||||
|
* DELETE /api/recipes/{id}/ingredients/{ingredientId}
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { AxiosInstance } from 'axios';
|
||||||
|
import type {
|
||||||
|
RecipeDTO,
|
||||||
|
IngredientDTO,
|
||||||
|
CreateRecipeRequest,
|
||||||
|
AddRecipeIngredientRequest,
|
||||||
|
} from '@effigenix/types';
|
||||||
|
|
||||||
|
export type RecipeType = 'RAW_MATERIAL' | 'INTERMEDIATE' | 'FINISHED_PRODUCT';
|
||||||
|
export type RecipeStatus = 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
|
||||||
|
|
||||||
|
export const RECIPE_TYPE_LABELS: Record<RecipeType, string> = {
|
||||||
|
RAW_MATERIAL: 'Rohstoff',
|
||||||
|
INTERMEDIATE: 'Halbfabrikat',
|
||||||
|
FINISHED_PRODUCT: 'Fertigprodukt',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
RecipeDTO,
|
||||||
|
IngredientDTO,
|
||||||
|
CreateRecipeRequest,
|
||||||
|
AddRecipeIngredientRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Resource factory ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const BASE = '/api/recipes';
|
||||||
|
|
||||||
|
export function createRecipesResource(client: AxiosInstance) {
|
||||||
|
return {
|
||||||
|
async list(): Promise<RecipeDTO[]> {
|
||||||
|
const res = await client.get<RecipeDTO[]>(BASE);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getById(id: string): Promise<RecipeDTO> {
|
||||||
|
const res = await client.get<RecipeDTO>(`${BASE}/${id}`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(request: CreateRecipeRequest): Promise<RecipeDTO> {
|
||||||
|
const res = await client.post<RecipeDTO>(BASE, request);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async addIngredient(id: string, request: AddRecipeIngredientRequest): Promise<RecipeDTO> {
|
||||||
|
const res = await client.post<RecipeDTO>(`${BASE}/${id}/ingredients`, request);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeIngredient(recipeId: string, ingredientId: string): Promise<RecipeDTO> {
|
||||||
|
await client.delete(`${BASE}/${recipeId}/ingredients/${ingredientId}`);
|
||||||
|
const res = await client.get<RecipeDTO>(`${BASE}/${recipeId}`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RecipesResource = ReturnType<typeof createRecipesResource>;
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
/**
|
||||||
|
* StorageLocations resource – Inventory BC.
|
||||||
|
* Endpoints: GET/POST /api/inventory/storage-locations,
|
||||||
|
* PUT /api/inventory/storage-locations/{id},
|
||||||
|
* PATCH /api/inventory/storage-locations/{id}/activate|deactivate
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { AxiosInstance } from 'axios';
|
||||||
|
import type {
|
||||||
|
StorageLocationDTO,
|
||||||
|
TemperatureRangeDTO,
|
||||||
|
CreateStorageLocationRequest,
|
||||||
|
UpdateStorageLocationRequest,
|
||||||
|
} from '@effigenix/types';
|
||||||
|
|
||||||
|
export type StorageType = 'COLD_ROOM' | 'FREEZER' | 'DRY_STORAGE' | 'DISPLAY_COUNTER' | 'PRODUCTION_AREA';
|
||||||
|
|
||||||
|
export const STORAGE_TYPE_LABELS: Record<StorageType, string> = {
|
||||||
|
COLD_ROOM: 'Kühlraum',
|
||||||
|
FREEZER: 'Tiefkühler',
|
||||||
|
DRY_STORAGE: 'Trockenlager',
|
||||||
|
DISPLAY_COUNTER: 'Vitrine',
|
||||||
|
PRODUCTION_AREA: 'Produktionsbereich',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
StorageLocationDTO,
|
||||||
|
TemperatureRangeDTO,
|
||||||
|
CreateStorageLocationRequest,
|
||||||
|
UpdateStorageLocationRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface StorageLocationFilter {
|
||||||
|
storageType?: string;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Resource factory ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const BASE = '/api/inventory/storage-locations';
|
||||||
|
|
||||||
|
export function createStorageLocationsResource(client: AxiosInstance) {
|
||||||
|
return {
|
||||||
|
async list(filter?: StorageLocationFilter): Promise<StorageLocationDTO[]> {
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
if (filter?.storageType) params['storageType'] = filter.storageType;
|
||||||
|
if (filter?.active !== undefined) params['active'] = String(filter.active);
|
||||||
|
const res = await client.get<StorageLocationDTO[]>(BASE, { params });
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getById(id: string): Promise<StorageLocationDTO> {
|
||||||
|
const res = await client.get<StorageLocationDTO>(`${BASE}/${id}`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(request: CreateStorageLocationRequest): Promise<StorageLocationDTO> {
|
||||||
|
const res = await client.post<StorageLocationDTO>(BASE, request);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(id: string, request: UpdateStorageLocationRequest): Promise<StorageLocationDTO> {
|
||||||
|
const res = await client.put<StorageLocationDTO>(`${BASE}/${id}`, request);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async activate(id: string): Promise<StorageLocationDTO> {
|
||||||
|
const res = await client.patch<StorageLocationDTO>(`${BASE}/${id}/activate`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deactivate(id: string): Promise<StorageLocationDTO> {
|
||||||
|
const res = await client.patch<StorageLocationDTO>(`${BASE}/${id}/deactivate`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StorageLocationsResource = ReturnType<typeof createStorageLocationsResource>;
|
||||||
|
|
@ -10,100 +10,35 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AxiosInstance } from 'axios';
|
import type { AxiosInstance } from 'axios';
|
||||||
|
import type {
|
||||||
export interface AddressDTO {
|
SupplierDTO,
|
||||||
street: string;
|
AddressDTO,
|
||||||
houseNumber: string | null;
|
ContactInfoDTO,
|
||||||
postalCode: string;
|
PaymentTermsDTO,
|
||||||
city: string;
|
QualityCertificateDTO,
|
||||||
country: string;
|
SupplierRatingDTO,
|
||||||
}
|
CreateSupplierRequest,
|
||||||
|
UpdateSupplierRequest,
|
||||||
export interface ContactInfoDTO {
|
RateSupplierRequest,
|
||||||
phone: string;
|
AddCertificateRequest,
|
||||||
email: string | null;
|
RemoveCertificateRequest,
|
||||||
contactPerson: string | null;
|
} from '@effigenix/types';
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaymentTermsDTO {
|
|
||||||
paymentDueDays: number;
|
|
||||||
paymentDescription: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QualityCertificateDTO {
|
|
||||||
certificateType: string;
|
|
||||||
issuer: string;
|
|
||||||
validFrom: string;
|
|
||||||
validUntil: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SupplierRatingDTO {
|
|
||||||
qualityScore: number;
|
|
||||||
deliveryScore: number;
|
|
||||||
priceScore: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SupplierStatus = 'ACTIVE' | 'INACTIVE';
|
export type SupplierStatus = 'ACTIVE' | 'INACTIVE';
|
||||||
|
|
||||||
export interface SupplierDTO {
|
export type {
|
||||||
id: string;
|
SupplierDTO,
|
||||||
name: string;
|
AddressDTO,
|
||||||
status: SupplierStatus;
|
ContactInfoDTO,
|
||||||
address: AddressDTO | null;
|
PaymentTermsDTO,
|
||||||
contactInfo: ContactInfoDTO;
|
QualityCertificateDTO,
|
||||||
paymentTerms: PaymentTermsDTO | null;
|
SupplierRatingDTO,
|
||||||
certificates: QualityCertificateDTO[];
|
CreateSupplierRequest,
|
||||||
rating: SupplierRatingDTO | null;
|
UpdateSupplierRequest,
|
||||||
createdAt: string;
|
RateSupplierRequest,
|
||||||
updatedAt: string;
|
AddCertificateRequest,
|
||||||
}
|
RemoveCertificateRequest,
|
||||||
|
};
|
||||||
export interface CreateSupplierRequest {
|
|
||||||
name: string;
|
|
||||||
phone: string;
|
|
||||||
email?: string;
|
|
||||||
contactPerson?: string;
|
|
||||||
street?: string;
|
|
||||||
houseNumber?: string;
|
|
||||||
postalCode?: string;
|
|
||||||
city?: string;
|
|
||||||
country?: string;
|
|
||||||
paymentDueDays?: number;
|
|
||||||
paymentDescription?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateSupplierRequest {
|
|
||||||
name?: string;
|
|
||||||
phone?: string;
|
|
||||||
email?: string | null;
|
|
||||||
contactPerson?: string | null;
|
|
||||||
street?: string | null;
|
|
||||||
houseNumber?: string | null;
|
|
||||||
postalCode?: string | null;
|
|
||||||
city?: string | null;
|
|
||||||
country?: string | null;
|
|
||||||
paymentDueDays?: number | null;
|
|
||||||
paymentDescription?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RateSupplierRequest {
|
|
||||||
qualityScore: number;
|
|
||||||
deliveryScore: number;
|
|
||||||
priceScore: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddCertificateRequest {
|
|
||||||
certificateType: string;
|
|
||||||
issuer: string;
|
|
||||||
validFrom: string;
|
|
||||||
validUntil: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RemoveCertificateRequest {
|
|
||||||
certificateType: string;
|
|
||||||
issuer: string;
|
|
||||||
validFrom: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Resource factory ─────────────────────────────────────────────────────────
|
// ── Resource factory ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,45 +4,23 @@
|
||||||
|
|
||||||
import type { AxiosInstance } from 'axios';
|
import type { AxiosInstance } from 'axios';
|
||||||
import { API_PATHS } from '@effigenix/config';
|
import { API_PATHS } from '@effigenix/config';
|
||||||
|
import type {
|
||||||
|
UserDTO,
|
||||||
|
RoleDTO,
|
||||||
|
CreateUserRequest,
|
||||||
|
UpdateUserRequest,
|
||||||
|
ChangePasswordRequest,
|
||||||
|
AssignRoleRequest,
|
||||||
|
} from '@effigenix/types';
|
||||||
|
|
||||||
export interface UserDTO {
|
export type {
|
||||||
id: string;
|
UserDTO,
|
||||||
username: string;
|
RoleDTO,
|
||||||
email: string;
|
CreateUserRequest,
|
||||||
roles: RoleDTO[];
|
UpdateUserRequest,
|
||||||
branchId?: string;
|
ChangePasswordRequest,
|
||||||
status: 'ACTIVE' | 'LOCKED';
|
AssignRoleRequest,
|
||||||
createdAt: string;
|
};
|
||||||
lastLogin?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RoleDTO {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
permissions: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateUserRequest {
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
roleNames: string[];
|
|
||||||
branchId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateUserRequest {
|
|
||||||
email?: string;
|
|
||||||
branchId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChangePasswordRequest {
|
|
||||||
currentPassword: string;
|
|
||||||
newPassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssignRoleRequest {
|
|
||||||
roleName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createUsersResource(client: AxiosInstance) {
|
export function createUsersResource(client: AxiosInstance) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
16
frontend/packages/types/src/article.ts
Normal file
16
frontend/packages/types/src/article.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Article types
|
||||||
|
* Re-exports types from the auto-generated OpenAPI schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { components } from './generated/api';
|
||||||
|
|
||||||
|
// Response DTOs
|
||||||
|
export type ArticleDTO = components['schemas']['ArticleResponse'];
|
||||||
|
export type SalesUnitDTO = components['schemas']['SalesUnitResponse'];
|
||||||
|
|
||||||
|
// Request types
|
||||||
|
export type CreateArticleRequest = components['schemas']['CreateArticleRequest'];
|
||||||
|
export type UpdateArticleRequest = components['schemas']['UpdateArticleRequest'];
|
||||||
|
export type AddSalesUnitRequest = components['schemas']['AddSalesUnitRequest'];
|
||||||
|
export type UpdateSalesUnitPriceRequest = components['schemas']['UpdateSalesUnitPriceRequest'];
|
||||||
13
frontend/packages/types/src/category.ts
Normal file
13
frontend/packages/types/src/category.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* Product Category types
|
||||||
|
* Re-exports types from the auto-generated OpenAPI schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { components } from './generated/api';
|
||||||
|
|
||||||
|
// Response DTOs
|
||||||
|
export type ProductCategoryDTO = components['schemas']['ProductCategoryResponse'];
|
||||||
|
|
||||||
|
// Request types
|
||||||
|
export type CreateCategoryRequest = components['schemas']['CreateProductCategoryRequest'];
|
||||||
|
export type UpdateCategoryRequest = components['schemas']['UpdateProductCategoryRequest'];
|
||||||
19
frontend/packages/types/src/customer.ts
Normal file
19
frontend/packages/types/src/customer.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Customer types
|
||||||
|
* Re-exports types from the auto-generated OpenAPI schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { components } from './generated/api';
|
||||||
|
|
||||||
|
// Response DTOs
|
||||||
|
export type CustomerDTO = components['schemas']['CustomerResponse'];
|
||||||
|
export type DeliveryAddressDTO = components['schemas']['DeliveryAddressResponse'];
|
||||||
|
export type FrameContractDTO = components['schemas']['FrameContractResponse'];
|
||||||
|
export type ContractLineItemDTO = components['schemas']['ContractLineItemResponse'];
|
||||||
|
|
||||||
|
// Request types
|
||||||
|
export type CreateCustomerRequest = components['schemas']['CreateCustomerRequest'];
|
||||||
|
export type UpdateCustomerRequest = components['schemas']['UpdateCustomerRequest'];
|
||||||
|
export type AddDeliveryAddressRequest = components['schemas']['AddDeliveryAddressRequest'];
|
||||||
|
export type SetFrameContractLineItem = components['schemas']['LineItem'];
|
||||||
|
export type SetFrameContractRequest = components['schemas']['SetFrameContractRequest'];
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,12 @@ export * from './user';
|
||||||
export * from './role';
|
export * from './role';
|
||||||
export * from './common';
|
export * from './common';
|
||||||
export * from './enums';
|
export * from './enums';
|
||||||
|
export * from './supplier';
|
||||||
|
export * from './category';
|
||||||
|
export * from './article';
|
||||||
|
export * from './customer';
|
||||||
|
export * from './inventory';
|
||||||
|
export * from './production';
|
||||||
|
|
||||||
// Re-export generated types for advanced usage
|
// Re-export generated types for advanced usage
|
||||||
export type { components, paths } from './generated/api';
|
export type { components, paths } from './generated/api';
|
||||||
|
|
|
||||||
14
frontend/packages/types/src/inventory.ts
Normal file
14
frontend/packages/types/src/inventory.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Inventory types
|
||||||
|
* Re-exports types from the auto-generated OpenAPI schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { components } from './generated/api';
|
||||||
|
|
||||||
|
// Response DTOs
|
||||||
|
export type StorageLocationDTO = components['schemas']['StorageLocationResponse'];
|
||||||
|
export type TemperatureRangeDTO = components['schemas']['TemperatureRangeResponse'];
|
||||||
|
|
||||||
|
// Request types
|
||||||
|
export type CreateStorageLocationRequest = components['schemas']['CreateStorageLocationRequest'];
|
||||||
|
export type UpdateStorageLocationRequest = components['schemas']['UpdateStorageLocationRequest'];
|
||||||
14
frontend/packages/types/src/production.ts
Normal file
14
frontend/packages/types/src/production.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Production types
|
||||||
|
* Re-exports types from the auto-generated OpenAPI schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { components } from './generated/api';
|
||||||
|
|
||||||
|
// Response DTOs
|
||||||
|
export type RecipeDTO = components['schemas']['RecipeResponse'];
|
||||||
|
export type IngredientDTO = components['schemas']['IngredientResponse'];
|
||||||
|
|
||||||
|
// Request types
|
||||||
|
export type CreateRecipeRequest = components['schemas']['CreateRecipeRequest'];
|
||||||
|
export type AddRecipeIngredientRequest = components['schemas']['AddRecipeIngredientRequest'];
|
||||||
21
frontend/packages/types/src/supplier.ts
Normal file
21
frontend/packages/types/src/supplier.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Supplier types
|
||||||
|
* Re-exports types from the auto-generated OpenAPI schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { components } from './generated/api';
|
||||||
|
|
||||||
|
// Response DTOs
|
||||||
|
export type SupplierDTO = components['schemas']['SupplierResponse'];
|
||||||
|
export type AddressDTO = components['schemas']['AddressResponse'];
|
||||||
|
export type ContactInfoDTO = components['schemas']['ContactInfoResponse'];
|
||||||
|
export type PaymentTermsDTO = components['schemas']['PaymentTermsResponse'];
|
||||||
|
export type QualityCertificateDTO = components['schemas']['QualityCertificateResponse'];
|
||||||
|
export type SupplierRatingDTO = components['schemas']['SupplierRatingResponse'];
|
||||||
|
|
||||||
|
// Request types
|
||||||
|
export type CreateSupplierRequest = components['schemas']['CreateSupplierRequest'];
|
||||||
|
export type UpdateSupplierRequest = components['schemas']['UpdateSupplierRequest'];
|
||||||
|
export type RateSupplierRequest = components['schemas']['RateSupplierRequest'];
|
||||||
|
export type AddCertificateRequest = components['schemas']['AddCertificateRequest'];
|
||||||
|
export type RemoveCertificateRequest = components['schemas']['RemoveCertificateRequest'];
|
||||||
Loading…
Add table
Add a link
Reference in a new issue