mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 10:19:35 +01:00
feat: TUI-Screens für Inventar und Produktion + API-Client Typ-Migration
Neue TUI-Features:
- Inventar: Lageorte auflisten, anlegen, bearbeiten, (de-)aktivieren
- Produktion: Rezepte auflisten, anlegen, Detail-Ansicht
- Navigation erweitert (Hauptmenü, Routing)
API-Client auf generierte OpenAPI-Typen umgestellt:
- 6 neue Alias-Dateien in @effigenix/types (supplier, category, article,
customer, inventory, production)
- api-client Re-Exports direkt von @effigenix/types statt via Resources
- Backend: @Schema(requiredProperties) auf 16 Response-Records
- Backend: OpenApiCustomizer für application-layer DTOs (UserDTO, RoleDTO)
Hinweis: Backend-Endpoints für GET /api/recipes und
GET /api/inventory/storage-locations/{id} fehlen noch (separate Issues).
This commit is contained in:
parent
bee3f28b5f
commit
c26d72fbe7
48 changed files with 2090 additions and 474 deletions
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -2,19 +2,21 @@ package de.effigenix.infrastructure.masterdata.web.dto;
|
|||
|
||||
import de.effigenix.domain.masterdata.Customer;
|
||||
import de.effigenix.domain.masterdata.CustomerPreference;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(requiredProperties = {"id", "name", "type", "billingAddress", "contactInfo", "deliveryAddresses", "preferences", "status", "createdAt", "updatedAt"})
|
||||
public record CustomerResponse(
|
||||
String id,
|
||||
String name,
|
||||
String type,
|
||||
AddressResponse billingAddress,
|
||||
ContactInfoResponse contactInfo,
|
||||
PaymentTermsResponse paymentTerms,
|
||||
@Schema(nullable = true) PaymentTermsResponse paymentTerms,
|
||||
List<DeliveryAddressResponse> deliveryAddresses,
|
||||
FrameContractResponse frameContract,
|
||||
@Schema(nullable = true) FrameContractResponse frameContract,
|
||||
List<String> preferences,
|
||||
String status,
|
||||
LocalDateTime createdAt,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||
|
||||
import de.effigenix.domain.masterdata.Supplier;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(requiredProperties = {"id", "name", "contactInfo", "certificates", "status", "createdAt", "updatedAt"})
|
||||
public record SupplierResponse(
|
||||
String id,
|
||||
String name,
|
||||
AddressResponse address,
|
||||
@Schema(nullable = true) AddressResponse address,
|
||||
ContactInfoResponse contactInfo,
|
||||
PaymentTermsResponse paymentTerms,
|
||||
@Schema(nullable = true) PaymentTermsResponse paymentTerms,
|
||||
List<QualityCertificateResponse> certificates,
|
||||
SupplierRatingResponse rating,
|
||||
@Schema(nullable = true) SupplierRatingResponse rating,
|
||||
String status,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,15 @@ import io.swagger.v3.oas.annotations.info.Info;
|
|||
import io.swagger.v3.oas.annotations.info.License;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.annotations.servers.Server;
|
||||
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* OpenAPI/Swagger Configuration.
|
||||
*
|
||||
|
|
@ -111,6 +118,35 @@ import org.springframework.context.annotation.Configuration;
|
|||
"""
|
||||
)
|
||||
public class OpenApiConfig {
|
||||
// Configuration is done via annotations
|
||||
// No additional beans needed
|
||||
|
||||
/**
|
||||
* Marks fields as required for application-layer DTOs that cannot carry
|
||||
* {@code @Schema} annotations (to preserve the clean architecture boundary).
|
||||
*/
|
||||
@Bean
|
||||
public OpenApiCustomizer applicationDtoRequiredFieldsCustomizer() {
|
||||
// Application-layer DTOs → all required except explicitly nullable
|
||||
Map<String, Set<String>> nullableFields = Map.of(
|
||||
"UserDTO", Set.of("branchId", "lastLogin"),
|
||||
"RoleDTO", Set.of()
|
||||
);
|
||||
|
||||
return openApi -> {
|
||||
var schemas = openApi.getComponents().getSchemas();
|
||||
if (schemas == null) return;
|
||||
|
||||
nullableFields.forEach((schemaName, nullable) -> {
|
||||
var schema = schemas.get(schemaName);
|
||||
if (schema == null || schema.getProperties() == null) return;
|
||||
|
||||
List<String> required = new ArrayList<>();
|
||||
schema.getProperties().keySet().forEach(prop -> {
|
||||
if (!nullable.contains(prop)) {
|
||||
required.add((String) prop);
|
||||
}
|
||||
});
|
||||
schema.setRequired(required);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' && <CustomerCreateScreen />}
|
||||
{current === 'customer-add-delivery-address' && <AddDeliveryAddressScreen />}
|
||||
{current === 'customer-set-preferences' && <SetPreferencesScreen />}
|
||||
{/* Lagerverwaltung */}
|
||||
{current === 'inventory-menu' && <InventoryMenu />}
|
||||
{current === 'storage-location-list' && <StorageLocationListScreen />}
|
||||
{current === 'storage-location-create' && <StorageLocationCreateScreen />}
|
||||
{current === 'storage-location-detail' && <StorageLocationDetailScreen />}
|
||||
{/* Produktion */}
|
||||
{current === 'production-menu' && <ProductionMenu />}
|
||||
{current === 'recipe-list' && <RecipeListScreen />}
|
||||
{current === 'recipe-create' && <RecipeCreateScreen />}
|
||||
{current === 'recipe-detail' && <RecipeDetailScreen />}
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
|
|
|
|||
64
frontend/apps/cli/src/components/inventory/InventoryMenu.tsx
Normal file
64
frontend/apps/cli/src/components/inventory/InventoryMenu.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { useNavigation } from '../../state/navigation-context.js';
|
||||
import type { Screen } from '../../state/navigation-context.js';
|
||||
|
||||
interface MenuItem {
|
||||
label: string;
|
||||
screen: Screen;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const MENU_ITEMS: MenuItem[] = [
|
||||
{ label: 'Lagerorte', screen: 'storage-location-list', description: 'Lagerorte verwalten (Kühlräume, Trockenlager, …)' },
|
||||
];
|
||||
|
||||
export function InventoryMenu() {
|
||||
const { navigate, back } = useNavigation();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
useInput((_input, key) => {
|
||||
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
||||
if (key.downArrow) setSelectedIndex((i) => Math.min(MENU_ITEMS.length - 1, i + 1));
|
||||
if (key.return) {
|
||||
const item = MENU_ITEMS[selectedIndex];
|
||||
if (item) navigate(item.screen);
|
||||
}
|
||||
if (key.backspace || key.escape) back();
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1}>
|
||||
<Box marginBottom={1}>
|
||||
<Text color="cyan" bold>Lagerverwaltung</Text>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="gray"
|
||||
paddingX={2}
|
||||
paddingY={1}
|
||||
width={50}
|
||||
>
|
||||
{MENU_ITEMS.map((item, index) => (
|
||||
<Box key={item.screen} flexDirection="column">
|
||||
<Text color={index === selectedIndex ? 'cyan' : 'white'}>
|
||||
{index === selectedIndex ? '▶ ' : ' '}
|
||||
{item.label}
|
||||
</Text>
|
||||
{index === selectedIndex && (
|
||||
<Box paddingLeft={4}>
|
||||
<Text color="gray" dimColor>{item.description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>↑↓ navigieren · Enter auswählen · Backspace Zurück</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { useNavigation } from '../../state/navigation-context.js';
|
||||
import { useStorageLocations } from '../../hooks/useStorageLocations.js';
|
||||
import { FormInput } from '../shared/FormInput.js';
|
||||
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||
import { STORAGE_TYPE_LABELS } from '@effigenix/api-client';
|
||||
import type { StorageType } from '@effigenix/api-client';
|
||||
|
||||
type Field = 'name' | 'storageType' | 'minTemperature' | 'maxTemperature';
|
||||
const FIELDS: Field[] = ['name', 'storageType', 'minTemperature', 'maxTemperature'];
|
||||
|
||||
const FIELD_LABELS: Record<Field, string> = {
|
||||
name: 'Name *',
|
||||
storageType: 'Lagertyp * (←→ wechseln)',
|
||||
minTemperature: 'Min. Temperatur (°C)',
|
||||
maxTemperature: 'Max. Temperatur (°C)',
|
||||
};
|
||||
|
||||
const STORAGE_TYPES: StorageType[] = ['COLD_ROOM', 'FREEZER', 'DRY_STORAGE', 'DISPLAY_COUNTER', 'PRODUCTION_AREA'];
|
||||
|
||||
export function StorageLocationCreateScreen() {
|
||||
const { navigate, back } = useNavigation();
|
||||
const { createStorageLocation, loading, error, clearError } = useStorageLocations();
|
||||
|
||||
const [values, setValues] = useState<Record<Field, string>>({
|
||||
name: '',
|
||||
storageType: 'DRY_STORAGE',
|
||||
minTemperature: '',
|
||||
maxTemperature: '',
|
||||
});
|
||||
const [activeField, setActiveField] = useState<Field>('name');
|
||||
const [fieldErrors, setFieldErrors] = useState<Partial<Record<Field, string>>>({});
|
||||
|
||||
const setField = (field: Field) => (value: string) => {
|
||||
setValues((v) => ({ ...v, [field]: value }));
|
||||
};
|
||||
|
||||
useInput((input, key) => {
|
||||
if (loading) return;
|
||||
|
||||
if (activeField === 'storageType') {
|
||||
if (key.leftArrow || key.rightArrow) {
|
||||
const idx = STORAGE_TYPES.indexOf(values.storageType as StorageType);
|
||||
const dir = key.rightArrow ? 1 : -1;
|
||||
const next = STORAGE_TYPES[(idx + dir + STORAGE_TYPES.length) % STORAGE_TYPES.length];
|
||||
if (next) setValues((v) => ({ ...v, storageType: next }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (key.tab || key.downArrow) {
|
||||
setActiveField((f) => {
|
||||
const idx = FIELDS.indexOf(f);
|
||||
return FIELDS[(idx + 1) % FIELDS.length] ?? f;
|
||||
});
|
||||
}
|
||||
if (key.upArrow) {
|
||||
setActiveField((f) => {
|
||||
const idx = FIELDS.indexOf(f);
|
||||
return FIELDS[(idx - 1 + FIELDS.length) % FIELDS.length] ?? f;
|
||||
});
|
||||
}
|
||||
if (key.escape) back();
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const errors: Partial<Record<Field, string>> = {};
|
||||
if (!values.name.trim()) errors.name = 'Name ist erforderlich.';
|
||||
if (!values.storageType) errors.storageType = 'Lagertyp ist erforderlich.';
|
||||
setFieldErrors(errors);
|
||||
if (Object.keys(errors).length > 0) return;
|
||||
|
||||
const result = await createStorageLocation({
|
||||
name: values.name.trim(),
|
||||
storageType: values.storageType,
|
||||
...(values.minTemperature.trim() ? { minTemperature: values.minTemperature.trim() } : {}),
|
||||
...(values.maxTemperature.trim() ? { maxTemperature: values.maxTemperature.trim() } : {}),
|
||||
});
|
||||
if (result) navigate('storage-location-list');
|
||||
};
|
||||
|
||||
const handleFieldSubmit = (field: Field) => (_value: string) => {
|
||||
const idx = FIELDS.indexOf(field);
|
||||
if (idx < FIELDS.length - 1) {
|
||||
setActiveField(FIELDS[idx + 1] ?? field);
|
||||
} else {
|
||||
void handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box flexDirection="column" alignItems="center" paddingY={2}>
|
||||
<LoadingSpinner label="Lagerort wird angelegt..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const storageTypeLabel = STORAGE_TYPE_LABELS[values.storageType as StorageType] ?? values.storageType;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="cyan" bold>Neuer Lagerort</Text>
|
||||
{error && <ErrorDisplay message={error} onDismiss={clearError} />}
|
||||
|
||||
<Box flexDirection="column" gap={1} width={60}>
|
||||
{FIELDS.map((field) => {
|
||||
if (field === 'storageType') {
|
||||
return (
|
||||
<Box key={field} flexDirection="column">
|
||||
<Text color={activeField === field ? 'cyan' : 'gray'}>
|
||||
{FIELD_LABELS[field]}: <Text bold color="white">{storageTypeLabel}</Text>
|
||||
</Text>
|
||||
{fieldErrors[field] && <Text color="red">{fieldErrors[field]}</Text>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormInput
|
||||
key={field}
|
||||
label={FIELD_LABELS[field]}
|
||||
value={values[field]}
|
||||
onChange={setField(field)}
|
||||
onSubmit={handleFieldSubmit(field)}
|
||||
focus={activeField === field}
|
||||
{...(fieldErrors[field] ? { error: fieldErrors[field] } : {})}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>
|
||||
Tab/↑↓ Feld wechseln · ←→ Lagertyp · Enter auf letztem Feld speichern · Escape Abbrechen
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import type { StorageLocationDTO, StorageType } from '@effigenix/api-client';
|
||||
import { STORAGE_TYPE_LABELS } from '@effigenix/api-client';
|
||||
import { useNavigation } from '../../state/navigation-context.js';
|
||||
import { useStorageLocations } from '../../hooks/useStorageLocations.js';
|
||||
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||
import { SuccessDisplay } from '../shared/SuccessDisplay.js';
|
||||
import { ConfirmDialog } from '../shared/ConfirmDialog.js';
|
||||
import { client } from '../../utils/api-client.js';
|
||||
|
||||
type MenuAction = 'toggle-status' | 'back';
|
||||
type Mode = 'menu' | 'confirm-status';
|
||||
|
||||
const MENU_ITEMS: { id: MenuAction; label: (loc: StorageLocationDTO) => string }[] = [
|
||||
{ id: 'toggle-status', label: (loc) => loc.active ? '[Deaktivieren]' : '[Aktivieren]' },
|
||||
{ id: 'back', label: () => '[Zurück]' },
|
||||
];
|
||||
|
||||
function errorMessage(err: unknown): string {
|
||||
return err instanceof Error ? err.message : 'Unbekannter Fehler';
|
||||
}
|
||||
|
||||
export function StorageLocationDetailScreen() {
|
||||
const { params, back } = useNavigation();
|
||||
const storageLocationId = params['storageLocationId'] ?? '';
|
||||
const { activateStorageLocation, deactivateStorageLocation } = useStorageLocations();
|
||||
|
||||
const [location, setLocation] = useState<StorageLocationDTO | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedAction, setSelectedAction] = useState(0);
|
||||
const [mode, setMode] = useState<Mode>('menu');
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
const loadLocation = useCallback(() => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
client.storageLocations.getById(storageLocationId)
|
||||
.then((loc) => { setLocation(loc); setLoading(false); })
|
||||
.catch((err: unknown) => { setError(errorMessage(err)); setLoading(false); });
|
||||
}, [storageLocationId]);
|
||||
|
||||
useEffect(() => { if (storageLocationId) loadLocation(); }, [loadLocation, storageLocationId]);
|
||||
|
||||
useInput((_input, key) => {
|
||||
if (loading || actionLoading) return;
|
||||
|
||||
if (mode !== 'menu') return;
|
||||
if (key.upArrow) setSelectedAction((i) => Math.max(0, i - 1));
|
||||
if (key.downArrow) setSelectedAction((i) => Math.min(MENU_ITEMS.length - 1, i + 1));
|
||||
if (key.return) void handleAction();
|
||||
if (key.backspace || key.escape) back();
|
||||
});
|
||||
|
||||
const handleAction = async () => {
|
||||
if (!location) return;
|
||||
const item = MENU_ITEMS[selectedAction];
|
||||
if (!item) return;
|
||||
|
||||
switch (item.id) {
|
||||
case 'toggle-status':
|
||||
setMode('confirm-status');
|
||||
break;
|
||||
case 'back':
|
||||
back();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleStatus = useCallback(async () => {
|
||||
if (!location) return;
|
||||
setMode('menu');
|
||||
setActionLoading(true);
|
||||
const fn = location.active ? deactivateStorageLocation : activateStorageLocation;
|
||||
const updated = await fn(location.id);
|
||||
setActionLoading(false);
|
||||
if (updated) {
|
||||
setLocation(updated);
|
||||
setSuccessMessage(location.active ? 'Lagerort deaktiviert.' : 'Lagerort aktiviert.');
|
||||
}
|
||||
}, [location, activateStorageLocation, deactivateStorageLocation]);
|
||||
|
||||
if (loading) return <LoadingSpinner label="Lade Lagerort..." />;
|
||||
if (error && !location) return <ErrorDisplay message={error} onDismiss={back} />;
|
||||
if (!location) return <Text color="red">Lagerort nicht gefunden.</Text>;
|
||||
|
||||
const statusColor = location.active ? 'green' : 'red';
|
||||
const typeName = STORAGE_TYPE_LABELS[location.storageType as StorageType] ?? location.storageType;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="cyan" bold>Lagerort: {location.name}</Text>
|
||||
|
||||
{error && <ErrorDisplay message={error} onDismiss={() => setError(null)} />}
|
||||
{successMessage && <SuccessDisplay message={successMessage} onDismiss={() => setSuccessMessage(null)} />}
|
||||
|
||||
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={2} paddingY={1}>
|
||||
<Box gap={2}>
|
||||
<Text color="gray">Status:</Text>
|
||||
<Text color={statusColor} bold>{location.active ? 'AKTIV' : 'INAKTIV'}</Text>
|
||||
</Box>
|
||||
<Box gap={2}>
|
||||
<Text color="gray">Lagertyp:</Text>
|
||||
<Text>{typeName}</Text>
|
||||
</Box>
|
||||
{location.temperatureRange && (
|
||||
<Box gap={2}>
|
||||
<Text color="gray">Temperatur:</Text>
|
||||
<Text>{location.temperatureRange.minTemperature}°C – {location.temperatureRange.maxTemperature}°C</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{mode === 'confirm-status' && (
|
||||
<ConfirmDialog
|
||||
message={location.active ? 'Lagerort deaktivieren?' : 'Lagerort aktivieren?'}
|
||||
onConfirm={() => void handleToggleStatus()}
|
||||
onCancel={() => setMode('menu')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === 'menu' && (
|
||||
<Box flexDirection="column">
|
||||
<Text color="gray" bold>Aktionen:</Text>
|
||||
{actionLoading && <LoadingSpinner label="Aktion wird ausgeführt..." />}
|
||||
{!actionLoading && MENU_ITEMS.map((item, index) => (
|
||||
<Box key={item.id}>
|
||||
<Text color={index === selectedAction ? 'cyan' : 'white'}>
|
||||
{index === selectedAction ? '▶ ' : ' '}{item.label(location)}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>↑↓ navigieren · Enter ausführen · Backspace Zurück</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { useNavigation } from '../../state/navigation-context.js';
|
||||
import { useStorageLocations } from '../../hooks/useStorageLocations.js';
|
||||
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||
import { STORAGE_TYPE_LABELS } from '@effigenix/api-client';
|
||||
import type { StorageType, StorageLocationFilter } from '@effigenix/api-client';
|
||||
|
||||
type Filter = 'ALL' | 'ACTIVE' | 'INACTIVE';
|
||||
|
||||
const STORAGE_TYPES: StorageType[] = ['COLD_ROOM', 'FREEZER', 'DRY_STORAGE', 'DISPLAY_COUNTER', 'PRODUCTION_AREA'];
|
||||
|
||||
export function StorageLocationListScreen() {
|
||||
const { navigate, back } = useNavigation();
|
||||
const { storageLocations, loading, error, fetchStorageLocations, clearError } = useStorageLocations();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [statusFilter, setStatusFilter] = useState<Filter>('ALL');
|
||||
const [typeFilter, setTypeFilter] = useState<StorageType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const filter: StorageLocationFilter = {};
|
||||
if (typeFilter) filter.storageType = typeFilter;
|
||||
if (statusFilter !== 'ALL') filter.active = statusFilter === 'ACTIVE';
|
||||
void fetchStorageLocations(filter);
|
||||
}, [fetchStorageLocations, statusFilter, typeFilter]);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (loading) return;
|
||||
|
||||
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
||||
if (key.downArrow) setSelectedIndex((i) => Math.min(storageLocations.length - 1, i + 1));
|
||||
|
||||
if (key.return && storageLocations.length > 0) {
|
||||
const loc = storageLocations[selectedIndex];
|
||||
if (loc) navigate('storage-location-detail', { storageLocationId: loc.id });
|
||||
}
|
||||
if (input === 'n') navigate('storage-location-create');
|
||||
if (input === 'a') { setStatusFilter('ALL'); setSelectedIndex(0); }
|
||||
if (input === 'A') { setStatusFilter('ACTIVE'); setSelectedIndex(0); }
|
||||
if (input === 'I') { setStatusFilter('INACTIVE'); setSelectedIndex(0); }
|
||||
if (input === 't') {
|
||||
setTypeFilter((current) => {
|
||||
if (!current) return STORAGE_TYPES[0] ?? null;
|
||||
const idx = STORAGE_TYPES.indexOf(current);
|
||||
if (idx >= STORAGE_TYPES.length - 1) return null;
|
||||
return STORAGE_TYPES[idx + 1] ?? null;
|
||||
});
|
||||
setSelectedIndex(0);
|
||||
}
|
||||
if (key.backspace || key.escape) back();
|
||||
});
|
||||
|
||||
const filterLabel: Record<Filter, string> = { ALL: 'Alle', ACTIVE: 'Aktiv', INACTIVE: 'Inaktiv' };
|
||||
const typeLabel = typeFilter ? STORAGE_TYPE_LABELS[typeFilter] : 'Alle';
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box gap={2}>
|
||||
<Text color="cyan" bold>Lagerorte</Text>
|
||||
<Text color="gray" dimColor>
|
||||
Status: <Text color="yellow">{filterLabel[statusFilter]}</Text>
|
||||
{' · '}Typ: <Text color="yellow">{typeLabel}</Text>
|
||||
{' '}({storageLocations.length})
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{loading && <LoadingSpinner label="Lade Lagerorte..." />}
|
||||
{error && !loading && <ErrorDisplay message={error} onDismiss={clearError} />}
|
||||
|
||||
{!loading && !error && (
|
||||
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
|
||||
<Box paddingX={1}>
|
||||
<Text color="gray" bold>{' Status Name'.padEnd(30)}</Text>
|
||||
<Text color="gray" bold>{'Typ'.padEnd(20)}</Text>
|
||||
<Text color="gray" bold>Temperatur</Text>
|
||||
</Box>
|
||||
{storageLocations.length === 0 && (
|
||||
<Box paddingX={1} paddingY={1}>
|
||||
<Text color="gray" dimColor>Keine Lagerorte gefunden.</Text>
|
||||
</Box>
|
||||
)}
|
||||
{storageLocations.map((loc, index) => {
|
||||
const isSelected = index === selectedIndex;
|
||||
const statusColor = loc.active ? 'green' : 'red';
|
||||
const textColor = isSelected ? 'cyan' : 'white';
|
||||
const typeName = STORAGE_TYPE_LABELS[loc.storageType as StorageType] ?? loc.storageType;
|
||||
const tempRange = loc.temperatureRange
|
||||
? `${loc.temperatureRange.minTemperature}°C – ${loc.temperatureRange.maxTemperature}°C`
|
||||
: '–';
|
||||
return (
|
||||
<Box key={loc.id} paddingX={1}>
|
||||
<Text color={textColor}>{isSelected ? '▶ ' : ' '}</Text>
|
||||
<Text color={statusColor}>{loc.active ? '● ' : '○ '}</Text>
|
||||
<Text color={textColor}>{loc.name.substring(0, 24).padEnd(25)}</Text>
|
||||
<Text color={isSelected ? 'cyan' : 'gray'}>{typeName.padEnd(20)}</Text>
|
||||
<Text color={isSelected ? 'cyan' : 'gray'}>{tempRange}</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>
|
||||
↑↓ nav · Enter Details · [n] Neu · [a] Alle · [A] Aktiv · [I] Inaktiv · [t] Typ · Backspace Zurück
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { useNavigation } from '../../state/navigation-context.js';
|
||||
import type { Screen } from '../../state/navigation-context.js';
|
||||
|
||||
interface MenuItem {
|
||||
label: string;
|
||||
screen: Screen;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const MENU_ITEMS: MenuItem[] = [
|
||||
{ label: 'Rezepte', screen: 'recipe-list', description: 'Rezepte anlegen und verwalten' },
|
||||
];
|
||||
|
||||
export function ProductionMenu() {
|
||||
const { navigate, back } = useNavigation();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
useInput((_input, key) => {
|
||||
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
||||
if (key.downArrow) setSelectedIndex((i) => Math.min(MENU_ITEMS.length - 1, i + 1));
|
||||
if (key.return) {
|
||||
const item = MENU_ITEMS[selectedIndex];
|
||||
if (item) navigate(item.screen);
|
||||
}
|
||||
if (key.backspace || key.escape) back();
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1}>
|
||||
<Box marginBottom={1}>
|
||||
<Text color="cyan" bold>Produktion</Text>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="gray"
|
||||
paddingX={2}
|
||||
paddingY={1}
|
||||
width={50}
|
||||
>
|
||||
{MENU_ITEMS.map((item, index) => (
|
||||
<Box key={item.screen} flexDirection="column">
|
||||
<Text color={index === selectedIndex ? 'cyan' : 'white'}>
|
||||
{index === selectedIndex ? '▶ ' : ' '}
|
||||
{item.label}
|
||||
</Text>
|
||||
{index === selectedIndex && (
|
||||
<Box paddingLeft={4}>
|
||||
<Text color="gray" dimColor>{item.description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>↑↓ navigieren · Enter auswählen · Backspace Zurück</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { useNavigation } from '../../state/navigation-context.js';
|
||||
import { useRecipes } from '../../hooks/useRecipes.js';
|
||||
import { FormInput } from '../shared/FormInput.js';
|
||||
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||
import { RECIPE_TYPE_LABELS } from '@effigenix/api-client';
|
||||
import type { RecipeType } from '@effigenix/api-client';
|
||||
|
||||
type Field = 'name' | 'version' | 'type' | 'description' | 'yieldPercentage' | 'shelfLifeDays' | 'outputQuantity' | 'outputUom';
|
||||
const FIELDS: Field[] = ['name', 'version', 'type', 'description', 'yieldPercentage', 'shelfLifeDays', 'outputQuantity', 'outputUom'];
|
||||
|
||||
const FIELD_LABELS: Record<Field, string> = {
|
||||
name: 'Name *',
|
||||
version: 'Version *',
|
||||
type: 'Rezepttyp * (←→ wechseln)',
|
||||
description: 'Beschreibung',
|
||||
yieldPercentage: 'Ausbeute (%) *',
|
||||
shelfLifeDays: 'Haltbarkeit (Tage)',
|
||||
outputQuantity: 'Ausgabemenge *',
|
||||
outputUom: 'Mengeneinheit *',
|
||||
};
|
||||
|
||||
const RECIPE_TYPES: RecipeType[] = ['RAW_MATERIAL', 'INTERMEDIATE', 'FINISHED_PRODUCT'];
|
||||
|
||||
export function RecipeCreateScreen() {
|
||||
const { navigate, back } = useNavigation();
|
||||
const { createRecipe, loading, error, clearError } = useRecipes();
|
||||
|
||||
const [values, setValues] = useState<Record<Field, string>>({
|
||||
name: '',
|
||||
version: '1',
|
||||
type: 'FINISHED_PRODUCT',
|
||||
description: '',
|
||||
yieldPercentage: '100',
|
||||
shelfLifeDays: '',
|
||||
outputQuantity: '',
|
||||
outputUom: '',
|
||||
});
|
||||
const [activeField, setActiveField] = useState<Field>('name');
|
||||
const [fieldErrors, setFieldErrors] = useState<Partial<Record<Field, string>>>({});
|
||||
|
||||
const setField = (field: Field) => (value: string) => {
|
||||
setValues((v) => ({ ...v, [field]: value }));
|
||||
};
|
||||
|
||||
useInput((input, key) => {
|
||||
if (loading) return;
|
||||
|
||||
if (activeField === 'type') {
|
||||
if (key.leftArrow || key.rightArrow) {
|
||||
const idx = RECIPE_TYPES.indexOf(values.type as RecipeType);
|
||||
const dir = key.rightArrow ? 1 : -1;
|
||||
const next = RECIPE_TYPES[(idx + dir + RECIPE_TYPES.length) % RECIPE_TYPES.length];
|
||||
if (next) setValues((v) => ({ ...v, type: next }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (key.tab || key.downArrow) {
|
||||
setActiveField((f) => {
|
||||
const idx = FIELDS.indexOf(f);
|
||||
return FIELDS[(idx + 1) % FIELDS.length] ?? f;
|
||||
});
|
||||
}
|
||||
if (key.upArrow) {
|
||||
setActiveField((f) => {
|
||||
const idx = FIELDS.indexOf(f);
|
||||
return FIELDS[(idx - 1 + FIELDS.length) % FIELDS.length] ?? f;
|
||||
});
|
||||
}
|
||||
if (key.escape) back();
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const errors: Partial<Record<Field, string>> = {};
|
||||
if (!values.name.trim()) errors.name = 'Name ist erforderlich.';
|
||||
if (!values.version.trim() || isNaN(parseInt(values.version, 10))) errors.version = 'Version muss eine Zahl sein.';
|
||||
if (!values.type) errors.type = 'Rezepttyp ist erforderlich.';
|
||||
if (!values.yieldPercentage.trim() || isNaN(parseInt(values.yieldPercentage, 10))) errors.yieldPercentage = 'Ausbeute muss eine Zahl sein.';
|
||||
if (!values.outputQuantity.trim()) errors.outputQuantity = 'Ausgabemenge ist erforderlich.';
|
||||
if (!values.outputUom.trim()) errors.outputUom = 'Mengeneinheit ist erforderlich.';
|
||||
setFieldErrors(errors);
|
||||
if (Object.keys(errors).length > 0) return;
|
||||
|
||||
const result = await createRecipe({
|
||||
name: values.name.trim(),
|
||||
version: parseInt(values.version, 10),
|
||||
type: values.type as RecipeType,
|
||||
...(values.description.trim() ? { description: values.description.trim() } : {}),
|
||||
yieldPercentage: parseInt(values.yieldPercentage, 10),
|
||||
...(values.shelfLifeDays.trim() ? { shelfLifeDays: parseInt(values.shelfLifeDays, 10) } : {}),
|
||||
outputQuantity: values.outputQuantity.trim(),
|
||||
outputUom: values.outputUom.trim(),
|
||||
});
|
||||
if (result) navigate('recipe-list');
|
||||
};
|
||||
|
||||
const handleFieldSubmit = (field: Field) => (_value: string) => {
|
||||
const idx = FIELDS.indexOf(field);
|
||||
if (idx < FIELDS.length - 1) {
|
||||
setActiveField(FIELDS[idx + 1] ?? field);
|
||||
} else {
|
||||
void handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box flexDirection="column" alignItems="center" paddingY={2}>
|
||||
<LoadingSpinner label="Rezept wird angelegt..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const typeLabel = RECIPE_TYPE_LABELS[values.type as RecipeType] ?? values.type;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="cyan" bold>Neues Rezept</Text>
|
||||
{error && <ErrorDisplay message={error} onDismiss={clearError} />}
|
||||
|
||||
<Box flexDirection="column" gap={1} width={60}>
|
||||
{FIELDS.map((field) => {
|
||||
if (field === 'type') {
|
||||
return (
|
||||
<Box key={field} flexDirection="column">
|
||||
<Text color={activeField === field ? 'cyan' : 'gray'}>
|
||||
{FIELD_LABELS[field]}: <Text bold color="white">{typeLabel}</Text>
|
||||
</Text>
|
||||
{fieldErrors[field] && <Text color="red">{fieldErrors[field]}</Text>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormInput
|
||||
key={field}
|
||||
label={FIELD_LABELS[field]}
|
||||
value={values[field]}
|
||||
onChange={setField(field)}
|
||||
onSubmit={handleFieldSubmit(field)}
|
||||
focus={activeField === field}
|
||||
{...(fieldErrors[field] ? { error: fieldErrors[field] } : {})}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>
|
||||
Tab/↑↓ Feld wechseln · ←→ Rezepttyp · Enter auf letztem Feld speichern · Escape Abbrechen
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import type { RecipeDTO, RecipeType } from '@effigenix/api-client';
|
||||
import { RECIPE_TYPE_LABELS } from '@effigenix/api-client';
|
||||
import { useNavigation } from '../../state/navigation-context.js';
|
||||
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||
import { client } from '../../utils/api-client.js';
|
||||
|
||||
function errorMessage(err: unknown): string {
|
||||
return err instanceof Error ? err.message : 'Unbekannter Fehler';
|
||||
}
|
||||
|
||||
export function RecipeDetailScreen() {
|
||||
const { params, back } = useNavigation();
|
||||
const recipeId = params['recipeId'] ?? '';
|
||||
|
||||
const [recipe, setRecipe] = useState<RecipeDTO | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadRecipe = useCallback(() => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
client.recipes.getById(recipeId)
|
||||
.then((r) => { setRecipe(r); setLoading(false); })
|
||||
.catch((err: unknown) => { setError(errorMessage(err)); setLoading(false); });
|
||||
}, [recipeId]);
|
||||
|
||||
useEffect(() => { if (recipeId) loadRecipe(); }, [loadRecipe, recipeId]);
|
||||
|
||||
useInput((_input, key) => {
|
||||
if (loading) return;
|
||||
if (key.backspace || key.escape) back();
|
||||
});
|
||||
|
||||
if (loading) return <LoadingSpinner label="Lade Rezept..." />;
|
||||
if (error && !recipe) return <ErrorDisplay message={error} onDismiss={back} />;
|
||||
if (!recipe) return <Text color="red">Rezept nicht gefunden.</Text>;
|
||||
|
||||
const typeName = RECIPE_TYPE_LABELS[recipe.type as RecipeType] ?? recipe.type;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="cyan" bold>Rezept: {recipe.name}</Text>
|
||||
|
||||
{error && <ErrorDisplay message={error} onDismiss={() => setError(null)} />}
|
||||
|
||||
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={2} paddingY={1}>
|
||||
<Box gap={2}>
|
||||
<Text color="gray">Status:</Text>
|
||||
<Text bold>{recipe.status}</Text>
|
||||
</Box>
|
||||
<Box gap={2}>
|
||||
<Text color="gray">Typ:</Text>
|
||||
<Text>{typeName}</Text>
|
||||
</Box>
|
||||
<Box gap={2}>
|
||||
<Text color="gray">Version:</Text>
|
||||
<Text>{recipe.version}</Text>
|
||||
</Box>
|
||||
{recipe.description && (
|
||||
<Box gap={2}>
|
||||
<Text color="gray">Beschreibung:</Text>
|
||||
<Text>{recipe.description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box gap={2}>
|
||||
<Text color="gray">Ausbeute:</Text>
|
||||
<Text>{recipe.yieldPercentage}%</Text>
|
||||
</Box>
|
||||
{recipe.shelfLifeDays !== null && (
|
||||
<Box gap={2}>
|
||||
<Text color="gray">Haltbarkeit:</Text>
|
||||
<Text>{recipe.shelfLifeDays} Tage</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box gap={2}>
|
||||
<Text color="gray">Ausgabemenge:</Text>
|
||||
<Text>{recipe.outputQuantity} {recipe.outputUom}</Text>
|
||||
</Box>
|
||||
|
||||
{recipe.ingredients.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color="gray">Zutaten:</Text>
|
||||
{recipe.ingredients.map((ing) => (
|
||||
<Box key={ing.id} paddingLeft={2} gap={1}>
|
||||
<Text color="yellow">{ing.position}.</Text>
|
||||
<Text>{ing.quantity} {ing.uom}</Text>
|
||||
<Text color="gray">(Artikel: {ing.articleId})</Text>
|
||||
{ing.substitutable && <Text color="green">[austauschbar]</Text>}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>Backspace Zurück</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { useNavigation } from '../../state/navigation-context.js';
|
||||
import { useRecipes } from '../../hooks/useRecipes.js';
|
||||
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||
import { RECIPE_TYPE_LABELS } from '@effigenix/api-client';
|
||||
import type { RecipeType } from '@effigenix/api-client';
|
||||
|
||||
export function RecipeListScreen() {
|
||||
const { navigate, back } = useNavigation();
|
||||
const { recipes, loading, error, fetchRecipes, clearError } = useRecipes();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchRecipes();
|
||||
}, [fetchRecipes]);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (loading) return;
|
||||
|
||||
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
||||
if (key.downArrow) setSelectedIndex((i) => Math.min(recipes.length - 1, i + 1));
|
||||
|
||||
if (key.return && recipes.length > 0) {
|
||||
const recipe = recipes[selectedIndex];
|
||||
if (recipe) navigate('recipe-detail', { recipeId: recipe.id });
|
||||
}
|
||||
if (input === 'n') navigate('recipe-create');
|
||||
if (key.backspace || key.escape) back();
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box gap={2}>
|
||||
<Text color="cyan" bold>Rezepte</Text>
|
||||
<Text color="gray" dimColor>({recipes.length})</Text>
|
||||
</Box>
|
||||
|
||||
{loading && <LoadingSpinner label="Lade Rezepte..." />}
|
||||
{error && !loading && <ErrorDisplay message={error} onDismiss={clearError} />}
|
||||
|
||||
{!loading && !error && (
|
||||
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
|
||||
<Box paddingX={1}>
|
||||
<Text color="gray" bold>{' Name'.padEnd(30)}</Text>
|
||||
<Text color="gray" bold>{'Typ'.padEnd(18)}</Text>
|
||||
<Text color="gray" bold>{'V.'.padEnd(5)}</Text>
|
||||
<Text color="gray" bold>Status</Text>
|
||||
</Box>
|
||||
{recipes.length === 0 && (
|
||||
<Box paddingX={1} paddingY={1}>
|
||||
<Text color="gray" dimColor>Keine Rezepte gefunden.</Text>
|
||||
</Box>
|
||||
)}
|
||||
{recipes.map((recipe, index) => {
|
||||
const isSelected = index === selectedIndex;
|
||||
const textColor = isSelected ? 'cyan' : 'white';
|
||||
const typeName = RECIPE_TYPE_LABELS[recipe.type as RecipeType] ?? recipe.type;
|
||||
return (
|
||||
<Box key={recipe.id} paddingX={1}>
|
||||
<Text color={textColor}>{isSelected ? '▶ ' : ' '}</Text>
|
||||
<Text color={textColor}>{recipe.name.substring(0, 26).padEnd(27)}</Text>
|
||||
<Text color={isSelected ? 'cyan' : 'gray'}>{typeName.padEnd(18)}</Text>
|
||||
<Text color={isSelected ? 'cyan' : 'gray'}>{String(recipe.version).padEnd(5)}</Text>
|
||||
<Text color={isSelected ? 'cyan' : 'gray'}>{recipe.status}</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>
|
||||
↑↓ nav · Enter Details · [n] Neu · Backspace Zurück
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
54
frontend/apps/cli/src/hooks/useRecipes.ts
Normal file
54
frontend/apps/cli/src/hooks/useRecipes.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import type { RecipeDTO, CreateRecipeRequest } from '@effigenix/api-client';
|
||||
import { client } from '../utils/api-client.js';
|
||||
|
||||
interface RecipesState {
|
||||
recipes: RecipeDTO[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown): string {
|
||||
return err instanceof Error ? err.message : 'Unbekannter Fehler';
|
||||
}
|
||||
|
||||
export function useRecipes() {
|
||||
const [state, setState] = useState<RecipesState>({
|
||||
recipes: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const fetchRecipes = useCallback(async () => {
|
||||
setState((s) => ({ ...s, loading: true, error: null }));
|
||||
try {
|
||||
const recipes = await client.recipes.list();
|
||||
setState({ recipes, loading: false, error: null });
|
||||
} catch (err) {
|
||||
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createRecipe = useCallback(async (request: CreateRecipeRequest) => {
|
||||
setState((s) => ({ ...s, loading: true, error: null }));
|
||||
try {
|
||||
const recipe = await client.recipes.create(request);
|
||||
setState((s) => ({ recipes: [...s.recipes, recipe], loading: false, error: null }));
|
||||
return recipe;
|
||||
} catch (err) {
|
||||
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setState((s) => ({ ...s, error: null }));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
fetchRecipes,
|
||||
createRecipe,
|
||||
clearError,
|
||||
};
|
||||
}
|
||||
104
frontend/apps/cli/src/hooks/useStorageLocations.ts
Normal file
104
frontend/apps/cli/src/hooks/useStorageLocations.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import type {
|
||||
StorageLocationDTO,
|
||||
CreateStorageLocationRequest,
|
||||
UpdateStorageLocationRequest,
|
||||
StorageLocationFilter,
|
||||
} from '@effigenix/api-client';
|
||||
import { client } from '../utils/api-client.js';
|
||||
|
||||
interface StorageLocationsState {
|
||||
storageLocations: StorageLocationDTO[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown): string {
|
||||
return err instanceof Error ? err.message : 'Unbekannter Fehler';
|
||||
}
|
||||
|
||||
export function useStorageLocations() {
|
||||
const [state, setState] = useState<StorageLocationsState>({
|
||||
storageLocations: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const fetchStorageLocations = useCallback(async (filter?: StorageLocationFilter) => {
|
||||
setState((s) => ({ ...s, loading: true, error: null }));
|
||||
try {
|
||||
const storageLocations = await client.storageLocations.list(filter);
|
||||
setState({ storageLocations, loading: false, error: null });
|
||||
} catch (err) {
|
||||
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createStorageLocation = useCallback(async (request: CreateStorageLocationRequest) => {
|
||||
setState((s) => ({ ...s, loading: true, error: null }));
|
||||
try {
|
||||
const location = await client.storageLocations.create(request);
|
||||
setState((s) => ({ storageLocations: [...s.storageLocations, location], loading: false, error: null }));
|
||||
return location;
|
||||
} catch (err) {
|
||||
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateStorageLocation = useCallback(async (id: string, request: UpdateStorageLocationRequest) => {
|
||||
try {
|
||||
const updated = await client.storageLocations.update(id, request);
|
||||
setState((s) => ({
|
||||
...s,
|
||||
storageLocations: s.storageLocations.map((loc) => (loc.id === id ? updated : loc)),
|
||||
}));
|
||||
return updated;
|
||||
} catch (err) {
|
||||
setState((s) => ({ ...s, error: errorMessage(err) }));
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const activateStorageLocation = useCallback(async (id: string) => {
|
||||
try {
|
||||
const updated = await client.storageLocations.activate(id);
|
||||
setState((s) => ({
|
||||
...s,
|
||||
storageLocations: s.storageLocations.map((loc) => (loc.id === id ? updated : loc)),
|
||||
}));
|
||||
return updated;
|
||||
} catch (err) {
|
||||
setState((s) => ({ ...s, error: errorMessage(err) }));
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const deactivateStorageLocation = useCallback(async (id: string) => {
|
||||
try {
|
||||
const updated = await client.storageLocations.deactivate(id);
|
||||
setState((s) => ({
|
||||
...s,
|
||||
storageLocations: s.storageLocations.map((loc) => (loc.id === id ? updated : loc)),
|
||||
}));
|
||||
return updated;
|
||||
} catch (err) {
|
||||
setState((s) => ({ ...s, error: errorMessage(err) }));
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setState((s) => ({ ...s, error: null }));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
fetchStorageLocations,
|
||||
createStorageLocation,
|
||||
updateStorageLocation,
|
||||
activateStorageLocation,
|
||||
deactivateStorageLocation,
|
||||
clearError,
|
||||
};
|
||||
}
|
||||
|
|
@ -27,7 +27,17 @@ export type Screen =
|
|||
| 'customer-detail'
|
||||
| 'customer-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;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -22,6 +22,8 @@ export { createCategoriesResource } from './resources/categories.js';
|
|||
export { createSuppliersResource } from './resources/suppliers.js';
|
||||
export { 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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<PriceModel, string> = {
|
|||
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 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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<DeliveryRhythm, string> = {
|
|||
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 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
66
frontend/packages/api-client/src/resources/recipes.ts
Normal file
66
frontend/packages/api-client/src/resources/recipes.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Recipes resource – Production BC.
|
||||
* Endpoints: POST /api/recipes,
|
||||
* POST /api/recipes/{id}/ingredients,
|
||||
* DELETE /api/recipes/{id}/ingredients/{ingredientId}
|
||||
*/
|
||||
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import type {
|
||||
RecipeDTO,
|
||||
IngredientDTO,
|
||||
CreateRecipeRequest,
|
||||
AddRecipeIngredientRequest,
|
||||
} from '@effigenix/types';
|
||||
|
||||
export type RecipeType = 'RAW_MATERIAL' | 'INTERMEDIATE' | 'FINISHED_PRODUCT';
|
||||
export type RecipeStatus = 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
|
||||
|
||||
export const RECIPE_TYPE_LABELS: Record<RecipeType, string> = {
|
||||
RAW_MATERIAL: 'Rohstoff',
|
||||
INTERMEDIATE: 'Halbfabrikat',
|
||||
FINISHED_PRODUCT: 'Fertigprodukt',
|
||||
};
|
||||
|
||||
export type {
|
||||
RecipeDTO,
|
||||
IngredientDTO,
|
||||
CreateRecipeRequest,
|
||||
AddRecipeIngredientRequest,
|
||||
};
|
||||
|
||||
// ── Resource factory ─────────────────────────────────────────────────────────
|
||||
|
||||
const BASE = '/api/recipes';
|
||||
|
||||
export function createRecipesResource(client: AxiosInstance) {
|
||||
return {
|
||||
async list(): Promise<RecipeDTO[]> {
|
||||
const res = await client.get<RecipeDTO[]>(BASE);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async getById(id: string): Promise<RecipeDTO> {
|
||||
const res = await client.get<RecipeDTO>(`${BASE}/${id}`);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async create(request: CreateRecipeRequest): Promise<RecipeDTO> {
|
||||
const res = await client.post<RecipeDTO>(BASE, request);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async addIngredient(id: string, request: AddRecipeIngredientRequest): Promise<RecipeDTO> {
|
||||
const res = await client.post<RecipeDTO>(`${BASE}/${id}/ingredients`, request);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async removeIngredient(recipeId: string, ingredientId: string): Promise<RecipeDTO> {
|
||||
await client.delete(`${BASE}/${recipeId}/ingredients/${ingredientId}`);
|
||||
const res = await client.get<RecipeDTO>(`${BASE}/${recipeId}`);
|
||||
return res.data;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type RecipesResource = ReturnType<typeof createRecipesResource>;
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* StorageLocations resource – Inventory BC.
|
||||
* Endpoints: GET/POST /api/inventory/storage-locations,
|
||||
* PUT /api/inventory/storage-locations/{id},
|
||||
* PATCH /api/inventory/storage-locations/{id}/activate|deactivate
|
||||
*/
|
||||
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import type {
|
||||
StorageLocationDTO,
|
||||
TemperatureRangeDTO,
|
||||
CreateStorageLocationRequest,
|
||||
UpdateStorageLocationRequest,
|
||||
} from '@effigenix/types';
|
||||
|
||||
export type StorageType = 'COLD_ROOM' | 'FREEZER' | 'DRY_STORAGE' | 'DISPLAY_COUNTER' | 'PRODUCTION_AREA';
|
||||
|
||||
export const STORAGE_TYPE_LABELS: Record<StorageType, string> = {
|
||||
COLD_ROOM: 'Kühlraum',
|
||||
FREEZER: 'Tiefkühler',
|
||||
DRY_STORAGE: 'Trockenlager',
|
||||
DISPLAY_COUNTER: 'Vitrine',
|
||||
PRODUCTION_AREA: 'Produktionsbereich',
|
||||
};
|
||||
|
||||
export type {
|
||||
StorageLocationDTO,
|
||||
TemperatureRangeDTO,
|
||||
CreateStorageLocationRequest,
|
||||
UpdateStorageLocationRequest,
|
||||
};
|
||||
|
||||
export interface StorageLocationFilter {
|
||||
storageType?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
// ── Resource factory ─────────────────────────────────────────────────────────
|
||||
|
||||
const BASE = '/api/inventory/storage-locations';
|
||||
|
||||
export function createStorageLocationsResource(client: AxiosInstance) {
|
||||
return {
|
||||
async list(filter?: StorageLocationFilter): Promise<StorageLocationDTO[]> {
|
||||
const params: Record<string, string> = {};
|
||||
if (filter?.storageType) params['storageType'] = filter.storageType;
|
||||
if (filter?.active !== undefined) params['active'] = String(filter.active);
|
||||
const res = await client.get<StorageLocationDTO[]>(BASE, { params });
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async getById(id: string): Promise<StorageLocationDTO> {
|
||||
const res = await client.get<StorageLocationDTO>(`${BASE}/${id}`);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async create(request: CreateStorageLocationRequest): Promise<StorageLocationDTO> {
|
||||
const res = await client.post<StorageLocationDTO>(BASE, request);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async update(id: string, request: UpdateStorageLocationRequest): Promise<StorageLocationDTO> {
|
||||
const res = await client.put<StorageLocationDTO>(`${BASE}/${id}`, request);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async activate(id: string): Promise<StorageLocationDTO> {
|
||||
const res = await client.patch<StorageLocationDTO>(`${BASE}/${id}/activate`);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async deactivate(id: string): Promise<StorageLocationDTO> {
|
||||
const res = await client.patch<StorageLocationDTO>(`${BASE}/${id}/deactivate`);
|
||||
return res.data;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type StorageLocationsResource = ReturnType<typeof createStorageLocationsResource>;
|
||||
|
|
@ -10,100 +10,35 @@
|
|||
*/
|
||||
|
||||
import type { AxiosInstance } from 'axios';
|
||||
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
16
frontend/packages/types/src/article.ts
Normal file
16
frontend/packages/types/src/article.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Article types
|
||||
* Re-exports types from the auto-generated OpenAPI schema
|
||||
*/
|
||||
|
||||
import type { components } from './generated/api';
|
||||
|
||||
// Response DTOs
|
||||
export type ArticleDTO = components['schemas']['ArticleResponse'];
|
||||
export type SalesUnitDTO = components['schemas']['SalesUnitResponse'];
|
||||
|
||||
// Request types
|
||||
export type CreateArticleRequest = components['schemas']['CreateArticleRequest'];
|
||||
export type UpdateArticleRequest = components['schemas']['UpdateArticleRequest'];
|
||||
export type AddSalesUnitRequest = components['schemas']['AddSalesUnitRequest'];
|
||||
export type UpdateSalesUnitPriceRequest = components['schemas']['UpdateSalesUnitPriceRequest'];
|
||||
13
frontend/packages/types/src/category.ts
Normal file
13
frontend/packages/types/src/category.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Product Category types
|
||||
* Re-exports types from the auto-generated OpenAPI schema
|
||||
*/
|
||||
|
||||
import type { components } from './generated/api';
|
||||
|
||||
// Response DTOs
|
||||
export type ProductCategoryDTO = components['schemas']['ProductCategoryResponse'];
|
||||
|
||||
// Request types
|
||||
export type CreateCategoryRequest = components['schemas']['CreateProductCategoryRequest'];
|
||||
export type UpdateCategoryRequest = components['schemas']['UpdateProductCategoryRequest'];
|
||||
19
frontend/packages/types/src/customer.ts
Normal file
19
frontend/packages/types/src/customer.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Customer types
|
||||
* Re-exports types from the auto-generated OpenAPI schema
|
||||
*/
|
||||
|
||||
import type { components } from './generated/api';
|
||||
|
||||
// Response DTOs
|
||||
export type CustomerDTO = components['schemas']['CustomerResponse'];
|
||||
export type DeliveryAddressDTO = components['schemas']['DeliveryAddressResponse'];
|
||||
export type FrameContractDTO = components['schemas']['FrameContractResponse'];
|
||||
export type ContractLineItemDTO = components['schemas']['ContractLineItemResponse'];
|
||||
|
||||
// Request types
|
||||
export type CreateCustomerRequest = components['schemas']['CreateCustomerRequest'];
|
||||
export type UpdateCustomerRequest = components['schemas']['UpdateCustomerRequest'];
|
||||
export type AddDeliveryAddressRequest = components['schemas']['AddDeliveryAddressRequest'];
|
||||
export type SetFrameContractLineItem = components['schemas']['LineItem'];
|
||||
export type SetFrameContractRequest = components['schemas']['SetFrameContractRequest'];
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,12 @@ export * from './user';
|
|||
export * from './role';
|
||||
export * from './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';
|
||||
|
|
|
|||
14
frontend/packages/types/src/inventory.ts
Normal file
14
frontend/packages/types/src/inventory.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Inventory types
|
||||
* Re-exports types from the auto-generated OpenAPI schema
|
||||
*/
|
||||
|
||||
import type { components } from './generated/api';
|
||||
|
||||
// Response DTOs
|
||||
export type StorageLocationDTO = components['schemas']['StorageLocationResponse'];
|
||||
export type TemperatureRangeDTO = components['schemas']['TemperatureRangeResponse'];
|
||||
|
||||
// Request types
|
||||
export type CreateStorageLocationRequest = components['schemas']['CreateStorageLocationRequest'];
|
||||
export type UpdateStorageLocationRequest = components['schemas']['UpdateStorageLocationRequest'];
|
||||
14
frontend/packages/types/src/production.ts
Normal file
14
frontend/packages/types/src/production.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Production types
|
||||
* Re-exports types from the auto-generated OpenAPI schema
|
||||
*/
|
||||
|
||||
import type { components } from './generated/api';
|
||||
|
||||
// Response DTOs
|
||||
export type RecipeDTO = components['schemas']['RecipeResponse'];
|
||||
export type IngredientDTO = components['schemas']['IngredientResponse'];
|
||||
|
||||
// Request types
|
||||
export type CreateRecipeRequest = components['schemas']['CreateRecipeRequest'];
|
||||
export type AddRecipeIngredientRequest = components['schemas']['AddRecipeIngredientRequest'];
|
||||
21
frontend/packages/types/src/supplier.ts
Normal file
21
frontend/packages/types/src/supplier.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Supplier types
|
||||
* Re-exports types from the auto-generated OpenAPI schema
|
||||
*/
|
||||
|
||||
import type { components } from './generated/api';
|
||||
|
||||
// Response DTOs
|
||||
export type SupplierDTO = components['schemas']['SupplierResponse'];
|
||||
export type AddressDTO = components['schemas']['AddressResponse'];
|
||||
export type ContactInfoDTO = components['schemas']['ContactInfoResponse'];
|
||||
export type PaymentTermsDTO = components['schemas']['PaymentTermsResponse'];
|
||||
export type QualityCertificateDTO = components['schemas']['QualityCertificateResponse'];
|
||||
export type SupplierRatingDTO = components['schemas']['SupplierRatingResponse'];
|
||||
|
||||
// Request types
|
||||
export type CreateSupplierRequest = components['schemas']['CreateSupplierRequest'];
|
||||
export type UpdateSupplierRequest = components['schemas']['UpdateSupplierRequest'];
|
||||
export type RateSupplierRequest = components['schemas']['RateSupplierRequest'];
|
||||
export type AddCertificateRequest = components['schemas']['AddCertificateRequest'];
|
||||
export type RemoveCertificateRequest = components['schemas']['RemoveCertificateRequest'];
|
||||
Loading…
Add table
Add a link
Reference in a new issue