1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 08:29:36 +01:00

feat(frontend): TUI-Screens für Rezept-Filter, Archivierung und Chargen-Einbuchung

Production: Rezeptliste mit Status-Filter (Draft/Active/Archived), Rezept
archivieren für aktive Rezepte, list() gibt RecipeSummaryDTO zurück.
Inventory: Charge einbuchen (AddBatch) mit neuem Stocks-Resource und Screens.
This commit is contained in:
Sebastian Frick 2026-02-19 22:46:38 +01:00
parent f2003a3093
commit ec736cf294
16 changed files with 553 additions and 16 deletions

View file

@ -36,6 +36,8 @@ 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';
import { StockBatchEntryScreen } from './components/inventory/StockBatchEntryScreen.js';
import { AddBatchScreen } from './components/inventory/AddBatchScreen.js';
// Produktion
import { ProductionMenu } from './components/production/ProductionMenu.js';
import { RecipeListScreen } from './components/production/RecipeListScreen.js';
@ -104,6 +106,8 @@ function ScreenRouter() {
{current === 'storage-location-list' && <StorageLocationListScreen />}
{current === 'storage-location-create' && <StorageLocationCreateScreen />}
{current === 'storage-location-detail' && <StorageLocationDetailScreen />}
{current === 'stock-batch-entry' && <StockBatchEntryScreen />}
{current === 'stock-add-batch' && <AddBatchScreen />}
{/* Produktion */}
{current === 'production-menu' && <ProductionMenu />}
{current === 'recipe-list' && <RecipeListScreen />}

View file

@ -0,0 +1,150 @@
import { useState } from 'react';
import { Box, Text, useInput } from 'ink';
import { useNavigation } from '../../state/navigation-context.js';
import { FormInput } from '../shared/FormInput.js';
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
import { SuccessDisplay } from '../shared/SuccessDisplay.js';
import { useStocks } from '../../hooks/useStocks.js';
import { BATCH_TYPE_LABELS } from '@effigenix/api-client';
import type { BatchType } from '@effigenix/api-client';
type Field = 'batchId' | 'batchType' | 'quantityAmount' | 'quantityUnit' | 'expiryDate';
const FIELDS: Field[] = ['batchId', 'batchType', 'quantityAmount', 'quantityUnit', 'expiryDate'];
const FIELD_LABELS: Record<Field, string> = {
batchId: 'Chargen-Nr. *',
batchType: 'Chargentyp *',
quantityAmount: 'Menge *',
quantityUnit: 'Einheit *',
expiryDate: 'Ablaufdatum (YYYY-MM-DD) *',
};
const BATCH_TYPES: BatchType[] = ['PURCHASED', 'PRODUCED'];
export function AddBatchScreen() {
const { params, navigate, back } = useNavigation();
const stockId = params['stockId'] ?? '';
const { addBatch, loading, error, clearError } = useStocks();
const [values, setValues] = useState<Record<Field, string>>({
batchId: '', batchType: 'PURCHASED', quantityAmount: '', quantityUnit: '', expiryDate: '',
});
const [activeField, setActiveField] = useState<Field>('batchId');
const [fieldErrors, setFieldErrors] = useState<Partial<Record<Field, string>>>({});
const [success, setSuccess] = useState<string | null>(null);
const setField = (field: Field) => (value: string) => {
setValues((v) => ({ ...v, [field]: value }));
};
useInput((_input, key) => {
if (loading) return;
if (activeField === 'batchType') {
if (key.leftArrow || key.rightArrow) {
const idx = BATCH_TYPES.indexOf(values.batchType as BatchType);
const next = key.rightArrow
? BATCH_TYPES[(idx + 1) % BATCH_TYPES.length]
: BATCH_TYPES[(idx - 1 + BATCH_TYPES.length) % BATCH_TYPES.length];
if (next) setValues((v) => ({ ...v, batchType: 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.batchId.trim()) errors.batchId = 'Chargen-Nr. ist erforderlich.';
if (!values.quantityAmount.trim()) errors.quantityAmount = 'Menge ist erforderlich.';
if (values.quantityAmount.trim() && isNaN(Number(values.quantityAmount))) errors.quantityAmount = 'Muss eine Zahl sein.';
if (!values.quantityUnit.trim()) errors.quantityUnit = 'Einheit ist erforderlich.';
if (!values.expiryDate.trim()) errors.expiryDate = 'Ablaufdatum ist erforderlich.';
if (values.expiryDate.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(values.expiryDate.trim())) {
errors.expiryDate = 'Format: YYYY-MM-DD';
}
setFieldErrors(errors);
if (Object.keys(errors).length > 0) return;
const batch = await addBatch(stockId, {
batchId: values.batchId.trim(),
batchType: values.batchType,
quantityAmount: values.quantityAmount.trim(),
quantityUnit: values.quantityUnit.trim(),
expiryDate: values.expiryDate.trim(),
});
if (batch) {
setSuccess(`Charge ${batch.batchId} erfolgreich eingebucht.`);
}
};
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 (!stockId) return <ErrorDisplay message="Keine Bestand-ID vorhanden." onDismiss={back} />;
if (loading) return <Box paddingY={2}><LoadingSpinner label="Charge wird eingebucht..." /></Box>;
return (
<Box flexDirection="column" gap={1}>
<Text color="cyan" bold>Charge einbuchen</Text>
{error && <ErrorDisplay message={error} onDismiss={clearError} />}
{success && <SuccessDisplay message={success} onDismiss={() => navigate('inventory-menu')} />}
{!success && (
<Box flexDirection="column" gap={1} width={60}>
{FIELDS.map((field) => {
if (field === 'batchType') {
const typeName = BATCH_TYPE_LABELS[values.batchType as BatchType] ?? values.batchType;
return (
<Box key={field} flexDirection="column">
<Text color={activeField === field ? 'cyan' : 'gray'}>
{FIELD_LABELS[field]}: {activeField === field ? `${typeName}` : typeName}
</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 · Typ wählen · Enter auf letztem Feld speichern · Escape Abbrechen
</Text>
</Box>
</Box>
);
}

View file

@ -11,6 +11,7 @@ interface MenuItem {
const MENU_ITEMS: MenuItem[] = [
{ label: 'Lagerorte', screen: 'storage-location-list', description: 'Lagerorte verwalten (Kühlräume, Trockenlager, …)' },
{ label: 'Charge einbuchen', screen: 'stock-batch-entry', description: 'Neue Charge in einen Bestand einbuchen' },
];
export function InventoryMenu() {

View file

@ -0,0 +1,51 @@
import { useState } from 'react';
import { Box, Text, useInput } from 'ink';
import { useNavigation } from '../../state/navigation-context.js';
import { FormInput } from '../shared/FormInput.js';
export function StockBatchEntryScreen() {
const { navigate, back } = useNavigation();
const [stockId, setStockId] = useState('');
const [error, setError] = useState<string | null>(null);
const handleChange = (value: string) => {
setStockId(value);
if (error) setError(null);
};
useInput((_input, key) => {
if (key.escape) back();
});
const handleSubmit = () => {
if (!stockId.trim()) {
setError('Bestand-ID ist erforderlich.');
return;
}
navigate('stock-add-batch', { stockId: stockId.trim() });
};
return (
<Box flexDirection="column" gap={1}>
<Text color="cyan" bold>Charge einbuchen</Text>
<Text color="gray">Geben Sie die Bestand-ID ein, um eine neue Charge einzubuchen.</Text>
<Box flexDirection="column" width={60}>
<FormInput
label="Bestand-ID *"
value={stockId}
onChange={handleChange}
onSubmit={handleSubmit}
focus={true}
{...(error ? { error } : {})}
/>
</Box>
<Box marginTop={1}>
<Text color="gray" dimColor>
Enter Weiter · Escape Zurück
</Text>
</Box>
</Box>
);
}

View file

@ -9,8 +9,8 @@ import { SuccessDisplay } from '../shared/SuccessDisplay.js';
import { ConfirmDialog } from '../shared/ConfirmDialog.js';
import { client } from '../../utils/api-client.js';
type MenuAction = 'add-ingredient' | 'remove-ingredient' | 'add-step' | 'remove-step' | 'activate' | 'back';
type Mode = 'menu' | 'select-step-to-remove' | 'confirm-remove' | 'select-ingredient-to-remove' | 'confirm-remove-ingredient' | 'confirm-activate';
type MenuAction = 'add-ingredient' | 'remove-ingredient' | 'add-step' | 'remove-step' | 'activate' | 'archive' | 'back';
type Mode = 'menu' | 'select-step-to-remove' | 'confirm-remove' | 'select-ingredient-to-remove' | 'confirm-remove-ingredient' | 'confirm-activate' | 'confirm-archive';
function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : 'Unbekannter Fehler';
@ -33,6 +33,7 @@ export function RecipeDetailScreen() {
const [ingredientToRemove, setIngredientToRemove] = useState<IngredientDTO | null>(null);
const isDraft = recipe?.status === 'DRAFT';
const isActive = recipe?.status === 'ACTIVE';
const menuItems: { id: MenuAction; label: string }[] = [
...(isDraft ? [
@ -42,6 +43,9 @@ export function RecipeDetailScreen() {
{ id: 'remove-step' as const, label: '[Schritt entfernen]' },
{ id: 'activate' as const, label: '[Rezept aktivieren]' },
] : []),
...(isActive ? [
{ id: 'archive' as const, label: '[Rezept archivieren]' },
] : []),
{ id: 'back' as const, label: '[Zurück]' },
];
@ -127,6 +131,9 @@ export function RecipeDetailScreen() {
case 'activate':
setMode('confirm-activate');
break;
case 'archive':
setMode('confirm-archive');
break;
case 'back':
back();
break;
@ -179,6 +186,20 @@ export function RecipeDetailScreen() {
setActionLoading(false);
}, [recipe]);
const handleArchive = useCallback(async () => {
if (!recipe) return;
setMode('menu');
setActionLoading(true);
try {
const updated = await client.recipes.archiveRecipe(recipe.id);
setRecipe(updated);
setSuccessMessage('Rezept wurde archiviert.');
} catch (err: unknown) {
setError(errorMessage(err));
}
setActionLoading(false);
}, [recipe]);
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>;
@ -307,6 +328,14 @@ export function RecipeDetailScreen() {
/>
)}
{mode === 'confirm-archive' && (
<ConfirmDialog
message="Rezept wirklich archivieren? Der Status wechselt zu ARCHIVED."
onConfirm={() => void handleArchive()}
onCancel={() => setMode('menu')}
/>
)}
{mode === 'menu' && (
<Box flexDirection="column">
<Text color="gray" bold>Aktionen:</Text>

View file

@ -5,16 +5,30 @@ 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';
import type { RecipeType, RecipeStatus } from '@effigenix/api-client';
const STATUS_FILTERS: { key: string; label: string; value: RecipeStatus | undefined }[] = [
{ key: 'a', label: 'ALLE', value: undefined },
{ key: 'D', label: 'DRAFT', value: 'DRAFT' },
{ key: 'A', label: 'ACTIVE', value: 'ACTIVE' },
{ key: 'R', label: 'ARCHIVED', value: 'ARCHIVED' },
];
const STATUS_COLORS: Record<string, string> = {
DRAFT: 'yellow',
ACTIVE: 'green',
ARCHIVED: 'gray',
};
export function RecipeListScreen() {
const { navigate, back } = useNavigation();
const { recipes, loading, error, fetchRecipes, clearError } = useRecipes();
const [selectedIndex, setSelectedIndex] = useState(0);
const [statusFilter, setStatusFilter] = useState<RecipeStatus | undefined>(undefined);
useEffect(() => {
void fetchRecipes();
}, [fetchRecipes]);
void fetchRecipes(statusFilter);
}, [fetchRecipes, statusFilter]);
useInput((input, key) => {
if (loading) return;
@ -28,13 +42,26 @@ export function RecipeListScreen() {
}
if (input === 'n') navigate('recipe-create');
if (key.backspace || key.escape) back();
// Status-Filter
for (const filter of STATUS_FILTERS) {
if (input === filter.key) {
setStatusFilter(filter.value);
setSelectedIndex(0);
break;
}
}
});
const activeFilterLabel = STATUS_FILTERS.find((f) => f.value === statusFilter)?.label ?? 'ALLE';
return (
<Box flexDirection="column" gap={1}>
<Box gap={2}>
<Text color="cyan" bold>Rezepte</Text>
<Text color="gray" dimColor>({recipes.length})</Text>
<Text color="gray" dimColor>Filter: </Text>
<Text color="yellow" bold>{activeFilterLabel}</Text>
</Box>
{loading && <LoadingSpinner label="Lade Rezepte..." />}
@ -57,13 +84,14 @@ export function RecipeListScreen() {
const isSelected = index === selectedIndex;
const textColor = isSelected ? 'cyan' : 'white';
const typeName = RECIPE_TYPE_LABELS[recipe.type as RecipeType] ?? recipe.type;
const statusColor = STATUS_COLORS[recipe.status] ?? 'white';
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>
<Text color={isSelected ? 'cyan' : statusColor}>{recipe.status}</Text>
</Box>
);
})}
@ -72,7 +100,7 @@ export function RecipeListScreen() {
<Box marginTop={1}>
<Text color="gray" dimColor>
nav · Enter Details · [n] Neu · Backspace Zurück
nav · Enter Details · [n] Neu · [a] Alle [D] Draft [A] Active [R] Archived · Backspace Zurück
</Text>
</Box>
</Box>

View file

@ -1,9 +1,9 @@
import { useState, useCallback } from 'react';
import type { RecipeDTO, CreateRecipeRequest } from '@effigenix/api-client';
import type { RecipeSummaryDTO, CreateRecipeRequest, RecipeStatus } from '@effigenix/api-client';
import { client } from '../utils/api-client.js';
interface RecipesState {
recipes: RecipeDTO[];
recipes: RecipeSummaryDTO[];
loading: boolean;
error: string | null;
}
@ -19,10 +19,10 @@ export function useRecipes() {
error: null,
});
const fetchRecipes = useCallback(async () => {
const fetchRecipes = useCallback(async (status?: RecipeStatus) => {
setState((s) => ({ ...s, loading: true, error: null }));
try {
const recipes = await client.recipes.list();
const recipes = await client.recipes.list(status);
setState({ recipes, loading: false, error: null });
} catch (err) {
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
@ -33,7 +33,8 @@ export function useRecipes() {
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 }));
const recipes = await client.recipes.list();
setState({ recipes, loading: false, error: null });
return recipe;
} catch (err) {
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));

View file

@ -0,0 +1,41 @@
import { useState, useCallback } from 'react';
import type { StockBatchDTO, AddStockBatchRequest } from '@effigenix/api-client';
import { client } from '../utils/api-client.js';
interface StocksState {
loading: boolean;
error: string | null;
}
function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : 'Unbekannter Fehler';
}
export function useStocks() {
const [state, setState] = useState<StocksState>({
loading: false,
error: null,
});
const addBatch = useCallback(async (stockId: string, request: AddStockBatchRequest): Promise<StockBatchDTO | null> => {
setState({ loading: true, error: null });
try {
const batch = await client.stocks.addBatch(stockId, request);
setState({ loading: false, error: null });
return batch;
} catch (err) {
setState({ loading: false, error: errorMessage(err) });
return null;
}
}, []);
const clearError = useCallback(() => {
setState((s) => ({ ...s, error: null }));
}, []);
return {
...state,
addBatch,
clearError,
};
}

View file

@ -33,6 +33,8 @@ export type Screen =
| 'storage-location-list'
| 'storage-location-create'
| 'storage-location-detail'
| 'stock-batch-entry'
| 'stock-add-batch'
// Produktion
| 'production-menu'
| 'recipe-list'

File diff suppressed because one or more lines are too long

View file

@ -24,6 +24,7 @@ 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 { createStocksResource } from './resources/stocks.js';
export {
ApiError,
AuthenticationError,
@ -81,11 +82,14 @@ export type {
CreateStorageLocationRequest,
UpdateStorageLocationRequest,
RecipeDTO,
RecipeSummaryDTO,
IngredientDTO,
ProductionStepDTO,
CreateRecipeRequest,
AddRecipeIngredientRequest,
AddProductionStepRequest,
StockBatchDTO,
AddStockBatchRequest,
} from '@effigenix/types';
// Resource types (runtime, stay in resource files)
@ -111,6 +115,8 @@ export type {
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';
export type { StocksResource, BatchType } from './resources/stocks.js';
export { BATCH_TYPE_LABELS } from './resources/stocks.js';
import { createApiClient } from './client.js';
import { createAuthResource } from './resources/auth.js';
@ -122,6 +128,7 @@ 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 { createStocksResource } from './resources/stocks.js';
import type { TokenProvider } from './token-provider.js';
import type { ApiConfig } from '@effigenix/config';
@ -145,6 +152,7 @@ export function createEffigenixClient(
customers: createCustomersResource(axiosClient),
storageLocations: createStorageLocationsResource(axiosClient),
recipes: createRecipesResource(axiosClient),
stocks: createStocksResource(axiosClient),
};
}

View file

@ -3,6 +3,7 @@
import type { AxiosInstance } from 'axios';
import type {
RecipeDTO,
RecipeSummaryDTO,
IngredientDTO,
ProductionStepDTO,
CreateRecipeRequest,
@ -21,6 +22,7 @@ export const RECIPE_TYPE_LABELS: Record<RecipeType, string> = {
export type {
RecipeDTO,
RecipeSummaryDTO,
IngredientDTO,
ProductionStepDTO,
CreateRecipeRequest,
@ -34,8 +36,10 @@ const BASE = '/api/recipes';
export function createRecipesResource(client: AxiosInstance) {
return {
async list(): Promise<RecipeDTO[]> {
const res = await client.get<RecipeDTO[]>(BASE);
async list(status?: RecipeStatus): Promise<RecipeSummaryDTO[]> {
const params: Record<string, string> = {};
if (status) params['status'] = status;
const res = await client.get<RecipeSummaryDTO[]>(BASE, { params });
return res.data;
},
@ -71,6 +75,11 @@ export function createRecipesResource(client: AxiosInstance) {
const res = await client.post<RecipeDTO>(`${BASE}/${id}/activate`);
return res.data;
},
async archiveRecipe(id: string): Promise<RecipeDTO> {
const res = await client.post<RecipeDTO>(`${BASE}/${id}/archive`);
return res.data;
},
};
}

View file

@ -0,0 +1,28 @@
/** Stocks resource Inventory BC. */
import type { AxiosInstance } from 'axios';
import type { StockBatchDTO, AddStockBatchRequest } from '@effigenix/types';
export type BatchType = 'PURCHASED' | 'PRODUCED';
export const BATCH_TYPE_LABELS: Record<BatchType, string> = {
PURCHASED: 'Eingekauft',
PRODUCED: 'Produziert',
};
export type { StockBatchDTO, AddStockBatchRequest };
// ── Resource factory ─────────────────────────────────────────────────────────
const BASE = '/api/inventory/stocks';
export function createStocksResource(client: AxiosInstance) {
return {
async addBatch(stockId: string, request: AddStockBatchRequest): Promise<StockBatchDTO> {
const res = await client.post<StockBatchDTO>(`${BASE}/${stockId}/batches`, request);
return res.data;
},
};
}
export type StocksResource = ReturnType<typeof createStocksResource>;

View file

@ -347,7 +347,7 @@ export interface paths {
path?: never;
cookie?: never;
};
get?: never;
get: operations["listRecipes"];
put?: never;
post: operations["createRecipe"];
delete?: never;
@ -388,6 +388,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/recipes/{id}/archive": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["archiveRecipe"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/recipes/{id}/activate": {
parameters: {
query?: never;
@ -436,6 +452,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/inventory/stocks/{stockId}/batches": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["addBatch"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/customers": {
parameters: {
query?: never;
@ -708,6 +740,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/recipes/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getRecipe"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/users/{id}/roles/{roleName}": {
parameters: {
query?: never;
@ -1209,6 +1257,25 @@ export interface components {
/** Format: int32 */
minimumShelfLifeDays?: number | null;
};
AddStockBatchRequest: {
batchId: string;
batchType: string;
quantityAmount: string;
quantityUnit: string;
expiryDate: string;
};
StockBatchResponse: {
id?: string;
batchId?: string;
batchType?: string;
quantityAmount?: number;
quantityUnit?: string;
/** Format: date */
expiryDate?: string;
status?: string;
/** Format: date-time */
receivedAt?: string;
};
CreateCustomerRequest: {
name: string;
/** @enum {string} */
@ -1303,6 +1370,29 @@ export interface components {
priceModel: "FIXED" | "WEIGHT_BASED";
price: number;
};
RecipeSummaryResponse: {
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;
/** Format: int32 */
ingredientCount: number;
/** Format: int32 */
stepCount: number;
/** Format: date-time */
createdAt: string;
/** Format: date-time */
updatedAt: string;
};
RemoveCertificateRequest: {
certificateType: string;
issuer?: string;
@ -2156,6 +2246,28 @@ export interface operations {
};
};
};
listRecipes: {
parameters: {
query?: {
status?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"*/*": components["schemas"]["RecipeSummaryResponse"][];
};
};
};
};
createRecipe: {
parameters: {
query?: never;
@ -2232,6 +2344,28 @@ export interface operations {
};
};
};
archiveRecipe: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"*/*": components["schemas"]["RecipeResponse"];
};
};
};
};
activateRecipe: {
parameters: {
query?: never;
@ -2325,6 +2459,32 @@ export interface operations {
};
};
};
addBatch: {
parameters: {
query?: never;
header?: never;
path: {
stockId: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["AddStockBatchRequest"];
};
};
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"*/*": components["schemas"]["StockBatchResponse"];
};
};
};
};
listCustomers: {
parameters: {
query?: {
@ -2811,6 +2971,28 @@ export interface operations {
};
};
};
getRecipe: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"*/*": components["schemas"]["RecipeResponse"];
};
};
};
};
removeRole: {
parameters: {
query?: never;

View file

@ -8,7 +8,9 @@ import type { components } from './generated/api';
// Response DTOs
export type StorageLocationDTO = components['schemas']['StorageLocationResponse'];
export type TemperatureRangeDTO = components['schemas']['TemperatureRangeResponse'];
export type StockBatchDTO = components['schemas']['StockBatchResponse'];
// Request types
export type CreateStorageLocationRequest = components['schemas']['CreateStorageLocationRequest'];
export type UpdateStorageLocationRequest = components['schemas']['UpdateStorageLocationRequest'];
export type AddStockBatchRequest = components['schemas']['AddStockBatchRequest'];

View file

@ -7,6 +7,7 @@ import type { components } from './generated/api';
// Response DTOs
export type RecipeDTO = components['schemas']['RecipeResponse'];
export type RecipeSummaryDTO = components['schemas']['RecipeSummaryResponse'];
export type IngredientDTO = components['schemas']['IngredientResponse'];
export type ProductionStepDTO = components['schemas']['ProductionStepResponse'];