1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 08:29:36 +01:00
effigenix/frontend/apps/cli/src/components/production/ProductionOrderDetailScreen.tsx
Sebastian Frick 26adf21162 fix(tui,seed): Seed-Batch-Nummern korrigieren und Produktionsergebnis anzeigen
Seed-Daten: BW-260223-01 → P-2026-02-23-001, LW-260222-01 → P-2026-02-22-001,
damit die Chargennummern dem BatchNumber-VO-Format entsprechen.

ProductionOrderDetailScreen: Rezeptname statt ID anzeigen, Batch-Daten
(Soll-/Ist-Menge, Ausschuss, Bemerkungen) bei verknüpfter Charge laden.
2026-02-26 08:52:00 +01:00

260 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useCallback } 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 { useRecipeNameLookup } from '../../hooks/useRecipeNameLookup.js';
import { client } from '../../utils/api-client.js';
import type { BatchDTO } from '@effigenix/api-client';
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> = {
PLANNED: 'gray',
RELEASED: 'yellow',
IN_PROGRESS: 'blue',
COMPLETED: 'green',
CANCELLED: 'red',
};
type Mode = 'view' | 'menu' | 'start-batch-input' | 'reschedule-input';
export function ProductionOrderDetailScreen() {
const { params, back } = useNavigation();
const {
productionOrder, loading, error,
fetchProductionOrder, releaseProductionOrder, rescheduleProductionOrder, startProductionOrder, clearError,
} = useProductionOrders();
const { recipeName } = useRecipeNameLookup();
const [mode, setMode] = useState<Mode>('view');
const [menuIndex, setMenuIndex] = useState(0);
const [batchId, setBatchId] = useState('');
const [newDate, setNewDate] = useState('');
const [success, setSuccess] = useState<string | null>(null);
const [batch, setBatch] = useState<BatchDTO | null>(null);
const orderId = params.orderId ?? '';
useEffect(() => {
if (orderId) void fetchProductionOrder(orderId);
}, [fetchProductionOrder, orderId]);
const loadBatch = useCallback(async (id: string) => {
try {
const b = await client.batches.getById(id);
setBatch(b);
} catch (err) {
setBatch(null);
}
}, []);
useEffect(() => {
if (productionOrder?.batchId) void loadBatch(productionOrder.batchId);
}, [loadBatch, productionOrder?.batchId]);
const getMenuItems = () => {
const items: { label: string; action: string }[] = [];
const status = productionOrder?.status;
if (status === 'PLANNED') {
items.push({ label: 'Freigeben', action: 'release' });
items.push({ label: 'Umterminieren', action: 'reschedule' });
}
if (status === 'RELEASED') {
items.push({ label: 'Produktion starten', action: 'start' });
items.push({ label: 'Umterminieren', action: 'reschedule' });
}
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('');
}
};
const handleReschedule = async () => {
if (!newDate.trim()) return;
const result = await rescheduleProductionOrder(orderId, newDate.trim());
if (result) {
setSuccess(`Umterminiert auf ${newDate.trim()}.`);
setMode('view');
setNewDate('');
}
};
useInput((_input, key) => {
if (loading) return;
if (mode === 'reschedule-input') {
if (key.escape) setMode('menu');
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 === 'reschedule') {
setMode('reschedule-input');
setNewDate('');
}
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: </Text><Text>{recipeName(productionOrder.recipeId ?? '') ?? productionOrder.recipeId}</Text><Text color="gray" dimColor> ({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-Nr: </Text><Text>{batch?.batchNumber ?? 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>
{batch && productionOrder.batchId && (
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
<Text color="cyan" bold>Produktionsergebnis</Text>
<Box><Text color="gray">Soll-Menge: </Text><Text>{batch.plannedQuantity} {batch.plannedQuantityUnit}</Text></Box>
{batch.actualQuantity && (
<Box><Text color="gray">Ist-Menge: </Text><Text color="green">{batch.actualQuantity} {batch.actualQuantityUnit}</Text></Box>
)}
{batch.waste && (
<Box><Text color="gray">Ausschuss: </Text><Text color="red">{batch.waste} {batch.wasteUnit}</Text></Box>
)}
{batch.remarks && (
<Box><Text color="gray">Bemerkungen: </Text><Text>{batch.remarks}</Text></Box>
)}
{batch.completedAt && (
<Box><Text color="gray">Abgeschl. am:</Text><Text> {new Date(batch.completedAt).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' })}</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>
)}
{mode === 'reschedule-input' && (
<Box flexDirection="column" borderStyle="round" borderColor="yellow" paddingX={1}>
<Text color="yellow" bold>Neues Datum (YYYY-MM-DD):</Text>
<Box>
<Text color="gray"> </Text>
<TextInput
value={newDate}
onChange={setNewDate}
onSubmit={() => void handleReschedule()}
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>
);
}