diff --git a/frontend/apps/cli/src/App.tsx b/frontend/apps/cli/src/App.tsx
index cd88b59..3b591d5 100644
--- a/frontend/apps/cli/src/App.tsx
+++ b/frontend/apps/cli/src/App.tsx
@@ -45,6 +45,14 @@ import { RecipeCreateScreen } from './components/production/RecipeCreateScreen.j
import { RecipeDetailScreen } from './components/production/RecipeDetailScreen.js';
import { AddProductionStepScreen } from './components/production/AddProductionStepScreen.js';
import { AddIngredientScreen } from './components/production/AddIngredientScreen.js';
+import { BatchListScreen } from './components/production/BatchListScreen.js';
+import { BatchDetailScreen } from './components/production/BatchDetailScreen.js';
+import { BatchPlanScreen } from './components/production/BatchPlanScreen.js';
+import { RecordConsumptionScreen } from './components/production/RecordConsumptionScreen.js';
+import { CompleteBatchScreen } from './components/production/CompleteBatchScreen.js';
+import { StockListScreen } from './components/inventory/StockListScreen.js';
+import { StockDetailScreen } from './components/inventory/StockDetailScreen.js';
+import { StockCreateScreen } from './components/inventory/StockCreateScreen.js';
function ScreenRouter() {
const { isAuthenticated, loading } = useAuth();
@@ -108,6 +116,9 @@ function ScreenRouter() {
{current === 'storage-location-detail' && }
{current === 'stock-batch-entry' && }
{current === 'stock-add-batch' && }
+ {current === 'stock-list' && }
+ {current === 'stock-detail' && }
+ {current === 'stock-create' && }
{/* Produktion */}
{current === 'production-menu' && }
{current === 'recipe-list' && }
@@ -115,6 +126,11 @@ function ScreenRouter() {
{current === 'recipe-detail' && }
{current === 'recipe-add-production-step' && }
{current === 'recipe-add-ingredient' && }
+ {current === 'batch-list' && }
+ {current === 'batch-detail' && }
+ {current === 'batch-plan' && }
+ {current === 'batch-record-consumption' && }
+ {current === 'batch-complete' && }
);
}
diff --git a/frontend/apps/cli/src/components/inventory/InventoryMenu.tsx b/frontend/apps/cli/src/components/inventory/InventoryMenu.tsx
index 074002e..4f72e55 100644
--- a/frontend/apps/cli/src/components/inventory/InventoryMenu.tsx
+++ b/frontend/apps/cli/src/components/inventory/InventoryMenu.tsx
@@ -11,6 +11,7 @@ interface MenuItem {
const MENU_ITEMS: MenuItem[] = [
{ label: 'Lagerorte', screen: 'storage-location-list', description: 'Lagerorte verwalten (Kühlräume, Trockenlager, …)' },
+ { label: 'Bestände', screen: 'stock-list', description: 'Bestände einsehen, anlegen und Chargen verwalten' },
{ label: 'Charge einbuchen', screen: 'stock-batch-entry', description: 'Neue Charge in einen Bestand einbuchen' },
];
diff --git a/frontend/apps/cli/src/components/inventory/StockCreateScreen.tsx b/frontend/apps/cli/src/components/inventory/StockCreateScreen.tsx
new file mode 100644
index 0000000..68571fa
--- /dev/null
+++ b/frontend/apps/cli/src/components/inventory/StockCreateScreen.tsx
@@ -0,0 +1,216 @@
+import React, { useEffect, useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import { useNavigation } from '../../state/navigation-context.js';
+import { useStocks } from '../../hooks/useStocks.js';
+import { useArticles } from '../../hooks/useArticles.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 { ArticlePicker } from '../shared/ArticlePicker.js';
+import { UOM_VALUES, UOM_LABELS } from '@effigenix/api-client';
+import type { UoM, ArticleDTO, StorageLocationDTO } from '@effigenix/api-client';
+
+type Field = 'articleId' | 'storageLocationId' | 'minimumLevelAmount' | 'minimumLevelUnit' | 'minimumShelfLifeDays';
+const FIELDS: Field[] = ['articleId', 'storageLocationId', 'minimumLevelAmount', 'minimumLevelUnit', 'minimumShelfLifeDays'];
+
+const FIELD_LABELS: Record = {
+ articleId: 'Artikel *',
+ storageLocationId: 'Lagerort * (↑↓ auswählen)',
+ minimumLevelAmount: 'Mindestbestand Menge',
+ minimumLevelUnit: 'Mindestbestand Einheit (←→ wechseln)',
+ minimumShelfLifeDays: 'Mindest-MHD (Tage)',
+};
+
+export function StockCreateScreen() {
+ const { replace, back } = useNavigation();
+ const { createStock, loading, error, clearError } = useStocks();
+ const { articles, fetchArticles } = useArticles();
+ const { storageLocations, fetchStorageLocations } = useStorageLocations();
+
+ const [articleQuery, setArticleQuery] = useState('');
+ const [selectedArticle, setSelectedArticle] = useState<{ id: string; name: string } | null>(null);
+ const [locationIdx, setLocationIdx] = useState(0);
+ const [minimumLevelAmount, setMinimumLevelAmount] = useState('');
+ const [uomIdx, setUomIdx] = useState(0);
+ const [minimumShelfLifeDays, setMinimumShelfLifeDays] = useState('');
+ const [activeField, setActiveField] = useState('articleId');
+ const [fieldErrors, setFieldErrors] = useState>>({});
+
+ useEffect(() => {
+ void fetchArticles();
+ void fetchStorageLocations();
+ }, [fetchArticles, fetchStorageLocations]);
+
+ const handleArticleSelect = (article: ArticleDTO) => {
+ setSelectedArticle({ id: article.id, name: `${article.name} (${article.articleNumber})` });
+ setArticleQuery('');
+ };
+
+ const handleSubmit = async () => {
+ const errors: Partial> = {};
+ if (!selectedArticle) errors.articleId = 'Artikel muss ausgewählt werden.';
+ if (storageLocations.length === 0) errors.storageLocationId = 'Kein Lagerort verfügbar.';
+ setFieldErrors(errors);
+ if (Object.keys(errors).length > 0) return;
+
+ const location = storageLocations[locationIdx] as StorageLocationDTO;
+ const result = await createStock({
+ articleId: selectedArticle!.id,
+ storageLocationId: location.id,
+ ...(minimumLevelAmount.trim() ? { minimumLevelAmount: minimumLevelAmount.trim() } : {}),
+ ...(minimumLevelAmount.trim() ? { minimumLevelUnit: UOM_VALUES[uomIdx] as string } : {}),
+ ...(minimumShelfLifeDays.trim() ? { minimumShelfLifeDays: parseInt(minimumShelfLifeDays, 10) } : {}),
+ });
+ if (result) replace('stock-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();
+ }
+ };
+
+ useInput((_input, key) => {
+ if (loading) return;
+
+ if (activeField === 'articleId') {
+ if (key.escape) back();
+ if (key.tab || key.downArrow) setActiveField('storageLocationId');
+ if (key.return && selectedArticle && !articleQuery) setActiveField('storageLocationId');
+ return;
+ }
+
+ if (activeField === 'storageLocationId') {
+ if (key.upArrow) {
+ if (locationIdx > 0) setLocationIdx((i) => i - 1);
+ else setActiveField('articleId');
+ return;
+ }
+ if (key.downArrow) {
+ if (locationIdx < storageLocations.length - 1) setLocationIdx((i) => i + 1);
+ else setActiveField('minimumLevelAmount');
+ return;
+ }
+ if (key.return || key.tab) {
+ setActiveField('minimumLevelAmount');
+ return;
+ }
+ if (key.escape) back();
+ return;
+ }
+
+ if (activeField === 'minimumLevelUnit') {
+ if (key.leftArrow || key.rightArrow) {
+ const dir = key.rightArrow ? 1 : -1;
+ setUomIdx((i) => (i + dir + UOM_VALUES.length) % UOM_VALUES.length);
+ return;
+ }
+ if (key.return) {
+ handleFieldSubmit('minimumLevelUnit')('');
+ 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();
+ });
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ const uomLabel = UOM_LABELS[UOM_VALUES[uomIdx] as UoM] ?? UOM_VALUES[uomIdx];
+
+ return (
+
+ Neuer Bestand
+ {error && }
+
+
+ {/* Article picker */}
+
+
+ {fieldErrors.articleId && {fieldErrors.articleId}}
+
+
+ {/* Storage location selector */}
+
+ {FIELD_LABELS.storageLocationId}
+ {fieldErrors.storageLocationId && {fieldErrors.storageLocationId}}
+
+ {storageLocations.length === 0 ? (
+ Keine Lagerorte verfügbar.
+ ) : (
+ storageLocations.slice(Math.max(0, locationIdx - 3), locationIdx + 4).map((loc, i) => {
+ const actualIdx = Math.max(0, locationIdx - 3) + i;
+ const isSelected = actualIdx === locationIdx;
+ return (
+
+ {isSelected ? '▶ ' : ' '}{loc.name} ({loc.storageType})
+
+ );
+ })
+ )}
+
+
+
+ {/* Minimum level */}
+
+
+
+
+ {FIELD_LABELS.minimumLevelUnit}: {activeField === 'minimumLevelUnit' ? `< ${uomLabel} >` : uomLabel}
+
+
+
+
+
+
+
+
+ Tab/↑↓ Feld wechseln · ←→ Einheit · Enter bestätigen/speichern · Escape Abbrechen
+
+
+
+ );
+}
diff --git a/frontend/apps/cli/src/components/inventory/StockDetailScreen.tsx b/frontend/apps/cli/src/components/inventory/StockDetailScreen.tsx
new file mode 100644
index 0000000..d7f00f1
--- /dev/null
+++ b/frontend/apps/cli/src/components/inventory/StockDetailScreen.tsx
@@ -0,0 +1,315 @@
+import React, { useEffect, useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import TextInput from 'ink-text-input';
+import { useNavigation } from '../../state/navigation-context.js';
+import { useStocks } from '../../hooks/useStocks.js';
+import { LoadingSpinner } from '../shared/LoadingSpinner.js';
+import { ErrorDisplay } from '../shared/ErrorDisplay.js';
+import { SuccessDisplay } from '../shared/SuccessDisplay.js';
+import { STOCK_BATCH_STATUS_LABELS } from '@effigenix/api-client';
+import type { StockBatchStatus } from '@effigenix/api-client';
+
+type Mode = 'view' | 'menu' | 'batch-actions' | 'block-reason' | 'remove-amount' | 'remove-unit';
+
+const BATCH_STATUS_COLORS: Record = {
+ AVAILABLE: 'green',
+ EXPIRING_SOON: 'yellow',
+ EXPIRED: 'red',
+ BLOCKED: 'magenta',
+};
+
+export function StockDetailScreen() {
+ const { params, back } = useNavigation();
+ const { stock, loading, error, fetchStock, blockBatch, unblockBatch, removeBatch, clearError } = useStocks();
+ const [mode, setMode] = useState('view');
+ const [menuIndex, setMenuIndex] = useState(0);
+ const [batchIndex, setBatchIndex] = useState(0);
+ const [batchActionIndex, setBatchActionIndex] = useState(0);
+ const [blockReason, setBlockReason] = useState('');
+ const [removeAmount, setRemoveAmount] = useState('');
+ const [removeUnit, setRemoveUnit] = useState('');
+ const [success, setSuccess] = useState(null);
+
+ const stockId = params.stockId ?? '';
+
+ useEffect(() => {
+ if (stockId) void fetchStock(stockId);
+ }, [fetchStock, stockId]);
+
+ const batches = stock?.batches ?? [];
+ const selectedBatch = batches[batchIndex];
+
+ const getBatchActions = () => {
+ if (!selectedBatch) return [];
+ const actions: { label: string; action: string }[] = [];
+ if (selectedBatch.status === 'AVAILABLE' || selectedBatch.status === 'EXPIRING_SOON') {
+ actions.push({ label: 'Sperren', action: 'block' });
+ actions.push({ label: 'Entfernen', action: 'remove' });
+ }
+ if (selectedBatch.status === 'BLOCKED') {
+ actions.push({ label: 'Entsperren', action: 'unblock' });
+ }
+ if (selectedBatch.status === 'EXPIRED') {
+ actions.push({ label: 'Entfernen', action: 'remove' });
+ }
+ return actions;
+ };
+
+ const batchActions = getBatchActions();
+
+ const handleBlock = async () => {
+ if (!selectedBatch || !blockReason.trim()) return;
+ const result = await blockBatch(stockId, selectedBatch.id!, blockReason.trim());
+ if (result) {
+ setSuccess('Charge gesperrt.');
+ setMode('view');
+ setBlockReason('');
+ }
+ };
+
+ const handleUnblock = async () => {
+ if (!selectedBatch) return;
+ const result = await unblockBatch(stockId, selectedBatch.id!);
+ if (result) {
+ setSuccess('Charge entsperrt.');
+ setMode('view');
+ }
+ };
+
+ const handleRemove = async () => {
+ if (!selectedBatch || !removeAmount.trim() || !removeUnit.trim()) return;
+ const result = await removeBatch(stockId, selectedBatch.id!, removeAmount.trim(), removeUnit.trim());
+ if (result) {
+ setSuccess('Menge entfernt.');
+ setMode('view');
+ setRemoveAmount('');
+ setRemoveUnit('');
+ }
+ };
+
+ const handleBatchAction = (action: string) => {
+ switch (action) {
+ case 'block':
+ setMode('block-reason');
+ setBlockReason('');
+ break;
+ case 'unblock':
+ void handleUnblock();
+ break;
+ case 'remove':
+ setMode('remove-amount');
+ setRemoveAmount('');
+ setRemoveUnit('');
+ break;
+ }
+ };
+
+ const MENU_ITEMS = [
+ { label: 'Chargen verwalten', action: 'batches' },
+ ];
+
+ useInput((input, key) => {
+ if (loading) return;
+
+ if (mode === 'block-reason') {
+ if (key.escape) setMode('batch-actions');
+ return;
+ }
+
+ if (mode === 'remove-amount') {
+ if (key.escape) setMode('batch-actions');
+ return;
+ }
+
+ if (mode === 'remove-unit') {
+ if (key.escape) setMode('remove-amount');
+ return;
+ }
+
+ if (mode === 'batch-actions') {
+ if (key.upArrow) setBatchActionIndex((i) => Math.max(0, i - 1));
+ if (key.downArrow) setBatchActionIndex((i) => Math.min(batchActions.length - 1, i + 1));
+ if (key.return && batchActions[batchActionIndex]) {
+ handleBatchAction(batchActions[batchActionIndex].action);
+ }
+ if (key.escape) setMode('view');
+ if (key.leftArrow) setBatchIndex((i) => Math.max(0, i - 1));
+ if (key.rightArrow) setBatchIndex((i) => Math.min(batches.length - 1, i + 1));
+ return;
+ }
+
+ if (mode === 'menu') {
+ if (key.upArrow) setMenuIndex((i) => Math.max(0, i - 1));
+ if (key.downArrow) setMenuIndex((i) => Math.min(MENU_ITEMS.length - 1, i + 1));
+ if (key.return && MENU_ITEMS[menuIndex]) {
+ if (MENU_ITEMS[menuIndex].action === 'batches' && batches.length > 0) {
+ setMode('batch-actions');
+ setBatchIndex(0);
+ setBatchActionIndex(0);
+ }
+ }
+ if (key.escape) setMode('view');
+ return;
+ }
+
+ // view mode
+ if (input === 'm') {
+ setMode('menu');
+ setMenuIndex(0);
+ }
+ if (key.backspace || key.escape) back();
+ });
+
+ if (loading && !stock) return ;
+
+ if (!stock) {
+ return (
+
+ {error && }
+ Bestand nicht gefunden.
+
+ );
+ }
+
+ return (
+
+
+ Bestand
+ {loading && (aktualisiere...)}
+
+
+ {error && }
+ {success && setSuccess(null)} />}
+
+
+ Artikel: {stock.articleId}
+ Lagerort: {stock.storageLocationId}
+ Gesamtmenge: {stock.totalQuantity}{stock.quantityUnit ? ` ${stock.quantityUnit}` : ''}
+ Verfügbar: {stock.availableQuantity}{stock.quantityUnit ? ` ${stock.quantityUnit}` : ''}
+ {stock.minimumLevel && (
+ Mindestbest.: {stock.minimumLevel.amount} {stock.minimumLevel.unit}
+ )}
+ {stock.minimumShelfLifeDays != null && (
+ Min-MHD (d): {stock.minimumShelfLifeDays}
+ )}
+
+
+ {/* Batches table */}
+
+ Chargen ({batches.length})
+ {batches.length === 0 ? (
+ Keine Chargen vorhanden.
+ ) : (
+ <>
+
+ {' Batch-ID'.padEnd(18)}
+ {'Typ'.padEnd(12)}
+ {'Menge'.padEnd(12)}
+ {'MHD'.padEnd(14)}
+ Status
+
+ {batches.map((b, i) => {
+ const isSelected = mode === 'batch-actions' && i === batchIndex;
+ const textColor = isSelected ? 'cyan' : 'white';
+ const statusColor = BATCH_STATUS_COLORS[b.status ?? ''] ?? 'white';
+ const statusLabel = STOCK_BATCH_STATUS_LABELS[(b.status ?? '') as StockBatchStatus] ?? b.status;
+ return (
+
+ {isSelected ? '▶ ' : ' '}
+ {(b.batchId ?? '').substring(0, 14).padEnd(15)}
+ {(b.batchType ?? '').padEnd(12)}
+ {`${b.quantityAmount ?? ''} ${b.quantityUnit ?? ''}`.padEnd(12)}
+ {(b.expiryDate ?? '').padEnd(14)}
+ {statusLabel}
+
+ );
+ })}
+ >
+ )}
+
+
+ {/* Modes */}
+ {mode === 'menu' && (
+
+ Aktionen
+ {MENU_ITEMS.map((item, i) => (
+
+ {i === menuIndex ? '▶ ' : ' '}{item.label}
+
+ ))}
+ ↑↓ nav · Enter ausführen · Escape zurück
+
+ )}
+
+ {mode === 'batch-actions' && selectedBatch && (
+
+ Charge: {selectedBatch.batchId} — Aktionen
+ {batchActions.length === 0 ? (
+ Keine Aktionen verfügbar.
+ ) : (
+ batchActions.map((a, i) => (
+
+ {i === batchActionIndex ? '▶ ' : ' '}{a.label}
+
+ ))
+ )}
+ ←→ Charge wechseln · ↑↓ Aktion · Enter ausführen · Escape zurück
+
+ )}
+
+ {mode === 'block-reason' && (
+
+ Sperrgrund eingeben:
+
+ ›
+ void handleBlock()}
+ focus={true}
+ />
+
+ Enter bestätigen · Escape abbrechen
+
+ )}
+
+ {mode === 'remove-amount' && (
+
+ Zu entfernende Menge:
+
+ ›
+ setMode('remove-unit')}
+ focus={true}
+ />
+
+ Enter weiter · Escape abbrechen
+
+ )}
+
+ {mode === 'remove-unit' && (
+
+ Mengeneinheit (z.B. KILOGRAM):
+
+ ›
+ void handleRemove()}
+ focus={true}
+ />
+
+ Enter bestätigen · Escape zurück
+
+ )}
+
+
+
+ [m] Aktionsmenü · Backspace Zurück
+
+
+
+ );
+}
diff --git a/frontend/apps/cli/src/components/inventory/StockListScreen.tsx b/frontend/apps/cli/src/components/inventory/StockListScreen.tsx
new file mode 100644
index 0000000..d494805
--- /dev/null
+++ b/frontend/apps/cli/src/components/inventory/StockListScreen.tsx
@@ -0,0 +1,83 @@
+import React, { useEffect, useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import { useNavigation } from '../../state/navigation-context.js';
+import { useStocks } from '../../hooks/useStocks.js';
+import { LoadingSpinner } from '../shared/LoadingSpinner.js';
+import { ErrorDisplay } from '../shared/ErrorDisplay.js';
+
+export function StockListScreen() {
+ const { navigate, back } = useNavigation();
+ const { stocks, loading, error, fetchStocks, clearError } = useStocks();
+ const [selectedIndex, setSelectedIndex] = useState(0);
+
+ useEffect(() => {
+ void fetchStocks();
+ }, [fetchStocks]);
+
+ useInput((input, key) => {
+ if (loading) return;
+
+ if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
+ if (key.downArrow) setSelectedIndex((i) => Math.min(stocks.length - 1, i + 1));
+
+ if (key.return && stocks.length > 0) {
+ const stock = stocks[selectedIndex];
+ if (stock) navigate('stock-detail', { stockId: stock.id });
+ }
+ if (input === 'n') navigate('stock-create');
+ if (input === 'r') void fetchStocks();
+ if (key.backspace || key.escape) back();
+ });
+
+ return (
+
+
+ Bestände
+ ({stocks.length})
+
+
+ {loading && }
+ {error && !loading && }
+
+ {!loading && !error && (
+
+
+ {' Artikel'.padEnd(20)}
+ {'Lagerort'.padEnd(20)}
+ {'Gesamt'.padEnd(14)}
+ {'Verfügbar'.padEnd(14)}
+ Chargen
+
+ {stocks.length === 0 && (
+
+ Keine Bestände gefunden.
+
+ )}
+ {stocks.map((stock, index) => {
+ const isSelected = index === selectedIndex;
+ const textColor = isSelected ? 'cyan' : 'white';
+ const totalStr = `${stock.totalQuantity}${stock.quantityUnit ? ` ${stock.quantityUnit}` : ''}`;
+ const availStr = `${stock.availableQuantity}${stock.quantityUnit ? ` ${stock.quantityUnit}` : ''}`;
+ const batchCount = stock.batches?.length ?? 0;
+ return (
+
+ {isSelected ? '▶ ' : ' '}
+ {stock.articleId.substring(0, 16).padEnd(17)}
+ {stock.storageLocationId.substring(0, 18).padEnd(20)}
+ {totalStr.substring(0, 12).padEnd(14)}
+ {availStr.substring(0, 12).padEnd(14)}
+ {String(batchCount)}
+
+ );
+ })}
+
+ )}
+
+
+
+ ↑↓ nav · Enter Details · [n] Neu · [r] Aktualisieren · Backspace Zurück
+
+
+
+ );
+}
diff --git a/frontend/apps/cli/src/components/production/BatchDetailScreen.tsx b/frontend/apps/cli/src/components/production/BatchDetailScreen.tsx
new file mode 100644
index 0000000..a5030a2
--- /dev/null
+++ b/frontend/apps/cli/src/components/production/BatchDetailScreen.tsx
@@ -0,0 +1,232 @@
+import React, { useEffect, useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import TextInput from 'ink-text-input';
+import { useNavigation } from '../../state/navigation-context.js';
+import { useBatches } from '../../hooks/useBatches.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 { BATCH_STATUS_LABELS, UOM_LABELS } from '@effigenix/api-client';
+import type { BatchStatus, UoM } from '@effigenix/api-client';
+
+type Mode = 'view' | 'menu' | 'confirm-start' | 'confirm-cancel' | 'cancel-reason';
+
+const STATUS_COLORS: Record = {
+ PLANNED: 'yellow',
+ IN_PRODUCTION: 'blue',
+ COMPLETED: 'green',
+ CANCELLED: 'red',
+};
+
+export function BatchDetailScreen() {
+ const { params, navigate, back } = useNavigation();
+ const { batch, loading, error, fetchBatch, startBatch, cancelBatch, clearError } = useBatches();
+ const [mode, setMode] = useState('view');
+ const [menuIndex, setMenuIndex] = useState(0);
+ const [success, setSuccess] = useState(null);
+ const [cancelReason, setCancelReason] = useState('');
+
+ const batchId = params.batchId ?? '';
+
+ useEffect(() => {
+ if (batchId) void fetchBatch(batchId);
+ }, [fetchBatch, batchId]);
+
+ const getMenuItems = () => {
+ if (!batch) return [];
+ const items: { label: string; action: string }[] = [];
+ if (batch.status === 'PLANNED') {
+ items.push({ label: 'Charge starten', action: 'start' });
+ items.push({ label: 'Charge stornieren', action: 'cancel' });
+ }
+ if (batch.status === 'IN_PRODUCTION') {
+ items.push({ label: 'Verbrauch erfassen', action: 'consumption' });
+ items.push({ label: 'Charge abschließen', action: 'complete' });
+ items.push({ label: 'Charge stornieren', action: 'cancel' });
+ }
+ return items;
+ };
+
+ const menuItems = getMenuItems();
+
+ const handleMenuAction = (action: string) => {
+ switch (action) {
+ case 'start':
+ setMode('confirm-start');
+ break;
+ case 'cancel':
+ setMode('cancel-reason');
+ setCancelReason('');
+ break;
+ case 'consumption':
+ navigate('batch-record-consumption', { batchId });
+ break;
+ case 'complete':
+ navigate('batch-complete', { batchId });
+ break;
+ }
+ };
+
+ const handleStart = async () => {
+ const result = await startBatch(batchId);
+ if (result) {
+ setSuccess('Charge wurde gestartet.');
+ setMode('view');
+ }
+ };
+
+ const handleCancel = async () => {
+ if (!cancelReason.trim()) return;
+ const result = await cancelBatch(batchId, cancelReason.trim());
+ if (result) {
+ setSuccess('Charge wurde storniert.');
+ setMode('view');
+ }
+ };
+
+ useInput((input, key) => {
+ if (loading) return;
+
+ if (mode === 'cancel-reason') {
+ if (key.escape) setMode('view');
+ return;
+ }
+
+ if (mode === 'confirm-start' || mode === 'confirm-cancel') return;
+
+ if (mode === 'menu') {
+ if (key.upArrow) setMenuIndex((i) => Math.max(0, i - 1));
+ if (key.downArrow) setMenuIndex((i) => Math.min(menuItems.length - 1, i + 1));
+ if (key.return && menuItems[menuIndex]) {
+ handleMenuAction(menuItems[menuIndex].action);
+ }
+ if (key.escape) setMode('view');
+ return;
+ }
+
+ // view mode
+ if (input === 'm' && menuItems.length > 0) {
+ setMode('menu');
+ setMenuIndex(0);
+ }
+ if (key.backspace || key.escape) back();
+ });
+
+ if (loading && !batch) {
+ return ;
+ }
+
+ if (!batch) {
+ return (
+
+ {error && }
+ Charge nicht gefunden.
+
+ );
+ }
+
+ const statusLabel = BATCH_STATUS_LABELS[batch.status as BatchStatus] ?? batch.status;
+ const statusColor = STATUS_COLORS[batch.status ?? ''] ?? 'white';
+ const uomLabel = (u: string | undefined) => (u ? UOM_LABELS[u as UoM] ?? u : '');
+
+ return (
+
+
+ Charge: {batch.batchNumber}
+ [{statusLabel}]
+ {loading && (aktualisiere...)}
+
+
+ {error && }
+ {success && setSuccess(null)} />}
+
+
+ Rezept-ID: {batch.recipeId}
+ Geplante Menge: {batch.plannedQuantity} {uomLabel(batch.plannedQuantityUnit)}
+ Prod.-Datum: {batch.productionDate}
+ MHD: {batch.bestBeforeDate}
+ {batch.actualQuantity && (
+ Ist-Menge: {batch.actualQuantity} {uomLabel(batch.actualQuantityUnit)}
+ )}
+ {batch.waste && (
+ Ausschuss: {batch.waste} {uomLabel(batch.wasteUnit)}
+ )}
+ {batch.remarks && (
+ Bemerkungen: {batch.remarks}
+ )}
+ {batch.completedAt && (
+ Abgeschlossen: {new Date(batch.completedAt).toLocaleString('de-DE')}
+ )}
+ {batch.cancellationReason && (
+ Stornogrund: {batch.cancellationReason}
+ )}
+ {batch.cancelledAt && (
+ Storniert am: {new Date(batch.cancelledAt).toLocaleString('de-DE')}
+ )}
+
+
+ {/* Consumptions */}
+ {batch.consumptions && batch.consumptions.length > 0 && (
+
+ Verbrauchsmaterial ({batch.consumptions.length})
+
+ {'Artikel'.padEnd(20)}
+ {'Menge'.padEnd(16)}
+ Erfasst am
+
+ {batch.consumptions.map((c) => (
+
+ {(c.articleId ?? '').substring(0, 18).padEnd(20)}
+ {`${c.quantityUsed ?? ''} ${uomLabel(c.quantityUsedUnit)}`.padEnd(16)}
+ {c.consumedAt ? new Date(c.consumedAt).toLocaleString('de-DE') : ''}
+
+ ))}
+
+ )}
+
+ {/* Modes */}
+ {mode === 'menu' && (
+
+ Aktionen
+ {menuItems.map((item, i) => (
+
+ {i === menuIndex ? '▶ ' : ' '}{item.label}
+
+ ))}
+ ↑↓ nav · Enter ausführen · Escape zurück
+
+ )}
+
+ {mode === 'confirm-start' && (
+ void handleStart()}
+ onCancel={() => setMode('view')}
+ />
+ )}
+
+ {mode === 'cancel-reason' && (
+
+ Stornogrund eingeben:
+
+ ›
+ void handleCancel()}
+ focus={true}
+ />
+
+ Enter bestätigen · Escape abbrechen
+
+ )}
+
+
+
+ {menuItems.length > 0 ? '[m] Aktionsmenü · ' : ''}Backspace Zurück
+
+
+
+ );
+}
diff --git a/frontend/apps/cli/src/components/production/BatchListScreen.tsx b/frontend/apps/cli/src/components/production/BatchListScreen.tsx
new file mode 100644
index 0000000..921ad2f
--- /dev/null
+++ b/frontend/apps/cli/src/components/production/BatchListScreen.tsx
@@ -0,0 +1,112 @@
+import React, { useEffect, useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import { useNavigation } from '../../state/navigation-context.js';
+import { useBatches } from '../../hooks/useBatches.js';
+import { LoadingSpinner } from '../shared/LoadingSpinner.js';
+import { ErrorDisplay } from '../shared/ErrorDisplay.js';
+import { BATCH_STATUS_LABELS } from '@effigenix/api-client';
+import type { BatchStatus } from '@effigenix/api-client';
+
+const STATUS_FILTERS: { key: string; label: string; value: BatchStatus | undefined }[] = [
+ { key: 'a', label: 'ALLE', value: undefined },
+ { key: 'P', label: 'GEPLANT', value: 'PLANNED' },
+ { key: 'I', label: 'IN PRODUKTION', value: 'IN_PRODUCTION' },
+ { key: 'C', label: 'ABGESCHLOSSEN', value: 'COMPLETED' },
+ { key: 'X', label: 'STORNIERT', value: 'CANCELLED' },
+];
+
+const STATUS_COLORS: Record = {
+ PLANNED: 'yellow',
+ IN_PRODUCTION: 'blue',
+ COMPLETED: 'green',
+ CANCELLED: 'red',
+};
+
+export function BatchListScreen() {
+ const { navigate, back } = useNavigation();
+ const { batches, loading, error, fetchBatches, clearError } = useBatches();
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const [statusFilter, setStatusFilter] = useState(undefined);
+
+ useEffect(() => {
+ void fetchBatches(statusFilter);
+ }, [fetchBatches, statusFilter]);
+
+ useInput((input, key) => {
+ if (loading) return;
+
+ if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
+ if (key.downArrow) setSelectedIndex((i) => Math.min(batches.length - 1, i + 1));
+
+ if (key.return && batches.length > 0) {
+ const batch = batches[selectedIndex];
+ if (batch?.id) navigate('batch-detail', { batchId: batch.id });
+ }
+ if (input === 'n') navigate('batch-plan');
+ if (key.backspace || key.escape) back();
+
+ 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 (
+
+
+ Chargen
+ ({batches.length})
+ Filter:
+ {activeFilterLabel}
+
+
+ {loading && }
+ {error && !loading && }
+
+ {!loading && !error && (
+
+
+ {' Chargen-Nr.'.padEnd(18)}
+ {'Menge'.padEnd(14)}
+ {'Prod.-Datum'.padEnd(14)}
+ {'MHD'.padEnd(14)}
+ Status
+
+ {batches.length === 0 && (
+
+ Keine Chargen gefunden.
+
+ )}
+ {batches.map((batch, index) => {
+ const isSelected = index === selectedIndex;
+ const textColor = isSelected ? 'cyan' : 'white';
+ const statusColor = STATUS_COLORS[batch.status ?? ''] ?? 'white';
+ const statusLabel = BATCH_STATUS_LABELS[(batch.status ?? '') as BatchStatus] ?? batch.status;
+ const qty = `${batch.plannedQuantity ?? ''} ${batch.plannedQuantityUnit ?? ''}`;
+ return (
+
+ {isSelected ? '▶ ' : ' '}
+ {(batch.batchNumber ?? '').substring(0, 14).padEnd(15)}
+ {qty.substring(0, 12).padEnd(14)}
+ {(batch.productionDate ?? '').padEnd(14)}
+ {(batch.bestBeforeDate ?? '').padEnd(14)}
+ {statusLabel}
+
+ );
+ })}
+
+ )}
+
+
+
+ ↑↓ nav · Enter Details · [n] Neu · [a] Alle [P] Geplant [I] In Prod. [C] Abgeschl. [X] Storniert · Backspace Zurück
+
+
+
+ );
+}
diff --git a/frontend/apps/cli/src/components/production/BatchPlanScreen.tsx b/frontend/apps/cli/src/components/production/BatchPlanScreen.tsx
new file mode 100644
index 0000000..cdd42e6
--- /dev/null
+++ b/frontend/apps/cli/src/components/production/BatchPlanScreen.tsx
@@ -0,0 +1,191 @@
+import React, { useEffect, useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import { useNavigation } from '../../state/navigation-context.js';
+import { useBatches } from '../../hooks/useBatches.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 { UOM_VALUES, UOM_LABELS } from '@effigenix/api-client';
+import type { UoM, RecipeSummaryDTO } from '@effigenix/api-client';
+
+type Field = 'recipe' | 'quantity' | 'unit' | 'productionDate' | 'bestBeforeDate';
+const FIELDS: Field[] = ['recipe', 'quantity', 'unit', 'productionDate', 'bestBeforeDate'];
+
+const FIELD_LABELS: Record = {
+ recipe: 'Rezept (↑↓ auswählen)',
+ quantity: 'Geplante Menge *',
+ unit: 'Mengeneinheit * (←→ wechseln)',
+ productionDate: 'Produktionsdatum * (YYYY-MM-DD)',
+ bestBeforeDate: 'MHD * (YYYY-MM-DD)',
+};
+
+export function BatchPlanScreen() {
+ const { replace, back } = useNavigation();
+ const { planBatch, loading, error, clearError } = useBatches();
+ const { recipes, fetchRecipes } = useRecipes();
+
+ const [quantity, setQuantity] = useState('');
+ const [uomIdx, setUomIdx] = useState(0);
+ const [productionDate, setProductionDate] = useState('');
+ const [bestBeforeDate, setBestBeforeDate] = useState('');
+ const [recipeIdx, setRecipeIdx] = useState(0);
+ const [activeField, setActiveField] = useState('recipe');
+ const [fieldErrors, setFieldErrors] = useState>>({});
+
+ useEffect(() => {
+ void fetchRecipes('ACTIVE');
+ }, [fetchRecipes]);
+
+ const handleSubmit = async () => {
+ const errors: Partial> = {};
+ if (recipes.length === 0) errors.recipe = 'Kein aktives Rezept verfügbar.';
+ if (!quantity.trim()) errors.quantity = 'Menge ist erforderlich.';
+ if (!productionDate.trim() || !/^\d{4}-\d{2}-\d{2}$/.test(productionDate)) errors.productionDate = 'Format: YYYY-MM-DD';
+ if (!bestBeforeDate.trim() || !/^\d{4}-\d{2}-\d{2}$/.test(bestBeforeDate)) errors.bestBeforeDate = 'Format: YYYY-MM-DD';
+ setFieldErrors(errors);
+ if (Object.keys(errors).length > 0) return;
+
+ const recipe = recipes[recipeIdx] as RecipeSummaryDTO;
+ const result = await planBatch({
+ recipeId: recipe.id,
+ plannedQuantity: quantity.trim(),
+ plannedQuantityUnit: UOM_VALUES[uomIdx] as string,
+ productionDate: productionDate.trim(),
+ bestBeforeDate: bestBeforeDate.trim(),
+ });
+ if (result?.id) replace('batch-detail', { batchId: result.id });
+ };
+
+ const handleFieldSubmit = (field: Field) => (_value: string) => {
+ const idx = FIELDS.indexOf(field);
+ if (idx < FIELDS.length - 1) {
+ setActiveField(FIELDS[idx + 1] ?? field);
+ } else {
+ void handleSubmit();
+ }
+ };
+
+ useInput((_input, key) => {
+ if (loading) return;
+
+ if (activeField === 'recipe') {
+ if (key.upArrow) setRecipeIdx((i) => Math.max(0, i - 1));
+ if (key.downArrow) setRecipeIdx((i) => Math.min(recipes.length - 1, i + 1));
+ if (key.return || key.tab) setActiveField('quantity');
+ if (key.escape) back();
+ return;
+ }
+
+ if (activeField === 'unit') {
+ if (key.leftArrow || key.rightArrow) {
+ const dir = key.rightArrow ? 1 : -1;
+ setUomIdx((i) => (i + dir + UOM_VALUES.length) % UOM_VALUES.length);
+ return;
+ }
+ if (key.return) {
+ handleFieldSubmit('unit')('');
+ 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();
+ });
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ const uomLabel = UOM_LABELS[UOM_VALUES[uomIdx] as UoM] ?? UOM_VALUES[uomIdx];
+
+ return (
+
+ Neue Charge planen
+ {error && }
+
+
+ {/* Recipe selector */}
+
+ {FIELD_LABELS.recipe}
+ {fieldErrors.recipe && {fieldErrors.recipe}}
+
+ {recipes.length === 0 ? (
+ Keine aktiven Rezepte gefunden.
+ ) : (
+ recipes.slice(Math.max(0, recipeIdx - 3), recipeIdx + 4).map((r, i) => {
+ const actualIdx = Math.max(0, recipeIdx - 3) + i;
+ const isSelected = actualIdx === recipeIdx;
+ return (
+
+ {isSelected ? '▶ ' : ' '}{r.name} (v{r.version})
+
+ );
+ })
+ )}
+
+
+
+ {/* Quantity */}
+
+
+ {/* Unit */}
+
+
+ {FIELD_LABELS.unit}: {activeField === 'unit' ? `< ${uomLabel} >` : uomLabel}
+
+
+
+ {/* Production Date */}
+
+
+ {/* Best Before Date */}
+
+
+
+
+
+ Tab/↑↓ Feld wechseln · ←→ Einheit · Enter bestätigen/speichern · Escape Abbrechen
+
+
+
+ );
+}
diff --git a/frontend/apps/cli/src/components/production/CompleteBatchScreen.tsx b/frontend/apps/cli/src/components/production/CompleteBatchScreen.tsx
new file mode 100644
index 0000000..1db175d
--- /dev/null
+++ b/frontend/apps/cli/src/components/production/CompleteBatchScreen.tsx
@@ -0,0 +1,160 @@
+import React, { useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import { useNavigation } from '../../state/navigation-context.js';
+import { useBatches } from '../../hooks/useBatches.js';
+import { FormInput } from '../shared/FormInput.js';
+import { LoadingSpinner } from '../shared/LoadingSpinner.js';
+import { ErrorDisplay } from '../shared/ErrorDisplay.js';
+import { UOM_VALUES, UOM_LABELS } from '@effigenix/api-client';
+import type { UoM } from '@effigenix/api-client';
+
+type Field = 'actualQuantity' | 'actualUnit' | 'waste' | 'wasteUnit' | 'remarks';
+const FIELDS: Field[] = ['actualQuantity', 'actualUnit', 'waste', 'wasteUnit', 'remarks'];
+
+const FIELD_LABELS: Record = {
+ actualQuantity: 'Ist-Menge *',
+ actualUnit: 'Ist-Einheit * (←→ wechseln)',
+ waste: 'Ausschuss *',
+ wasteUnit: 'Ausschuss-Einheit * (←→ wechseln)',
+ remarks: 'Bemerkungen',
+};
+
+export function CompleteBatchScreen() {
+ const { params, back } = useNavigation();
+ const { completeBatch, loading, error, clearError } = useBatches();
+
+ const batchId = params.batchId ?? '';
+ const [values, setValues] = useState({ actualQuantity: '', waste: '', remarks: '' });
+ const [actualUomIdx, setActualUomIdx] = useState(0);
+ const [wasteUomIdx, setWasteUomIdx] = useState(0);
+ const [activeField, setActiveField] = useState('actualQuantity');
+ const [fieldErrors, setFieldErrors] = useState>>({});
+ const [success, setSuccess] = useState(false);
+
+ const setField = (field: keyof typeof values) => (value: string) => {
+ setValues((v) => ({ ...v, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ const errors: Partial> = {};
+ if (!values.actualQuantity.trim()) errors.actualQuantity = 'Ist-Menge erforderlich.';
+ if (!values.waste.trim()) errors.waste = 'Ausschuss erforderlich (0 wenn kein Ausschuss).';
+ setFieldErrors(errors);
+ if (Object.keys(errors).length > 0) return;
+
+ const result = await completeBatch(batchId, {
+ actualQuantity: values.actualQuantity.trim(),
+ actualQuantityUnit: UOM_VALUES[actualUomIdx] as string,
+ waste: values.waste.trim(),
+ wasteUnit: UOM_VALUES[wasteUomIdx] as string,
+ ...(values.remarks.trim() ? { remarks: values.remarks.trim() } : {}),
+ });
+ if (result) {
+ setSuccess(true);
+ setTimeout(() => back(), 1500);
+ }
+ };
+
+ const handleFieldSubmit = (field: Field) => (_value: string) => {
+ const idx = FIELDS.indexOf(field);
+ if (idx < FIELDS.length - 1) {
+ setActiveField(FIELDS[idx + 1] ?? field);
+ } else {
+ void handleSubmit();
+ }
+ };
+
+ useInput((_input, key) => {
+ if (loading || success) return;
+
+ if (activeField === 'actualUnit' || activeField === 'wasteUnit') {
+ const setIdx = activeField === 'actualUnit' ? setActualUomIdx : setWasteUomIdx;
+ if (key.leftArrow || key.rightArrow) {
+ const dir = key.rightArrow ? 1 : -1;
+ setIdx((i) => (i + dir + UOM_VALUES.length) % UOM_VALUES.length);
+ return;
+ }
+ if (key.return) {
+ handleFieldSubmit(activeField)('');
+ 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();
+ });
+
+ if (loading) return ;
+
+ if (success) {
+ return (
+
+ Charge erfolgreich abgeschlossen.
+
+ );
+ }
+
+ const actualUomLabel = UOM_LABELS[UOM_VALUES[actualUomIdx] as UoM] ?? UOM_VALUES[actualUomIdx];
+ const wasteUomLabel = UOM_LABELS[UOM_VALUES[wasteUomIdx] as UoM] ?? UOM_VALUES[wasteUomIdx];
+
+ return (
+
+ Charge abschließen
+ Charge: {batchId}
+ {error && }
+
+
+
+
+
+ {FIELD_LABELS.actualUnit}: {activeField === 'actualUnit' ? `< ${actualUomLabel} >` : actualUomLabel}
+
+
+
+
+
+ {FIELD_LABELS.wasteUnit}: {activeField === 'wasteUnit' ? `< ${wasteUomLabel} >` : wasteUomLabel}
+
+
+
+
+
+
+
+ Tab/↑↓ Feld wechseln · ←→ Einheit · Enter bestätigen/speichern · Escape Abbrechen
+
+
+
+ );
+}
diff --git a/frontend/apps/cli/src/components/production/ProductionMenu.tsx b/frontend/apps/cli/src/components/production/ProductionMenu.tsx
index ca6436c..07fceee 100644
--- a/frontend/apps/cli/src/components/production/ProductionMenu.tsx
+++ b/frontend/apps/cli/src/components/production/ProductionMenu.tsx
@@ -11,6 +11,7 @@ interface MenuItem {
const MENU_ITEMS: MenuItem[] = [
{ label: 'Rezepte', screen: 'recipe-list', description: 'Rezepte anlegen und verwalten' },
+ { label: 'Chargen', screen: 'batch-list', description: 'Produktionschargen planen, starten und abschließen' },
];
export function ProductionMenu() {
diff --git a/frontend/apps/cli/src/components/production/RecordConsumptionScreen.tsx b/frontend/apps/cli/src/components/production/RecordConsumptionScreen.tsx
new file mode 100644
index 0000000..7371e58
--- /dev/null
+++ b/frontend/apps/cli/src/components/production/RecordConsumptionScreen.tsx
@@ -0,0 +1,152 @@
+import React, { useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import { useNavigation } from '../../state/navigation-context.js';
+import { useBatches } from '../../hooks/useBatches.js';
+import { FormInput } from '../shared/FormInput.js';
+import { LoadingSpinner } from '../shared/LoadingSpinner.js';
+import { ErrorDisplay } from '../shared/ErrorDisplay.js';
+import { UOM_VALUES, UOM_LABELS } from '@effigenix/api-client';
+import type { UoM } from '@effigenix/api-client';
+
+type Field = 'inputBatchId' | 'articleId' | 'quantityUsed' | 'quantityUnit';
+const FIELDS: Field[] = ['inputBatchId', 'articleId', 'quantityUsed', 'quantityUnit'];
+
+const FIELD_LABELS: Record = {
+ inputBatchId: 'Input-Chargen-ID *',
+ articleId: 'Artikel-ID *',
+ quantityUsed: 'Verbrauchte Menge *',
+ quantityUnit: 'Mengeneinheit * (←→ wechseln)',
+};
+
+export function RecordConsumptionScreen() {
+ const { params, back } = useNavigation();
+ const { recordConsumption, loading, error, clearError } = useBatches();
+
+ const batchId = params.batchId ?? '';
+ const [values, setValues] = useState({ inputBatchId: '', articleId: '', quantityUsed: '' });
+ const [uomIdx, setUomIdx] = useState(0);
+ const [activeField, setActiveField] = useState('inputBatchId');
+ const [fieldErrors, setFieldErrors] = useState>>({});
+ const [success, setSuccess] = useState(false);
+
+ const setField = (field: keyof typeof values) => (value: string) => {
+ setValues((v) => ({ ...v, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ const errors: Partial> = {};
+ if (!values.inputBatchId.trim()) errors.inputBatchId = 'Chargen-ID erforderlich.';
+ if (!values.articleId.trim()) errors.articleId = 'Artikel-ID erforderlich.';
+ if (!values.quantityUsed.trim()) errors.quantityUsed = 'Menge erforderlich.';
+ setFieldErrors(errors);
+ if (Object.keys(errors).length > 0) return;
+
+ const result = await recordConsumption(batchId, {
+ inputBatchId: values.inputBatchId.trim(),
+ articleId: values.articleId.trim(),
+ quantityUsed: values.quantityUsed.trim(),
+ quantityUnit: UOM_VALUES[uomIdx] as string,
+ });
+ if (result) {
+ setSuccess(true);
+ setTimeout(() => back(), 1500);
+ }
+ };
+
+ const handleFieldSubmit = (field: Field) => (_value: string) => {
+ const idx = FIELDS.indexOf(field);
+ if (idx < FIELDS.length - 1) {
+ setActiveField(FIELDS[idx + 1] ?? field);
+ } else {
+ void handleSubmit();
+ }
+ };
+
+ useInput((_input, key) => {
+ if (loading || success) return;
+
+ if (activeField === 'quantityUnit') {
+ if (key.leftArrow || key.rightArrow) {
+ const dir = key.rightArrow ? 1 : -1;
+ setUomIdx((i) => (i + dir + UOM_VALUES.length) % UOM_VALUES.length);
+ return;
+ }
+ if (key.return) {
+ void handleSubmit();
+ 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();
+ });
+
+ if (loading) return ;
+
+ if (success) {
+ return (
+
+ Verbrauch erfolgreich erfasst.
+
+ );
+ }
+
+ const uomLabel = UOM_LABELS[UOM_VALUES[uomIdx] as UoM] ?? UOM_VALUES[uomIdx];
+
+ return (
+
+ Verbrauch erfassen
+ Charge: {batchId}
+ {error && }
+
+
+
+
+
+
+
+ {FIELD_LABELS.quantityUnit}: {activeField === 'quantityUnit' ? `< ${uomLabel} >` : uomLabel}
+
+
+
+
+
+
+ Tab/↑↓ Feld wechseln · ←→ Einheit · Enter bestätigen/speichern · Escape Abbrechen
+
+
+
+ );
+}
diff --git a/frontend/apps/cli/src/hooks/useBatches.ts b/frontend/apps/cli/src/hooks/useBatches.ts
new file mode 100644
index 0000000..2536e04
--- /dev/null
+++ b/frontend/apps/cli/src/hooks/useBatches.ts
@@ -0,0 +1,120 @@
+import { useState, useCallback } from 'react';
+import type { BatchSummaryDTO, BatchDTO, PlanBatchRequest, BatchStatus } from '@effigenix/api-client';
+import { client } from '../utils/api-client.js';
+
+interface BatchesState {
+ batches: BatchSummaryDTO[];
+ batch: BatchDTO | null;
+ loading: boolean;
+ error: string | null;
+}
+
+function errorMessage(err: unknown): string {
+ return err instanceof Error ? err.message : 'Unbekannter Fehler';
+}
+
+export function useBatches() {
+ const [state, setState] = useState({
+ batches: [],
+ batch: null,
+ loading: false,
+ error: null,
+ });
+
+ const fetchBatches = useCallback(async (status?: BatchStatus) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ const batches = await client.batches.list(status);
+ setState((s) => ({ ...s, batches, loading: false, error: null }));
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ }
+ }, []);
+
+ const fetchBatch = useCallback(async (id: string) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ const batch = await client.batches.getById(id);
+ setState((s) => ({ ...s, batch, loading: false, error: null }));
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ }
+ }, []);
+
+ const planBatch = useCallback(async (request: PlanBatchRequest) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ const batch = await client.batches.plan(request);
+ setState((s) => ({ ...s, loading: false, error: null }));
+ return batch;
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ return null;
+ }
+ }, []);
+
+ const startBatch = useCallback(async (id: string) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ const batch = await client.batches.start(id);
+ setState((s) => ({ ...s, batch, loading: false, error: null }));
+ return batch;
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ return null;
+ }
+ }, []);
+
+ const recordConsumption = useCallback(async (id: string, request: Parameters[1]) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ await client.batches.recordConsumption(id, request);
+ const batch = await client.batches.getById(id);
+ setState((s) => ({ ...s, batch, loading: false, error: null }));
+ return true;
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ return false;
+ }
+ }, []);
+
+ const completeBatch = useCallback(async (id: string, request: Parameters[1]) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ const batch = await client.batches.complete(id, request);
+ setState((s) => ({ ...s, batch, loading: false, error: null }));
+ return batch;
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ return null;
+ }
+ }, []);
+
+ const cancelBatch = useCallback(async (id: string, reason: string) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ const batch = await client.batches.cancel(id, { reason });
+ setState((s) => ({ ...s, batch, loading: false, error: null }));
+ return batch;
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ return null;
+ }
+ }, []);
+
+ const clearError = useCallback(() => {
+ setState((s) => ({ ...s, error: null }));
+ }, []);
+
+ return {
+ ...state,
+ fetchBatches,
+ fetchBatch,
+ planBatch,
+ startBatch,
+ recordConsumption,
+ completeBatch,
+ cancelBatch,
+ clearError,
+ };
+}
diff --git a/frontend/apps/cli/src/hooks/useStocks.ts b/frontend/apps/cli/src/hooks/useStocks.ts
index 73d81b1..2246c10 100644
--- a/frontend/apps/cli/src/hooks/useStocks.ts
+++ b/frontend/apps/cli/src/hooks/useStocks.ts
@@ -1,8 +1,17 @@
import { useState, useCallback } from 'react';
-import type { StockBatchDTO, AddStockBatchRequest } from '@effigenix/api-client';
+import type {
+ StockDTO,
+ StockBatchDTO,
+ AddStockBatchRequest,
+ CreateStockRequest,
+ UpdateStockRequest,
+ StockFilter,
+} from '@effigenix/api-client';
import { client } from '../utils/api-client.js';
interface StocksState {
+ stocks: StockDTO[];
+ stock: StockDTO | null;
loading: boolean;
error: string | null;
}
@@ -13,29 +22,121 @@ function errorMessage(err: unknown): string {
export function useStocks() {
const [state, setState] = useState({
+ stocks: [],
+ stock: null,
loading: false,
error: null,
});
+ const fetchStocks = useCallback(async (filter?: StockFilter) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ const stocks = await client.stocks.list(filter);
+ setState((s) => ({ ...s, stocks, loading: false, error: null }));
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ }
+ }, []);
+
+ const fetchStock = useCallback(async (id: string) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ const stock = await client.stocks.getById(id);
+ setState((s) => ({ ...s, stock, loading: false, error: null }));
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ }
+ }, []);
+
+ const createStock = useCallback(async (request: CreateStockRequest) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ const result = await client.stocks.create(request);
+ setState((s) => ({ ...s, loading: false, error: null }));
+ return result;
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ return null;
+ }
+ }, []);
+
+ const updateStock = useCallback(async (id: string, request: UpdateStockRequest) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ const stock = await client.stocks.update(id, request);
+ setState((s) => ({ ...s, stock, loading: false, error: null }));
+ return stock;
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ return null;
+ }
+ }, []);
+
const addBatch = useCallback(async (stockId: string, request: AddStockBatchRequest): Promise => {
- setState({ loading: true, error: null });
+ setState((s) => ({ ...s, loading: true, error: null }));
try {
const batch = await client.stocks.addBatch(stockId, request);
- setState({ loading: false, error: null });
+ setState((s) => ({ ...s, loading: false, error: null }));
return batch;
} catch (err) {
- setState({ loading: false, error: errorMessage(err) });
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
return null;
}
}, []);
+ const removeBatch = useCallback(async (stockId: string, batchId: string, quantityAmount: string, quantityUnit: string) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ await client.stocks.removeBatch(stockId, batchId, { quantityAmount, quantityUnit });
+ const stock = await client.stocks.getById(stockId);
+ setState((s) => ({ ...s, stock, loading: false, error: null }));
+ return true;
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ return false;
+ }
+ }, []);
+
+ const blockBatch = useCallback(async (stockId: string, batchId: string, reason: string) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ await client.stocks.blockBatch(stockId, batchId, { reason });
+ const stock = await client.stocks.getById(stockId);
+ setState((s) => ({ ...s, stock, loading: false, error: null }));
+ return true;
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ return false;
+ }
+ }, []);
+
+ const unblockBatch = useCallback(async (stockId: string, batchId: string) => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ try {
+ await client.stocks.unblockBatch(stockId, batchId);
+ const stock = await client.stocks.getById(stockId);
+ setState((s) => ({ ...s, stock, loading: false, error: null }));
+ return true;
+ } catch (err) {
+ setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
+ return false;
+ }
+ }, []);
+
const clearError = useCallback(() => {
setState((s) => ({ ...s, error: null }));
}, []);
return {
...state,
+ fetchStocks,
+ fetchStock,
+ createStock,
+ updateStock,
addBatch,
+ removeBatch,
+ blockBatch,
+ unblockBatch,
clearError,
};
}
diff --git a/frontend/apps/cli/src/state/navigation-context.tsx b/frontend/apps/cli/src/state/navigation-context.tsx
index d455619..5bac906 100644
--- a/frontend/apps/cli/src/state/navigation-context.tsx
+++ b/frontend/apps/cli/src/state/navigation-context.tsx
@@ -35,13 +35,21 @@ export type Screen =
| 'storage-location-detail'
| 'stock-batch-entry'
| 'stock-add-batch'
+ | 'stock-list'
+ | 'stock-detail'
+ | 'stock-create'
// Produktion
| 'production-menu'
| 'recipe-list'
| 'recipe-create'
| 'recipe-detail'
| 'recipe-add-production-step'
- | 'recipe-add-ingredient';
+ | 'recipe-add-ingredient'
+ | 'batch-list'
+ | 'batch-detail'
+ | 'batch-plan'
+ | 'batch-record-consumption'
+ | 'batch-complete';
interface NavigationState {
current: Screen;
diff --git a/frontend/openapi.json b/frontend/openapi.json
index bf768b9..cd4936f 100644
--- a/frontend/openapi.json
+++ b/frontend/openapi.json
@@ -1 +1 @@
-{"openapi":"3.0.1","info":{"title":"Effigenix Fleischerei ERP API","description":"RESTful API for Effigenix Fleischerei ERP System.\n\n## Authentication\n\nAll endpoints (except /api/auth/login and /api/auth/refresh) require JWT authentication.\n\n1. Login via POST /api/auth/login with username and password\n2. Copy the returned access token\n3. Click \"Authorize\" button (top right)\n4. Enter: Bearer \n5. Click \"Authorize\"\n\n## User Management\n\n- **Authentication**: Login, logout, refresh token\n- **User Management**: Create, update, list users (ADMIN only)\n- **Role Management**: Assign roles, lock/unlock users (ADMIN only)\n- **Password Management**: Change password (requires current password)\n\n## Error Handling\n\nAll errors return a consistent error response format:\n\n```json\n{\n \"code\": \"USER_NOT_FOUND\",\n \"message\": \"User with ID 'user-123' not found\",\n \"status\": 404,\n \"timestamp\": \"2026-02-17T12:00:00\",\n \"path\": \"/api/users/user-123\",\n \"validationErrors\": null\n}\n```\n\n## Architecture\n\nBuilt with:\n- Domain-Driven Design (DDD)\n- Clean Architecture (Hexagonal Architecture)\n- Spring Boot 3.2\n- Java 21\n- PostgreSQL\n","contact":{"name":"Effigenix Development Team","url":"https://effigenix.com","email":"dev@effigenix.com"},"license":{"name":"Proprietary","url":"https://effigenix.com/license"},"version":"0.1.0"},"servers":[{"url":"http://localhost:8080","description":"Local Development Server"},{"url":"https://api.effigenix.com","description":"Production Server"}],"tags":[{"name":"Storage Locations","description":"Storage location management endpoints"},{"name":"Product Categories","description":"Product category management endpoints"},{"name":"User Management","description":"User management endpoints (requires authentication)"},{"name":"Articles","description":"Article management endpoints"},{"name":"Recipes","description":"Recipe management endpoints"},{"name":"Role Management","description":"Role management endpoints (ADMIN only)"},{"name":"Batches","description":"Production batch management endpoints"},{"name":"Stocks","description":"Stock management endpoints"},{"name":"Customers","description":"Customer management endpoints"},{"name":"Suppliers","description":"Supplier management endpoints"},{"name":"Authentication","description":"Authentication and session management endpoints"}],"paths":{"/api/users/{id}":{"get":{"tags":["User Management"],"summary":"Get user by ID","description":"Retrieve a single user by their ID.","operationId":"getUserById","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["User Management"],"summary":"Update user","description":"Update user details (email, branchId).","operationId":"updateUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"409":{"description":"Email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User updated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/password":{"put":{"tags":["User Management"],"summary":"Change password","description":"Change user password. Requires current password for verification.","operationId":"changePassword","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"400":{"description":"Invalid password"},"401":{"description":"Invalid current password"},"404":{"description":"User not found"},"204":{"description":"Password changed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}":{"get":{"tags":["Suppliers"],"operationId":"getSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Suppliers"],"operationId":"updateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}":{"put":{"tags":["Storage Locations"],"operationId":"updateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStorageLocationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}":{"get":{"tags":["Customers"],"operationId":"getCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Customers"],"operationId":"updateCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/preferences":{"put":{"tags":["Customers"],"operationId":"setPreferences","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPreferencesRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/frame-contract":{"put":{"tags":["Customers"],"operationId":"setFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetFrameContractRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Customers"],"operationId":"removeFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/categories/{id}":{"put":{"tags":["Product Categories"],"operationId":"updateCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Product Categories"],"operationId":"deleteCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}":{"get":{"tags":["Articles"],"operationId":"getArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Articles"],"operationId":"updateArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}/price":{"put":{"tags":["Articles"],"operationId":"updateSalesUnitPrice","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSalesUnitPriceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users":{"get":{"tags":["User Management"],"summary":"List all users","description":"Get a list of all users in the system.","operationId":"listUsers","responses":{"200":{"description":"Users retrieved successfully","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["User Management"],"summary":"Create user (ADMIN only)","description":"Create a new user account with specified roles.","operationId":"createUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"400":{"description":"Validation error or invalid password","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"201":{"description":"User created successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Username or email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/unlock":{"post":{"tags":["User Management"],"summary":"Unlock user (ADMIN only)","description":"Unlock a user account (allows login).","operationId":"unlockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User unlocked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Invalid status transition","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles":{"post":{"tags":["User Management"],"summary":"Assign role (ADMIN only)","description":"Assign a role to a user.","operationId":"assignRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User or role not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"Role assigned successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/lock":{"post":{"tags":["User Management"],"summary":"Lock user (ADMIN only)","description":"Lock a user account (prevents login).","operationId":"lockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User locked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Invalid status transition","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers":{"get":{"tags":["Suppliers"],"operationId":"listSuppliers","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SupplierResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Suppliers"],"operationId":"createSupplier","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/rating":{"post":{"tags":["Suppliers"],"operationId":"rateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/deactivate":{"post":{"tags":["Suppliers"],"operationId":"deactivate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/certificates":{"post":{"tags":["Suppliers"],"operationId":"addCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Suppliers"],"operationId":"removeCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/activate":{"post":{"tags":["Suppliers"],"operationId":"activate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes":{"get":{"tags":["Recipes"],"operationId":"listRecipes","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RecipeSummaryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Recipes"],"operationId":"createRecipe","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRecipeRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/steps":{"post":{"tags":["Recipes"],"operationId":"addProductionStep","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddProductionStepRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/ingredients":{"post":{"tags":["Recipes"],"operationId":"addIngredient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRecipeIngredientRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/archive":{"post":{"tags":["Recipes"],"operationId":"archiveRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/activate":{"post":{"tags":["Recipes"],"operationId":"activateRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches":{"post":{"tags":["Batches"],"operationId":"planBatch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations":{"get":{"tags":["Storage Locations"],"operationId":"listStorageLocations","parameters":[{"name":"storageType","in":"query","required":false,"schema":{"type":"string"}},{"name":"active","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Storage Locations"],"operationId":"createStorageLocation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStorageLocationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks":{"post":{"tags":["Stocks"],"operationId":"createStock","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStockRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches":{"post":{"tags":["Stocks"],"operationId":"addBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockBatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/unblock":{"post":{"tags":["Stocks"],"operationId":"unblockBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/remove":{"post":{"tags":["Stocks"],"operationId":"removeBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/block":{"post":{"tags":["Stocks"],"operationId":"blockBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/customers":{"get":{"tags":["Customers"],"operationId":"listCustomers","parameters":[{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["B2C","B2B"]}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CustomerResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Customers"],"operationId":"createCustomer","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses":{"post":{"tags":["Customers"],"operationId":"addDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddDeliveryAddressRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/deactivate":{"post":{"tags":["Customers"],"operationId":"deactivate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/activate":{"post":{"tags":["Customers"],"operationId":"activate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/categories":{"get":{"tags":["Product Categories"],"operationId":"listCategories","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Product Categories"],"operationId":"createCategory","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Refresh access token","description":"Refresh an expired access token using a valid refresh token.","operationId":"refresh","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"401":{"description":"Invalid or expired refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"200":{"description":"Token refresh successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/auth/logout":{"post":{"tags":["Authentication"],"summary":"User logout","description":"Invalidate current JWT token.","operationId":"logout","responses":{"401":{"description":"Invalid or missing authentication token"},"204":{"description":"Logout successful"}}}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"User login","description":"Authenticate user with username and password. Returns JWT tokens.","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"429":{"description":"Too many login attempts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid credentials, user locked, or user inactive","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"200":{"description":"Login successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/articles":{"get":{"tags":["Articles"],"operationId":"listArticles","parameters":[{"name":"categoryId","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ArticleResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Articles"],"operationId":"createArticle","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers":{"post":{"tags":["Articles"],"operationId":"assignSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units":{"post":{"tags":["Articles"],"operationId":"addSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSalesUnitRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/deactivate":{"post":{"tags":["Articles"],"operationId":"deactivate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/activate":{"post":{"tags":["Articles"],"operationId":"activate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}/deactivate":{"patch":{"tags":["Storage Locations"],"operationId":"deactivateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}/activate":{"patch":{"tags":["Storage Locations"],"operationId":"activateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/roles":{"get":{"tags":["Role Management"],"summary":"List all roles (ADMIN only)","description":"Get a list of all available roles in the system. Requires USER_MANAGEMENT permission.","operationId":"listRoles","responses":{"200":{"description":"Roles retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RoleDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}":{"get":{"tags":["Recipes"],"operationId":"getRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles/{roleName}":{"delete":{"tags":["User Management"],"summary":"Remove role (ADMIN only)","description":"Remove a role from a user.","operationId":"removeRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}},{"name":"roleName","in":"path","description":"Role name","required":true,"schema":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}}],"responses":{"403":{"description":"Missing permission"},"404":{"description":"User or role not found"},"204":{"description":"Role removed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/steps/{stepNumber}":{"delete":{"tags":["Recipes"],"operationId":"removeProductionStep","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"stepNumber","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/ingredients/{ingredientId}":{"delete":{"tags":["Recipes"],"operationId":"removeIngredient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"ingredientId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses/{label}":{"delete":{"tags":["Customers"],"operationId":"removeDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"label","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers/{supplierId}":{"delete":{"tags":["Articles"],"operationId":"removeSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"supplierId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}":{"delete":{"tags":["Articles"],"operationId":"removeSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}}},"components":{"schemas":{"UpdateUserRequest":{"type":"object","properties":{"email":{"type":"string","description":"New email address","example":"newemail@example.com"},"branchId":{"type":"string","description":"New branch ID","example":"BRANCH-002"}},"description":"Request to update user details"},"RoleDTO":{"required":["description","id","name","permissions"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]},"permissions":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["RECIPE_READ","RECIPE_WRITE","RECIPE_DELETE","BATCH_READ","BATCH_WRITE","BATCH_COMPLETE","BATCH_DELETE","PRODUCTION_ORDER_READ","PRODUCTION_ORDER_WRITE","PRODUCTION_ORDER_DELETE","HACCP_READ","HACCP_WRITE","TEMPERATURE_LOG_READ","TEMPERATURE_LOG_WRITE","CLEANING_RECORD_READ","CLEANING_RECORD_WRITE","GOODS_INSPECTION_READ","GOODS_INSPECTION_WRITE","STOCK_READ","STOCK_WRITE","STOCK_MOVEMENT_READ","STOCK_MOVEMENT_WRITE","INVENTORY_COUNT_READ","INVENTORY_COUNT_WRITE","PURCHASE_ORDER_READ","PURCHASE_ORDER_WRITE","PURCHASE_ORDER_DELETE","GOODS_RECEIPT_READ","GOODS_RECEIPT_WRITE","SUPPLIER_READ","SUPPLIER_WRITE","SUPPLIER_DELETE","ORDER_READ","ORDER_WRITE","ORDER_DELETE","INVOICE_READ","INVOICE_WRITE","INVOICE_DELETE","CUSTOMER_READ","CUSTOMER_WRITE","CUSTOMER_DELETE","LABEL_READ","LABEL_WRITE","LABEL_PRINT","MASTERDATA_READ","MASTERDATA_WRITE","BRANCH_READ","BRANCH_WRITE","BRANCH_DELETE","USER_READ","USER_WRITE","USER_DELETE","USER_LOCK","USER_UNLOCK","ROLE_READ","ROLE_WRITE","ROLE_ASSIGN","ROLE_REMOVE","REPORT_READ","REPORT_GENERATE","NOTIFICATION_READ","NOTIFICATION_SEND","AUDIT_LOG_READ","SYSTEM_SETTINGS_READ","SYSTEM_SETTINGS_WRITE"]}},"description":{"type":"string"}}},"UserDTO":{"required":["createdAt","email","id","roles","status","username"],"type":"object","properties":{"id":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"roles":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}},"branchId":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","LOCKED"]},"createdAt":{"type":"string","format":"date-time"},"lastLogin":{"type":"string","format":"date-time"}}},"ChangePasswordRequest":{"required":["currentPassword","newPassword"],"type":"object","properties":{"currentPassword":{"type":"string","description":"Current password","example":"OldPass123"},"newPassword":{"maxLength":2147483647,"minLength":8,"type":"string","description":"New password (min 8 characters)","example":"NewSecurePass456"}},"description":"Request to change user password"},"UpdateSupplierRequest":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddressResponse":{"required":["city","country","houseNumber","postalCode","street"],"type":"object","properties":{"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"}},"nullable":true},"ContactInfoResponse":{"required":["contactPerson","email","phone"],"type":"object","properties":{"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"}}},"PaymentTermsResponse":{"required":["paymentDescription","paymentDueDays"],"type":"object","properties":{"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}},"nullable":true},"QualityCertificateResponse":{"required":["certificateType","issuer","validFrom","validUntil"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"SupplierRatingResponse":{"required":["deliveryScore","priceScore","qualityScore"],"type":"object","properties":{"qualityScore":{"type":"integer","format":"int32"},"deliveryScore":{"type":"integer","format":"int32"},"priceScore":{"type":"integer","format":"int32"}},"nullable":true},"SupplierResponse":{"required":["certificates","contactInfo","createdAt","id","name","status","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"certificates":{"type":"array","items":{"$ref":"#/components/schemas/QualityCertificateResponse"}},"rating":{"$ref":"#/components/schemas/SupplierRatingResponse"},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"UpdateStorageLocationRequest":{"type":"object","properties":{"name":{"type":"string"},"minTemperature":{"type":"string"},"maxTemperature":{"type":"string"}}},"StorageLocationResponse":{"required":["active","id","name","storageType"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"storageType":{"type":"string"},"temperatureRange":{"$ref":"#/components/schemas/TemperatureRangeResponse"},"active":{"type":"boolean"}}},"TemperatureRangeResponse":{"required":["maxTemperature","minTemperature"],"type":"object","properties":{"minTemperature":{"type":"number"},"maxTemperature":{"type":"number"}},"nullable":true},"UpdateCustomerRequest":{"type":"object","properties":{"name":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"ContractLineItemResponse":{"required":["agreedPrice","agreedQuantity","articleId","unit"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string"}}},"CustomerResponse":{"required":["billingAddress","contactInfo","createdAt","deliveryAddresses","id","name","preferences","status","type","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"billingAddress":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"deliveryAddresses":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryAddressResponse"}},"frameContract":{"$ref":"#/components/schemas/FrameContractResponse"},"preferences":{"type":"array","items":{"type":"string"}},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"DeliveryAddressResponse":{"required":["address","contactPerson","deliveryNotes","label"],"type":"object","properties":{"label":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"FrameContractResponse":{"required":["deliveryRhythm","id","lineItems","validFrom","validUntil"],"type":"object","properties":{"id":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"deliveryRhythm":{"type":"string"},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/ContractLineItemResponse"}}},"nullable":true},"SetPreferencesRequest":{"required":["preferences"],"type":"object","properties":{"preferences":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["BIO","REGIONAL","TIERWOHL","HALAL","KOSHER","GLUTENFREI","LAKTOSEFREI"]}}}},"LineItem":{"required":["agreedPrice","articleId"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]}}},"SetFrameContractRequest":{"required":["lineItems","rhythm"],"type":"object","properties":{"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"rhythm":{"type":"string","enum":["DAILY","WEEKLY","BIWEEKLY","MONTHLY","ON_DEMAND"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/LineItem"}}}},"UpdateProductCategoryRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ProductCategoryResponse":{"required":["description","id","name"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}},"UpdateArticleRequest":{"type":"object","properties":{"name":{"type":"string"},"categoryId":{"type":"string"}}},"ArticleResponse":{"required":["articleNumber","categoryId","createdAt","id","name","salesUnits","status","supplierIds","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"salesUnits":{"type":"array","items":{"$ref":"#/components/schemas/SalesUnitResponse"}},"status":{"type":"string"},"supplierIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SalesUnitResponse":{"required":["id","price","priceModel","unit"],"type":"object","properties":{"id":{"type":"string"},"unit":{"type":"string"},"priceModel":{"type":"string"},"price":{"type":"number"}}},"UpdateSalesUnitPriceRequest":{"required":["price"],"type":"object","properties":{"price":{"type":"number"}}},"CreateUserRequest":{"required":["email","password","roleNames","username"],"type":"object","properties":{"username":{"maxLength":50,"minLength":3,"type":"string","description":"Username (unique)","example":"john.doe"},"email":{"type":"string","description":"Email address (unique)","example":"john.doe@example.com"},"password":{"maxLength":2147483647,"minLength":8,"type":"string","description":"Password (min 8 characters)","example":"SecurePass123"},"roleNames":{"uniqueItems":true,"type":"array","description":"Role names to assign","example":["USER","MANAGER"],"items":{"type":"string","description":"Role names to assign","example":"[\"USER\",\"MANAGER\"]","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"branchId":{"type":"string","description":"Branch ID (optional)","example":"BRANCH-001"}},"description":"Request to create a new user"},"AssignRoleRequest":{"required":["roleName"],"type":"object","properties":{"roleName":{"type":"string","description":"Role name to assign","example":"MANAGER","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"description":"Request to assign a role to a user"},"CreateSupplierRequest":{"required":["name","phone"],"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"RateSupplierRequest":{"type":"object","properties":{"qualityScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"deliveryScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"priceScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"}}},"AddCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"CreateRecipeRequest":{"required":["articleId","name","outputQuantity","outputUom","type"],"type":"object","properties":{"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string","enum":["RAW_MATERIAL","INTERMEDIATE","FINISHED_PRODUCT"]},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32"},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"}}},"IngredientResponse":{"required":["articleId","id","position","quantity","substitutable","uom"],"type":"object","properties":{"id":{"type":"string"},"position":{"type":"integer","format":"int32"},"articleId":{"type":"string"},"quantity":{"type":"string"},"uom":{"type":"string"},"subRecipeId":{"type":"string","nullable":true},"substitutable":{"type":"boolean"}}},"ProductionStepResponse":{"required":["description","id","stepNumber"],"type":"object","properties":{"id":{"type":"string"},"stepNumber":{"type":"integer","format":"int32"},"description":{"type":"string"},"durationMinutes":{"type":"integer","format":"int32","nullable":true},"temperatureCelsius":{"type":"integer","format":"int32","nullable":true}}},"RecipeResponse":{"required":["articleId","createdAt","description","id","ingredients","name","outputQuantity","outputUom","productionSteps","status","type","updatedAt","version","yieldPercentage"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string"},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32","nullable":true},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"},"status":{"type":"string"},"ingredients":{"type":"array","items":{"$ref":"#/components/schemas/IngredientResponse"}},"productionSteps":{"type":"array","items":{"$ref":"#/components/schemas/ProductionStepResponse"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"AddProductionStepRequest":{"required":["description"],"type":"object","properties":{"stepNumber":{"minimum":1,"type":"integer","format":"int32"},"description":{"maxLength":500,"minLength":0,"type":"string"},"durationMinutes":{"minimum":1,"type":"integer","format":"int32"},"temperatureCelsius":{"maximum":1000,"minimum":-273,"type":"integer","format":"int32"}}},"AddRecipeIngredientRequest":{"required":["articleId","quantity","uom"],"type":"object","properties":{"position":{"minimum":1,"type":"integer","format":"int32"},"articleId":{"type":"string"},"quantity":{"type":"string"},"uom":{"type":"string"},"subRecipeId":{"type":"string"},"substitutable":{"type":"boolean"}}},"PlanBatchRequest":{"required":["bestBeforeDate","plannedQuantity","plannedQuantityUnit","productionDate","recipeId"],"type":"object","properties":{"recipeId":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"productionDate":{"type":"string","format":"date"},"bestBeforeDate":{"type":"string","format":"date"}}},"BatchResponse":{"type":"object","properties":{"id":{"type":"string"},"batchNumber":{"type":"string"},"recipeId":{"type":"string"},"status":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"productionDate":{"type":"string","format":"date"},"bestBeforeDate":{"type":"string","format":"date"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"CreateStorageLocationRequest":{"required":["name","storageType"],"type":"object","properties":{"name":{"type":"string"},"storageType":{"type":"string"},"minTemperature":{"type":"string"},"maxTemperature":{"type":"string"}}},"CreateStockRequest":{"required":["articleId","storageLocationId"],"type":"object","properties":{"articleId":{"type":"string"},"storageLocationId":{"type":"string"},"minimumLevelAmount":{"type":"string"},"minimumLevelUnit":{"type":"string"},"minimumShelfLifeDays":{"type":"integer","format":"int32"}}},"MinimumLevelResponse":{"required":["amount","unit"],"type":"object","properties":{"amount":{"type":"number"},"unit":{"type":"string"}},"nullable":true},"StockResponse":{"required":["articleId","id","storageLocationId"],"type":"object","properties":{"id":{"type":"string"},"articleId":{"type":"string"},"storageLocationId":{"type":"string"},"minimumLevel":{"$ref":"#/components/schemas/MinimumLevelResponse"},"minimumShelfLifeDays":{"type":"integer","format":"int32","nullable":true}}},"AddStockBatchRequest":{"required":["batchId","batchType","expiryDate","quantityAmount","quantityUnit"],"type":"object","properties":{"batchId":{"type":"string"},"batchType":{"type":"string"},"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"},"expiryDate":{"type":"string"}}},"StockBatchResponse":{"type":"object","properties":{"id":{"type":"string"},"batchId":{"type":"string"},"batchType":{"type":"string"},"quantityAmount":{"type":"number"},"quantityUnit":{"type":"string"},"expiryDate":{"type":"string","format":"date"},"status":{"type":"string"},"receivedAt":{"type":"string","format":"date-time"}}},"RemoveStockBatchRequest":{"required":["quantityAmount","quantityUnit"],"type":"object","properties":{"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"}}},"BlockStockBatchRequest":{"required":["reason"],"type":"object","properties":{"reason":{"type":"string"}}},"CreateCustomerRequest":{"required":["city","country","name","phone","postalCode","street","type"],"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["B2C","B2B"]},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddDeliveryAddressRequest":{"required":["city","country","postalCode","street"],"type":"object","properties":{"label":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"CreateProductCategoryRequest":{"required":["name"],"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"RefreshTokenRequest":{"required":["refreshToken"],"type":"object","properties":{"refreshToken":{"type":"string","description":"Refresh token"}},"description":"Refresh token request"},"LoginResponse":{"required":["accessToken","expiresAt","expiresIn","refreshToken","tokenType"],"type":"object","properties":{"accessToken":{"type":"string","description":"JWT access token","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"tokenType":{"type":"string","description":"Token type","example":"Bearer"},"expiresIn":{"type":"integer","description":"Token expiration time in seconds","format":"int64","example":3600},"expiresAt":{"type":"string","description":"Token expiration timestamp","format":"date-time"},"refreshToken":{"type":"string","description":"Refresh token for obtaining new access token"}},"description":"Login response with JWT tokens"},"LoginRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string","description":"Username","example":"admin"},"password":{"type":"string","description":"Password","example":"admin123"}},"description":"Login request with username and password"},"CreateArticleRequest":{"required":["articleNumber","categoryId","name","price","priceModel","unit"],"type":"object","properties":{"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"AssignSupplierRequest":{"required":["supplierId"],"type":"object","properties":{"supplierId":{"type":"string"}}},"AddSalesUnitRequest":{"required":["price","priceModel","unit"],"type":"object","properties":{"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"RecipeSummaryResponse":{"required":["articleId","createdAt","description","id","ingredientCount","name","outputQuantity","outputUom","status","stepCount","type","updatedAt","version","yieldPercentage"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string"},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32","nullable":true},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"},"status":{"type":"string"},"ingredientCount":{"type":"integer","format":"int32"},"stepCount":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"RemoveCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"}}}},"securitySchemes":{"Bearer Authentication":{"type":"http","description":"JWT authentication token obtained from POST /api/auth/login.\n\nFormat: Bearer \n\nExample:\nBearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\n","scheme":"bearer","bearerFormat":"JWT"}}}}
\ No newline at end of file
+{"openapi":"3.0.1","info":{"title":"Effigenix Fleischerei ERP API","description":"RESTful API for Effigenix Fleischerei ERP System.\n\n## Authentication\n\nAll endpoints (except /api/auth/login and /api/auth/refresh) require JWT authentication.\n\n1. Login via POST /api/auth/login with username and password\n2. Copy the returned access token\n3. Click \"Authorize\" button (top right)\n4. Enter: Bearer \n5. Click \"Authorize\"\n\n## User Management\n\n- **Authentication**: Login, logout, refresh token\n- **User Management**: Create, update, list users (ADMIN only)\n- **Role Management**: Assign roles, lock/unlock users (ADMIN only)\n- **Password Management**: Change password (requires current password)\n\n## Error Handling\n\nAll errors return a consistent error response format:\n\n```json\n{\n \"code\": \"USER_NOT_FOUND\",\n \"message\": \"User with ID 'user-123' not found\",\n \"status\": 404,\n \"timestamp\": \"2026-02-17T12:00:00\",\n \"path\": \"/api/users/user-123\",\n \"validationErrors\": null\n}\n```\n\n## Architecture\n\nBuilt with:\n- Domain-Driven Design (DDD)\n- Clean Architecture (Hexagonal Architecture)\n- Spring Boot 3.2\n- Java 21\n- PostgreSQL\n","contact":{"name":"Effigenix Development Team","url":"https://effigenix.com","email":"dev@effigenix.com"},"license":{"name":"Proprietary","url":"https://effigenix.com/license"},"version":"0.1.0"},"servers":[{"url":"http://localhost:8080","description":"Local Development Server"},{"url":"https://api.effigenix.com","description":"Production Server"}],"tags":[{"name":"Storage Locations","description":"Storage location management endpoints"},{"name":"Product Categories","description":"Product category management endpoints"},{"name":"User Management","description":"User management endpoints (requires authentication)"},{"name":"Articles","description":"Article management endpoints"},{"name":"Recipes","description":"Recipe management endpoints"},{"name":"Batches","description":"Production batch management endpoints"},{"name":"Role Management","description":"Role management endpoints (ADMIN only)"},{"name":"Stocks","description":"Stock management endpoints"},{"name":"Customers","description":"Customer management endpoints"},{"name":"Suppliers","description":"Supplier management endpoints"},{"name":"Authentication","description":"Authentication and session management endpoints"}],"paths":{"/api/users/{id}":{"get":{"tags":["User Management"],"summary":"Get user by ID","description":"Retrieve a single user by their ID.","operationId":"getUserById","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["User Management"],"summary":"Update user","description":"Update user details (email, branchId).","operationId":"updateUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"409":{"description":"Email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User updated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/password":{"put":{"tags":["User Management"],"summary":"Change password","description":"Change user password. Requires current password for verification.","operationId":"changePassword","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"400":{"description":"Invalid password"},"401":{"description":"Invalid current password"},"404":{"description":"User not found"},"204":{"description":"Password changed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}":{"get":{"tags":["Suppliers"],"operationId":"getSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Suppliers"],"operationId":"updateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}":{"put":{"tags":["Storage Locations"],"operationId":"updateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStorageLocationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{id}":{"get":{"tags":["Stocks"],"operationId":"getStock","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Stocks"],"operationId":"updateStock","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStockRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}":{"get":{"tags":["Customers"],"operationId":"getCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Customers"],"operationId":"updateCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/preferences":{"put":{"tags":["Customers"],"operationId":"setPreferences","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPreferencesRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/frame-contract":{"put":{"tags":["Customers"],"operationId":"setFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetFrameContractRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Customers"],"operationId":"removeFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/categories/{id}":{"put":{"tags":["Product Categories"],"operationId":"updateCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Product Categories"],"operationId":"deleteCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}":{"get":{"tags":["Articles"],"operationId":"getArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Articles"],"operationId":"updateArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}/price":{"put":{"tags":["Articles"],"operationId":"updateSalesUnitPrice","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSalesUnitPriceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users":{"get":{"tags":["User Management"],"summary":"List all users","description":"Get a list of all users in the system.","operationId":"listUsers","responses":{"200":{"description":"Users retrieved successfully","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["User Management"],"summary":"Create user (ADMIN only)","description":"Create a new user account with specified roles.","operationId":"createUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"400":{"description":"Validation error or invalid password","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Username or email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"201":{"description":"User created successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/unlock":{"post":{"tags":["User Management"],"summary":"Unlock user (ADMIN only)","description":"Unlock a user account (allows login).","operationId":"unlockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User unlocked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Invalid status transition","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles":{"post":{"tags":["User Management"],"summary":"Assign role (ADMIN only)","description":"Assign a role to a user.","operationId":"assignRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"200":{"description":"Role assigned successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User or role not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/lock":{"post":{"tags":["User Management"],"summary":"Lock user (ADMIN only)","description":"Lock a user account (prevents login).","operationId":"lockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User locked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Invalid status transition","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers":{"get":{"tags":["Suppliers"],"operationId":"listSuppliers","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SupplierResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Suppliers"],"operationId":"createSupplier","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/rating":{"post":{"tags":["Suppliers"],"operationId":"rateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/deactivate":{"post":{"tags":["Suppliers"],"operationId":"deactivate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/certificates":{"post":{"tags":["Suppliers"],"operationId":"addCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Suppliers"],"operationId":"removeCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/activate":{"post":{"tags":["Suppliers"],"operationId":"activate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes":{"get":{"tags":["Recipes"],"operationId":"listRecipes","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RecipeSummaryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Recipes"],"operationId":"createRecipe","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRecipeRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/steps":{"post":{"tags":["Recipes"],"operationId":"addProductionStep","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddProductionStepRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/ingredients":{"post":{"tags":["Recipes"],"operationId":"addIngredient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRecipeIngredientRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/archive":{"post":{"tags":["Recipes"],"operationId":"archiveRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/activate":{"post":{"tags":["Recipes"],"operationId":"activateRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches":{"get":{"tags":["Batches"],"operationId":"listBatches","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"productionDate","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"articleId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BatchSummaryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Batches"],"operationId":"planBatch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/start":{"post":{"tags":["Batches"],"operationId":"startBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/consumptions":{"post":{"tags":["Batches"],"operationId":"recordConsumption","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordConsumptionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConsumptionResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/complete":{"post":{"tags":["Batches"],"operationId":"completeBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/cancel":{"post":{"tags":["Batches"],"operationId":"cancelBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations":{"get":{"tags":["Storage Locations"],"operationId":"listStorageLocations","parameters":[{"name":"storageType","in":"query","required":false,"schema":{"type":"string"}},{"name":"active","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Storage Locations"],"operationId":"createStorageLocation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStorageLocationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks":{"get":{"tags":["Stocks"],"operationId":"listStocks","parameters":[{"name":"storageLocationId","in":"query","required":false,"schema":{"type":"string"}},{"name":"articleId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StockResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Stocks"],"operationId":"createStock","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStockRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CreateStockResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches":{"post":{"tags":["Stocks"],"operationId":"addBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockBatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/unblock":{"post":{"tags":["Stocks"],"operationId":"unblockBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/remove":{"post":{"tags":["Stocks"],"operationId":"removeBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/block":{"post":{"tags":["Stocks"],"operationId":"blockBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/customers":{"get":{"tags":["Customers"],"operationId":"listCustomers","parameters":[{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["B2C","B2B"]}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CustomerResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Customers"],"operationId":"createCustomer","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses":{"post":{"tags":["Customers"],"operationId":"addDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddDeliveryAddressRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/deactivate":{"post":{"tags":["Customers"],"operationId":"deactivate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/activate":{"post":{"tags":["Customers"],"operationId":"activate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/categories":{"get":{"tags":["Product Categories"],"operationId":"listCategories","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Product Categories"],"operationId":"createCategory","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Refresh access token","description":"Refresh an expired access token using a valid refresh token.","operationId":"refresh","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"401":{"description":"Invalid or expired refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"200":{"description":"Token refresh successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/auth/logout":{"post":{"tags":["Authentication"],"summary":"User logout","description":"Invalidate current JWT token.","operationId":"logout","responses":{"401":{"description":"Invalid or missing authentication token"},"204":{"description":"Logout successful"}}}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"User login","description":"Authenticate user with username and password. Returns JWT tokens.","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"429":{"description":"Too many login attempts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid credentials, user locked, or user inactive","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"200":{"description":"Login successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/articles":{"get":{"tags":["Articles"],"operationId":"listArticles","parameters":[{"name":"categoryId","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ArticleResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Articles"],"operationId":"createArticle","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers":{"post":{"tags":["Articles"],"operationId":"assignSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units":{"post":{"tags":["Articles"],"operationId":"addSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSalesUnitRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/deactivate":{"post":{"tags":["Articles"],"operationId":"deactivate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/activate":{"post":{"tags":["Articles"],"operationId":"activate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}/deactivate":{"patch":{"tags":["Storage Locations"],"operationId":"deactivateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}/activate":{"patch":{"tags":["Storage Locations"],"operationId":"activateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/roles":{"get":{"tags":["Role Management"],"summary":"List all roles (ADMIN only)","description":"Get a list of all available roles in the system. Requires USER_MANAGEMENT permission.","operationId":"listRoles","responses":{"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"200":{"description":"Roles retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RoleDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}":{"get":{"tags":["Recipes"],"operationId":"getRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}":{"get":{"tags":["Batches"],"operationId":"getBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/by-number/{batchNumber}":{"get":{"tags":["Batches"],"operationId":"findByNumber","parameters":[{"name":"batchNumber","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/below-minimum":{"get":{"tags":["Stocks"],"operationId":"listStocksBelowMinimum","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StockResponse"}}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles/{roleName}":{"delete":{"tags":["User Management"],"summary":"Remove role (ADMIN only)","description":"Remove a role from a user.","operationId":"removeRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}},{"name":"roleName","in":"path","description":"Role name","required":true,"schema":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}}],"responses":{"403":{"description":"Missing permission"},"404":{"description":"User or role not found"},"204":{"description":"Role removed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/steps/{stepNumber}":{"delete":{"tags":["Recipes"],"operationId":"removeProductionStep","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"stepNumber","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/ingredients/{ingredientId}":{"delete":{"tags":["Recipes"],"operationId":"removeIngredient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"ingredientId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses/{label}":{"delete":{"tags":["Customers"],"operationId":"removeDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"label","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers/{supplierId}":{"delete":{"tags":["Articles"],"operationId":"removeSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"supplierId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}":{"delete":{"tags":["Articles"],"operationId":"removeSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}}},"components":{"schemas":{"UpdateUserRequest":{"type":"object","properties":{"email":{"type":"string","description":"New email address","example":"newemail@example.com"},"branchId":{"type":"string","description":"New branch ID","example":"BRANCH-002"}},"description":"Request to update user details"},"RoleDTO":{"required":["description","id","name","permissions"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]},"permissions":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["RECIPE_READ","RECIPE_WRITE","RECIPE_DELETE","BATCH_READ","BATCH_WRITE","BATCH_COMPLETE","BATCH_CANCEL","BATCH_DELETE","PRODUCTION_ORDER_READ","PRODUCTION_ORDER_WRITE","PRODUCTION_ORDER_DELETE","HACCP_READ","HACCP_WRITE","TEMPERATURE_LOG_READ","TEMPERATURE_LOG_WRITE","CLEANING_RECORD_READ","CLEANING_RECORD_WRITE","GOODS_INSPECTION_READ","GOODS_INSPECTION_WRITE","STOCK_READ","STOCK_WRITE","STOCK_MOVEMENT_READ","STOCK_MOVEMENT_WRITE","INVENTORY_COUNT_READ","INVENTORY_COUNT_WRITE","PURCHASE_ORDER_READ","PURCHASE_ORDER_WRITE","PURCHASE_ORDER_DELETE","GOODS_RECEIPT_READ","GOODS_RECEIPT_WRITE","SUPPLIER_READ","SUPPLIER_WRITE","SUPPLIER_DELETE","ORDER_READ","ORDER_WRITE","ORDER_DELETE","INVOICE_READ","INVOICE_WRITE","INVOICE_DELETE","CUSTOMER_READ","CUSTOMER_WRITE","CUSTOMER_DELETE","LABEL_READ","LABEL_WRITE","LABEL_PRINT","MASTERDATA_READ","MASTERDATA_WRITE","BRANCH_READ","BRANCH_WRITE","BRANCH_DELETE","USER_READ","USER_WRITE","USER_DELETE","USER_LOCK","USER_UNLOCK","ROLE_READ","ROLE_WRITE","ROLE_ASSIGN","ROLE_REMOVE","REPORT_READ","REPORT_GENERATE","NOTIFICATION_READ","NOTIFICATION_SEND","AUDIT_LOG_READ","SYSTEM_SETTINGS_READ","SYSTEM_SETTINGS_WRITE"]}},"description":{"type":"string"}}},"UserDTO":{"required":["createdAt","email","id","roles","status","username"],"type":"object","properties":{"id":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"roles":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}},"branchId":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","LOCKED"]},"createdAt":{"type":"string","format":"date-time"},"lastLogin":{"type":"string","format":"date-time"}}},"ChangePasswordRequest":{"required":["currentPassword","newPassword"],"type":"object","properties":{"currentPassword":{"type":"string","description":"Current password","example":"OldPass123"},"newPassword":{"maxLength":2147483647,"minLength":8,"type":"string","description":"New password (min 8 characters)","example":"NewSecurePass456"}},"description":"Request to change user password"},"UpdateSupplierRequest":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddressResponse":{"required":["city","country","houseNumber","postalCode","street"],"type":"object","properties":{"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"}},"nullable":true},"ContactInfoResponse":{"required":["contactPerson","email","phone"],"type":"object","properties":{"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"}}},"PaymentTermsResponse":{"required":["paymentDescription","paymentDueDays"],"type":"object","properties":{"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}},"nullable":true},"QualityCertificateResponse":{"required":["certificateType","issuer","validFrom","validUntil"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"SupplierRatingResponse":{"required":["deliveryScore","priceScore","qualityScore"],"type":"object","properties":{"qualityScore":{"type":"integer","format":"int32"},"deliveryScore":{"type":"integer","format":"int32"},"priceScore":{"type":"integer","format":"int32"}},"nullable":true},"SupplierResponse":{"required":["certificates","contactInfo","createdAt","id","name","status","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"certificates":{"type":"array","items":{"$ref":"#/components/schemas/QualityCertificateResponse"}},"rating":{"$ref":"#/components/schemas/SupplierRatingResponse"},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"UpdateStorageLocationRequest":{"type":"object","properties":{"name":{"type":"string"},"minTemperature":{"type":"string"},"maxTemperature":{"type":"string"}}},"StorageLocationResponse":{"required":["active","id","name","storageType"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"storageType":{"type":"string"},"temperatureRange":{"$ref":"#/components/schemas/TemperatureRangeResponse"},"active":{"type":"boolean"}}},"TemperatureRangeResponse":{"required":["maxTemperature","minTemperature"],"type":"object","properties":{"minTemperature":{"type":"number"},"maxTemperature":{"type":"number"}},"nullable":true},"UpdateStockRequest":{"type":"object","properties":{"minimumLevelAmount":{"type":"string"},"minimumLevelUnit":{"type":"string"},"minimumShelfLifeDays":{"type":"integer","format":"int32"}}},"MinimumLevelResponse":{"required":["amount","unit"],"type":"object","properties":{"amount":{"type":"number"},"unit":{"type":"string"}},"nullable":true},"StockBatchResponse":{"type":"object","properties":{"id":{"type":"string"},"batchId":{"type":"string"},"batchType":{"type":"string"},"quantityAmount":{"type":"number"},"quantityUnit":{"type":"string"},"expiryDate":{"type":"string","format":"date"},"status":{"type":"string"},"receivedAt":{"type":"string","format":"date-time"}}},"StockResponse":{"required":["articleId","availableQuantity","batches","id","storageLocationId","totalQuantity"],"type":"object","properties":{"id":{"type":"string"},"articleId":{"type":"string"},"storageLocationId":{"type":"string"},"minimumLevel":{"$ref":"#/components/schemas/MinimumLevelResponse"},"minimumShelfLifeDays":{"type":"integer","format":"int32","nullable":true},"batches":{"type":"array","items":{"$ref":"#/components/schemas/StockBatchResponse"}},"totalQuantity":{"type":"number"},"quantityUnit":{"type":"string","nullable":true},"availableQuantity":{"type":"number"}}},"UpdateCustomerRequest":{"type":"object","properties":{"name":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"ContractLineItemResponse":{"required":["agreedPrice","agreedQuantity","articleId","unit"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string"}}},"CustomerResponse":{"required":["billingAddress","contactInfo","createdAt","deliveryAddresses","id","name","preferences","status","type","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"billingAddress":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"deliveryAddresses":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryAddressResponse"}},"frameContract":{"$ref":"#/components/schemas/FrameContractResponse"},"preferences":{"type":"array","items":{"type":"string"}},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"DeliveryAddressResponse":{"required":["address","contactPerson","deliveryNotes","label"],"type":"object","properties":{"label":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"FrameContractResponse":{"required":["deliveryRhythm","id","lineItems","validFrom","validUntil"],"type":"object","properties":{"id":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"deliveryRhythm":{"type":"string"},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/ContractLineItemResponse"}}},"nullable":true},"SetPreferencesRequest":{"required":["preferences"],"type":"object","properties":{"preferences":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["BIO","REGIONAL","TIERWOHL","HALAL","KOSHER","GLUTENFREI","LAKTOSEFREI"]}}}},"LineItem":{"required":["agreedPrice","articleId"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]}}},"SetFrameContractRequest":{"required":["lineItems","rhythm"],"type":"object","properties":{"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"rhythm":{"type":"string","enum":["DAILY","WEEKLY","BIWEEKLY","MONTHLY","ON_DEMAND"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/LineItem"}}}},"UpdateProductCategoryRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ProductCategoryResponse":{"required":["description","id","name"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}},"UpdateArticleRequest":{"type":"object","properties":{"name":{"type":"string"},"categoryId":{"type":"string"}}},"ArticleResponse":{"required":["articleNumber","categoryId","createdAt","id","name","salesUnits","status","supplierIds","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"salesUnits":{"type":"array","items":{"$ref":"#/components/schemas/SalesUnitResponse"}},"status":{"type":"string"},"supplierIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SalesUnitResponse":{"required":["id","price","priceModel","unit"],"type":"object","properties":{"id":{"type":"string"},"unit":{"type":"string"},"priceModel":{"type":"string"},"price":{"type":"number"}}},"UpdateSalesUnitPriceRequest":{"required":["price"],"type":"object","properties":{"price":{"type":"number"}}},"CreateUserRequest":{"required":["email","password","roleNames","username"],"type":"object","properties":{"username":{"maxLength":50,"minLength":3,"type":"string","description":"Username (unique)","example":"john.doe"},"email":{"type":"string","description":"Email address (unique)","example":"john.doe@example.com"},"password":{"maxLength":2147483647,"minLength":8,"type":"string","description":"Password (min 8 characters)","example":"SecurePass123"},"roleNames":{"uniqueItems":true,"type":"array","description":"Role names to assign","example":["USER","MANAGER"],"items":{"type":"string","description":"Role names to assign","example":"[\"USER\",\"MANAGER\"]","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"branchId":{"type":"string","description":"Branch ID (optional)","example":"BRANCH-001"}},"description":"Request to create a new user"},"AssignRoleRequest":{"required":["roleName"],"type":"object","properties":{"roleName":{"type":"string","description":"Role name to assign","example":"MANAGER","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"description":"Request to assign a role to a user"},"CreateSupplierRequest":{"required":["name","phone"],"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"RateSupplierRequest":{"type":"object","properties":{"qualityScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"deliveryScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"priceScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"}}},"AddCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"CreateRecipeRequest":{"required":["articleId","name","outputQuantity","outputUom","type"],"type":"object","properties":{"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string","enum":["RAW_MATERIAL","INTERMEDIATE","FINISHED_PRODUCT"]},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32"},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"}}},"IngredientResponse":{"required":["articleId","id","position","quantity","substitutable","uom"],"type":"object","properties":{"id":{"type":"string"},"position":{"type":"integer","format":"int32"},"articleId":{"type":"string"},"quantity":{"type":"string"},"uom":{"type":"string"},"subRecipeId":{"type":"string","nullable":true},"substitutable":{"type":"boolean"}}},"ProductionStepResponse":{"required":["description","id","stepNumber"],"type":"object","properties":{"id":{"type":"string"},"stepNumber":{"type":"integer","format":"int32"},"description":{"type":"string"},"durationMinutes":{"type":"integer","format":"int32","nullable":true},"temperatureCelsius":{"type":"integer","format":"int32","nullable":true}}},"RecipeResponse":{"required":["articleId","createdAt","description","id","ingredients","name","outputQuantity","outputUom","productionSteps","status","type","updatedAt","version","yieldPercentage"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string"},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32","nullable":true},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"},"status":{"type":"string"},"ingredients":{"type":"array","items":{"$ref":"#/components/schemas/IngredientResponse"}},"productionSteps":{"type":"array","items":{"$ref":"#/components/schemas/ProductionStepResponse"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"AddProductionStepRequest":{"required":["description"],"type":"object","properties":{"stepNumber":{"minimum":1,"type":"integer","format":"int32"},"description":{"maxLength":500,"minLength":0,"type":"string"},"durationMinutes":{"minimum":1,"type":"integer","format":"int32"},"temperatureCelsius":{"maximum":1000,"minimum":-273,"type":"integer","format":"int32"}}},"AddRecipeIngredientRequest":{"required":["articleId","quantity","uom"],"type":"object","properties":{"position":{"minimum":1,"type":"integer","format":"int32"},"articleId":{"type":"string"},"quantity":{"type":"string"},"uom":{"type":"string"},"subRecipeId":{"type":"string"},"substitutable":{"type":"boolean"}}},"PlanBatchRequest":{"required":["bestBeforeDate","plannedQuantity","plannedQuantityUnit","productionDate","recipeId"],"type":"object","properties":{"recipeId":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"productionDate":{"type":"string","format":"date"},"bestBeforeDate":{"type":"string","format":"date"}}},"BatchResponse":{"type":"object","properties":{"id":{"type":"string"},"batchNumber":{"type":"string"},"recipeId":{"type":"string"},"status":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"actualQuantity":{"type":"string"},"actualQuantityUnit":{"type":"string"},"waste":{"type":"string"},"wasteUnit":{"type":"string"},"remarks":{"type":"string"},"productionDate":{"type":"string","format":"date"},"bestBeforeDate":{"type":"string","format":"date"},"consumptions":{"type":"array","items":{"$ref":"#/components/schemas/ConsumptionResponse"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"cancellationReason":{"type":"string"},"cancelledAt":{"type":"string","format":"date-time"}}},"ConsumptionResponse":{"type":"object","properties":{"id":{"type":"string"},"inputBatchId":{"type":"string"},"articleId":{"type":"string"},"quantityUsed":{"type":"string"},"quantityUsedUnit":{"type":"string"},"consumedAt":{"type":"string","format":"date-time"}}},"RecordConsumptionRequest":{"required":["articleId","inputBatchId","quantityUnit","quantityUsed"],"type":"object","properties":{"inputBatchId":{"type":"string"},"articleId":{"type":"string"},"quantityUsed":{"type":"string"},"quantityUnit":{"type":"string"}}},"CompleteBatchRequest":{"required":["actualQuantity","actualQuantityUnit","waste","wasteUnit"],"type":"object","properties":{"actualQuantity":{"type":"string"},"actualQuantityUnit":{"type":"string"},"waste":{"type":"string"},"wasteUnit":{"type":"string"},"remarks":{"type":"string"}}},"CancelBatchRequest":{"required":["reason"],"type":"object","properties":{"reason":{"type":"string"}}},"CreateStorageLocationRequest":{"required":["name","storageType"],"type":"object","properties":{"name":{"type":"string"},"storageType":{"type":"string"},"minTemperature":{"type":"string"},"maxTemperature":{"type":"string"}}},"CreateStockRequest":{"required":["articleId","storageLocationId"],"type":"object","properties":{"articleId":{"type":"string"},"storageLocationId":{"type":"string"},"minimumLevelAmount":{"type":"string"},"minimumLevelUnit":{"type":"string"},"minimumShelfLifeDays":{"type":"integer","format":"int32"}}},"CreateStockResponse":{"required":["articleId","id","storageLocationId"],"type":"object","properties":{"id":{"type":"string"},"articleId":{"type":"string"},"storageLocationId":{"type":"string"},"minimumLevel":{"$ref":"#/components/schemas/MinimumLevelResponse"},"minimumShelfLifeDays":{"type":"integer","format":"int32","nullable":true}}},"AddStockBatchRequest":{"required":["batchId","batchType","expiryDate","quantityAmount","quantityUnit"],"type":"object","properties":{"batchId":{"type":"string"},"batchType":{"type":"string"},"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"},"expiryDate":{"type":"string"}}},"RemoveStockBatchRequest":{"required":["quantityAmount","quantityUnit"],"type":"object","properties":{"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"}}},"BlockStockBatchRequest":{"required":["reason"],"type":"object","properties":{"reason":{"type":"string"}}},"CreateCustomerRequest":{"required":["city","country","name","phone","postalCode","street","type"],"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["B2C","B2B"]},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddDeliveryAddressRequest":{"required":["city","country","postalCode","street"],"type":"object","properties":{"label":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"CreateProductCategoryRequest":{"required":["name"],"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"RefreshTokenRequest":{"required":["refreshToken"],"type":"object","properties":{"refreshToken":{"type":"string","description":"Refresh token"}},"description":"Refresh token request"},"LoginResponse":{"required":["accessToken","expiresAt","expiresIn","refreshToken","tokenType"],"type":"object","properties":{"accessToken":{"type":"string","description":"JWT access token","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"tokenType":{"type":"string","description":"Token type","example":"Bearer"},"expiresIn":{"type":"integer","description":"Token expiration time in seconds","format":"int64","example":3600},"expiresAt":{"type":"string","description":"Token expiration timestamp","format":"date-time"},"refreshToken":{"type":"string","description":"Refresh token for obtaining new access token"}},"description":"Login response with JWT tokens"},"LoginRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string","description":"Username","example":"admin"},"password":{"type":"string","description":"Password","example":"admin123"}},"description":"Login request with username and password"},"CreateArticleRequest":{"required":["articleNumber","categoryId","name","price","priceModel","unit"],"type":"object","properties":{"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"AssignSupplierRequest":{"required":["supplierId"],"type":"object","properties":{"supplierId":{"type":"string"}}},"AddSalesUnitRequest":{"required":["price","priceModel","unit"],"type":"object","properties":{"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"RecipeSummaryResponse":{"required":["articleId","createdAt","description","id","ingredientCount","name","outputQuantity","outputUom","status","stepCount","type","updatedAt","version","yieldPercentage"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string"},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32","nullable":true},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"},"status":{"type":"string"},"ingredientCount":{"type":"integer","format":"int32"},"stepCount":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"BatchSummaryResponse":{"type":"object","properties":{"id":{"type":"string"},"batchNumber":{"type":"string"},"recipeId":{"type":"string"},"status":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"productionDate":{"type":"string","format":"date"},"bestBeforeDate":{"type":"string","format":"date"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"RemoveCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"}}}},"securitySchemes":{"Bearer Authentication":{"type":"http","description":"JWT authentication token obtained from POST /api/auth/login.\n\nFormat: Bearer \n\nExample:\nBearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\n","scheme":"bearer","bearerFormat":"JWT"}}}}
\ No newline at end of file
diff --git a/frontend/packages/api-client/src/index.ts b/frontend/packages/api-client/src/index.ts
index f1201b4..dabffce 100644
--- a/frontend/packages/api-client/src/index.ts
+++ b/frontend/packages/api-client/src/index.ts
@@ -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 { createBatchesResource } from './resources/batches.js';
export { createStocksResource } from './resources/stocks.js';
export {
ApiError,
@@ -90,6 +91,20 @@ export type {
AddProductionStepRequest,
StockBatchDTO,
AddStockBatchRequest,
+ BatchDTO,
+ BatchSummaryDTO,
+ ConsumptionDTO,
+ PlanBatchRequest,
+ CompleteBatchRequest,
+ RecordConsumptionRequest,
+ CancelBatchRequest,
+ StockDTO,
+ CreateStockRequest,
+ CreateStockResponse,
+ UpdateStockRequest,
+ RemoveStockBatchRequest,
+ BlockStockBatchRequest,
+ MinimumLevelDTO,
} from '@effigenix/types';
// Resource types (runtime, stay in resource files)
@@ -115,8 +130,10 @@ export type {
export { STORAGE_TYPE_LABELS } from './resources/storage-locations.js';
export type { RecipesResource, RecipeType, RecipeStatus, UoM } from './resources/recipes.js';
export { RECIPE_TYPE_LABELS, UOM_VALUES, UOM_LABELS } from './resources/recipes.js';
-export type { StocksResource, BatchType } from './resources/stocks.js';
-export { BATCH_TYPE_LABELS } from './resources/stocks.js';
+export type { BatchesResource, BatchStatus } from './resources/batches.js';
+export { BATCH_STATUS_LABELS } from './resources/batches.js';
+export type { StocksResource, BatchType, StockBatchStatus, StockFilter } from './resources/stocks.js';
+export { BATCH_TYPE_LABELS, STOCK_BATCH_STATUS_LABELS } from './resources/stocks.js';
import { createApiClient } from './client.js';
import { createAuthResource } from './resources/auth.js';
@@ -128,6 +145,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 { createBatchesResource } from './resources/batches.js';
import { createStocksResource } from './resources/stocks.js';
import type { TokenProvider } from './token-provider.js';
import type { ApiConfig } from '@effigenix/config';
@@ -152,6 +170,7 @@ export function createEffigenixClient(
customers: createCustomersResource(axiosClient),
storageLocations: createStorageLocationsResource(axiosClient),
recipes: createRecipesResource(axiosClient),
+ batches: createBatchesResource(axiosClient),
stocks: createStocksResource(axiosClient),
};
}
diff --git a/frontend/packages/api-client/src/resources/batches.ts b/frontend/packages/api-client/src/resources/batches.ts
new file mode 100644
index 0000000..f13ca07
--- /dev/null
+++ b/frontend/packages/api-client/src/resources/batches.ts
@@ -0,0 +1,81 @@
+/** Batches resource – Production BC. */
+
+import type { AxiosInstance } from 'axios';
+import type {
+ BatchDTO,
+ BatchSummaryDTO,
+ ConsumptionDTO,
+ PlanBatchRequest,
+ CompleteBatchRequest,
+ RecordConsumptionRequest,
+ CancelBatchRequest,
+} from '@effigenix/types';
+
+export type BatchStatus = 'PLANNED' | 'IN_PRODUCTION' | 'COMPLETED' | 'CANCELLED';
+
+export const BATCH_STATUS_LABELS: Record = {
+ PLANNED: 'Geplant',
+ IN_PRODUCTION: 'In Produktion',
+ COMPLETED: 'Abgeschlossen',
+ CANCELLED: 'Storniert',
+};
+
+export type {
+ BatchDTO,
+ BatchSummaryDTO,
+ ConsumptionDTO,
+ PlanBatchRequest,
+ CompleteBatchRequest,
+ RecordConsumptionRequest,
+ CancelBatchRequest,
+};
+
+const BASE = '/api/production/batches';
+
+export function createBatchesResource(client: AxiosInstance) {
+ return {
+ async list(status?: BatchStatus): Promise {
+ const params: Record = {};
+ if (status) params.status = status;
+ const res = await client.get(BASE, { params });
+ return res.data;
+ },
+
+ async getById(id: string): Promise {
+ const res = await client.get(`${BASE}/${id}`);
+ return res.data;
+ },
+
+ async getByNumber(batchNumber: string): Promise {
+ const res = await client.get(`${BASE}/by-number/${batchNumber}`);
+ return res.data;
+ },
+
+ async plan(request: PlanBatchRequest): Promise {
+ const res = await client.post(BASE, request);
+ return res.data;
+ },
+
+ async start(id: string): Promise {
+ const res = await client.post(`${BASE}/${id}/start`);
+ return res.data;
+ },
+
+ async recordConsumption(id: string, request: RecordConsumptionRequest): Promise {
+ const res = await client.post(`${BASE}/${id}/consumptions`, request);
+ return res.data;
+ },
+
+ async complete(id: string, request: CompleteBatchRequest): Promise {
+ const res = await client.post(`${BASE}/${id}/complete`, request);
+ return res.data;
+ },
+
+ async cancel(id: string, request: CancelBatchRequest): Promise {
+ const res = await client.post(`${BASE}/${id}/cancel`, request);
+ return res.data;
+ },
+ };
+}
+
+export type BatchesResource = ReturnType;
diff --git a/frontend/packages/api-client/src/resources/stocks.ts b/frontend/packages/api-client/src/resources/stocks.ts
index 4874d26..9e20e2b 100644
--- a/frontend/packages/api-client/src/resources/stocks.ts
+++ b/frontend/packages/api-client/src/resources/stocks.ts
@@ -1,7 +1,16 @@
/** Stocks resource – Inventory BC. */
import type { AxiosInstance } from 'axios';
-import type { StockBatchDTO, AddStockBatchRequest } from '@effigenix/types';
+import type {
+ StockDTO,
+ StockBatchDTO,
+ AddStockBatchRequest,
+ CreateStockRequest,
+ CreateStockResponse,
+ UpdateStockRequest,
+ RemoveStockBatchRequest,
+ BlockStockBatchRequest,
+} from '@effigenix/types';
export type BatchType = 'PURCHASED' | 'PRODUCED';
@@ -10,7 +19,30 @@ export const BATCH_TYPE_LABELS: Record = {
PRODUCED: 'Produziert',
};
-export type { StockBatchDTO, AddStockBatchRequest };
+export type StockBatchStatus = 'AVAILABLE' | 'EXPIRING_SOON' | 'EXPIRED' | 'BLOCKED';
+
+export const STOCK_BATCH_STATUS_LABELS: Record = {
+ AVAILABLE: 'Verfügbar',
+ EXPIRING_SOON: 'Bald ablaufend',
+ EXPIRED: 'Abgelaufen',
+ BLOCKED: 'Gesperrt',
+};
+
+export type {
+ StockDTO,
+ StockBatchDTO,
+ AddStockBatchRequest,
+ CreateStockRequest,
+ CreateStockResponse,
+ UpdateStockRequest,
+ RemoveStockBatchRequest,
+ BlockStockBatchRequest,
+};
+
+export interface StockFilter {
+ storageLocationId?: string;
+ articleId?: string;
+}
// ── Resource factory ─────────────────────────────────────────────────────────
@@ -18,10 +50,45 @@ const BASE = '/api/inventory/stocks';
export function createStocksResource(client: AxiosInstance) {
return {
+ async list(filter?: StockFilter): Promise {
+ const params: Record = {};
+ if (filter?.storageLocationId) params.storageLocationId = filter.storageLocationId;
+ if (filter?.articleId) params.articleId = filter.articleId;
+ const res = await client.get(BASE, { params });
+ return res.data;
+ },
+
+ async getById(id: string): Promise {
+ const res = await client.get(`${BASE}/${id}`);
+ return res.data;
+ },
+
+ async create(request: CreateStockRequest): Promise {
+ const res = await client.post(BASE, request);
+ return res.data;
+ },
+
+ async update(id: string, request: UpdateStockRequest): Promise {
+ const res = await client.put(`${BASE}/${id}`, request);
+ return res.data;
+ },
+
async addBatch(stockId: string, request: AddStockBatchRequest): Promise {
const res = await client.post(`${BASE}/${stockId}/batches`, request);
return res.data;
},
+
+ async removeBatch(stockId: string, batchId: string, request: RemoveStockBatchRequest): Promise {
+ await client.post(`${BASE}/${stockId}/batches/${batchId}/remove`, request);
+ },
+
+ async blockBatch(stockId: string, batchId: string, request: BlockStockBatchRequest): Promise {
+ await client.post(`${BASE}/${stockId}/batches/${batchId}/block`, request);
+ },
+
+ async unblockBatch(stockId: string, batchId: string): Promise {
+ await client.post(`${BASE}/${stockId}/batches/${batchId}/unblock`);
+ },
};
}
diff --git a/frontend/packages/types/src/generated/api.ts b/frontend/packages/types/src/generated/api.ts
index 298a30c..e797b53 100644
--- a/frontend/packages/types/src/generated/api.ts
+++ b/frontend/packages/types/src/generated/api.ts
@@ -80,6 +80,22 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/api/inventory/stocks/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["getStock"];
+ put: operations["updateStock"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/customers/{id}": {
parameters: {
query?: never;
@@ -427,7 +443,7 @@ export interface paths {
path?: never;
cookie?: never;
};
- get?: never;
+ get: operations["listBatches"];
put?: never;
post: operations["planBatch"];
delete?: never;
@@ -436,6 +452,70 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/api/production/batches/{id}/start": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["startBatch"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/production/batches/{id}/consumptions": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["recordConsumption"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/production/batches/{id}/complete": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["completeBatch"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/production/batches/{id}/cancel": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["cancelBatch"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/inventory/storage-locations": {
parameters: {
query?: never;
@@ -459,7 +539,7 @@ export interface paths {
path?: never;
cookie?: never;
};
- get?: never;
+ get: operations["listStocks"];
put?: never;
post: operations["createStock"];
delete?: never;
@@ -820,6 +900,54 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/api/production/batches/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["getBatch"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/production/batches/by-number/{batchNumber}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["findByNumber"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/inventory/stocks/below-minimum": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["listStocksBelowMinimum"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/users/{id}/roles/{roleName}": {
parameters: {
query?: never;
@@ -941,7 +1069,7 @@ export interface components {
id: string;
/** @enum {string} */
name: "ADMIN" | "PRODUCTION_MANAGER" | "PRODUCTION_WORKER" | "QUALITY_MANAGER" | "QUALITY_INSPECTOR" | "PROCUREMENT_MANAGER" | "WAREHOUSE_WORKER" | "SALES_MANAGER" | "SALES_STAFF";
- permissions: ("RECIPE_READ" | "RECIPE_WRITE" | "RECIPE_DELETE" | "BATCH_READ" | "BATCH_WRITE" | "BATCH_COMPLETE" | "BATCH_DELETE" | "PRODUCTION_ORDER_READ" | "PRODUCTION_ORDER_WRITE" | "PRODUCTION_ORDER_DELETE" | "HACCP_READ" | "HACCP_WRITE" | "TEMPERATURE_LOG_READ" | "TEMPERATURE_LOG_WRITE" | "CLEANING_RECORD_READ" | "CLEANING_RECORD_WRITE" | "GOODS_INSPECTION_READ" | "GOODS_INSPECTION_WRITE" | "STOCK_READ" | "STOCK_WRITE" | "STOCK_MOVEMENT_READ" | "STOCK_MOVEMENT_WRITE" | "INVENTORY_COUNT_READ" | "INVENTORY_COUNT_WRITE" | "PURCHASE_ORDER_READ" | "PURCHASE_ORDER_WRITE" | "PURCHASE_ORDER_DELETE" | "GOODS_RECEIPT_READ" | "GOODS_RECEIPT_WRITE" | "SUPPLIER_READ" | "SUPPLIER_WRITE" | "SUPPLIER_DELETE" | "ORDER_READ" | "ORDER_WRITE" | "ORDER_DELETE" | "INVOICE_READ" | "INVOICE_WRITE" | "INVOICE_DELETE" | "CUSTOMER_READ" | "CUSTOMER_WRITE" | "CUSTOMER_DELETE" | "LABEL_READ" | "LABEL_WRITE" | "LABEL_PRINT" | "MASTERDATA_READ" | "MASTERDATA_WRITE" | "BRANCH_READ" | "BRANCH_WRITE" | "BRANCH_DELETE" | "USER_READ" | "USER_WRITE" | "USER_DELETE" | "USER_LOCK" | "USER_UNLOCK" | "ROLE_READ" | "ROLE_WRITE" | "ROLE_ASSIGN" | "ROLE_REMOVE" | "REPORT_READ" | "REPORT_GENERATE" | "NOTIFICATION_READ" | "NOTIFICATION_SEND" | "AUDIT_LOG_READ" | "SYSTEM_SETTINGS_READ" | "SYSTEM_SETTINGS_WRITE")[];
+ permissions: ("RECIPE_READ" | "RECIPE_WRITE" | "RECIPE_DELETE" | "BATCH_READ" | "BATCH_WRITE" | "BATCH_COMPLETE" | "BATCH_CANCEL" | "BATCH_DELETE" | "PRODUCTION_ORDER_READ" | "PRODUCTION_ORDER_WRITE" | "PRODUCTION_ORDER_DELETE" | "HACCP_READ" | "HACCP_WRITE" | "TEMPERATURE_LOG_READ" | "TEMPERATURE_LOG_WRITE" | "CLEANING_RECORD_READ" | "CLEANING_RECORD_WRITE" | "GOODS_INSPECTION_READ" | "GOODS_INSPECTION_WRITE" | "STOCK_READ" | "STOCK_WRITE" | "STOCK_MOVEMENT_READ" | "STOCK_MOVEMENT_WRITE" | "INVENTORY_COUNT_READ" | "INVENTORY_COUNT_WRITE" | "PURCHASE_ORDER_READ" | "PURCHASE_ORDER_WRITE" | "PURCHASE_ORDER_DELETE" | "GOODS_RECEIPT_READ" | "GOODS_RECEIPT_WRITE" | "SUPPLIER_READ" | "SUPPLIER_WRITE" | "SUPPLIER_DELETE" | "ORDER_READ" | "ORDER_WRITE" | "ORDER_DELETE" | "INVOICE_READ" | "INVOICE_WRITE" | "INVOICE_DELETE" | "CUSTOMER_READ" | "CUSTOMER_WRITE" | "CUSTOMER_DELETE" | "LABEL_READ" | "LABEL_WRITE" | "LABEL_PRINT" | "MASTERDATA_READ" | "MASTERDATA_WRITE" | "BRANCH_READ" | "BRANCH_WRITE" | "BRANCH_DELETE" | "USER_READ" | "USER_WRITE" | "USER_DELETE" | "USER_LOCK" | "USER_UNLOCK" | "ROLE_READ" | "ROLE_WRITE" | "ROLE_ASSIGN" | "ROLE_REMOVE" | "REPORT_READ" | "REPORT_GENERATE" | "NOTIFICATION_READ" | "NOTIFICATION_SEND" | "AUDIT_LOG_READ" | "SYSTEM_SETTINGS_READ" | "SYSTEM_SETTINGS_WRITE")[];
description: string;
};
UserDTO: {
@@ -1047,6 +1175,40 @@ export interface components {
minTemperature: number;
maxTemperature: number;
} | null;
+ UpdateStockRequest: {
+ minimumLevelAmount?: string;
+ minimumLevelUnit?: string;
+ /** Format: int32 */
+ minimumShelfLifeDays?: number;
+ };
+ MinimumLevelResponse: {
+ amount: number;
+ unit: string;
+ } | null;
+ StockBatchResponse: {
+ id?: string;
+ batchId?: string;
+ batchType?: string;
+ quantityAmount?: number;
+ quantityUnit?: string;
+ /** Format: date */
+ expiryDate?: string;
+ status?: string;
+ /** Format: date-time */
+ receivedAt?: string;
+ };
+ StockResponse: {
+ id: string;
+ articleId: string;
+ storageLocationId: string;
+ minimumLevel?: components["schemas"]["MinimumLevelResponse"];
+ /** Format: int32 */
+ minimumShelfLifeDays?: number | null;
+ batches: components["schemas"]["StockBatchResponse"][];
+ totalQuantity: number;
+ quantityUnit?: string | null;
+ availableQuantity: number;
+ };
UpdateCustomerRequest: {
name?: string;
street?: string;
@@ -1313,14 +1475,50 @@ export interface components {
status?: string;
plannedQuantity?: string;
plannedQuantityUnit?: string;
+ actualQuantity?: string;
+ actualQuantityUnit?: string;
+ waste?: string;
+ wasteUnit?: string;
+ remarks?: string;
/** Format: date */
productionDate?: string;
/** Format: date */
bestBeforeDate?: string;
+ consumptions?: components["schemas"]["ConsumptionResponse"][];
/** Format: date-time */
createdAt?: string;
/** Format: date-time */
updatedAt?: string;
+ /** Format: date-time */
+ completedAt?: string;
+ cancellationReason?: string;
+ /** Format: date-time */
+ cancelledAt?: string;
+ };
+ ConsumptionResponse: {
+ id?: string;
+ inputBatchId?: string;
+ articleId?: string;
+ quantityUsed?: string;
+ quantityUsedUnit?: string;
+ /** Format: date-time */
+ consumedAt?: string;
+ };
+ RecordConsumptionRequest: {
+ inputBatchId: string;
+ articleId: string;
+ quantityUsed: string;
+ quantityUnit: string;
+ };
+ CompleteBatchRequest: {
+ actualQuantity: string;
+ actualQuantityUnit: string;
+ waste: string;
+ wasteUnit: string;
+ remarks?: string;
+ };
+ CancelBatchRequest: {
+ reason: string;
};
CreateStorageLocationRequest: {
name: string;
@@ -1336,11 +1534,7 @@ export interface components {
/** Format: int32 */
minimumShelfLifeDays?: number;
};
- MinimumLevelResponse: {
- amount: number;
- unit: string;
- } | null;
- StockResponse: {
+ CreateStockResponse: {
id: string;
articleId: string;
storageLocationId: string;
@@ -1355,18 +1549,6 @@ export interface components {
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;
- };
RemoveStockBatchRequest: {
quantityAmount: string;
quantityUnit: string;
@@ -1492,6 +1674,22 @@ export interface components {
/** Format: date-time */
updatedAt: string;
};
+ BatchSummaryResponse: {
+ id?: string;
+ batchNumber?: string;
+ recipeId?: string;
+ status?: string;
+ plannedQuantity?: string;
+ plannedQuantityUnit?: string;
+ /** Format: date */
+ productionDate?: string;
+ /** Format: date */
+ bestBeforeDate?: string;
+ /** Format: date-time */
+ createdAt?: string;
+ /** Format: date-time */
+ updatedAt?: string;
+ };
RemoveCertificateRequest: {
certificateType: string;
issuer?: string;
@@ -1713,6 +1911,54 @@ export interface operations {
};
};
};
+ getStock: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": components["schemas"]["StockResponse"];
+ };
+ };
+ };
+ };
+ updateStock: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateStockRequest"];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": components["schemas"]["StockResponse"];
+ };
+ };
+ };
+ };
getCustomer: {
parameters: {
query?: never;
@@ -2487,6 +2733,30 @@ export interface operations {
};
};
};
+ listBatches: {
+ parameters: {
+ query?: {
+ status?: string;
+ productionDate?: string;
+ articleId?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": components["schemas"]["BatchSummaryResponse"][];
+ };
+ };
+ };
+ };
planBatch: {
parameters: {
query?: never;
@@ -2511,6 +2781,106 @@ export interface operations {
};
};
};
+ startBatch: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": components["schemas"]["BatchResponse"];
+ };
+ };
+ };
+ };
+ recordConsumption: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["RecordConsumptionRequest"];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": components["schemas"]["ConsumptionResponse"];
+ };
+ };
+ };
+ };
+ completeBatch: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CompleteBatchRequest"];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": components["schemas"]["BatchResponse"];
+ };
+ };
+ };
+ };
+ cancelBatch: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CancelBatchRequest"];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": components["schemas"]["BatchResponse"];
+ };
+ };
+ };
+ };
listStorageLocations: {
parameters: {
query?: {
@@ -2558,6 +2928,29 @@ export interface operations {
};
};
};
+ listStocks: {
+ parameters: {
+ query?: {
+ storageLocationId?: string;
+ articleId?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": components["schemas"]["StockResponse"][];
+ };
+ };
+ };
+ };
createStock: {
parameters: {
query?: never;
@@ -2577,7 +2970,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "*/*": components["schemas"]["StockResponse"];
+ "*/*": components["schemas"]["CreateStockResponse"];
};
};
};
@@ -3187,6 +3580,70 @@ export interface operations {
};
};
};
+ getBatch: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": components["schemas"]["BatchResponse"];
+ };
+ };
+ };
+ };
+ findByNumber: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ batchNumber: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": components["schemas"]["BatchResponse"];
+ };
+ };
+ };
+ };
+ listStocksBelowMinimum: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": components["schemas"]["StockResponse"][];
+ };
+ };
+ };
+ };
removeRole: {
parameters: {
query?: never;
diff --git a/frontend/packages/types/src/inventory.ts b/frontend/packages/types/src/inventory.ts
index 4be2059..7e6db59 100644
--- a/frontend/packages/types/src/inventory.ts
+++ b/frontend/packages/types/src/inventory.ts
@@ -14,3 +14,14 @@ export type StockBatchDTO = components['schemas']['StockBatchResponse'];
export type CreateStorageLocationRequest = components['schemas']['CreateStorageLocationRequest'];
export type UpdateStorageLocationRequest = components['schemas']['UpdateStorageLocationRequest'];
export type AddStockBatchRequest = components['schemas']['AddStockBatchRequest'];
+
+// Stock response DTOs
+export type MinimumLevelDTO = components['schemas']['MinimumLevelResponse'];
+export type StockDTO = components['schemas']['StockResponse'];
+
+// Stock request types
+export type CreateStockRequest = components['schemas']['CreateStockRequest'];
+export type CreateStockResponse = components['schemas']['CreateStockResponse'];
+export type UpdateStockRequest = components['schemas']['UpdateStockRequest'];
+export type RemoveStockBatchRequest = components['schemas']['RemoveStockBatchRequest'];
+export type BlockStockBatchRequest = components['schemas']['BlockStockBatchRequest'];
diff --git a/frontend/packages/types/src/production.ts b/frontend/packages/types/src/production.ts
index 1ba14ff..a75f686 100644
--- a/frontend/packages/types/src/production.ts
+++ b/frontend/packages/types/src/production.ts
@@ -15,3 +15,14 @@ export type ProductionStepDTO = components['schemas']['ProductionStepResponse'];
export type CreateRecipeRequest = components['schemas']['CreateRecipeRequest'];
export type AddRecipeIngredientRequest = components['schemas']['AddRecipeIngredientRequest'];
export type AddProductionStepRequest = components['schemas']['AddProductionStepRequest'];
+
+// Batch response DTOs
+export type ConsumptionDTO = components['schemas']['ConsumptionResponse'];
+export type BatchDTO = components['schemas']['BatchResponse'];
+export type BatchSummaryDTO = components['schemas']['BatchSummaryResponse'];
+
+// Batch request types
+export type PlanBatchRequest = components['schemas']['PlanBatchRequest'];
+export type CompleteBatchRequest = components['schemas']['CompleteBatchRequest'];
+export type RecordConsumptionRequest = components['schemas']['RecordConsumptionRequest'];
+export type CancelBatchRequest = components['schemas']['CancelBatchRequest'];