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:
parent
0474b5fa93
commit
7d721f9ef0
18 changed files with 1279 additions and 9 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue