mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 12:09:35 +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:
parent
58ed0a3810
commit
a0ebf46329
28 changed files with 798 additions and 47 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue