mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 10:19:35 +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:
parent
f2003a3093
commit
ec736cf294
16 changed files with 553 additions and 16 deletions
|
|
@ -36,6 +36,8 @@ import { InventoryMenu } from './components/inventory/InventoryMenu.js';
|
||||||
import { StorageLocationListScreen } from './components/inventory/StorageLocationListScreen.js';
|
import { StorageLocationListScreen } from './components/inventory/StorageLocationListScreen.js';
|
||||||
import { StorageLocationCreateScreen } from './components/inventory/StorageLocationCreateScreen.js';
|
import { StorageLocationCreateScreen } from './components/inventory/StorageLocationCreateScreen.js';
|
||||||
import { StorageLocationDetailScreen } from './components/inventory/StorageLocationDetailScreen.js';
|
import { StorageLocationDetailScreen } from './components/inventory/StorageLocationDetailScreen.js';
|
||||||
|
import { StockBatchEntryScreen } from './components/inventory/StockBatchEntryScreen.js';
|
||||||
|
import { AddBatchScreen } from './components/inventory/AddBatchScreen.js';
|
||||||
// Produktion
|
// Produktion
|
||||||
import { ProductionMenu } from './components/production/ProductionMenu.js';
|
import { ProductionMenu } from './components/production/ProductionMenu.js';
|
||||||
import { RecipeListScreen } from './components/production/RecipeListScreen.js';
|
import { RecipeListScreen } from './components/production/RecipeListScreen.js';
|
||||||
|
|
@ -104,6 +106,8 @@ function ScreenRouter() {
|
||||||
{current === 'storage-location-list' && <StorageLocationListScreen />}
|
{current === 'storage-location-list' && <StorageLocationListScreen />}
|
||||||
{current === 'storage-location-create' && <StorageLocationCreateScreen />}
|
{current === 'storage-location-create' && <StorageLocationCreateScreen />}
|
||||||
{current === 'storage-location-detail' && <StorageLocationDetailScreen />}
|
{current === 'storage-location-detail' && <StorageLocationDetailScreen />}
|
||||||
|
{current === 'stock-batch-entry' && <StockBatchEntryScreen />}
|
||||||
|
{current === 'stock-add-batch' && <AddBatchScreen />}
|
||||||
{/* Produktion */}
|
{/* Produktion */}
|
||||||
{current === 'production-menu' && <ProductionMenu />}
|
{current === 'production-menu' && <ProductionMenu />}
|
||||||
{current === 'recipe-list' && <RecipeListScreen />}
|
{current === 'recipe-list' && <RecipeListScreen />}
|
||||||
|
|
|
||||||
150
frontend/apps/cli/src/components/inventory/AddBatchScreen.tsx
Normal file
150
frontend/apps/cli/src/components/inventory/AddBatchScreen.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ interface MenuItem {
|
||||||
|
|
||||||
const MENU_ITEMS: MenuItem[] = [
|
const MENU_ITEMS: MenuItem[] = [
|
||||||
{ label: 'Lagerorte', screen: 'storage-location-list', description: 'Lagerorte verwalten (Kühlräume, Trockenlager, …)' },
|
{ 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() {
|
export function InventoryMenu() {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,8 +9,8 @@ import { SuccessDisplay } from '../shared/SuccessDisplay.js';
|
||||||
import { ConfirmDialog } from '../shared/ConfirmDialog.js';
|
import { ConfirmDialog } from '../shared/ConfirmDialog.js';
|
||||||
import { client } from '../../utils/api-client.js';
|
import { client } from '../../utils/api-client.js';
|
||||||
|
|
||||||
type MenuAction = 'add-ingredient' | 'remove-ingredient' | 'add-step' | 'remove-step' | 'activate' | 'back';
|
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';
|
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 {
|
function errorMessage(err: unknown): string {
|
||||||
return err instanceof Error ? err.message : 'Unbekannter Fehler';
|
return err instanceof Error ? err.message : 'Unbekannter Fehler';
|
||||||
|
|
@ -33,6 +33,7 @@ export function RecipeDetailScreen() {
|
||||||
const [ingredientToRemove, setIngredientToRemove] = useState<IngredientDTO | null>(null);
|
const [ingredientToRemove, setIngredientToRemove] = useState<IngredientDTO | null>(null);
|
||||||
|
|
||||||
const isDraft = recipe?.status === 'DRAFT';
|
const isDraft = recipe?.status === 'DRAFT';
|
||||||
|
const isActive = recipe?.status === 'ACTIVE';
|
||||||
|
|
||||||
const menuItems: { id: MenuAction; label: string }[] = [
|
const menuItems: { id: MenuAction; label: string }[] = [
|
||||||
...(isDraft ? [
|
...(isDraft ? [
|
||||||
|
|
@ -42,6 +43,9 @@ export function RecipeDetailScreen() {
|
||||||
{ id: 'remove-step' as const, label: '[Schritt entfernen]' },
|
{ id: 'remove-step' as const, label: '[Schritt entfernen]' },
|
||||||
{ id: 'activate' as const, label: '[Rezept aktivieren]' },
|
{ id: 'activate' as const, label: '[Rezept aktivieren]' },
|
||||||
] : []),
|
] : []),
|
||||||
|
...(isActive ? [
|
||||||
|
{ id: 'archive' as const, label: '[Rezept archivieren]' },
|
||||||
|
] : []),
|
||||||
{ id: 'back' as const, label: '[Zurück]' },
|
{ id: 'back' as const, label: '[Zurück]' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -127,6 +131,9 @@ export function RecipeDetailScreen() {
|
||||||
case 'activate':
|
case 'activate':
|
||||||
setMode('confirm-activate');
|
setMode('confirm-activate');
|
||||||
break;
|
break;
|
||||||
|
case 'archive':
|
||||||
|
setMode('confirm-archive');
|
||||||
|
break;
|
||||||
case 'back':
|
case 'back':
|
||||||
back();
|
back();
|
||||||
break;
|
break;
|
||||||
|
|
@ -179,6 +186,20 @@ export function RecipeDetailScreen() {
|
||||||
setActionLoading(false);
|
setActionLoading(false);
|
||||||
}, [recipe]);
|
}, [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 (loading) return <LoadingSpinner label="Lade Rezept..." />;
|
||||||
if (error && !recipe) return <ErrorDisplay message={error} onDismiss={back} />;
|
if (error && !recipe) return <ErrorDisplay message={error} onDismiss={back} />;
|
||||||
if (!recipe) return <Text color="red">Rezept nicht gefunden.</Text>;
|
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' && (
|
{mode === 'menu' && (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Text color="gray" bold>Aktionen:</Text>
|
<Text color="gray" bold>Aktionen:</Text>
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,30 @@ import { useRecipes } from '../../hooks/useRecipes.js';
|
||||||
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||||
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||||
import { RECIPE_TYPE_LABELS } from '@effigenix/api-client';
|
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() {
|
export function RecipeListScreen() {
|
||||||
const { navigate, back } = useNavigation();
|
const { navigate, back } = useNavigation();
|
||||||
const { recipes, loading, error, fetchRecipes, clearError } = useRecipes();
|
const { recipes, loading, error, fetchRecipes, clearError } = useRecipes();
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
const [statusFilter, setStatusFilter] = useState<RecipeStatus | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void fetchRecipes();
|
void fetchRecipes(statusFilter);
|
||||||
}, [fetchRecipes]);
|
}, [fetchRecipes, statusFilter]);
|
||||||
|
|
||||||
useInput((input, key) => {
|
useInput((input, key) => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
|
|
@ -28,13 +42,26 @@ export function RecipeListScreen() {
|
||||||
}
|
}
|
||||||
if (input === 'n') navigate('recipe-create');
|
if (input === 'n') navigate('recipe-create');
|
||||||
if (key.backspace || key.escape) back();
|
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 (
|
return (
|
||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
<Box gap={2}>
|
<Box gap={2}>
|
||||||
<Text color="cyan" bold>Rezepte</Text>
|
<Text color="cyan" bold>Rezepte</Text>
|
||||||
<Text color="gray" dimColor>({recipes.length})</Text>
|
<Text color="gray" dimColor>({recipes.length})</Text>
|
||||||
|
<Text color="gray" dimColor>Filter: </Text>
|
||||||
|
<Text color="yellow" bold>{activeFilterLabel}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{loading && <LoadingSpinner label="Lade Rezepte..." />}
|
{loading && <LoadingSpinner label="Lade Rezepte..." />}
|
||||||
|
|
@ -57,13 +84,14 @@ export function RecipeListScreen() {
|
||||||
const isSelected = index === selectedIndex;
|
const isSelected = index === selectedIndex;
|
||||||
const textColor = isSelected ? 'cyan' : 'white';
|
const textColor = isSelected ? 'cyan' : 'white';
|
||||||
const typeName = RECIPE_TYPE_LABELS[recipe.type as RecipeType] ?? recipe.type;
|
const typeName = RECIPE_TYPE_LABELS[recipe.type as RecipeType] ?? recipe.type;
|
||||||
|
const statusColor = STATUS_COLORS[recipe.status] ?? 'white';
|
||||||
return (
|
return (
|
||||||
<Box key={recipe.id} paddingX={1}>
|
<Box key={recipe.id} paddingX={1}>
|
||||||
<Text color={textColor}>{isSelected ? '▶ ' : ' '}</Text>
|
<Text color={textColor}>{isSelected ? '▶ ' : ' '}</Text>
|
||||||
<Text color={textColor}>{recipe.name.substring(0, 26).padEnd(27)}</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'}>{typeName.padEnd(18)}</Text>
|
||||||
<Text color={isSelected ? 'cyan' : 'gray'}>{String(recipe.version).padEnd(5)}</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>
|
</Box>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -72,7 +100,7 @@ export function RecipeListScreen() {
|
||||||
|
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text color="gray" dimColor>
|
<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>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useState, useCallback } from 'react';
|
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';
|
import { client } from '../utils/api-client.js';
|
||||||
|
|
||||||
interface RecipesState {
|
interface RecipesState {
|
||||||
recipes: RecipeDTO[];
|
recipes: RecipeSummaryDTO[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
@ -19,10 +19,10 @@ export function useRecipes() {
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchRecipes = useCallback(async () => {
|
const fetchRecipes = useCallback(async (status?: RecipeStatus) => {
|
||||||
setState((s) => ({ ...s, loading: true, error: null }));
|
setState((s) => ({ ...s, loading: true, error: null }));
|
||||||
try {
|
try {
|
||||||
const recipes = await client.recipes.list();
|
const recipes = await client.recipes.list(status);
|
||||||
setState({ recipes, loading: false, error: null });
|
setState({ recipes, loading: false, error: null });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||||
|
|
@ -33,7 +33,8 @@ export function useRecipes() {
|
||||||
setState((s) => ({ ...s, loading: true, error: null }));
|
setState((s) => ({ ...s, loading: true, error: null }));
|
||||||
try {
|
try {
|
||||||
const recipe = await client.recipes.create(request);
|
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;
|
return recipe;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||||
|
|
|
||||||
41
frontend/apps/cli/src/hooks/useStocks.ts
Normal file
41
frontend/apps/cli/src/hooks/useStocks.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,8 @@ export type Screen =
|
||||||
| 'storage-location-list'
|
| 'storage-location-list'
|
||||||
| 'storage-location-create'
|
| 'storage-location-create'
|
||||||
| 'storage-location-detail'
|
| 'storage-location-detail'
|
||||||
|
| 'stock-batch-entry'
|
||||||
|
| 'stock-add-batch'
|
||||||
// Produktion
|
// Produktion
|
||||||
| 'production-menu'
|
| 'production-menu'
|
||||||
| 'recipe-list'
|
| 'recipe-list'
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -24,6 +24,7 @@ export { createArticlesResource } from './resources/articles.js';
|
||||||
export { createCustomersResource } from './resources/customers.js';
|
export { createCustomersResource } from './resources/customers.js';
|
||||||
export { createStorageLocationsResource } from './resources/storage-locations.js';
|
export { createStorageLocationsResource } from './resources/storage-locations.js';
|
||||||
export { createRecipesResource } from './resources/recipes.js';
|
export { createRecipesResource } from './resources/recipes.js';
|
||||||
|
export { createStocksResource } from './resources/stocks.js';
|
||||||
export {
|
export {
|
||||||
ApiError,
|
ApiError,
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
|
|
@ -81,11 +82,14 @@ export type {
|
||||||
CreateStorageLocationRequest,
|
CreateStorageLocationRequest,
|
||||||
UpdateStorageLocationRequest,
|
UpdateStorageLocationRequest,
|
||||||
RecipeDTO,
|
RecipeDTO,
|
||||||
|
RecipeSummaryDTO,
|
||||||
IngredientDTO,
|
IngredientDTO,
|
||||||
ProductionStepDTO,
|
ProductionStepDTO,
|
||||||
CreateRecipeRequest,
|
CreateRecipeRequest,
|
||||||
AddRecipeIngredientRequest,
|
AddRecipeIngredientRequest,
|
||||||
AddProductionStepRequest,
|
AddProductionStepRequest,
|
||||||
|
StockBatchDTO,
|
||||||
|
AddStockBatchRequest,
|
||||||
} from '@effigenix/types';
|
} from '@effigenix/types';
|
||||||
|
|
||||||
// Resource types (runtime, stay in resource files)
|
// Resource types (runtime, stay in resource files)
|
||||||
|
|
@ -111,6 +115,8 @@ export type {
|
||||||
export { STORAGE_TYPE_LABELS } from './resources/storage-locations.js';
|
export { STORAGE_TYPE_LABELS } from './resources/storage-locations.js';
|
||||||
export type { RecipesResource, RecipeType, RecipeStatus } from './resources/recipes.js';
|
export type { RecipesResource, RecipeType, RecipeStatus } from './resources/recipes.js';
|
||||||
export { RECIPE_TYPE_LABELS } 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 { createApiClient } from './client.js';
|
||||||
import { createAuthResource } from './resources/auth.js';
|
import { createAuthResource } from './resources/auth.js';
|
||||||
|
|
@ -122,6 +128,7 @@ import { createArticlesResource } from './resources/articles.js';
|
||||||
import { createCustomersResource } from './resources/customers.js';
|
import { createCustomersResource } from './resources/customers.js';
|
||||||
import { createStorageLocationsResource } from './resources/storage-locations.js';
|
import { createStorageLocationsResource } from './resources/storage-locations.js';
|
||||||
import { createRecipesResource } from './resources/recipes.js';
|
import { createRecipesResource } from './resources/recipes.js';
|
||||||
|
import { createStocksResource } from './resources/stocks.js';
|
||||||
import type { TokenProvider } from './token-provider.js';
|
import type { TokenProvider } from './token-provider.js';
|
||||||
import type { ApiConfig } from '@effigenix/config';
|
import type { ApiConfig } from '@effigenix/config';
|
||||||
|
|
||||||
|
|
@ -145,6 +152,7 @@ export function createEffigenixClient(
|
||||||
customers: createCustomersResource(axiosClient),
|
customers: createCustomersResource(axiosClient),
|
||||||
storageLocations: createStorageLocationsResource(axiosClient),
|
storageLocations: createStorageLocationsResource(axiosClient),
|
||||||
recipes: createRecipesResource(axiosClient),
|
recipes: createRecipesResource(axiosClient),
|
||||||
|
stocks: createStocksResource(axiosClient),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import type { AxiosInstance } from 'axios';
|
import type { AxiosInstance } from 'axios';
|
||||||
import type {
|
import type {
|
||||||
RecipeDTO,
|
RecipeDTO,
|
||||||
|
RecipeSummaryDTO,
|
||||||
IngredientDTO,
|
IngredientDTO,
|
||||||
ProductionStepDTO,
|
ProductionStepDTO,
|
||||||
CreateRecipeRequest,
|
CreateRecipeRequest,
|
||||||
|
|
@ -21,6 +22,7 @@ export const RECIPE_TYPE_LABELS: Record<RecipeType, string> = {
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
RecipeDTO,
|
RecipeDTO,
|
||||||
|
RecipeSummaryDTO,
|
||||||
IngredientDTO,
|
IngredientDTO,
|
||||||
ProductionStepDTO,
|
ProductionStepDTO,
|
||||||
CreateRecipeRequest,
|
CreateRecipeRequest,
|
||||||
|
|
@ -34,8 +36,10 @@ const BASE = '/api/recipes';
|
||||||
|
|
||||||
export function createRecipesResource(client: AxiosInstance) {
|
export function createRecipesResource(client: AxiosInstance) {
|
||||||
return {
|
return {
|
||||||
async list(): Promise<RecipeDTO[]> {
|
async list(status?: RecipeStatus): Promise<RecipeSummaryDTO[]> {
|
||||||
const res = await client.get<RecipeDTO[]>(BASE);
|
const params: Record<string, string> = {};
|
||||||
|
if (status) params['status'] = status;
|
||||||
|
const res = await client.get<RecipeSummaryDTO[]>(BASE, { params });
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -71,6 +75,11 @@ export function createRecipesResource(client: AxiosInstance) {
|
||||||
const res = await client.post<RecipeDTO>(`${BASE}/${id}/activate`);
|
const res = await client.post<RecipeDTO>(`${BASE}/${id}/activate`);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async archiveRecipe(id: string): Promise<RecipeDTO> {
|
||||||
|
const res = await client.post<RecipeDTO>(`${BASE}/${id}/archive`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
28
frontend/packages/api-client/src/resources/stocks.ts
Normal file
28
frontend/packages/api-client/src/resources/stocks.ts
Normal 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>;
|
||||||
|
|
@ -347,7 +347,7 @@ export interface paths {
|
||||||
path?: never;
|
path?: never;
|
||||||
cookie?: never;
|
cookie?: never;
|
||||||
};
|
};
|
||||||
get?: never;
|
get: operations["listRecipes"];
|
||||||
put?: never;
|
put?: never;
|
||||||
post: operations["createRecipe"];
|
post: operations["createRecipe"];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
|
|
@ -388,6 +388,22 @@ export interface paths {
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: 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": {
|
"/api/recipes/{id}/activate": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
@ -436,6 +452,22 @@ export interface paths {
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: 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": {
|
"/api/customers": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
@ -708,6 +740,22 @@ export interface paths {
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: 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}": {
|
"/api/users/{id}/roles/{roleName}": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
@ -1209,6 +1257,25 @@ export interface components {
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
minimumShelfLifeDays?: number | null;
|
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: {
|
CreateCustomerRequest: {
|
||||||
name: string;
|
name: string;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
|
|
@ -1303,6 +1370,29 @@ export interface components {
|
||||||
priceModel: "FIXED" | "WEIGHT_BASED";
|
priceModel: "FIXED" | "WEIGHT_BASED";
|
||||||
price: number;
|
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: {
|
RemoveCertificateRequest: {
|
||||||
certificateType: string;
|
certificateType: string;
|
||||||
issuer?: 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: {
|
createRecipe: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
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: {
|
activateRecipe: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
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: {
|
listCustomers: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
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: {
|
removeRole: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ import type { components } from './generated/api';
|
||||||
// Response DTOs
|
// Response DTOs
|
||||||
export type StorageLocationDTO = components['schemas']['StorageLocationResponse'];
|
export type StorageLocationDTO = components['schemas']['StorageLocationResponse'];
|
||||||
export type TemperatureRangeDTO = components['schemas']['TemperatureRangeResponse'];
|
export type TemperatureRangeDTO = components['schemas']['TemperatureRangeResponse'];
|
||||||
|
export type StockBatchDTO = components['schemas']['StockBatchResponse'];
|
||||||
|
|
||||||
// Request types
|
// Request types
|
||||||
export type CreateStorageLocationRequest = components['schemas']['CreateStorageLocationRequest'];
|
export type CreateStorageLocationRequest = components['schemas']['CreateStorageLocationRequest'];
|
||||||
export type UpdateStorageLocationRequest = components['schemas']['UpdateStorageLocationRequest'];
|
export type UpdateStorageLocationRequest = components['schemas']['UpdateStorageLocationRequest'];
|
||||||
|
export type AddStockBatchRequest = components['schemas']['AddStockBatchRequest'];
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import type { components } from './generated/api';
|
||||||
|
|
||||||
// Response DTOs
|
// Response DTOs
|
||||||
export type RecipeDTO = components['schemas']['RecipeResponse'];
|
export type RecipeDTO = components['schemas']['RecipeResponse'];
|
||||||
|
export type RecipeSummaryDTO = components['schemas']['RecipeSummaryResponse'];
|
||||||
export type IngredientDTO = components['schemas']['IngredientResponse'];
|
export type IngredientDTO = components['schemas']['IngredientResponse'];
|
||||||
export type ProductionStepDTO = components['schemas']['ProductionStepResponse'];
|
export type ProductionStepDTO = components['schemas']['ProductionStepResponse'];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue