1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 12:19:35 +01:00

feat(tui): Bestandsbewegungen und Produktionsaufträge anbinden

- StockMovement: API-Client, Hook, List/Detail/Record-Screens mit Typ-Filter
- ProductionOrder: list/getById/start im API-Client, List/Detail-Screens mit Freigabe- und Start-Aktion
- Inventar-Menü um Bestandsbewegungen erweitert
- Produktionsmenü zeigt jetzt Auftragsliste statt direkt Create
- OpenAPI-Typen regeneriert (StockMovementResponse, StartProductionOrderRequest, batchId in ProductionOrderResponse)
This commit is contained in:
Sebastian Frick 2026-02-25 12:36:42 +01:00
parent 0474b5fa93
commit 7d721f9ef0
18 changed files with 1279 additions and 9 deletions

View file

@ -0,0 +1,185 @@
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 { useProductionOrders } from '../../hooks/useProductionOrders.js';
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
import { SuccessDisplay } from '../shared/SuccessDisplay.js';
import { PRODUCTION_ORDER_STATUS_LABELS, PRIORITY_LABELS } from '@effigenix/api-client';
import type { ProductionOrderStatus, Priority } from '@effigenix/api-client';
const STATUS_COLORS: Record<string, string> = {
CREATED: 'gray',
RELEASED: 'yellow',
IN_PRODUCTION: 'blue',
COMPLETED: 'green',
CANCELLED: 'red',
};
type Mode = 'view' | 'menu' | 'start-batch-input';
export function ProductionOrderDetailScreen() {
const { params, back } = useNavigation();
const {
productionOrder, loading, error,
fetchProductionOrder, releaseProductionOrder, startProductionOrder, clearError,
} = useProductionOrders();
const [mode, setMode] = useState<Mode>('view');
const [menuIndex, setMenuIndex] = useState(0);
const [batchId, setBatchId] = useState('');
const [success, setSuccess] = useState<string | null>(null);
const orderId = params.orderId ?? '';
useEffect(() => {
if (orderId) void fetchProductionOrder(orderId);
}, [fetchProductionOrder, orderId]);
const getMenuItems = () => {
const items: { label: string; action: string }[] = [];
const status = productionOrder?.status;
if (status === 'CREATED') {
items.push({ label: 'Freigeben', action: 'release' });
}
if (status === 'RELEASED') {
items.push({ label: 'Produktion starten', action: 'start' });
}
return items;
};
const menuItems = getMenuItems();
const handleRelease = async () => {
const result = await releaseProductionOrder(orderId);
if (result) {
setSuccess('Produktionsauftrag freigegeben.');
setMode('view');
}
};
const handleStart = async () => {
if (!batchId.trim()) return;
const result = await startProductionOrder(orderId, { batchId: batchId.trim() });
if (result) {
setSuccess('Produktion gestartet.');
setMode('view');
setBatchId('');
}
};
useInput((_input, key) => {
if (loading) return;
if (mode === 'start-batch-input') {
if (key.escape) setMode('menu');
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]) {
const action = menuItems[menuIndex].action;
if (action === 'release') void handleRelease();
if (action === 'start') {
setMode('start-batch-input');
setBatchId('');
}
}
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 && !productionOrder) return <LoadingSpinner label="Lade Auftrag..." />;
if (!productionOrder) {
return (
<Box flexDirection="column">
{error && <ErrorDisplay message={error} onDismiss={clearError} />}
<Text color="red">Produktionsauftrag nicht gefunden.</Text>
</Box>
);
}
const statusLabel = PRODUCTION_ORDER_STATUS_LABELS[(productionOrder.status ?? '') as ProductionOrderStatus] ?? productionOrder.status;
const statusColor = STATUS_COLORS[productionOrder.status ?? ''] ?? 'white';
const prioLabel = PRIORITY_LABELS[(productionOrder.priority ?? '') as Priority] ?? productionOrder.priority;
const createdAt = productionOrder.createdAt
? new Date(productionOrder.createdAt).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' })
: '';
const updatedAt = productionOrder.updatedAt
? new Date(productionOrder.updatedAt).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' })
: '';
return (
<Box flexDirection="column" gap={1}>
<Box gap={2}>
<Text color="cyan" bold>Produktionsauftrag</Text>
{loading && <Text color="gray"> (aktualisiere...)</Text>}
</Box>
{error && <ErrorDisplay message={error} onDismiss={clearError} />}
{success && <SuccessDisplay message={success} onDismiss={() => setSuccess(null)} />}
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
<Box><Text color="gray">ID: </Text><Text>{productionOrder.id}</Text></Box>
<Box><Text color="gray">Rezept-ID: </Text><Text>{productionOrder.recipeId}</Text></Box>
<Box><Text color="gray">Status: </Text><Text color={statusColor}>{statusLabel}</Text></Box>
<Box><Text color="gray">Menge: </Text><Text>{productionOrder.plannedQuantity} {productionOrder.plannedQuantityUnit}</Text></Box>
<Box><Text color="gray">Geplant am: </Text><Text>{productionOrder.plannedDate}</Text></Box>
<Box><Text color="gray">Priorität: </Text><Text>{prioLabel}</Text></Box>
{productionOrder.batchId && (
<Box><Text color="gray">Chargen-ID: </Text><Text>{productionOrder.batchId}</Text></Box>
)}
{productionOrder.notes && (
<Box><Text color="gray">Notizen: </Text><Text>{productionOrder.notes}</Text></Box>
)}
<Box><Text color="gray">Erstellt: </Text><Text>{createdAt}</Text></Box>
<Box><Text color="gray">Aktualisiert:</Text><Text> {updatedAt}</Text></Box>
</Box>
{mode === 'menu' && (
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1}>
<Text color="cyan" bold>Aktionen</Text>
{menuItems.map((item, i) => (
<Text key={item.action} color={i === menuIndex ? 'cyan' : 'white'}>
{i === menuIndex ? '▶ ' : ' '}{item.label}
</Text>
))}
<Text color="gray" dimColor> nav · Enter ausführen · Escape zurück</Text>
</Box>
)}
{mode === 'start-batch-input' && (
<Box flexDirection="column" borderStyle="round" borderColor="yellow" paddingX={1}>
<Text color="yellow" bold>Chargen-ID eingeben:</Text>
<Box>
<Text color="gray"> </Text>
<TextInput
value={batchId}
onChange={setBatchId}
onSubmit={() => void handleStart()}
focus={true}
/>
</Box>
<Text color="gray" dimColor>Enter bestätigen · Escape abbrechen</Text>
</Box>
)}
<Box marginTop={1}>
<Text color="gray" dimColor>
{menuItems.length > 0 ? '[m] Aktionsmenü · ' : ''}Backspace Zurück
</Text>
</Box>
</Box>
);
}