mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 17:19:56 +01:00
feat(tui): neue Backend-Features anbinden und Status-Werte korrigieren
TUI-Anbindung für Reservierung bestätigen (US-4.3), Produktionsauftrag
umterminieren und filtern (US-P17). Status-Werte CREATED→PLANNED und
IN_PRODUCTION→IN_PROGRESS korrigiert. Fehlenden GET /{id} Endpoint für
Produktionsaufträge im Backend ergänzt.
This commit is contained in:
parent
8a84bf5f25
commit
417f8fcdae
14 changed files with 467 additions and 27 deletions
|
|
@ -10,7 +10,7 @@ import { SuccessDisplay } from '../shared/SuccessDisplay.js';
|
|||
import { STOCK_BATCH_STATUS_LABELS, REFERENCE_TYPE_LABELS, RESERVATION_PRIORITY_LABELS } from '@effigenix/api-client';
|
||||
import type { StockBatchStatus, ReferenceType, ReservationPriority, ReservationDTO, StockBatchDTO } from '@effigenix/api-client';
|
||||
|
||||
type Mode = 'view' | 'menu' | 'batch-actions' | 'block-reason' | 'remove-amount' | 'remove-unit' | 'reservation-actions' | 'confirm-release-reservation';
|
||||
type Mode = 'view' | 'menu' | 'batch-actions' | 'block-reason' | 'remove-amount' | 'remove-unit' | 'reservation-actions' | 'confirm-release-reservation' | 'confirm-reservation-actions' | 'confirm-confirm-reservation';
|
||||
|
||||
const BATCH_STATUS_COLORS: Record<string, string> = {
|
||||
AVAILABLE: 'green',
|
||||
|
|
@ -21,7 +21,7 @@ const BATCH_STATUS_COLORS: Record<string, string> = {
|
|||
|
||||
export function StockDetailScreen() {
|
||||
const { params, back, navigate } = useNavigation();
|
||||
const { stock, loading, error, fetchStock, blockBatch, unblockBatch, removeBatch, releaseReservation, clearError } = useStocks();
|
||||
const { stock, loading, error, fetchStock, blockBatch, unblockBatch, removeBatch, releaseReservation, confirmReservation, clearError } = useStocks();
|
||||
const { articleName, locationName } = useStockNameLookup();
|
||||
const [mode, setMode] = useState<Mode>('view');
|
||||
const [menuIndex, setMenuIndex] = useState(0);
|
||||
|
|
@ -31,6 +31,7 @@ export function StockDetailScreen() {
|
|||
const [removeAmount, setRemoveAmount] = useState('');
|
||||
const [removeUnit, setRemoveUnit] = useState('');
|
||||
const [reservationIndex, setReservationIndex] = useState(0);
|
||||
const [confirmResIndex, setConfirmResIndex] = useState(0);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
const stockId = params.stockId ?? '';
|
||||
|
|
@ -43,6 +44,7 @@ export function StockDetailScreen() {
|
|||
const selectedBatch = batches[batchIndex];
|
||||
const reservations: ReservationDTO[] = (stock as { reservations?: ReservationDTO[] })?.reservations ?? [];
|
||||
const selectedReservation = reservations[reservationIndex];
|
||||
const selectedConfirmReservation = reservations[confirmResIndex];
|
||||
|
||||
const getBatchActions = () => {
|
||||
if (!selectedBatch) return [];
|
||||
|
|
@ -119,10 +121,21 @@ export function StockDetailScreen() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleConfirmReservation = async () => {
|
||||
if (!selectedConfirmReservation?.id) return;
|
||||
const result = await confirmReservation(stockId, selectedConfirmReservation.id);
|
||||
if (result) {
|
||||
setSuccess('Reservierung bestätigt – Material entnommen.');
|
||||
setMode('view');
|
||||
setConfirmResIndex(0);
|
||||
}
|
||||
};
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{ label: 'Chargen verwalten', action: 'batches' },
|
||||
{ label: 'Bestand reservieren', action: 'reserve' },
|
||||
...(reservations.length > 0 ? [{ label: 'Reservierung freigeben', action: 'release-reservation' }] : []),
|
||||
...(reservations.length > 0 ? [{ label: 'Reservierung bestätigen', action: 'confirm-reservation' }] : []),
|
||||
];
|
||||
|
||||
useInput((input, key) => {
|
||||
|
|
@ -143,6 +156,24 @@ export function StockDetailScreen() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (mode === 'confirm-confirm-reservation') {
|
||||
if (input.toLowerCase() === 'j') {
|
||||
void handleConfirmReservation();
|
||||
}
|
||||
if (input.toLowerCase() === 'n' || key.escape) setMode('confirm-reservation-actions');
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'confirm-reservation-actions') {
|
||||
if (key.upArrow) setConfirmResIndex((i) => Math.max(0, i - 1));
|
||||
if (key.downArrow) setConfirmResIndex((i) => Math.min(reservations.length - 1, i + 1));
|
||||
if (key.return && reservations[confirmResIndex]) {
|
||||
setMode('confirm-confirm-reservation');
|
||||
}
|
||||
if (key.escape) setMode('view');
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'confirm-release-reservation') {
|
||||
if (input.toLowerCase() === 'j') {
|
||||
void handleReleaseReservation();
|
||||
|
|
@ -187,6 +218,9 @@ export function StockDetailScreen() {
|
|||
} else if (action === 'release-reservation') {
|
||||
setMode('reservation-actions');
|
||||
setReservationIndex(0);
|
||||
} else if (action === 'confirm-reservation') {
|
||||
setMode('confirm-reservation-actions');
|
||||
setConfirmResIndex(0);
|
||||
}
|
||||
}
|
||||
if (key.escape) setMode('view');
|
||||
|
|
@ -281,7 +315,7 @@ export function StockDetailScreen() {
|
|||
<Text color="gray" bold>Reserviert am</Text>
|
||||
</Box>
|
||||
{reservations.map((r, i) => {
|
||||
const isSelected = mode === 'reservation-actions' && i === reservationIndex;
|
||||
const isSelected = (mode === 'reservation-actions' && i === reservationIndex) || (mode === 'confirm-reservation-actions' && i === confirmResIndex);
|
||||
const textColor = isSelected ? 'cyan' : 'white';
|
||||
const refLabel = REFERENCE_TYPE_LABELS[(r.referenceType ?? '') as ReferenceType] ?? r.referenceType;
|
||||
const prioLabel = RESERVATION_PRIORITY_LABELS[(r.priority ?? '') as ReservationPriority] ?? r.priority;
|
||||
|
|
@ -393,6 +427,22 @@ export function StockDetailScreen() {
|
|||
</Box>
|
||||
)}
|
||||
|
||||
{mode === 'confirm-reservation-actions' && (
|
||||
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1}>
|
||||
<Text color="cyan" bold>Reservierung zum Bestätigen auswählen</Text>
|
||||
<Text color="gray" dimColor>↑↓ Reservierung wählen · Enter bestätigen · Escape zurück</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{mode === 'confirm-confirm-reservation' && selectedConfirmReservation && (
|
||||
<Box flexDirection="column" borderStyle="round" borderColor="yellow" paddingX={1}>
|
||||
<Text color="yellow" bold>Reservierung bestätigen – Material entnehmen?</Text>
|
||||
<Text>Referenz: {selectedConfirmReservation.referenceType} / {selectedConfirmReservation.referenceId}</Text>
|
||||
<Text>Menge: {selectedConfirmReservation.quantityAmount} {selectedConfirmReservation.quantityUnit}</Text>
|
||||
<Text color="gray" dimColor>[J] Ja · [N] Nein</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>
|
||||
[m] Aktionsmenü · Backspace Zurück
|
||||
|
|
|
|||
|
|
@ -10,24 +10,25 @@ import { PRODUCTION_ORDER_STATUS_LABELS, PRIORITY_LABELS } from '@effigenix/api-
|
|||
import type { ProductionOrderStatus, Priority } from '@effigenix/api-client';
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
CREATED: 'gray',
|
||||
PLANNED: 'gray',
|
||||
RELEASED: 'yellow',
|
||||
IN_PRODUCTION: 'blue',
|
||||
IN_PROGRESS: 'blue',
|
||||
COMPLETED: 'green',
|
||||
CANCELLED: 'red',
|
||||
};
|
||||
|
||||
type Mode = 'view' | 'menu' | 'start-batch-input';
|
||||
type Mode = 'view' | 'menu' | 'start-batch-input' | 'reschedule-input';
|
||||
|
||||
export function ProductionOrderDetailScreen() {
|
||||
const { params, back } = useNavigation();
|
||||
const {
|
||||
productionOrder, loading, error,
|
||||
fetchProductionOrder, releaseProductionOrder, startProductionOrder, clearError,
|
||||
fetchProductionOrder, releaseProductionOrder, rescheduleProductionOrder, startProductionOrder, clearError,
|
||||
} = useProductionOrders();
|
||||
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 orderId = params.orderId ?? '';
|
||||
|
|
@ -39,11 +40,13 @@ export function ProductionOrderDetailScreen() {
|
|||
const getMenuItems = () => {
|
||||
const items: { label: string; action: string }[] = [];
|
||||
const status = productionOrder?.status;
|
||||
if (status === 'CREATED') {
|
||||
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;
|
||||
};
|
||||
|
|
@ -68,9 +71,24 @@ export function ProductionOrderDetailScreen() {
|
|||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
|
@ -82,6 +100,10 @@ export function ProductionOrderDetailScreen() {
|
|||
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('');
|
||||
|
|
@ -175,6 +197,22 @@ export function ProductionOrderDetailScreen() {
|
|||
</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
|
||||
|
|
|
|||
|
|
@ -1,16 +1,25 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
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 { PRODUCTION_ORDER_STATUS_LABELS, PRIORITY_LABELS } from '@effigenix/api-client';
|
||||
import type { ProductionOrderStatus, Priority } from '@effigenix/api-client';
|
||||
import type { ProductionOrderStatus, Priority, ProductionOrderFilter } from '@effigenix/api-client';
|
||||
|
||||
const STATUS_FILTER_KEYS: Record<string, { status?: ProductionOrderStatus; label: string }> = {
|
||||
a: { label: 'Alle' },
|
||||
p: { status: 'PLANNED', label: 'Geplant' },
|
||||
f: { status: 'RELEASED', label: 'Freigegeben' },
|
||||
i: { status: 'IN_PROGRESS', label: 'In Produktion' },
|
||||
c: { status: 'COMPLETED', label: 'Abgeschlossen' },
|
||||
x: { status: 'CANCELLED', label: 'Storniert' },
|
||||
};
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
CREATED: 'gray',
|
||||
PLANNED: 'gray',
|
||||
RELEASED: 'yellow',
|
||||
IN_PRODUCTION: 'blue',
|
||||
IN_PROGRESS: 'blue',
|
||||
COMPLETED: 'green',
|
||||
CANCELLED: 'red',
|
||||
};
|
||||
|
|
@ -19,6 +28,14 @@ export function ProductionOrderListScreen() {
|
|||
const { navigate, back } = useNavigation();
|
||||
const { productionOrders, loading, error, fetchProductionOrders, clearError } = useProductionOrders();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [activeFilter, setActiveFilter] = useState<{ status?: ProductionOrderStatus; label: string }>({ label: 'Alle' });
|
||||
|
||||
const loadWithFilter = useCallback((filter: { status?: ProductionOrderStatus; label: string }) => {
|
||||
setActiveFilter(filter);
|
||||
setSelectedIndex(0);
|
||||
const f: ProductionOrderFilter | undefined = filter.status ? { status: filter.status } : undefined;
|
||||
void fetchProductionOrders(f);
|
||||
}, [fetchProductionOrders]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchProductionOrders();
|
||||
|
|
@ -35,7 +52,11 @@ export function ProductionOrderListScreen() {
|
|||
if (order?.id) navigate('production-order-detail', { orderId: order.id });
|
||||
}
|
||||
if (input === 'n') navigate('production-order-create');
|
||||
if (input === 'r') void fetchProductionOrders();
|
||||
if (input === 'r') loadWithFilter(activeFilter);
|
||||
|
||||
const filterDef = STATUS_FILTER_KEYS[input];
|
||||
if (filterDef) loadWithFilter(filterDef);
|
||||
|
||||
if (key.backspace || key.escape) back();
|
||||
});
|
||||
|
||||
|
|
@ -44,6 +65,7 @@ export function ProductionOrderListScreen() {
|
|||
<Box gap={2}>
|
||||
<Text color="cyan" bold>Produktionsaufträge</Text>
|
||||
<Text color="gray" dimColor>({productionOrders.length})</Text>
|
||||
{activeFilter.status && <Text color="yellow">[{activeFilter.label}]</Text>}
|
||||
</Box>
|
||||
|
||||
{loading && <LoadingSpinner label="Lade Aufträge..." />}
|
||||
|
|
@ -86,7 +108,7 @@ export function ProductionOrderListScreen() {
|
|||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>
|
||||
↑↓ nav · Enter Details · [n] Neu · [r] Aktualisieren · Backspace Zurück
|
||||
↑↓ nav · Enter Details · [n] Neu · [r] Aktualisieren · [a]lle [p]lan [f]rei [i]n Prod [c]omp [x]storno · Bksp Zurück
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import type { ProductionOrderDTO, CreateProductionOrderRequest, StartProductionOrderRequest } from '@effigenix/api-client';
|
||||
import type { ProductionOrderDTO, CreateProductionOrderRequest, StartProductionOrderRequest, ProductionOrderFilter } from '@effigenix/api-client';
|
||||
import { client } from '../utils/api-client.js';
|
||||
|
||||
interface ProductionOrdersState {
|
||||
|
|
@ -21,10 +21,10 @@ export function useProductionOrders() {
|
|||
error: null,
|
||||
});
|
||||
|
||||
const fetchProductionOrders = useCallback(async () => {
|
||||
const fetchProductionOrders = useCallback(async (filter?: ProductionOrderFilter) => {
|
||||
setState((s) => ({ ...s, loading: true, error: null }));
|
||||
try {
|
||||
const productionOrders = await client.productionOrders.list();
|
||||
const productionOrders = await client.productionOrders.list(filter);
|
||||
setState((s) => ({ ...s, productionOrders, loading: false, error: null }));
|
||||
} catch (err) {
|
||||
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||
|
|
@ -65,6 +65,18 @@ export function useProductionOrders() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const rescheduleProductionOrder = useCallback(async (id: string, newPlannedDate: string) => {
|
||||
setState((s) => ({ ...s, loading: true, error: null }));
|
||||
try {
|
||||
const productionOrder = await client.productionOrders.reschedule(id, { newPlannedDate });
|
||||
setState((s) => ({ ...s, productionOrder, loading: false, error: null }));
|
||||
return productionOrder;
|
||||
} catch (err) {
|
||||
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const startProductionOrder = useCallback(async (id: string, request: StartProductionOrderRequest) => {
|
||||
setState((s) => ({ ...s, loading: true, error: null }));
|
||||
try {
|
||||
|
|
@ -87,6 +99,7 @@ export function useProductionOrders() {
|
|||
fetchProductionOrder,
|
||||
createProductionOrder,
|
||||
releaseProductionOrder,
|
||||
rescheduleProductionOrder,
|
||||
startProductionOrder,
|
||||
clearError,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -137,6 +137,19 @@ export function useStocks() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const confirmReservation = useCallback(async (stockId: string, reservationId: string) => {
|
||||
setState((s) => ({ ...s, loading: true, error: null }));
|
||||
try {
|
||||
await client.stocks.confirmReservation(stockId, reservationId);
|
||||
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 releaseReservation = useCallback(async (stockId: string, reservationId: string) => {
|
||||
setState((s) => ({ ...s, loading: true, error: null }));
|
||||
try {
|
||||
|
|
@ -166,6 +179,7 @@ export function useStocks() {
|
|||
unblockBatch,
|
||||
reserveStock,
|
||||
releaseReservation,
|
||||
confirmReservation,
|
||||
clearError,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue