diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/StorageLocationResponse.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/StorageLocationResponse.java index 83582a6..3df1350 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/StorageLocationResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/StorageLocationResponse.java @@ -1,14 +1,16 @@ package de.effigenix.infrastructure.inventory.web.dto; import de.effigenix.domain.inventory.StorageLocation; +import io.swagger.v3.oas.annotations.media.Schema; import java.math.BigDecimal; +@Schema(requiredProperties = {"id", "name", "storageType", "active"}) public record StorageLocationResponse( String id, String name, String storageType, - TemperatureRangeResponse temperatureRange, + @Schema(nullable = true) TemperatureRangeResponse temperatureRange, boolean active ) { @@ -30,5 +32,6 @@ public record StorageLocationResponse( ); } + @Schema(requiredProperties = {"minTemperature", "maxTemperature"}) public record TemperatureRangeResponse(BigDecimal minTemperature, BigDecimal maxTemperature) {} } diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddressResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddressResponse.java index aa4790a..7230d2f 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddressResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddressResponse.java @@ -1,7 +1,9 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.shared.common.Address; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema(requiredProperties = {"street", "houseNumber", "postalCode", "city", "country"}) public record AddressResponse( String street, String houseNumber, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ArticleResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ArticleResponse.java index bd5ecaa..12e3c56 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ArticleResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ArticleResponse.java @@ -2,10 +2,12 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.domain.masterdata.Article; import de.effigenix.domain.masterdata.SupplierId; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; import java.util.List; +@Schema(requiredProperties = {"id", "name", "articleNumber", "categoryId", "salesUnits", "status", "supplierIds", "createdAt", "updatedAt"}) public record ArticleResponse( String id, String name, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContactInfoResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContactInfoResponse.java index 240ecb2..024af23 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContactInfoResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContactInfoResponse.java @@ -1,7 +1,9 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.shared.common.ContactInfo; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema(requiredProperties = {"phone", "email", "contactPerson"}) public record ContactInfoResponse( String phone, String email, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContractLineItemResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContractLineItemResponse.java index e533d29..baf0b67 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContractLineItemResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContractLineItemResponse.java @@ -1,9 +1,11 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.domain.masterdata.ContractLineItem; +import io.swagger.v3.oas.annotations.media.Schema; import java.math.BigDecimal; +@Schema(requiredProperties = {"articleId", "agreedPrice", "agreedQuantity", "unit"}) public record ContractLineItemResponse( String articleId, BigDecimal agreedPrice, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CustomerResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CustomerResponse.java index 8073d23..f8ea0ed 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CustomerResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CustomerResponse.java @@ -2,19 +2,21 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.domain.masterdata.Customer; import de.effigenix.domain.masterdata.CustomerPreference; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; import java.util.List; +@Schema(requiredProperties = {"id", "name", "type", "billingAddress", "contactInfo", "deliveryAddresses", "preferences", "status", "createdAt", "updatedAt"}) public record CustomerResponse( String id, String name, String type, AddressResponse billingAddress, ContactInfoResponse contactInfo, - PaymentTermsResponse paymentTerms, + @Schema(nullable = true) PaymentTermsResponse paymentTerms, List deliveryAddresses, - FrameContractResponse frameContract, + @Schema(nullable = true) FrameContractResponse frameContract, List preferences, String status, LocalDateTime createdAt, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/DeliveryAddressResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/DeliveryAddressResponse.java index e4bc037..b702a1e 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/DeliveryAddressResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/DeliveryAddressResponse.java @@ -1,7 +1,9 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.domain.masterdata.DeliveryAddress; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema(requiredProperties = {"label", "address", "contactPerson", "deliveryNotes"}) public record DeliveryAddressResponse( String label, AddressResponse address, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/FrameContractResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/FrameContractResponse.java index 8294c3a..d17d039 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/FrameContractResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/FrameContractResponse.java @@ -1,10 +1,12 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.domain.masterdata.FrameContract; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; import java.util.List; +@Schema(requiredProperties = {"id", "validFrom", "validUntil", "deliveryRhythm", "lineItems"}) public record FrameContractResponse( String id, LocalDate validFrom, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/PaymentTermsResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/PaymentTermsResponse.java index 148324b..c912421 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/PaymentTermsResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/PaymentTermsResponse.java @@ -1,7 +1,9 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.shared.common.PaymentTerms; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema(requiredProperties = {"paymentDueDays", "paymentDescription"}) public record PaymentTermsResponse( int paymentDueDays, String paymentDescription diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ProductCategoryResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ProductCategoryResponse.java index 0fddd36..1f77245 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ProductCategoryResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ProductCategoryResponse.java @@ -1,7 +1,9 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.domain.masterdata.ProductCategory; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema(requiredProperties = {"id", "name", "description"}) public record ProductCategoryResponse( String id, String name, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/QualityCertificateResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/QualityCertificateResponse.java index 014ff99..2addf10 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/QualityCertificateResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/QualityCertificateResponse.java @@ -1,9 +1,11 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.domain.masterdata.QualityCertificate; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; +@Schema(requiredProperties = {"certificateType", "issuer", "validFrom", "validUntil"}) public record QualityCertificateResponse( String certificateType, String issuer, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SalesUnitResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SalesUnitResponse.java index 0672866..594d684 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SalesUnitResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SalesUnitResponse.java @@ -1,9 +1,11 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.domain.masterdata.SalesUnit; +import io.swagger.v3.oas.annotations.media.Schema; import java.math.BigDecimal; +@Schema(requiredProperties = {"id", "unit", "priceModel", "price"}) public record SalesUnitResponse( String id, String unit, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierRatingResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierRatingResponse.java index e0e92d1..cec941f 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierRatingResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierRatingResponse.java @@ -1,7 +1,9 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.domain.masterdata.SupplierRating; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema(requiredProperties = {"qualityScore", "deliveryScore", "priceScore"}) public record SupplierRatingResponse( int qualityScore, int deliveryScore, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierResponse.java index 5f99354..c4cef8c 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierResponse.java @@ -1,18 +1,20 @@ package de.effigenix.infrastructure.masterdata.web.dto; import de.effigenix.domain.masterdata.Supplier; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; import java.util.List; +@Schema(requiredProperties = {"id", "name", "contactInfo", "certificates", "status", "createdAt", "updatedAt"}) public record SupplierResponse( String id, String name, - AddressResponse address, + @Schema(nullable = true) AddressResponse address, ContactInfoResponse contactInfo, - PaymentTermsResponse paymentTerms, + @Schema(nullable = true) PaymentTermsResponse paymentTerms, List certificates, - SupplierRatingResponse rating, + @Schema(nullable = true) SupplierRatingResponse rating, String status, LocalDateTime createdAt, LocalDateTime updatedAt diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/IngredientResponse.java b/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/IngredientResponse.java index 1f63d29..a59b944 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/IngredientResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/IngredientResponse.java @@ -1,14 +1,16 @@ package de.effigenix.infrastructure.production.web.dto; import de.effigenix.domain.production.Ingredient; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema(requiredProperties = {"id", "position", "articleId", "quantity", "uom", "substitutable"}) public record IngredientResponse( String id, int position, String articleId, String quantity, String uom, - String subRecipeId, + @Schema(nullable = true) String subRecipeId, boolean substitutable ) { public static IngredientResponse from(Ingredient ingredient) { diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/RecipeResponse.java b/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/RecipeResponse.java index 7fcf588..bf8e649 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/RecipeResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/RecipeResponse.java @@ -1,10 +1,12 @@ package de.effigenix.infrastructure.production.web.dto; import de.effigenix.domain.production.Recipe; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; import java.util.List; +@Schema(requiredProperties = {"id", "name", "version", "type", "description", "yieldPercentage", "outputQuantity", "outputUom", "status", "ingredients", "createdAt", "updatedAt"}) public record RecipeResponse( String id, String name, @@ -12,7 +14,7 @@ public record RecipeResponse( String type, String description, int yieldPercentage, - Integer shelfLifeDays, + @Schema(nullable = true) Integer shelfLifeDays, String outputQuantity, String outputUom, String status, diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/dto/LoginResponse.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/dto/LoginResponse.java index 6f9a92a..9294871 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/dto/LoginResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/dto/LoginResponse.java @@ -12,7 +12,8 @@ import java.time.LocalDateTime; * Client should store the access token and send it in Authorization header * for subsequent requests. */ -@Schema(description = "Login response with JWT tokens") +@Schema(description = "Login response with JWT tokens", + requiredProperties = {"accessToken", "tokenType", "expiresIn", "expiresAt", "refreshToken"}) public record LoginResponse( @Schema(description = "JWT access token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") String accessToken, diff --git a/backend/src/main/java/de/effigenix/infrastructure/web/config/OpenApiConfig.java b/backend/src/main/java/de/effigenix/infrastructure/web/config/OpenApiConfig.java index da96e83..74e5c27 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/web/config/OpenApiConfig.java +++ b/backend/src/main/java/de/effigenix/infrastructure/web/config/OpenApiConfig.java @@ -7,8 +7,15 @@ import io.swagger.v3.oas.annotations.info.Info; import io.swagger.v3.oas.annotations.info.License; import io.swagger.v3.oas.annotations.security.SecurityScheme; import io.swagger.v3.oas.annotations.servers.Server; +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + /** * OpenAPI/Swagger Configuration. * @@ -111,6 +118,35 @@ import org.springframework.context.annotation.Configuration; """ ) public class OpenApiConfig { - // Configuration is done via annotations - // No additional beans needed + + /** + * Marks fields as required for application-layer DTOs that cannot carry + * {@code @Schema} annotations (to preserve the clean architecture boundary). + */ + @Bean + public OpenApiCustomizer applicationDtoRequiredFieldsCustomizer() { + // Application-layer DTOs → all required except explicitly nullable + Map> 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 required = new ArrayList<>(); + schema.getProperties().keySet().forEach(prop -> { + if (!nullable.contains(prop)) { + required.add((String) prop); + } + }); + schema.setRequired(required); + }); + }; + } } diff --git a/frontend/apps/cli/src/App.tsx b/frontend/apps/cli/src/App.tsx index b5fbe40..cfe9cd7 100644 --- a/frontend/apps/cli/src/App.tsx +++ b/frontend/apps/cli/src/App.tsx @@ -31,6 +31,16 @@ import { CustomerDetailScreen } from './components/masterdata/customers/Customer import { CustomerCreateScreen } from './components/masterdata/customers/CustomerCreateScreen.js'; import { AddDeliveryAddressScreen } from './components/masterdata/customers/AddDeliveryAddressScreen.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() { const { isAuthenticated, loading } = useAuth(); @@ -87,6 +97,16 @@ function ScreenRouter() { {current === 'customer-create' && } {current === 'customer-add-delivery-address' && } {current === 'customer-set-preferences' && } + {/* Lagerverwaltung */} + {current === 'inventory-menu' && } + {current === 'storage-location-list' && } + {current === 'storage-location-create' && } + {current === 'storage-location-detail' && } + {/* Produktion */} + {current === 'production-menu' && } + {current === 'recipe-list' && } + {current === 'recipe-create' && } + {current === 'recipe-detail' && } ); } diff --git a/frontend/apps/cli/src/components/MainMenu.tsx b/frontend/apps/cli/src/components/MainMenu.tsx index 4208247..1335998 100644 --- a/frontend/apps/cli/src/components/MainMenu.tsx +++ b/frontend/apps/cli/src/components/MainMenu.tsx @@ -17,6 +17,8 @@ export function MainMenu() { const items: MenuItem[] = [ { label: 'Stammdaten', screen: 'masterdata-menu' }, + { label: 'Lagerverwaltung', screen: 'inventory-menu' }, + { label: 'Produktion', screen: 'production-menu' }, { label: 'Benutzer verwalten', screen: 'user-list' }, { label: 'Rollen anzeigen', screen: 'role-list' }, { label: 'Abmelden', action: () => void logout() }, diff --git a/frontend/apps/cli/src/components/inventory/InventoryMenu.tsx b/frontend/apps/cli/src/components/inventory/InventoryMenu.tsx new file mode 100644 index 0000000..33a4ce1 --- /dev/null +++ b/frontend/apps/cli/src/components/inventory/InventoryMenu.tsx @@ -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 ( + + + Lagerverwaltung + + + + {MENU_ITEMS.map((item, index) => ( + + + {index === selectedIndex ? '▶ ' : ' '} + {item.label} + + {index === selectedIndex && ( + + {item.description} + + )} + + ))} + + + ↑↓ navigieren · Enter auswählen · Backspace Zurück + + + + ); +} diff --git a/frontend/apps/cli/src/components/inventory/StorageLocationCreateScreen.tsx b/frontend/apps/cli/src/components/inventory/StorageLocationCreateScreen.tsx new file mode 100644 index 0000000..a12fb25 --- /dev/null +++ b/frontend/apps/cli/src/components/inventory/StorageLocationCreateScreen.tsx @@ -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 = { + 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>({ + name: '', + storageType: 'DRY_STORAGE', + minTemperature: '', + maxTemperature: '', + }); + const [activeField, setActiveField] = useState('name'); + const [fieldErrors, setFieldErrors] = useState>>({}); + + 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> = {}; + 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 ( + + + + ); + } + + const storageTypeLabel = STORAGE_TYPE_LABELS[values.storageType as StorageType] ?? values.storageType; + + return ( + + Neuer Lagerort + {error && } + + + {FIELDS.map((field) => { + if (field === 'storageType') { + return ( + + + {FIELD_LABELS[field]}: {storageTypeLabel} + + {fieldErrors[field] && {fieldErrors[field]}} + + ); + } + return ( + + ); + })} + + + + + Tab/↑↓ Feld wechseln · ←→ Lagertyp · Enter auf letztem Feld speichern · Escape Abbrechen + + + + ); +} diff --git a/frontend/apps/cli/src/components/inventory/StorageLocationDetailScreen.tsx b/frontend/apps/cli/src/components/inventory/StorageLocationDetailScreen.tsx new file mode 100644 index 0000000..6893442 --- /dev/null +++ b/frontend/apps/cli/src/components/inventory/StorageLocationDetailScreen.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedAction, setSelectedAction] = useState(0); + const [mode, setMode] = useState('menu'); + const [actionLoading, setActionLoading] = useState(false); + const [successMessage, setSuccessMessage] = useState(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 ; + if (error && !location) return ; + if (!location) return Lagerort nicht gefunden.; + + const statusColor = location.active ? 'green' : 'red'; + const typeName = STORAGE_TYPE_LABELS[location.storageType as StorageType] ?? location.storageType; + + return ( + + Lagerort: {location.name} + + {error && setError(null)} />} + {successMessage && setSuccessMessage(null)} />} + + + + Status: + {location.active ? 'AKTIV' : 'INAKTIV'} + + + Lagertyp: + {typeName} + + {location.temperatureRange && ( + + Temperatur: + {location.temperatureRange.minTemperature}°C – {location.temperatureRange.maxTemperature}°C + + )} + + + {mode === 'confirm-status' && ( + void handleToggleStatus()} + onCancel={() => setMode('menu')} + /> + )} + + {mode === 'menu' && ( + + Aktionen: + {actionLoading && } + {!actionLoading && MENU_ITEMS.map((item, index) => ( + + + {index === selectedAction ? '▶ ' : ' '}{item.label(location)} + + + ))} + + )} + + + ↑↓ navigieren · Enter ausführen · Backspace Zurück + + + ); +} diff --git a/frontend/apps/cli/src/components/inventory/StorageLocationListScreen.tsx b/frontend/apps/cli/src/components/inventory/StorageLocationListScreen.tsx new file mode 100644 index 0000000..0890434 --- /dev/null +++ b/frontend/apps/cli/src/components/inventory/StorageLocationListScreen.tsx @@ -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('ALL'); + const [typeFilter, setTypeFilter] = useState(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 = { ALL: 'Alle', ACTIVE: 'Aktiv', INACTIVE: 'Inaktiv' }; + const typeLabel = typeFilter ? STORAGE_TYPE_LABELS[typeFilter] : 'Alle'; + + return ( + + + Lagerorte + + Status: {filterLabel[statusFilter]} + {' · '}Typ: {typeLabel} + {' '}({storageLocations.length}) + + + + {loading && } + {error && !loading && } + + {!loading && !error && ( + + + {' Status Name'.padEnd(30)} + {'Typ'.padEnd(20)} + Temperatur + + {storageLocations.length === 0 && ( + + Keine Lagerorte gefunden. + + )} + {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 ( + + {isSelected ? '▶ ' : ' '} + {loc.active ? '● ' : '○ '} + {loc.name.substring(0, 24).padEnd(25)} + {typeName.padEnd(20)} + {tempRange} + + ); + })} + + )} + + + + ↑↓ nav · Enter Details · [n] Neu · [a] Alle · [A] Aktiv · [I] Inaktiv · [t] Typ · Backspace Zurück + + + + ); +} diff --git a/frontend/apps/cli/src/components/production/ProductionMenu.tsx b/frontend/apps/cli/src/components/production/ProductionMenu.tsx new file mode 100644 index 0000000..ca6436c --- /dev/null +++ b/frontend/apps/cli/src/components/production/ProductionMenu.tsx @@ -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 ( + + + Produktion + + + + {MENU_ITEMS.map((item, index) => ( + + + {index === selectedIndex ? '▶ ' : ' '} + {item.label} + + {index === selectedIndex && ( + + {item.description} + + )} + + ))} + + + ↑↓ navigieren · Enter auswählen · Backspace Zurück + + + + ); +} diff --git a/frontend/apps/cli/src/components/production/RecipeCreateScreen.tsx b/frontend/apps/cli/src/components/production/RecipeCreateScreen.tsx new file mode 100644 index 0000000..d77797e --- /dev/null +++ b/frontend/apps/cli/src/components/production/RecipeCreateScreen.tsx @@ -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 = { + 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>({ + name: '', + version: '1', + type: 'FINISHED_PRODUCT', + description: '', + yieldPercentage: '100', + shelfLifeDays: '', + outputQuantity: '', + outputUom: '', + }); + const [activeField, setActiveField] = useState('name'); + const [fieldErrors, setFieldErrors] = useState>>({}); + + 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> = {}; + 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 ( + + + + ); + } + + const typeLabel = RECIPE_TYPE_LABELS[values.type as RecipeType] ?? values.type; + + return ( + + Neues Rezept + {error && } + + + {FIELDS.map((field) => { + if (field === 'type') { + return ( + + + {FIELD_LABELS[field]}: {typeLabel} + + {fieldErrors[field] && {fieldErrors[field]}} + + ); + } + return ( + + ); + })} + + + + + Tab/↑↓ Feld wechseln · ←→ Rezepttyp · Enter auf letztem Feld speichern · Escape Abbrechen + + + + ); +} diff --git a/frontend/apps/cli/src/components/production/RecipeDetailScreen.tsx b/frontend/apps/cli/src/components/production/RecipeDetailScreen.tsx new file mode 100644 index 0000000..250cee1 --- /dev/null +++ b/frontend/apps/cli/src/components/production/RecipeDetailScreen.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ; + if (error && !recipe) return ; + if (!recipe) return Rezept nicht gefunden.; + + const typeName = RECIPE_TYPE_LABELS[recipe.type as RecipeType] ?? recipe.type; + + return ( + + Rezept: {recipe.name} + + {error && setError(null)} />} + + + + Status: + {recipe.status} + + + Typ: + {typeName} + + + Version: + {recipe.version} + + {recipe.description && ( + + Beschreibung: + {recipe.description} + + )} + + Ausbeute: + {recipe.yieldPercentage}% + + {recipe.shelfLifeDays !== null && ( + + Haltbarkeit: + {recipe.shelfLifeDays} Tage + + )} + + Ausgabemenge: + {recipe.outputQuantity} {recipe.outputUom} + + + {recipe.ingredients.length > 0 && ( + + Zutaten: + {recipe.ingredients.map((ing) => ( + + {ing.position}. + {ing.quantity} {ing.uom} + (Artikel: {ing.articleId}) + {ing.substitutable && [austauschbar]} + + ))} + + )} + + + + Backspace Zurück + + + ); +} diff --git a/frontend/apps/cli/src/components/production/RecipeListScreen.tsx b/frontend/apps/cli/src/components/production/RecipeListScreen.tsx new file mode 100644 index 0000000..dbcbefd --- /dev/null +++ b/frontend/apps/cli/src/components/production/RecipeListScreen.tsx @@ -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 ( + + + Rezepte + ({recipes.length}) + + + {loading && } + {error && !loading && } + + {!loading && !error && ( + + + {' Name'.padEnd(30)} + {'Typ'.padEnd(18)} + {'V.'.padEnd(5)} + Status + + {recipes.length === 0 && ( + + Keine Rezepte gefunden. + + )} + {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 ( + + {isSelected ? '▶ ' : ' '} + {recipe.name.substring(0, 26).padEnd(27)} + {typeName.padEnd(18)} + {String(recipe.version).padEnd(5)} + {recipe.status} + + ); + })} + + )} + + + + ↑↓ nav · Enter Details · [n] Neu · Backspace Zurück + + + + ); +} diff --git a/frontend/apps/cli/src/hooks/useRecipes.ts b/frontend/apps/cli/src/hooks/useRecipes.ts new file mode 100644 index 0000000..4256e72 --- /dev/null +++ b/frontend/apps/cli/src/hooks/useRecipes.ts @@ -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({ + 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, + }; +} diff --git a/frontend/apps/cli/src/hooks/useStorageLocations.ts b/frontend/apps/cli/src/hooks/useStorageLocations.ts new file mode 100644 index 0000000..8db0892 --- /dev/null +++ b/frontend/apps/cli/src/hooks/useStorageLocations.ts @@ -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({ + 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, + }; +} diff --git a/frontend/apps/cli/src/state/navigation-context.tsx b/frontend/apps/cli/src/state/navigation-context.tsx index d423ee2..db69836 100644 --- a/frontend/apps/cli/src/state/navigation-context.tsx +++ b/frontend/apps/cli/src/state/navigation-context.tsx @@ -27,7 +27,17 @@ export type Screen = | 'customer-detail' | 'customer-create' | '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 { current: Screen; diff --git a/frontend/openapi.json b/frontend/openapi.json index 9a70da4..ea50133 100644 --- a/frontend/openapi.json +++ b/frontend/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.1","info":{"title":"Effigenix Fleischerei ERP API","description":"RESTful API for Effigenix Fleischerei ERP System.\n\n## Authentication\n\nAll endpoints (except /api/auth/login and /api/auth/refresh) require JWT authentication.\n\n1. Login via POST /api/auth/login with username and password\n2. Copy the returned access token\n3. Click \"Authorize\" button (top right)\n4. Enter: Bearer \n5. Click \"Authorize\"\n\n## User Management\n\n- **Authentication**: Login, logout, refresh token\n- **User Management**: Create, update, list users (ADMIN only)\n- **Role Management**: Assign roles, lock/unlock users (ADMIN only)\n- **Password Management**: Change password (requires current password)\n\n## Error Handling\n\nAll errors return a consistent error response format:\n\n```json\n{\n \"code\": \"USER_NOT_FOUND\",\n \"message\": \"User with ID 'user-123' not found\",\n \"status\": 404,\n \"timestamp\": \"2026-02-17T12:00:00\",\n \"path\": \"/api/users/user-123\",\n \"validationErrors\": null\n}\n```\n\n## Architecture\n\nBuilt with:\n- Domain-Driven Design (DDD)\n- Clean Architecture (Hexagonal Architecture)\n- Spring Boot 3.2\n- Java 21\n- PostgreSQL\n","contact":{"name":"Effigenix Development Team","url":"https://effigenix.com","email":"dev@effigenix.com"},"license":{"name":"Proprietary","url":"https://effigenix.com/license"},"version":"0.1.0"},"servers":[{"url":"http://localhost:8080","description":"Local Development Server"},{"url":"https://api.effigenix.com","description":"Production Server"}],"tags":[{"name":"Product Categories","description":"Product category management endpoints"},{"name":"User Management","description":"User management endpoints (requires authentication)"},{"name":"Articles","description":"Article management endpoints"},{"name":"Role Management","description":"Role management endpoints (ADMIN only)"},{"name":"Customers","description":"Customer management endpoints"},{"name":"Suppliers","description":"Supplier management endpoints"},{"name":"Authentication","description":"Authentication and session management endpoints"}],"paths":{"/api/users/{id}":{"get":{"tags":["User Management"],"summary":"Get user by ID","description":"Retrieve a single user by their ID. Requires authentication.","operationId":"getUserById","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"responses":{"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["User Management"],"summary":"Update user","description":"Update user details (email, branchId). Only provided fields will be updated.","operationId":"updateUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"409":{"description":"Email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User updated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/password":{"put":{"tags":["User Management"],"summary":"Change password","description":"Change user password. Requires current password for verification.","operationId":"changePassword","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"400":{"description":"Invalid password"},"401":{"description":"Invalid current password or authentication required"},"404":{"description":"User not found"},"204":{"description":"Password changed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}":{"get":{"tags":["Suppliers"],"operationId":"getSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Suppliers"],"operationId":"updateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}":{"get":{"tags":["Customers"],"operationId":"getCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Customers"],"operationId":"updateCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/preferences":{"put":{"tags":["Customers"],"operationId":"setPreferences","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPreferencesRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/frame-contract":{"put":{"tags":["Customers"],"operationId":"setFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetFrameContractRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Customers"],"operationId":"removeFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/categories/{id}":{"put":{"tags":["Product Categories"],"operationId":"updateCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Product Categories"],"operationId":"deleteCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}":{"get":{"tags":["Articles"],"operationId":"getArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Articles"],"operationId":"updateArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}/price":{"put":{"tags":["Articles"],"operationId":"updateSalesUnitPrice","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSalesUnitPriceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users":{"get":{"tags":["User Management"],"summary":"List all users","description":"Get a list of all users in the system. Requires authentication.","operationId":"listUsers","responses":{"200":{"description":"Users retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["User Management"],"summary":"Create user (ADMIN only)","description":"Create a new user account with specified roles. Requires USER_MANAGEMENT permission.","operationId":"createUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"201":{"description":"User created successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"400":{"description":"Validation error or invalid password","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Username or email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/unlock":{"post":{"tags":["User Management"],"summary":"Unlock user (ADMIN only)","description":"Unlock a user account (allows login). Requires USER_MANAGEMENT permission.","operationId":"unlockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"responses":{"200":{"description":"User unlocked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles":{"post":{"tags":["User Management"],"summary":"Assign role (ADMIN only)","description":"Assign a role to a user. Requires USER_MANAGEMENT permission.","operationId":"assignRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"200":{"description":"Role assigned successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User or role not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/lock":{"post":{"tags":["User Management"],"summary":"Lock user (ADMIN only)","description":"Lock a user account (prevents login). Requires USER_MANAGEMENT permission.","operationId":"lockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"responses":{"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User locked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers":{"get":{"tags":["Suppliers"],"operationId":"listSuppliers","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SupplierResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Suppliers"],"operationId":"createSupplier","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/rating":{"post":{"tags":["Suppliers"],"operationId":"rateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/deactivate":{"post":{"tags":["Suppliers"],"operationId":"deactivate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/certificates":{"post":{"tags":["Suppliers"],"operationId":"addCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Suppliers"],"operationId":"removeCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/activate":{"post":{"tags":["Suppliers"],"operationId":"activate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers":{"get":{"tags":["Customers"],"operationId":"listCustomers","parameters":[{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["B2C","B2B"]}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CustomerResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Customers"],"operationId":"createCustomer","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses":{"post":{"tags":["Customers"],"operationId":"addDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddDeliveryAddressRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/deactivate":{"post":{"tags":["Customers"],"operationId":"deactivate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/activate":{"post":{"tags":["Customers"],"operationId":"activate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/categories":{"get":{"tags":["Product Categories"],"operationId":"listCategories","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Product Categories"],"operationId":"createCategory","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Refresh access token","description":"Refresh an expired access token using a valid refresh token. Returns new access token and refresh token.","operationId":"refresh","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"200":{"description":"Token refresh successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid or expired refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"description":"Validation error (missing refresh token)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/auth/logout":{"post":{"tags":["Authentication"],"summary":"User logout","description":"Invalidate current JWT token. Requires authentication.","operationId":"logout","responses":{"401":{"description":"Invalid or missing authentication token"},"204":{"description":"Logout successful"}}}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"User login","description":"Authenticate user with username and password. Returns JWT access token and refresh token.","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Login successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid credentials, user locked, or user inactive","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"description":"Validation error (missing username or password)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/articles":{"get":{"tags":["Articles"],"operationId":"listArticles","parameters":[{"name":"categoryId","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ArticleResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Articles"],"operationId":"createArticle","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers":{"post":{"tags":["Articles"],"operationId":"assignSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units":{"post":{"tags":["Articles"],"operationId":"addSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSalesUnitRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/deactivate":{"post":{"tags":["Articles"],"operationId":"deactivate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/activate":{"post":{"tags":["Articles"],"operationId":"activate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/roles":{"get":{"tags":["Role Management"],"summary":"List all roles (ADMIN only)","description":"Get a list of all available roles in the system. Requires USER_MANAGEMENT permission.","operationId":"listRoles","responses":{"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"200":{"description":"Roles retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RoleDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles/{roleName}":{"delete":{"tags":["User Management"],"summary":"Remove role (ADMIN only)","description":"Remove a role from a user. Requires USER_MANAGEMENT permission.","operationId":"removeRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"},{"name":"roleName","in":"path","description":"Role name","required":true,"schema":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]},"example":"MANAGER"}],"responses":{"404":{"description":"User or role not found"},"403":{"description":"Missing USER_MANAGEMENT permission"},"204":{"description":"Role removed successfully"},"401":{"description":"Authentication required"}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses/{label}":{"delete":{"tags":["Customers"],"operationId":"removeDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"label","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers/{supplierId}":{"delete":{"tags":["Articles"],"operationId":"removeSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"supplierId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}":{"delete":{"tags":["Articles"],"operationId":"removeSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}}},"components":{"schemas":{"UpdateUserRequest":{"type":"object","properties":{"email":{"type":"string","description":"New email address","example":"newemail@example.com"},"branchId":{"type":"string","description":"New branch ID","example":"BRANCH-002"}},"description":"Request to update user details"},"RoleDTO":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]},"permissions":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["RECIPE_READ","RECIPE_WRITE","RECIPE_DELETE","BATCH_READ","BATCH_WRITE","BATCH_COMPLETE","BATCH_DELETE","PRODUCTION_ORDER_READ","PRODUCTION_ORDER_WRITE","PRODUCTION_ORDER_DELETE","HACCP_READ","HACCP_WRITE","TEMPERATURE_LOG_READ","TEMPERATURE_LOG_WRITE","CLEANING_RECORD_READ","CLEANING_RECORD_WRITE","GOODS_INSPECTION_READ","GOODS_INSPECTION_WRITE","STOCK_READ","STOCK_WRITE","STOCK_MOVEMENT_READ","STOCK_MOVEMENT_WRITE","INVENTORY_COUNT_READ","INVENTORY_COUNT_WRITE","PURCHASE_ORDER_READ","PURCHASE_ORDER_WRITE","PURCHASE_ORDER_DELETE","GOODS_RECEIPT_READ","GOODS_RECEIPT_WRITE","SUPPLIER_READ","SUPPLIER_WRITE","SUPPLIER_DELETE","ORDER_READ","ORDER_WRITE","ORDER_DELETE","INVOICE_READ","INVOICE_WRITE","INVOICE_DELETE","CUSTOMER_READ","CUSTOMER_WRITE","CUSTOMER_DELETE","LABEL_READ","LABEL_WRITE","LABEL_PRINT","MASTERDATA_READ","MASTERDATA_WRITE","BRANCH_READ","BRANCH_WRITE","BRANCH_DELETE","USER_READ","USER_WRITE","USER_DELETE","USER_LOCK","USER_UNLOCK","ROLE_READ","ROLE_WRITE","ROLE_ASSIGN","ROLE_REMOVE","REPORT_READ","REPORT_GENERATE","NOTIFICATION_READ","NOTIFICATION_SEND","AUDIT_LOG_READ","SYSTEM_SETTINGS_READ","SYSTEM_SETTINGS_WRITE"]}},"description":{"type":"string"}}},"UserDTO":{"type":"object","properties":{"id":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"roles":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}},"branchId":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","LOCKED"]},"createdAt":{"type":"string","format":"date-time"},"lastLogin":{"type":"string","format":"date-time"}}},"ChangePasswordRequest":{"required":["currentPassword","newPassword"],"type":"object","properties":{"currentPassword":{"type":"string","description":"Current password","example":"OldPass123"},"newPassword":{"maxLength":2147483647,"minLength":8,"type":"string","description":"New password (min 8 characters)","example":"NewSecurePass456"}},"description":"Request to change user password"},"UpdateSupplierRequest":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddressResponse":{"type":"object","properties":{"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"}}},"ContactInfoResponse":{"type":"object","properties":{"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"}}},"PaymentTermsResponse":{"type":"object","properties":{"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"QualityCertificateResponse":{"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"SupplierRatingResponse":{"type":"object","properties":{"qualityScore":{"type":"integer","format":"int32"},"deliveryScore":{"type":"integer","format":"int32"},"priceScore":{"type":"integer","format":"int32"}}},"SupplierResponse":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"certificates":{"type":"array","items":{"$ref":"#/components/schemas/QualityCertificateResponse"}},"rating":{"$ref":"#/components/schemas/SupplierRatingResponse"},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"UpdateCustomerRequest":{"type":"object","properties":{"name":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"ContractLineItemResponse":{"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string"}}},"CustomerResponse":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"billingAddress":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"deliveryAddresses":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryAddressResponse"}},"frameContract":{"$ref":"#/components/schemas/FrameContractResponse"},"preferences":{"type":"array","items":{"type":"string"}},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"DeliveryAddressResponse":{"type":"object","properties":{"label":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"FrameContractResponse":{"type":"object","properties":{"id":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"deliveryRhythm":{"type":"string"},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/ContractLineItemResponse"}}}},"SetPreferencesRequest":{"required":["preferences"],"type":"object","properties":{"preferences":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["BIO","REGIONAL","TIERWOHL","HALAL","KOSHER","GLUTENFREI","LAKTOSEFREI"]}}}},"LineItem":{"required":["agreedPrice","articleId"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]}}},"SetFrameContractRequest":{"required":["lineItems","rhythm"],"type":"object","properties":{"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"rhythm":{"type":"string","enum":["DAILY","WEEKLY","BIWEEKLY","MONTHLY","ON_DEMAND"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/LineItem"}}}},"UpdateProductCategoryRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ProductCategoryResponse":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}},"UpdateArticleRequest":{"type":"object","properties":{"name":{"type":"string"},"categoryId":{"type":"string"}}},"ArticleResponse":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"salesUnits":{"type":"array","items":{"$ref":"#/components/schemas/SalesUnitResponse"}},"status":{"type":"string"},"supplierIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SalesUnitResponse":{"type":"object","properties":{"id":{"type":"string"},"unit":{"type":"string"},"priceModel":{"type":"string"},"price":{"type":"number"}}},"UpdateSalesUnitPriceRequest":{"required":["price"],"type":"object","properties":{"price":{"type":"number"}}},"CreateUserRequest":{"required":["email","password","roleNames","username"],"type":"object","properties":{"username":{"maxLength":50,"minLength":3,"type":"string","description":"Username (unique)","example":"john.doe"},"email":{"type":"string","description":"Email address (unique)","example":"john.doe@example.com"},"password":{"maxLength":2147483647,"minLength":8,"type":"string","description":"Password (min 8 characters)","example":"SecurePass123"},"roleNames":{"uniqueItems":true,"type":"array","description":"Role names to assign","example":["USER","MANAGER"],"items":{"type":"string","description":"Role names to assign","example":"[\"USER\",\"MANAGER\"]","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"branchId":{"type":"string","description":"Branch ID (optional)","example":"BRANCH-001"}},"description":"Request to create a new user"},"AssignRoleRequest":{"required":["roleName"],"type":"object","properties":{"roleName":{"type":"string","description":"Role name to assign","example":"MANAGER","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"description":"Request to assign a role to a user"},"CreateSupplierRequest":{"required":["name","phone"],"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"RateSupplierRequest":{"type":"object","properties":{"qualityScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"deliveryScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"priceScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"}}},"AddCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"CreateCustomerRequest":{"required":["city","country","name","phone","postalCode","street","type"],"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["B2C","B2B"]},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddDeliveryAddressRequest":{"required":["city","country","postalCode","street"],"type":"object","properties":{"label":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"CreateProductCategoryRequest":{"required":["name"],"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"RefreshTokenRequest":{"required":["refreshToken"],"type":"object","properties":{"refreshToken":{"type":"string","description":"Refresh token"}},"description":"Refresh token request"},"LoginResponse":{"type":"object","properties":{"accessToken":{"type":"string","description":"JWT access token","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"tokenType":{"type":"string","description":"Token type","example":"Bearer"},"expiresIn":{"type":"integer","description":"Token expiration time in seconds","format":"int64","example":3600},"expiresAt":{"type":"string","description":"Token expiration timestamp","format":"date-time"},"refreshToken":{"type":"string","description":"Refresh token for obtaining new access token"}},"description":"Login response with JWT tokens"},"LoginRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string","description":"Username","example":"admin"},"password":{"type":"string","description":"Password","example":"admin123"}},"description":"Login request with username and password"},"CreateArticleRequest":{"required":["articleNumber","categoryId","name","price","priceModel","unit"],"type":"object","properties":{"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"AssignSupplierRequest":{"required":["supplierId"],"type":"object","properties":{"supplierId":{"type":"string"}}},"AddSalesUnitRequest":{"required":["price","priceModel","unit"],"type":"object","properties":{"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"RemoveCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"}}}},"securitySchemes":{"Bearer Authentication":{"type":"http","description":"JWT authentication token obtained from POST /api/auth/login.\n\nFormat: Bearer \n\nExample:\nBearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\n","scheme":"bearer","bearerFormat":"JWT"}}}} \ No newline at end of file +{"openapi":"3.0.1","info":{"title":"Effigenix Fleischerei ERP API","description":"RESTful API for Effigenix Fleischerei ERP System.\n\n## Authentication\n\nAll endpoints (except /api/auth/login and /api/auth/refresh) require JWT authentication.\n\n1. Login via POST /api/auth/login with username and password\n2. Copy the returned access token\n3. Click \"Authorize\" button (top right)\n4. Enter: Bearer \n5. Click \"Authorize\"\n\n## User Management\n\n- **Authentication**: Login, logout, refresh token\n- **User Management**: Create, update, list users (ADMIN only)\n- **Role Management**: Assign roles, lock/unlock users (ADMIN only)\n- **Password Management**: Change password (requires current password)\n\n## Error Handling\n\nAll errors return a consistent error response format:\n\n```json\n{\n \"code\": \"USER_NOT_FOUND\",\n \"message\": \"User with ID 'user-123' not found\",\n \"status\": 404,\n \"timestamp\": \"2026-02-17T12:00:00\",\n \"path\": \"/api/users/user-123\",\n \"validationErrors\": null\n}\n```\n\n## Architecture\n\nBuilt with:\n- Domain-Driven Design (DDD)\n- Clean Architecture (Hexagonal Architecture)\n- Spring Boot 3.2\n- Java 21\n- PostgreSQL\n","contact":{"name":"Effigenix Development Team","url":"https://effigenix.com","email":"dev@effigenix.com"},"license":{"name":"Proprietary","url":"https://effigenix.com/license"},"version":"0.1.0"},"servers":[{"url":"http://localhost:8080","description":"Local Development Server"},{"url":"https://api.effigenix.com","description":"Production Server"}],"tags":[{"name":"Storage Locations","description":"Storage location management endpoints"},{"name":"Product Categories","description":"Product category management endpoints"},{"name":"User Management","description":"User management endpoints (requires authentication)"},{"name":"Articles","description":"Article management endpoints"},{"name":"Recipes","description":"Recipe management endpoints"},{"name":"Role Management","description":"Role management endpoints (ADMIN only)"},{"name":"Customers","description":"Customer management endpoints"},{"name":"Suppliers","description":"Supplier management endpoints"},{"name":"Authentication","description":"Authentication and session management endpoints"}],"paths":{"/api/users/{id}":{"get":{"tags":["User Management"],"summary":"Get user by ID","description":"Retrieve a single user by their ID.","operationId":"getUserById","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["User Management"],"summary":"Update user","description":"Update user details (email, branchId).","operationId":"updateUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"200":{"description":"User updated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/password":{"put":{"tags":["User Management"],"summary":"Change password","description":"Change user password. Requires current password for verification.","operationId":"changePassword","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"400":{"description":"Invalid password"},"401":{"description":"Invalid current password"},"404":{"description":"User not found"},"204":{"description":"Password changed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}":{"get":{"tags":["Suppliers"],"operationId":"getSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Suppliers"],"operationId":"updateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}":{"put":{"tags":["Storage Locations"],"operationId":"updateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStorageLocationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}":{"get":{"tags":["Customers"],"operationId":"getCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Customers"],"operationId":"updateCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/preferences":{"put":{"tags":["Customers"],"operationId":"setPreferences","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPreferencesRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/frame-contract":{"put":{"tags":["Customers"],"operationId":"setFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetFrameContractRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Customers"],"operationId":"removeFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/categories/{id}":{"put":{"tags":["Product Categories"],"operationId":"updateCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Product Categories"],"operationId":"deleteCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}":{"get":{"tags":["Articles"],"operationId":"getArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Articles"],"operationId":"updateArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}/price":{"put":{"tags":["Articles"],"operationId":"updateSalesUnitPrice","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSalesUnitPriceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users":{"get":{"tags":["User Management"],"summary":"List all users","description":"Get a list of all users in the system.","operationId":"listUsers","responses":{"200":{"description":"Users retrieved successfully","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["User Management"],"summary":"Create user (ADMIN only)","description":"Create a new user account with specified roles.","operationId":"createUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"400":{"description":"Validation error or invalid password","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Username or email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"201":{"description":"User created successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/unlock":{"post":{"tags":["User Management"],"summary":"Unlock user (ADMIN only)","description":"Unlock a user account (allows login).","operationId":"unlockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User unlocked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Invalid status transition","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles":{"post":{"tags":["User Management"],"summary":"Assign role (ADMIN only)","description":"Assign a role to a user.","operationId":"assignRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User or role not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"Role assigned successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/lock":{"post":{"tags":["User Management"],"summary":"Lock user (ADMIN only)","description":"Lock a user account (prevents login).","operationId":"lockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User locked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Invalid status transition","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers":{"get":{"tags":["Suppliers"],"operationId":"listSuppliers","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SupplierResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Suppliers"],"operationId":"createSupplier","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/rating":{"post":{"tags":["Suppliers"],"operationId":"rateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/deactivate":{"post":{"tags":["Suppliers"],"operationId":"deactivate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/certificates":{"post":{"tags":["Suppliers"],"operationId":"addCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Suppliers"],"operationId":"removeCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/activate":{"post":{"tags":["Suppliers"],"operationId":"activate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes":{"post":{"tags":["Recipes"],"operationId":"createRecipe","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRecipeRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/ingredients":{"post":{"tags":["Recipes"],"operationId":"addIngredient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRecipeIngredientRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations":{"get":{"tags":["Storage Locations"],"operationId":"listStorageLocations","parameters":[{"name":"storageType","in":"query","required":false,"schema":{"type":"string"}},{"name":"active","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Storage Locations"],"operationId":"createStorageLocation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStorageLocationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers":{"get":{"tags":["Customers"],"operationId":"listCustomers","parameters":[{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["B2C","B2B"]}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CustomerResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Customers"],"operationId":"createCustomer","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses":{"post":{"tags":["Customers"],"operationId":"addDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddDeliveryAddressRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/deactivate":{"post":{"tags":["Customers"],"operationId":"deactivate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/activate":{"post":{"tags":["Customers"],"operationId":"activate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/categories":{"get":{"tags":["Product Categories"],"operationId":"listCategories","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Product Categories"],"operationId":"createCategory","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Refresh access token","description":"Refresh an expired access token using a valid refresh token.","operationId":"refresh","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"200":{"description":"Token refresh successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid or expired refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/auth/logout":{"post":{"tags":["Authentication"],"summary":"User logout","description":"Invalidate current JWT token.","operationId":"logout","responses":{"401":{"description":"Invalid or missing authentication token"},"204":{"description":"Logout successful"}}}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"User login","description":"Authenticate user with username and password. Returns JWT tokens.","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"429":{"description":"Too many login attempts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid credentials, user locked, or user inactive","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"200":{"description":"Login successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/articles":{"get":{"tags":["Articles"],"operationId":"listArticles","parameters":[{"name":"categoryId","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ArticleResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Articles"],"operationId":"createArticle","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers":{"post":{"tags":["Articles"],"operationId":"assignSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units":{"post":{"tags":["Articles"],"operationId":"addSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSalesUnitRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/deactivate":{"post":{"tags":["Articles"],"operationId":"deactivate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/activate":{"post":{"tags":["Articles"],"operationId":"activate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}/deactivate":{"patch":{"tags":["Storage Locations"],"operationId":"deactivateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}/activate":{"patch":{"tags":["Storage Locations"],"operationId":"activateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/roles":{"get":{"tags":["Role Management"],"summary":"List all roles (ADMIN only)","description":"Get a list of all available roles in the system. Requires USER_MANAGEMENT permission.","operationId":"listRoles","responses":{"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"200":{"description":"Roles retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RoleDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles/{roleName}":{"delete":{"tags":["User Management"],"summary":"Remove role (ADMIN only)","description":"Remove a role from a user.","operationId":"removeRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}},{"name":"roleName","in":"path","description":"Role name","required":true,"schema":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}}],"responses":{"403":{"description":"Missing permission"},"404":{"description":"User or role not found"},"204":{"description":"Role removed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/ingredients/{ingredientId}":{"delete":{"tags":["Recipes"],"operationId":"removeIngredient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"ingredientId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses/{label}":{"delete":{"tags":["Customers"],"operationId":"removeDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"label","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers/{supplierId}":{"delete":{"tags":["Articles"],"operationId":"removeSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"supplierId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}":{"delete":{"tags":["Articles"],"operationId":"removeSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}}},"components":{"schemas":{"UpdateUserRequest":{"type":"object","properties":{"email":{"type":"string","description":"New email address","example":"newemail@example.com"},"branchId":{"type":"string","description":"New branch ID","example":"BRANCH-002"}},"description":"Request to update user details"},"RoleDTO":{"required":["description","id","name","permissions"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]},"permissions":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["RECIPE_READ","RECIPE_WRITE","RECIPE_DELETE","BATCH_READ","BATCH_WRITE","BATCH_COMPLETE","BATCH_DELETE","PRODUCTION_ORDER_READ","PRODUCTION_ORDER_WRITE","PRODUCTION_ORDER_DELETE","HACCP_READ","HACCP_WRITE","TEMPERATURE_LOG_READ","TEMPERATURE_LOG_WRITE","CLEANING_RECORD_READ","CLEANING_RECORD_WRITE","GOODS_INSPECTION_READ","GOODS_INSPECTION_WRITE","STOCK_READ","STOCK_WRITE","STOCK_MOVEMENT_READ","STOCK_MOVEMENT_WRITE","INVENTORY_COUNT_READ","INVENTORY_COUNT_WRITE","PURCHASE_ORDER_READ","PURCHASE_ORDER_WRITE","PURCHASE_ORDER_DELETE","GOODS_RECEIPT_READ","GOODS_RECEIPT_WRITE","SUPPLIER_READ","SUPPLIER_WRITE","SUPPLIER_DELETE","ORDER_READ","ORDER_WRITE","ORDER_DELETE","INVOICE_READ","INVOICE_WRITE","INVOICE_DELETE","CUSTOMER_READ","CUSTOMER_WRITE","CUSTOMER_DELETE","LABEL_READ","LABEL_WRITE","LABEL_PRINT","MASTERDATA_READ","MASTERDATA_WRITE","BRANCH_READ","BRANCH_WRITE","BRANCH_DELETE","USER_READ","USER_WRITE","USER_DELETE","USER_LOCK","USER_UNLOCK","ROLE_READ","ROLE_WRITE","ROLE_ASSIGN","ROLE_REMOVE","REPORT_READ","REPORT_GENERATE","NOTIFICATION_READ","NOTIFICATION_SEND","AUDIT_LOG_READ","SYSTEM_SETTINGS_READ","SYSTEM_SETTINGS_WRITE"]}},"description":{"type":"string"}}},"UserDTO":{"required":["createdAt","email","id","roles","status","username"],"type":"object","properties":{"id":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"roles":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}},"branchId":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","LOCKED"]},"createdAt":{"type":"string","format":"date-time"},"lastLogin":{"type":"string","format":"date-time"}}},"ChangePasswordRequest":{"required":["currentPassword","newPassword"],"type":"object","properties":{"currentPassword":{"type":"string","description":"Current password","example":"OldPass123"},"newPassword":{"maxLength":2147483647,"minLength":8,"type":"string","description":"New password (min 8 characters)","example":"NewSecurePass456"}},"description":"Request to change user password"},"UpdateSupplierRequest":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddressResponse":{"required":["city","country","houseNumber","postalCode","street"],"type":"object","properties":{"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"}},"nullable":true},"ContactInfoResponse":{"required":["contactPerson","email","phone"],"type":"object","properties":{"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"}}},"PaymentTermsResponse":{"required":["paymentDescription","paymentDueDays"],"type":"object","properties":{"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}},"nullable":true},"QualityCertificateResponse":{"required":["certificateType","issuer","validFrom","validUntil"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"SupplierRatingResponse":{"required":["deliveryScore","priceScore","qualityScore"],"type":"object","properties":{"qualityScore":{"type":"integer","format":"int32"},"deliveryScore":{"type":"integer","format":"int32"},"priceScore":{"type":"integer","format":"int32"}},"nullable":true},"SupplierResponse":{"required":["certificates","contactInfo","createdAt","id","name","status","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"certificates":{"type":"array","items":{"$ref":"#/components/schemas/QualityCertificateResponse"}},"rating":{"$ref":"#/components/schemas/SupplierRatingResponse"},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"UpdateStorageLocationRequest":{"type":"object","properties":{"name":{"type":"string"},"minTemperature":{"type":"string"},"maxTemperature":{"type":"string"}}},"StorageLocationResponse":{"required":["active","id","name","storageType"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"storageType":{"type":"string"},"temperatureRange":{"$ref":"#/components/schemas/TemperatureRangeResponse"},"active":{"type":"boolean"}}},"TemperatureRangeResponse":{"required":["maxTemperature","minTemperature"],"type":"object","properties":{"minTemperature":{"type":"number"},"maxTemperature":{"type":"number"}},"nullable":true},"UpdateCustomerRequest":{"type":"object","properties":{"name":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"ContractLineItemResponse":{"required":["agreedPrice","agreedQuantity","articleId","unit"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string"}}},"CustomerResponse":{"required":["billingAddress","contactInfo","createdAt","deliveryAddresses","id","name","preferences","status","type","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"billingAddress":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"deliveryAddresses":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryAddressResponse"}},"frameContract":{"$ref":"#/components/schemas/FrameContractResponse"},"preferences":{"type":"array","items":{"type":"string"}},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"DeliveryAddressResponse":{"required":["address","contactPerson","deliveryNotes","label"],"type":"object","properties":{"label":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"FrameContractResponse":{"required":["deliveryRhythm","id","lineItems","validFrom","validUntil"],"type":"object","properties":{"id":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"deliveryRhythm":{"type":"string"},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/ContractLineItemResponse"}}},"nullable":true},"SetPreferencesRequest":{"required":["preferences"],"type":"object","properties":{"preferences":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["BIO","REGIONAL","TIERWOHL","HALAL","KOSHER","GLUTENFREI","LAKTOSEFREI"]}}}},"LineItem":{"required":["agreedPrice","articleId"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]}}},"SetFrameContractRequest":{"required":["lineItems","rhythm"],"type":"object","properties":{"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"rhythm":{"type":"string","enum":["DAILY","WEEKLY","BIWEEKLY","MONTHLY","ON_DEMAND"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/LineItem"}}}},"UpdateProductCategoryRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ProductCategoryResponse":{"required":["description","id","name"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}},"UpdateArticleRequest":{"type":"object","properties":{"name":{"type":"string"},"categoryId":{"type":"string"}}},"ArticleResponse":{"required":["articleNumber","categoryId","createdAt","id","name","salesUnits","status","supplierIds","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"salesUnits":{"type":"array","items":{"$ref":"#/components/schemas/SalesUnitResponse"}},"status":{"type":"string"},"supplierIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SalesUnitResponse":{"required":["id","price","priceModel","unit"],"type":"object","properties":{"id":{"type":"string"},"unit":{"type":"string"},"priceModel":{"type":"string"},"price":{"type":"number"}}},"UpdateSalesUnitPriceRequest":{"required":["price"],"type":"object","properties":{"price":{"type":"number"}}},"CreateUserRequest":{"required":["email","password","roleNames","username"],"type":"object","properties":{"username":{"maxLength":50,"minLength":3,"type":"string","description":"Username (unique)","example":"john.doe"},"email":{"type":"string","description":"Email address (unique)","example":"john.doe@example.com"},"password":{"maxLength":2147483647,"minLength":8,"type":"string","description":"Password (min 8 characters)","example":"SecurePass123"},"roleNames":{"uniqueItems":true,"type":"array","description":"Role names to assign","example":["USER","MANAGER"],"items":{"type":"string","description":"Role names to assign","example":"[\"USER\",\"MANAGER\"]","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"branchId":{"type":"string","description":"Branch ID (optional)","example":"BRANCH-001"}},"description":"Request to create a new user"},"AssignRoleRequest":{"required":["roleName"],"type":"object","properties":{"roleName":{"type":"string","description":"Role name to assign","example":"MANAGER","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"description":"Request to assign a role to a user"},"CreateSupplierRequest":{"required":["name","phone"],"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"RateSupplierRequest":{"type":"object","properties":{"qualityScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"deliveryScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"priceScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"}}},"AddCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"CreateRecipeRequest":{"required":["name","outputQuantity","outputUom","type"],"type":"object","properties":{"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string","enum":["RAW_MATERIAL","INTERMEDIATE","FINISHED_PRODUCT"]},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32"},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"}}},"IngredientResponse":{"required":["articleId","id","position","quantity","substitutable","uom"],"type":"object","properties":{"id":{"type":"string"},"position":{"type":"integer","format":"int32"},"articleId":{"type":"string"},"quantity":{"type":"string"},"uom":{"type":"string"},"subRecipeId":{"type":"string","nullable":true},"substitutable":{"type":"boolean"}}},"RecipeResponse":{"required":["createdAt","description","id","ingredients","name","outputQuantity","outputUom","status","type","updatedAt","version","yieldPercentage"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string"},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32","nullable":true},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"status":{"type":"string"},"ingredients":{"type":"array","items":{"$ref":"#/components/schemas/IngredientResponse"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"AddRecipeIngredientRequest":{"required":["articleId","quantity","uom"],"type":"object","properties":{"position":{"minimum":1,"type":"integer","format":"int32"},"articleId":{"type":"string"},"quantity":{"type":"string"},"uom":{"type":"string"},"subRecipeId":{"type":"string"},"substitutable":{"type":"boolean"}}},"CreateStorageLocationRequest":{"required":["name","storageType"],"type":"object","properties":{"name":{"type":"string"},"storageType":{"type":"string"},"minTemperature":{"type":"string"},"maxTemperature":{"type":"string"}}},"CreateCustomerRequest":{"required":["city","country","name","phone","postalCode","street","type"],"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["B2C","B2B"]},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddDeliveryAddressRequest":{"required":["city","country","postalCode","street"],"type":"object","properties":{"label":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"CreateProductCategoryRequest":{"required":["name"],"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"RefreshTokenRequest":{"required":["refreshToken"],"type":"object","properties":{"refreshToken":{"type":"string","description":"Refresh token"}},"description":"Refresh token request"},"LoginResponse":{"required":["accessToken","expiresAt","expiresIn","refreshToken","tokenType"],"type":"object","properties":{"accessToken":{"type":"string","description":"JWT access token","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"tokenType":{"type":"string","description":"Token type","example":"Bearer"},"expiresIn":{"type":"integer","description":"Token expiration time in seconds","format":"int64","example":3600},"expiresAt":{"type":"string","description":"Token expiration timestamp","format":"date-time"},"refreshToken":{"type":"string","description":"Refresh token for obtaining new access token"}},"description":"Login response with JWT tokens"},"LoginRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string","description":"Username","example":"admin"},"password":{"type":"string","description":"Password","example":"admin123"}},"description":"Login request with username and password"},"CreateArticleRequest":{"required":["articleNumber","categoryId","name","price","priceModel","unit"],"type":"object","properties":{"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"AssignSupplierRequest":{"required":["supplierId"],"type":"object","properties":{"supplierId":{"type":"string"}}},"AddSalesUnitRequest":{"required":["price","priceModel","unit"],"type":"object","properties":{"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"RemoveCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"}}}},"securitySchemes":{"Bearer Authentication":{"type":"http","description":"JWT authentication token obtained from POST /api/auth/login.\n\nFormat: Bearer \n\nExample:\nBearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\n","scheme":"bearer","bearerFormat":"JWT"}}}} \ No newline at end of file diff --git a/frontend/packages/api-client/src/index.ts b/frontend/packages/api-client/src/index.ts index b692c6d..a479b85 100644 --- a/frontend/packages/api-client/src/index.ts +++ b/frontend/packages/api-client/src/index.ts @@ -22,6 +22,8 @@ export { createCategoriesResource } from './resources/categories.js'; export { createSuppliersResource } from './resources/suppliers.js'; export { createArticlesResource } from './resources/articles.js'; export { createCustomersResource } from './resources/customers.js'; +export { createStorageLocationsResource } from './resources/storage-locations.js'; +export { createRecipesResource } from './resources/recipes.js'; export { ApiError, AuthenticationError, @@ -29,12 +31,15 @@ export { RefreshTokenExpiredError, } from './errors.js'; export type { ValidationErrorDetail } from './errors.js'; +// Auth types (no generated alias, stay in resource) export type { LoginRequest, LoginResponse, RefreshTokenRequest, AuthResource, } from './resources/auth.js'; + +// Types from @effigenix/types (generated OpenAPI aliases) export type { UserDTO, RoleDTO, @@ -42,18 +47,7 @@ export type { UpdateUserRequest, ChangePasswordRequest, 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, - SupplierStatus, AddressDTO, ContactInfoDTO, PaymentTermsDTO, @@ -64,36 +58,57 @@ export type { RateSupplierRequest, AddCertificateRequest, RemoveCertificateRequest, - SuppliersResource, -} from './resources/suppliers.js'; -export type { + ProductCategoryDTO, + CreateCategoryRequest, + UpdateCategoryRequest, ArticleDTO, - ArticleStatus, SalesUnitDTO, - Unit, - PriceModel, CreateArticleRequest, UpdateArticleRequest, AddSalesUnitRequest, UpdateSalesUnitPriceRequest, - ArticlesResource, -} from './resources/articles.js'; -export { UNIT_LABELS, PRICE_MODEL_LABELS } from './resources/articles.js'; -export type { CustomerDTO, - CustomerType, - CustomerStatus, - CustomerPreference, - DeliveryRhythm, DeliveryAddressDTO, FrameContractDTO, ContractLineItemDTO, CreateCustomerRequest, UpdateCustomerRequest, 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, + CustomerType, + CustomerStatus, + CustomerPreference, + DeliveryRhythm, } 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 { createAuthResource } from './resources/auth.js'; @@ -103,6 +118,8 @@ import { createCategoriesResource } from './resources/categories.js'; import { createSuppliersResource } from './resources/suppliers.js'; import { createArticlesResource } from './resources/articles.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 { ApiConfig } from '@effigenix/config'; @@ -124,6 +141,8 @@ export function createEffigenixClient( suppliers: createSuppliersResource(axiosClient), articles: createArticlesResource(axiosClient), customers: createCustomersResource(axiosClient), + storageLocations: createStorageLocationsResource(axiosClient), + recipes: createRecipesResource(axiosClient), }; } diff --git a/frontend/packages/api-client/src/resources/articles.ts b/frontend/packages/api-client/src/resources/articles.ts index 18caff7..7c101bb 100644 --- a/frontend/packages/api-client/src/resources/articles.ts +++ b/frontend/packages/api-client/src/resources/articles.ts @@ -10,6 +10,14 @@ */ 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 PriceModel = 'FIXED' | 'WEIGHT_BASED'; @@ -27,48 +35,14 @@ export const PRICE_MODEL_LABELS: Record = { WEIGHT_BASED: 'Gewichtsbasiert', }; -export interface SalesUnitDTO { - id: string; - unit: Unit; - priceModel: PriceModel; - price: number; -} - -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; -} +export type { + ArticleDTO, + SalesUnitDTO, + CreateArticleRequest, + UpdateArticleRequest, + AddSalesUnitRequest, + UpdateSalesUnitPriceRequest, +}; // ── Resource factory ───────────────────────────────────────────────────────── diff --git a/frontend/packages/api-client/src/resources/categories.ts b/frontend/packages/api-client/src/resources/categories.ts index 0bcc9e0..0ca2526 100644 --- a/frontend/packages/api-client/src/resources/categories.ts +++ b/frontend/packages/api-client/src/resources/categories.ts @@ -7,22 +7,17 @@ */ import type { AxiosInstance } from 'axios'; +import type { + ProductCategoryDTO, + CreateCategoryRequest, + UpdateCategoryRequest, +} from '@effigenix/types'; -export interface ProductCategoryDTO { - id: string; - name: string; - description: string | null; -} - -export interface CreateCategoryRequest { - name: string; - description?: string; -} - -export interface UpdateCategoryRequest { - name?: string; - description?: string | null; -} +export type { + ProductCategoryDTO, + CreateCategoryRequest, + UpdateCategoryRequest, +}; // ── Resource factory ───────────────────────────────────────────────────────── diff --git a/frontend/packages/api-client/src/resources/customers.ts b/frontend/packages/api-client/src/resources/customers.ts index 0a72fb4..0a80655 100644 --- a/frontend/packages/api-client/src/resources/customers.ts +++ b/frontend/packages/api-client/src/resources/customers.ts @@ -12,7 +12,16 @@ */ 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 CustomerStatus = 'ACTIVE' | 'INACTIVE'; @@ -44,96 +53,16 @@ export const DELIVERY_RHYTHM_LABELS: Record = { ON_DEMAND: 'Nach Bedarf', }; -export interface DeliveryAddressDTO { - label: string; - address: AddressDTO; - contactPerson: string | null; - deliveryNotes: string | null; -} - -export interface ContractLineItemDTO { - articleId: string; - 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[]; -} +export type { + CustomerDTO, + DeliveryAddressDTO, + FrameContractDTO, + ContractLineItemDTO, + CreateCustomerRequest, + UpdateCustomerRequest, + AddDeliveryAddressRequest, + SetFrameContractRequest, +}; // ── Resource factory ───────────────────────────────────────────────────────── diff --git a/frontend/packages/api-client/src/resources/recipes.ts b/frontend/packages/api-client/src/resources/recipes.ts new file mode 100644 index 0000000..fb56726 --- /dev/null +++ b/frontend/packages/api-client/src/resources/recipes.ts @@ -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 = { + 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 { + const res = await client.get(BASE); + return res.data; + }, + + async getById(id: string): Promise { + const res = await client.get(`${BASE}/${id}`); + return res.data; + }, + + async create(request: CreateRecipeRequest): Promise { + const res = await client.post(BASE, request); + return res.data; + }, + + async addIngredient(id: string, request: AddRecipeIngredientRequest): Promise { + const res = await client.post(`${BASE}/${id}/ingredients`, request); + return res.data; + }, + + async removeIngredient(recipeId: string, ingredientId: string): Promise { + await client.delete(`${BASE}/${recipeId}/ingredients/${ingredientId}`); + const res = await client.get(`${BASE}/${recipeId}`); + return res.data; + }, + }; +} + +export type RecipesResource = ReturnType; diff --git a/frontend/packages/api-client/src/resources/storage-locations.ts b/frontend/packages/api-client/src/resources/storage-locations.ts new file mode 100644 index 0000000..468054e --- /dev/null +++ b/frontend/packages/api-client/src/resources/storage-locations.ts @@ -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 = { + 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 { + const params: Record = {}; + if (filter?.storageType) params['storageType'] = filter.storageType; + if (filter?.active !== undefined) params['active'] = String(filter.active); + const res = await client.get(BASE, { params }); + return res.data; + }, + + async getById(id: string): Promise { + const res = await client.get(`${BASE}/${id}`); + return res.data; + }, + + async create(request: CreateStorageLocationRequest): Promise { + const res = await client.post(BASE, request); + return res.data; + }, + + async update(id: string, request: UpdateStorageLocationRequest): Promise { + const res = await client.put(`${BASE}/${id}`, request); + return res.data; + }, + + async activate(id: string): Promise { + const res = await client.patch(`${BASE}/${id}/activate`); + return res.data; + }, + + async deactivate(id: string): Promise { + const res = await client.patch(`${BASE}/${id}/deactivate`); + return res.data; + }, + }; +} + +export type StorageLocationsResource = ReturnType; diff --git a/frontend/packages/api-client/src/resources/suppliers.ts b/frontend/packages/api-client/src/resources/suppliers.ts index f953ac2..1b0609b 100644 --- a/frontend/packages/api-client/src/resources/suppliers.ts +++ b/frontend/packages/api-client/src/resources/suppliers.ts @@ -10,100 +10,35 @@ */ import type { AxiosInstance } from 'axios'; - -export interface AddressDTO { - street: string; - houseNumber: string | null; - postalCode: string; - city: string; - country: string; -} - -export interface ContactInfoDTO { - phone: string; - email: string | null; - contactPerson: string | null; -} - -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; -} +import type { + SupplierDTO, + AddressDTO, + ContactInfoDTO, + PaymentTermsDTO, + QualityCertificateDTO, + SupplierRatingDTO, + CreateSupplierRequest, + UpdateSupplierRequest, + RateSupplierRequest, + AddCertificateRequest, + RemoveCertificateRequest, +} from '@effigenix/types'; export type SupplierStatus = 'ACTIVE' | 'INACTIVE'; -export interface SupplierDTO { - id: string; - name: string; - status: SupplierStatus; - address: AddressDTO | null; - contactInfo: ContactInfoDTO; - paymentTerms: PaymentTermsDTO | null; - certificates: QualityCertificateDTO[]; - rating: SupplierRatingDTO | null; - createdAt: string; - updatedAt: string; -} - -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; -} +export type { + SupplierDTO, + AddressDTO, + ContactInfoDTO, + PaymentTermsDTO, + QualityCertificateDTO, + SupplierRatingDTO, + CreateSupplierRequest, + UpdateSupplierRequest, + RateSupplierRequest, + AddCertificateRequest, + RemoveCertificateRequest, +}; // ── Resource factory ───────────────────────────────────────────────────────── diff --git a/frontend/packages/api-client/src/resources/users.ts b/frontend/packages/api-client/src/resources/users.ts index 17489bf..fd0b8dc 100644 --- a/frontend/packages/api-client/src/resources/users.ts +++ b/frontend/packages/api-client/src/resources/users.ts @@ -4,45 +4,23 @@ import type { AxiosInstance } from 'axios'; import { API_PATHS } from '@effigenix/config'; +import type { + UserDTO, + RoleDTO, + CreateUserRequest, + UpdateUserRequest, + ChangePasswordRequest, + AssignRoleRequest, +} from '@effigenix/types'; -export interface UserDTO { - id: string; - username: string; - email: string; - roles: RoleDTO[]; - branchId?: string; - status: 'ACTIVE' | 'LOCKED'; - 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 type { + UserDTO, + RoleDTO, + CreateUserRequest, + UpdateUserRequest, + ChangePasswordRequest, + AssignRoleRequest, +}; export function createUsersResource(client: AxiosInstance) { return { diff --git a/frontend/packages/types/src/article.ts b/frontend/packages/types/src/article.ts new file mode 100644 index 0000000..2171ef9 --- /dev/null +++ b/frontend/packages/types/src/article.ts @@ -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']; diff --git a/frontend/packages/types/src/category.ts b/frontend/packages/types/src/category.ts new file mode 100644 index 0000000..a79aca2 --- /dev/null +++ b/frontend/packages/types/src/category.ts @@ -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']; diff --git a/frontend/packages/types/src/customer.ts b/frontend/packages/types/src/customer.ts new file mode 100644 index 0000000..48c7c61 --- /dev/null +++ b/frontend/packages/types/src/customer.ts @@ -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']; diff --git a/frontend/packages/types/src/generated/api.ts b/frontend/packages/types/src/generated/api.ts index c37b877..94c491a 100644 --- a/frontend/packages/types/src/generated/api.ts +++ b/frontend/packages/types/src/generated/api.ts @@ -13,12 +13,12 @@ export interface paths { }; /** * Get user by ID - * @description Retrieve a single user by their ID. Requires authentication. + * @description Retrieve a single user by their ID. */ get: operations["getUserById"]; /** * Update user - * @description Update user details (email, branchId). Only provided fields will be updated. + * @description Update user details (email, branchId). */ put: operations["updateUser"]; post?: never; @@ -64,6 +64,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/inventory/storage-locations/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put: operations["updateStorageLocation"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/customers/{id}": { parameters: { query?: never; @@ -169,13 +185,13 @@ export interface paths { }; /** * List all users - * @description Get a list of all users in the system. Requires authentication. + * @description Get a list of all users in the system. */ get: operations["listUsers"]; put?: never; /** * Create user (ADMIN only) - * @description Create a new user account with specified roles. Requires USER_MANAGEMENT permission. + * @description Create a new user account with specified roles. */ post: operations["createUser"]; delete?: never; @@ -195,7 +211,7 @@ export interface paths { put?: never; /** * Unlock user (ADMIN only) - * @description Unlock a user account (allows login). Requires USER_MANAGEMENT permission. + * @description Unlock a user account (allows login). */ post: operations["unlockUser"]; delete?: never; @@ -215,7 +231,7 @@ export interface paths { put?: never; /** * Assign role (ADMIN only) - * @description Assign a role to a user. Requires USER_MANAGEMENT permission. + * @description Assign a role to a user. */ post: operations["assignRole"]; delete?: never; @@ -235,7 +251,7 @@ export interface paths { put?: never; /** * Lock user (ADMIN only) - * @description Lock a user account (prevents login). Requires USER_MANAGEMENT permission. + * @description Lock a user account (prevents login). */ post: operations["lockUser"]; delete?: never; @@ -324,6 +340,54 @@ export interface paths { patch?: never; trace?: never; }; + "/api/recipes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createRecipe"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/recipes/{id}/ingredients": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["addIngredient"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/inventory/storage-locations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listStorageLocations"]; + put?: never; + post: operations["createStorageLocation"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/customers": { parameters: { query?: never; @@ -415,7 +479,7 @@ export interface paths { put?: never; /** * Refresh access token - * @description Refresh an expired access token using a valid refresh token. Returns new access token and refresh token. + * @description Refresh an expired access token using a valid refresh token. */ post: operations["refresh"]; delete?: never; @@ -435,7 +499,7 @@ export interface paths { put?: never; /** * User logout - * @description Invalidate current JWT token. Requires authentication. + * @description Invalidate current JWT token. */ post: operations["logout"]; delete?: never; @@ -455,7 +519,7 @@ export interface paths { put?: never; /** * User login - * @description Authenticate user with username and password. Returns JWT access token and refresh token. + * @description Authenticate user with username and password. Returns JWT tokens. */ post: operations["login"]; delete?: never; @@ -544,6 +608,38 @@ export interface paths { patch?: never; trace?: never; }; + "/api/inventory/storage-locations/{id}/deactivate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: operations["deactivateStorageLocation"]; + trace?: never; + }; + "/api/inventory/storage-locations/{id}/activate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: operations["activateStorageLocation"]; + trace?: never; + }; "/api/roles": { parameters: { query?: never; @@ -576,7 +672,7 @@ export interface paths { post?: never; /** * Remove role (ADMIN only) - * @description Remove a role from a user. Requires USER_MANAGEMENT permission. + * @description Remove a role from a user. */ delete: operations["removeRole"]; options?: never; @@ -584,6 +680,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/recipes/{id}/ingredients/{ingredientId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["removeIngredient"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/customers/{id}/delivery-addresses/{label}": { parameters: { query?: never; @@ -650,22 +762,22 @@ export interface components { branchId?: string; }; RoleDTO: { - id?: string; + id: string; /** @enum {string} */ - name?: "ADMIN" | "PRODUCTION_MANAGER" | "PRODUCTION_WORKER" | "QUALITY_MANAGER" | "QUALITY_INSPECTOR" | "PROCUREMENT_MANAGER" | "WAREHOUSE_WORKER" | "SALES_MANAGER" | "SALES_STAFF"; - permissions?: ("RECIPE_READ" | "RECIPE_WRITE" | "RECIPE_DELETE" | "BATCH_READ" | "BATCH_WRITE" | "BATCH_COMPLETE" | "BATCH_DELETE" | "PRODUCTION_ORDER_READ" | "PRODUCTION_ORDER_WRITE" | "PRODUCTION_ORDER_DELETE" | "HACCP_READ" | "HACCP_WRITE" | "TEMPERATURE_LOG_READ" | "TEMPERATURE_LOG_WRITE" | "CLEANING_RECORD_READ" | "CLEANING_RECORD_WRITE" | "GOODS_INSPECTION_READ" | "GOODS_INSPECTION_WRITE" | "STOCK_READ" | "STOCK_WRITE" | "STOCK_MOVEMENT_READ" | "STOCK_MOVEMENT_WRITE" | "INVENTORY_COUNT_READ" | "INVENTORY_COUNT_WRITE" | "PURCHASE_ORDER_READ" | "PURCHASE_ORDER_WRITE" | "PURCHASE_ORDER_DELETE" | "GOODS_RECEIPT_READ" | "GOODS_RECEIPT_WRITE" | "SUPPLIER_READ" | "SUPPLIER_WRITE" | "SUPPLIER_DELETE" | "ORDER_READ" | "ORDER_WRITE" | "ORDER_DELETE" | "INVOICE_READ" | "INVOICE_WRITE" | "INVOICE_DELETE" | "CUSTOMER_READ" | "CUSTOMER_WRITE" | "CUSTOMER_DELETE" | "LABEL_READ" | "LABEL_WRITE" | "LABEL_PRINT" | "BRANCH_READ" | "BRANCH_WRITE" | "BRANCH_DELETE" | "USER_READ" | "USER_WRITE" | "USER_DELETE" | "USER_LOCK" | "USER_UNLOCK" | "ROLE_READ" | "ROLE_WRITE" | "ROLE_ASSIGN" | "ROLE_REMOVE" | "REPORT_READ" | "REPORT_GENERATE" | "NOTIFICATION_READ" | "NOTIFICATION_SEND" | "AUDIT_LOG_READ" | "SYSTEM_SETTINGS_READ" | "SYSTEM_SETTINGS_WRITE")[]; - description?: string; + name: "ADMIN" | "PRODUCTION_MANAGER" | "PRODUCTION_WORKER" | "QUALITY_MANAGER" | "QUALITY_INSPECTOR" | "PROCUREMENT_MANAGER" | "WAREHOUSE_WORKER" | "SALES_MANAGER" | "SALES_STAFF"; + permissions: ("RECIPE_READ" | "RECIPE_WRITE" | "RECIPE_DELETE" | "BATCH_READ" | "BATCH_WRITE" | "BATCH_COMPLETE" | "BATCH_DELETE" | "PRODUCTION_ORDER_READ" | "PRODUCTION_ORDER_WRITE" | "PRODUCTION_ORDER_DELETE" | "HACCP_READ" | "HACCP_WRITE" | "TEMPERATURE_LOG_READ" | "TEMPERATURE_LOG_WRITE" | "CLEANING_RECORD_READ" | "CLEANING_RECORD_WRITE" | "GOODS_INSPECTION_READ" | "GOODS_INSPECTION_WRITE" | "STOCK_READ" | "STOCK_WRITE" | "STOCK_MOVEMENT_READ" | "STOCK_MOVEMENT_WRITE" | "INVENTORY_COUNT_READ" | "INVENTORY_COUNT_WRITE" | "PURCHASE_ORDER_READ" | "PURCHASE_ORDER_WRITE" | "PURCHASE_ORDER_DELETE" | "GOODS_RECEIPT_READ" | "GOODS_RECEIPT_WRITE" | "SUPPLIER_READ" | "SUPPLIER_WRITE" | "SUPPLIER_DELETE" | "ORDER_READ" | "ORDER_WRITE" | "ORDER_DELETE" | "INVOICE_READ" | "INVOICE_WRITE" | "INVOICE_DELETE" | "CUSTOMER_READ" | "CUSTOMER_WRITE" | "CUSTOMER_DELETE" | "LABEL_READ" | "LABEL_WRITE" | "LABEL_PRINT" | "MASTERDATA_READ" | "MASTERDATA_WRITE" | "BRANCH_READ" | "BRANCH_WRITE" | "BRANCH_DELETE" | "USER_READ" | "USER_WRITE" | "USER_DELETE" | "USER_LOCK" | "USER_UNLOCK" | "ROLE_READ" | "ROLE_WRITE" | "ROLE_ASSIGN" | "ROLE_REMOVE" | "REPORT_READ" | "REPORT_GENERATE" | "NOTIFICATION_READ" | "NOTIFICATION_SEND" | "AUDIT_LOG_READ" | "SYSTEM_SETTINGS_READ" | "SYSTEM_SETTINGS_WRITE")[]; + description: string; }; UserDTO: { - id?: string; - username?: string; - email?: string; - roles?: components["schemas"]["RoleDTO"][]; + id: string; + username: string; + email: string; + roles: components["schemas"]["RoleDTO"][]; branchId?: string; /** @enum {string} */ - status?: "ACTIVE" | "INACTIVE" | "LOCKED"; + status: "ACTIVE" | "INACTIVE" | "LOCKED"; /** Format: date-time */ - createdAt?: string; + createdAt: string; /** Format: date-time */ lastLogin?: string; }; @@ -696,7 +808,69 @@ export interface components { paymentDueDays?: number; paymentDescription?: string; }; - Supplier: Record; + AddressResponse: { + street: string; + houseNumber: string; + postalCode: string; + city: string; + country: string; + } | null; + ContactInfoResponse: { + phone: string; + email: string; + contactPerson: string; + }; + PaymentTermsResponse: { + /** Format: int32 */ + paymentDueDays: number; + paymentDescription: string; + } | null; + QualityCertificateResponse: { + certificateType: string; + issuer: string; + /** Format: date */ + validFrom: string; + /** Format: date */ + validUntil: string; + }; + SupplierRatingResponse: { + /** Format: int32 */ + qualityScore: number; + /** Format: int32 */ + deliveryScore: number; + /** Format: int32 */ + priceScore: number; + } | null; + SupplierResponse: { + id: string; + name: string; + address?: components["schemas"]["AddressResponse"]; + contactInfo: components["schemas"]["ContactInfoResponse"]; + paymentTerms?: components["schemas"]["PaymentTermsResponse"]; + certificates: components["schemas"]["QualityCertificateResponse"][]; + rating?: components["schemas"]["SupplierRatingResponse"]; + status: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + }; + UpdateStorageLocationRequest: { + name?: string; + minTemperature?: string; + maxTemperature?: string; + }; + StorageLocationResponse: { + id: string; + name: string; + storageType: string; + temperatureRange?: components["schemas"]["TemperatureRangeResponse"]; + active: boolean; + }; + TemperatureRangeResponse: { + minTemperature: number; + maxTemperature: number; + } | null; UpdateCustomerRequest: { name?: string; street?: string; @@ -711,14 +885,43 @@ export interface components { paymentDueDays?: number; paymentDescription?: string; }; - Customer: { - frameContract?: components["schemas"]["ResultCustomerErrorVoid"]; - preferences?: ("BIO" | "REGIONAL" | "TIERWOHL" | "HALAL" | "KOSHER" | "GLUTENFREI" | "LAKTOSEFREI")[]; + ContractLineItemResponse: { + articleId: string; + agreedPrice: number; + agreedQuantity: number; + unit: string; }; - ResultCustomerErrorVoid: { - failure?: boolean; - success?: boolean; + CustomerResponse: { + id: string; + name: string; + type: string; + billingAddress: components["schemas"]["AddressResponse"]; + contactInfo: components["schemas"]["ContactInfoResponse"]; + paymentTerms?: components["schemas"]["PaymentTermsResponse"]; + deliveryAddresses: components["schemas"]["DeliveryAddressResponse"][]; + frameContract?: components["schemas"]["FrameContractResponse"]; + preferences: string[]; + status: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; }; + DeliveryAddressResponse: { + label: string; + address: components["schemas"]["AddressResponse"]; + contactPerson: string; + deliveryNotes: string; + }; + FrameContractResponse: { + id: string; + /** Format: date */ + validFrom: string; + /** Format: date */ + validUntil: string; + deliveryRhythm: string; + lineItems: components["schemas"]["ContractLineItemResponse"][]; + } | null; SetPreferencesRequest: { preferences: ("BIO" | "REGIONAL" | "TIERWOHL" | "HALAL" | "KOSHER" | "GLUTENFREI" | "LAKTOSEFREI")[]; }; @@ -742,12 +945,34 @@ export interface components { name?: string; description?: string; }; - ProductCategory: Record; + ProductCategoryResponse: { + id: string; + name: string; + description: string; + }; UpdateArticleRequest: { name?: string; categoryId?: string; }; - Article: Record; + ArticleResponse: { + id: string; + name: string; + articleNumber: string; + categoryId: string; + salesUnits: components["schemas"]["SalesUnitResponse"][]; + status: string; + supplierIds: string[]; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + }; + SalesUnitResponse: { + id: string; + unit: string; + priceModel: string; + price: number; + }; UpdateSalesUnitPriceRequest: { price: number; }; @@ -821,6 +1046,65 @@ export interface components { /** Format: date */ validUntil?: string; }; + CreateRecipeRequest: { + name: string; + /** Format: int32 */ + version?: number; + /** @enum {string} */ + type: "RAW_MATERIAL" | "INTERMEDIATE" | "FINISHED_PRODUCT"; + description?: string; + /** Format: int32 */ + yieldPercentage?: number; + /** Format: int32 */ + shelfLifeDays?: number; + outputQuantity: string; + outputUom: string; + }; + IngredientResponse: { + id: string; + /** Format: int32 */ + position: number; + articleId: string; + quantity: string; + uom: string; + subRecipeId?: string | null; + substitutable: boolean; + }; + RecipeResponse: { + id: string; + name: string; + /** Format: int32 */ + version: number; + type: string; + description: string; + /** Format: int32 */ + yieldPercentage: number; + /** Format: int32 */ + shelfLifeDays?: number | null; + outputQuantity: string; + outputUom: string; + status: string; + ingredients: components["schemas"]["IngredientResponse"][]; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + }; + AddRecipeIngredientRequest: { + /** Format: int32 */ + position?: number; + articleId: string; + quantity: string; + uom: string; + subRecipeId?: string; + substitutable?: boolean; + }; + CreateStorageLocationRequest: { + name: string; + storageType: string; + minTemperature?: string; + maxTemperature?: string; + }; CreateCustomerRequest: { name: string; /** @enum {string} */ @@ -862,25 +1146,25 @@ export interface components { * @description JWT access token * @example eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... */ - accessToken?: string; + accessToken: string; /** * @description Token type * @example Bearer */ - tokenType?: string; + tokenType: string; /** * Format: int64 * @description Token expiration time in seconds * @example 3600 */ - expiresIn?: number; + expiresIn: number; /** * Format: date-time * @description Token expiration timestamp */ - expiresAt?: string; + expiresAt: string; /** @description Refresh token for obtaining new access token */ - refreshToken?: string; + refreshToken: string; }; /** @description Login request with username and password */ LoginRequest: { @@ -935,10 +1219,7 @@ export interface operations { query?: never; header?: never; path: { - /** - * @description User ID - * @example user-uuid - */ + /** @description User ID */ id: string; }; cookie?: never; @@ -979,10 +1260,7 @@ export interface operations { query?: never; header?: never; path: { - /** - * @description User ID - * @example user-uuid - */ + /** @description User ID */ id: string; }; cookie?: never; @@ -1002,15 +1280,6 @@ export interface operations { "*/*": components["schemas"]["UserDTO"]; }; }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "*/*": components["schemas"]["UserDTO"]; - }; - }; /** @description User not found */ 404: { headers: { @@ -1036,10 +1305,7 @@ export interface operations { query?: never; header?: never; path: { - /** - * @description User ID - * @example user-uuid - */ + /** @description User ID */ id: string; }; cookie?: never; @@ -1064,7 +1330,7 @@ export interface operations { }; content?: never; }; - /** @description Invalid current password or authentication required */ + /** @description Invalid current password */ 401: { headers: { [name: string]: unknown; @@ -1097,7 +1363,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Supplier"]; + "*/*": components["schemas"]["SupplierResponse"]; }; }; }; @@ -1123,7 +1389,33 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Supplier"]; + "*/*": components["schemas"]["SupplierResponse"]; + }; + }; + }; + }; + updateStorageLocation: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateStorageLocationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["StorageLocationResponse"]; }; }; }; @@ -1145,7 +1437,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Customer"]; + "*/*": components["schemas"]["CustomerResponse"]; }; }; }; @@ -1171,7 +1463,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Customer"]; + "*/*": components["schemas"]["CustomerResponse"]; }; }; }; @@ -1197,7 +1489,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Customer"]; + "*/*": components["schemas"]["CustomerResponse"]; }; }; }; @@ -1223,7 +1515,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Customer"]; + "*/*": components["schemas"]["CustomerResponse"]; }; }; }; @@ -1269,7 +1561,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["ProductCategory"]; + "*/*": components["schemas"]["ProductCategoryResponse"]; }; }; }; @@ -1311,7 +1603,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Article"]; + "*/*": components["schemas"]["ArticleResponse"]; }; }; }; @@ -1337,7 +1629,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Article"]; + "*/*": components["schemas"]["ArticleResponse"]; }; }; }; @@ -1364,7 +1656,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Article"]; + "*/*": components["schemas"]["ArticleResponse"]; }; }; }; @@ -1384,7 +1676,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["UserDTO"]; + "*/*": components["schemas"]["UserDTO"][]; }; }; /** @description Authentication required */ @@ -1429,7 +1721,7 @@ export interface operations { "*/*": components["schemas"]["UserDTO"]; }; }; - /** @description Missing USER_MANAGEMENT permission */ + /** @description Missing permission */ 403: { headers: { [name: string]: unknown; @@ -1454,10 +1746,7 @@ export interface operations { query?: never; header?: never; path: { - /** - * @description User ID - * @example user-uuid - */ + /** @description User ID */ id: string; }; cookie?: never; @@ -1473,16 +1762,7 @@ export interface operations { "*/*": components["schemas"]["UserDTO"]; }; }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "*/*": components["schemas"]["UserDTO"]; - }; - }; - /** @description Missing USER_MANAGEMENT permission */ + /** @description Missing permission */ 403: { headers: { [name: string]: unknown; @@ -1500,6 +1780,15 @@ export interface operations { "*/*": components["schemas"]["UserDTO"]; }; }; + /** @description Invalid status transition */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["UserDTO"]; + }; + }; }; }; assignRole: { @@ -1507,10 +1796,7 @@ export interface operations { query?: never; header?: never; path: { - /** - * @description User ID - * @example user-uuid - */ + /** @description User ID */ id: string; }; cookie?: never; @@ -1530,16 +1816,7 @@ export interface operations { "*/*": components["schemas"]["UserDTO"]; }; }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "*/*": components["schemas"]["UserDTO"]; - }; - }; - /** @description Missing USER_MANAGEMENT permission */ + /** @description Missing permission */ 403: { headers: { [name: string]: unknown; @@ -1564,10 +1841,7 @@ export interface operations { query?: never; header?: never; path: { - /** - * @description User ID - * @example user-uuid - */ + /** @description User ID */ id: string; }; cookie?: never; @@ -1583,16 +1857,7 @@ export interface operations { "*/*": components["schemas"]["UserDTO"]; }; }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "*/*": components["schemas"]["UserDTO"]; - }; - }; - /** @description Missing USER_MANAGEMENT permission */ + /** @description Missing permission */ 403: { headers: { [name: string]: unknown; @@ -1610,6 +1875,15 @@ export interface operations { "*/*": components["schemas"]["UserDTO"]; }; }; + /** @description Invalid status transition */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["UserDTO"]; + }; + }; }; }; listSuppliers: { @@ -1629,7 +1903,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Supplier"][]; + "*/*": components["schemas"]["SupplierResponse"][]; }; }; }; @@ -1653,7 +1927,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Supplier"]; + "*/*": components["schemas"]["SupplierResponse"]; }; }; }; @@ -1679,7 +1953,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Supplier"]; + "*/*": components["schemas"]["SupplierResponse"]; }; }; }; @@ -1701,7 +1975,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Supplier"]; + "*/*": components["schemas"]["SupplierResponse"]; }; }; }; @@ -1727,7 +2001,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Supplier"]; + "*/*": components["schemas"]["SupplierResponse"]; }; }; }; @@ -1773,7 +2047,104 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Supplier"]; + "*/*": components["schemas"]["SupplierResponse"]; + }; + }; + }; + }; + createRecipe: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateRecipeRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["RecipeResponse"]; + }; + }; + }; + }; + addIngredient: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AddRecipeIngredientRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["RecipeResponse"]; + }; + }; + }; + }; + listStorageLocations: { + parameters: { + query?: { + storageType?: string; + active?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["StorageLocationResponse"][]; + }; + }; + }; + }; + createStorageLocation: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateStorageLocationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["StorageLocationResponse"]; }; }; }; @@ -1796,7 +2167,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Customer"][]; + "*/*": components["schemas"]["CustomerResponse"][]; }; }; }; @@ -1820,7 +2191,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Customer"]; + "*/*": components["schemas"]["CustomerResponse"]; }; }; }; @@ -1846,7 +2217,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Customer"]; + "*/*": components["schemas"]["CustomerResponse"]; }; }; }; @@ -1868,7 +2239,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Customer"]; + "*/*": components["schemas"]["CustomerResponse"]; }; }; }; @@ -1890,7 +2261,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Customer"]; + "*/*": components["schemas"]["CustomerResponse"]; }; }; }; @@ -1910,7 +2281,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["ProductCategory"][]; + "*/*": components["schemas"]["ProductCategoryResponse"][]; }; }; }; @@ -1934,7 +2305,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["ProductCategory"]; + "*/*": components["schemas"]["ProductCategoryResponse"]; }; }; }; @@ -1961,15 +2332,6 @@ export interface operations { "*/*": components["schemas"]["LoginResponse"]; }; }; - /** @description Validation error (missing refresh token) */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "*/*": components["schemas"]["LoginResponse"]; - }; - }; /** @description Invalid or expired refresh token */ 401: { headers: { @@ -2028,8 +2390,8 @@ export interface operations { "*/*": components["schemas"]["LoginResponse"]; }; }; - /** @description Validation error (missing username or password) */ - 400: { + /** @description Invalid credentials, user locked, or user inactive */ + 401: { headers: { [name: string]: unknown; }; @@ -2037,8 +2399,8 @@ export interface operations { "*/*": components["schemas"]["LoginResponse"]; }; }; - /** @description Invalid credentials, user locked, or user inactive */ - 401: { + /** @description Too many login attempts */ + 429: { headers: { [name: string]: unknown; }; @@ -2066,7 +2428,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Article"][]; + "*/*": components["schemas"]["ArticleResponse"][]; }; }; }; @@ -2090,7 +2452,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Article"]; + "*/*": components["schemas"]["ArticleResponse"]; }; }; }; @@ -2116,7 +2478,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Article"]; + "*/*": components["schemas"]["ArticleResponse"]; }; }; }; @@ -2142,7 +2504,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Article"]; + "*/*": components["schemas"]["ArticleResponse"]; }; }; }; @@ -2164,7 +2526,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Article"]; + "*/*": components["schemas"]["ArticleResponse"]; }; }; }; @@ -2186,7 +2548,51 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Article"]; + "*/*": components["schemas"]["ArticleResponse"]; + }; + }; + }; + }; + deactivateStorageLocation: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["StorageLocationResponse"]; + }; + }; + }; + }; + activateStorageLocation: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["StorageLocationResponse"]; }; }; }; @@ -2234,15 +2640,9 @@ export interface operations { query?: never; header?: never; path: { - /** - * @description User ID - * @example user-uuid - */ + /** @description User ID */ id: string; - /** - * @description Role name - * @example MANAGER - */ + /** @description Role name */ roleName: "ADMIN" | "PRODUCTION_MANAGER" | "PRODUCTION_WORKER" | "QUALITY_MANAGER" | "QUALITY_INSPECTOR" | "PROCUREMENT_MANAGER" | "WAREHOUSE_WORKER" | "SALES_MANAGER" | "SALES_STAFF"; }; cookie?: never; @@ -2256,14 +2656,7 @@ export interface operations { }; content?: never; }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Missing USER_MANAGEMENT permission */ + /** @description Missing permission */ 403: { headers: { [name: string]: unknown; @@ -2279,6 +2672,27 @@ export interface operations { }; }; }; + removeIngredient: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + ingredientId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; removeDeliveryAddress: { parameters: { query?: never; diff --git a/frontend/packages/types/src/index.ts b/frontend/packages/types/src/index.ts index b9333a6..a536969 100644 --- a/frontend/packages/types/src/index.ts +++ b/frontend/packages/types/src/index.ts @@ -11,6 +11,12 @@ export * from './user'; export * from './role'; export * from './common'; 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 export type { components, paths } from './generated/api'; diff --git a/frontend/packages/types/src/inventory.ts b/frontend/packages/types/src/inventory.ts new file mode 100644 index 0000000..5934e51 --- /dev/null +++ b/frontend/packages/types/src/inventory.ts @@ -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']; diff --git a/frontend/packages/types/src/production.ts b/frontend/packages/types/src/production.ts new file mode 100644 index 0000000..43ff811 --- /dev/null +++ b/frontend/packages/types/src/production.ts @@ -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']; diff --git a/frontend/packages/types/src/supplier.ts b/frontend/packages/types/src/supplier.ts new file mode 100644 index 0000000..e38eea3 --- /dev/null +++ b/frontend/packages/types/src/supplier.ts @@ -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'];