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

feat(inventory): Inventur abbrechen und nach Status filtern (US-6.4)

Ermöglicht das Abbrechen von Inventuren (OPEN/COUNTING → CANCELLED) mit
Pflicht-Begründung sowie das Filtern der Inventurliste nach Status.
This commit is contained in:
Sebastian Frick 2026-03-19 11:39:56 +01:00
parent 58ed0a3810
commit a0ebf46329
28 changed files with 798 additions and 47 deletions

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { Box, Text, useInput } from 'ink';
import TextInput from 'ink-text-input';
import { useNavigation } from '../../state/navigation-context.js';
@ -11,7 +11,7 @@ import { SuccessDisplay } from '../shared/SuccessDisplay.js';
import { INVENTORY_COUNT_STATUS_LABELS } from '@effigenix/api-client';
import type { InventoryCountStatus, CountItemDTO } from '@effigenix/api-client';
type Mode = 'view' | 'menu' | 'record-item' | 'record-amount' | 'confirm-start' | 'confirm-complete';
type Mode = 'view' | 'menu' | 'record-item' | 'record-amount' | 'confirm-start' | 'confirm-complete' | 'cancel-reason' | 'confirm-cancel';
const STATUS_COLORS: Record<InventoryCountStatus, string> = {
OPEN: 'yellow',
@ -33,6 +33,7 @@ export function InventoryCountDetailScreen() {
startInventoryCount,
recordCountItem,
completeInventoryCount,
cancelInventoryCount,
clearError,
} = useInventoryCounts();
const { articleName, locationName } = useStockNameLookup();
@ -42,6 +43,7 @@ export function InventoryCountDetailScreen() {
const [itemIndex, setItemIndex] = useState(0);
const [amount, setAmount] = useState('');
const [success, setSuccess] = useState<string | null>(null);
const [cancelReason, setCancelReason] = useState('');
const countId = params.inventoryCountId ?? '';
@ -61,12 +63,14 @@ export function InventoryCountDetailScreen() {
const actions: { label: string; action: string }[] = [];
if (inventoryCount.status === 'OPEN') {
actions.push({ label: 'Zählung starten', action: 'start' });
actions.push({ label: 'Inventur abbrechen', action: 'cancel' });
}
if (inventoryCount.status === 'COUNTING') {
actions.push({ label: 'Position erfassen', action: 'record' });
if (allCounted && isDifferentUser) {
actions.push({ label: 'Inventur abschließen', action: 'complete' });
}
actions.push({ label: 'Inventur abbrechen', action: 'cancel' });
}
return actions;
};
@ -108,6 +112,16 @@ export function InventoryCountDetailScreen() {
}
};
const handleCancel = async () => {
if (!cancelReason.trim()) return;
const result = await cancelInventoryCount(countId, cancelReason.trim());
if (result) {
setSuccess('Inventur abgebrochen.');
setCancelReason('');
setMode('view');
}
};
useInput((input, key) => {
if (loading) return;
@ -116,6 +130,17 @@ export function InventoryCountDetailScreen() {
return;
}
if (mode === 'cancel-reason') {
if (key.escape) { setCancelReason(''); setMode('view'); }
return;
}
if (mode === 'confirm-cancel') {
if (input.toLowerCase() === 'j') void handleCancel();
if (input.toLowerCase() === 'n' || key.escape) setMode('view');
return;
}
if (mode === 'confirm-start') {
if (input.toLowerCase() === 'j') void handleStart();
if (input.toLowerCase() === 'n' || key.escape) setMode('view');
@ -147,6 +172,7 @@ export function InventoryCountDetailScreen() {
if (action === 'start') setMode('confirm-start');
else if (action === 'record') { setMode('record-item'); setItemIndex(0); }
else if (action === 'complete') setMode('confirm-complete');
else if (action === 'cancel') { setCancelReason(''); setMode('cancel-reason'); }
}
if (key.escape) setMode('view');
return;
@ -207,6 +233,9 @@ export function InventoryCountDetailScreen() {
{inventoryCount.completedBy && (
<Box><Text color="gray">Abgeschl. von: </Text><Text>{inventoryCount.completedBy}</Text></Box>
)}
{inventoryCount.cancellationReason && (
<Box><Text color="gray">Abbruchgrund: </Text><Text color="red">{inventoryCount.cancellationReason}</Text></Box>
)}
<Box>
<Text color="gray">Fortschritt: </Text>
<Text color={allCounted ? 'green' : 'yellow'}>{progressBar} {countedCount}/{items.length} ({progressPct}%)</Text>
@ -311,6 +340,33 @@ export function InventoryCountDetailScreen() {
</Box>
)}
{/* Cancel Reason */}
{mode === 'cancel-reason' && (
<Box flexDirection="column" borderStyle="round" borderColor="red" paddingX={1}>
<Text color="red" bold>Inventur abbrechen</Text>
<Text color="gray">Bitte Grund eingeben:</Text>
<Box>
<Text color="gray"> </Text>
<TextInput
value={cancelReason}
onChange={setCancelReason}
onSubmit={() => { if (cancelReason.trim()) setMode('confirm-cancel'); }}
focus={true}
/>
</Box>
<Text color="gray" dimColor>Enter bestätigen · Escape abbrechen</Text>
</Box>
)}
{/* Confirm Cancel */}
{mode === 'confirm-cancel' && (
<Box flexDirection="column" borderStyle="round" borderColor="red" paddingX={1}>
<Text color="red" bold>Inventur wirklich abbrechen?</Text>
<Text>Grund: {cancelReason}</Text>
<Text color="gray" dimColor>[J] Ja · [N] Nein</Text>
</Box>
)}
{/* Four-eyes hint */}
{inventoryCount.status === 'COUNTING' && allCounted && !isDifferentUser && (
<Box>

View file

@ -94,6 +94,18 @@ export function useInventoryCounts() {
}
}, []);
const cancelInventoryCount = useCallback(async (id: string, reason: string) => {
setState((s) => ({ ...s, loading: true, error: null }));
try {
const inventoryCount = await client.inventoryCounts.cancel(id, { reason });
setState((s) => ({ ...s, inventoryCount, loading: false, error: null }));
return inventoryCount;
} catch (err) {
setState((s) => ({ ...s, loading: false, error: errorMessage(err) }));
return null;
}
}, []);
const clearError = useCallback(() => {
setState((s) => ({ ...s, error: null }));
}, []);
@ -106,6 +118,7 @@ export function useInventoryCounts() {
startInventoryCount,
recordCountItem,
completeInventoryCount,
cancelInventoryCount,
clearError,
};
}