mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 15:29:34 +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 { 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 />}
|
||||
|
|
|
|||
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[] = [
|
||||
{ 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() {
|
||||
|
|
|
|||
|
|
@ -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 { 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) }));
|
||||
|
|
|
|||
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-create'
|
||||
| 'storage-location-detail'
|
||||
| 'stock-batch-entry'
|
||||
| 'stock-add-batch'
|
||||
// Produktion
|
||||
| 'production-menu'
|
||||
| 'recipe-list'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue