mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 10:19:35 +01:00
feat(tui): Stock-Suche mit Namensanzeige für Bestände
StockPicker-Komponente für Bestandssuche nach Artikel-/Lagerort-Namen. StockBatchEntryScreen nutzt StockPicker statt manueller UUID-Eingabe. StockListScreen mit Suchfilter [s] und Namensanzeige statt IDs. StockDetailScreen zeigt Artikel-/Lagerort-Namen im Header.
This commit is contained in:
parent
376557925a
commit
e25d4437d9
5 changed files with 281 additions and 47 deletions
|
|
@ -1,49 +1,61 @@
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Box, Text, useInput } from 'ink';
|
import { Box, Text, useInput } from 'ink';
|
||||||
import { useNavigation } from '../../state/navigation-context.js';
|
import { useNavigation } from '../../state/navigation-context.js';
|
||||||
import { FormInput } from '../shared/FormInput.js';
|
import { useStocks } from '../../hooks/useStocks.js';
|
||||||
|
import { useStockNameLookup } from '../../hooks/useStockNameLookup.js';
|
||||||
|
import { StockPicker } from '../shared/StockPicker.js';
|
||||||
|
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||||
|
|
||||||
export function StockBatchEntryScreen() {
|
export function StockBatchEntryScreen() {
|
||||||
const { navigate, back } = useNavigation();
|
const { navigate, back } = useNavigation();
|
||||||
const [stockId, setStockId] = useState('');
|
const { stocks, loading, fetchStocks } = useStocks();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const { articleName, locationName } = useStockNameLookup();
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
const [selectedLabel, setSelectedLabel] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const handleChange = (value: string) => {
|
useEffect(() => {
|
||||||
setStockId(value);
|
void fetchStocks();
|
||||||
if (error) setError(null);
|
}, [fetchStocks]);
|
||||||
};
|
|
||||||
|
|
||||||
useInput((_input, key) => {
|
useInput((_input, key) => {
|
||||||
if (key.escape || key.backspace) back();
|
if (key.escape) back();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (!stockId.trim()) {
|
|
||||||
setError('Bestand-ID ist erforderlich.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigate('stock-add-batch', { stockId: stockId.trim() });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
<Text color="cyan" bold>Charge einbuchen</Text>
|
<Text color="cyan" bold>Charge einbuchen</Text>
|
||||||
<Text color="gray">Geben Sie die Bestand-ID ein, um eine neue Charge einzubuchen.</Text>
|
<Text color="gray">Suchen Sie den Bestand, um eine neue Charge einzubuchen.</Text>
|
||||||
|
|
||||||
<Box flexDirection="column" width={60}>
|
{loading && <LoadingSpinner label="Lade Bestände..." />}
|
||||||
<FormInput
|
|
||||||
label="Bestand-ID *"
|
{!loading && (
|
||||||
value={stockId}
|
<Box flexDirection="column" width={80}>
|
||||||
onChange={handleChange}
|
<StockPicker
|
||||||
onSubmit={handleSubmit}
|
stocks={stocks}
|
||||||
focus={true}
|
query={query}
|
||||||
{...(error ? { error } : {})}
|
onQueryChange={(q) => {
|
||||||
/>
|
setQuery(q);
|
||||||
</Box>
|
setSelectedId(null);
|
||||||
|
setSelectedLabel(undefined);
|
||||||
|
}}
|
||||||
|
onSelect={(stock) => {
|
||||||
|
setSelectedId(stock.id);
|
||||||
|
setSelectedLabel(`${articleName(stock.articleId)} | ${locationName(stock.storageLocationId)}`);
|
||||||
|
setQuery('');
|
||||||
|
navigate('stock-add-batch', { stockId: stock.id });
|
||||||
|
}}
|
||||||
|
focus={!selectedId}
|
||||||
|
selectedLabel={selectedLabel}
|
||||||
|
articleName={articleName}
|
||||||
|
locationName={locationName}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text color="gray" dimColor>
|
<Text color="gray" dimColor>
|
||||||
Enter Weiter · Escape Zurück
|
↑↓ nav · Enter Auswählen · Escape Zurück
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@ import { Box, Text, useInput } from 'ink';
|
||||||
import TextInput from 'ink-text-input';
|
import TextInput from 'ink-text-input';
|
||||||
import { useNavigation } from '../../state/navigation-context.js';
|
import { useNavigation } from '../../state/navigation-context.js';
|
||||||
import { useStocks } from '../../hooks/useStocks.js';
|
import { useStocks } from '../../hooks/useStocks.js';
|
||||||
|
import { useStockNameLookup } from '../../hooks/useStockNameLookup.js';
|
||||||
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||||
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||||
import { SuccessDisplay } from '../shared/SuccessDisplay.js';
|
import { SuccessDisplay } from '../shared/SuccessDisplay.js';
|
||||||
import { STOCK_BATCH_STATUS_LABELS, REFERENCE_TYPE_LABELS, RESERVATION_PRIORITY_LABELS } from '@effigenix/api-client';
|
import { STOCK_BATCH_STATUS_LABELS, REFERENCE_TYPE_LABELS, RESERVATION_PRIORITY_LABELS } from '@effigenix/api-client';
|
||||||
import type { StockBatchStatus, ReferenceType, ReservationPriority } 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';
|
||||||
|
|
||||||
|
|
@ -21,6 +22,7 @@ const BATCH_STATUS_COLORS: Record<string, string> = {
|
||||||
export function StockDetailScreen() {
|
export function StockDetailScreen() {
|
||||||
const { params, back, navigate } = useNavigation();
|
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, clearError } = useStocks();
|
||||||
|
const { articleName, locationName } = useStockNameLookup();
|
||||||
const [mode, setMode] = useState<Mode>('view');
|
const [mode, setMode] = useState<Mode>('view');
|
||||||
const [menuIndex, setMenuIndex] = useState(0);
|
const [menuIndex, setMenuIndex] = useState(0);
|
||||||
const [batchIndex, setBatchIndex] = useState(0);
|
const [batchIndex, setBatchIndex] = useState(0);
|
||||||
|
|
@ -37,9 +39,9 @@ export function StockDetailScreen() {
|
||||||
if (stockId) void fetchStock(stockId);
|
if (stockId) void fetchStock(stockId);
|
||||||
}, [fetchStock, stockId]);
|
}, [fetchStock, stockId]);
|
||||||
|
|
||||||
const batches = stock?.batches ?? [];
|
const batches: StockBatchDTO[] = stock?.batches ?? [];
|
||||||
const selectedBatch = batches[batchIndex];
|
const selectedBatch = batches[batchIndex];
|
||||||
const reservations = stock?.reservations ?? [];
|
const reservations: ReservationDTO[] = (stock as { reservations?: ReservationDTO[] })?.reservations ?? [];
|
||||||
const selectedReservation = reservations[reservationIndex];
|
const selectedReservation = reservations[reservationIndex];
|
||||||
|
|
||||||
const getBatchActions = () => {
|
const getBatchActions = () => {
|
||||||
|
|
@ -221,8 +223,8 @@ export function StockDetailScreen() {
|
||||||
{success && <SuccessDisplay message={success} onDismiss={() => setSuccess(null)} />}
|
{success && <SuccessDisplay message={success} onDismiss={() => setSuccess(null)} />}
|
||||||
|
|
||||||
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
|
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
|
||||||
<Box><Text color="gray">Artikel: </Text><Text>{stock.articleId}</Text></Box>
|
<Box><Text color="gray">Artikel: </Text><Text>{articleName(stock.articleId)}</Text></Box>
|
||||||
<Box><Text color="gray">Lagerort: </Text><Text>{stock.storageLocationId}</Text></Box>
|
<Box><Text color="gray">Lagerort: </Text><Text>{locationName(stock.storageLocationId)}</Text></Box>
|
||||||
<Box><Text color="gray">Gesamtmenge: </Text><Text>{stock.totalQuantity}{stock.quantityUnit ? ` ${stock.quantityUnit}` : ''}</Text></Box>
|
<Box><Text color="gray">Gesamtmenge: </Text><Text>{stock.totalQuantity}{stock.quantityUnit ? ` ${stock.quantityUnit}` : ''}</Text></Box>
|
||||||
<Box><Text color="gray">Verfügbar: </Text><Text color="green">{stock.availableQuantity}{stock.quantityUnit ? ` ${stock.quantityUnit}` : ''}</Text></Box>
|
<Box><Text color="gray">Verfügbar: </Text><Text color="green">{stock.availableQuantity}{stock.quantityUnit ? ` ${stock.quantityUnit}` : ''}</Text></Box>
|
||||||
{stock.minimumLevel && (
|
{stock.minimumLevel && (
|
||||||
|
|
|
||||||
|
|
@ -2,30 +2,83 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { Box, Text, useInput } from 'ink';
|
import { Box, Text, useInput } from 'ink';
|
||||||
import { useNavigation } from '../../state/navigation-context.js';
|
import { useNavigation } from '../../state/navigation-context.js';
|
||||||
import { useStocks } from '../../hooks/useStocks.js';
|
import { useStocks } from '../../hooks/useStocks.js';
|
||||||
|
import { useStockNameLookup } from '../../hooks/useStockNameLookup.js';
|
||||||
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
|
||||||
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
|
||||||
|
|
||||||
export function StockListScreen() {
|
export function StockListScreen() {
|
||||||
const { navigate, back } = useNavigation();
|
const { navigate, back } = useNavigation();
|
||||||
const { stocks, loading, error, fetchStocks, clearError } = useStocks();
|
const { stocks, loading, error, fetchStocks, clearError } = useStocks();
|
||||||
|
const { articleName, locationName } = useStockNameLookup();
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
const [searchMode, setSearchMode] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void fetchStocks();
|
void fetchStocks();
|
||||||
}, [fetchStocks]);
|
}, [fetchStocks]);
|
||||||
|
|
||||||
|
const filtered = React.useMemo(() => {
|
||||||
|
if (!searchQuery) return stocks;
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
return stocks.filter(
|
||||||
|
(s) =>
|
||||||
|
articleName(s.articleId).toLowerCase().includes(q) ||
|
||||||
|
locationName(s.storageLocationId).toLowerCase().includes(q) ||
|
||||||
|
s.articleId.toLowerCase().includes(q) ||
|
||||||
|
s.storageLocationId.toLowerCase().includes(q),
|
||||||
|
);
|
||||||
|
}, [stocks, searchQuery, articleName, locationName]);
|
||||||
|
|
||||||
useInput((input, key) => {
|
useInput((input, key) => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
|
|
||||||
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
if (searchMode) {
|
||||||
if (key.downArrow) setSelectedIndex((i) => Math.min(stocks.length - 1, i + 1));
|
if (key.escape) {
|
||||||
|
setSearchMode(false);
|
||||||
|
setSearchQuery('');
|
||||||
|
setSelectedIndex(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.return && filtered.length > 0) {
|
||||||
|
const stock = filtered[selectedIndex];
|
||||||
|
if (stock) navigate('stock-detail', { stockId: stock.id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.upArrow) {
|
||||||
|
setSelectedIndex((i) => Math.max(0, i - 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.downArrow) {
|
||||||
|
setSelectedIndex((i) => Math.min(filtered.length - 1, i + 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.backspace || key.delete) {
|
||||||
|
setSearchQuery((q) => q.slice(0, -1));
|
||||||
|
setSelectedIndex(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.tab || key.ctrl || key.meta) return;
|
||||||
|
if (input && !key.upArrow && !key.downArrow) {
|
||||||
|
setSearchQuery((q) => q + input);
|
||||||
|
setSelectedIndex(0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (key.return && stocks.length > 0) {
|
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
||||||
const stock = stocks[selectedIndex];
|
if (key.downArrow) setSelectedIndex((i) => Math.min(filtered.length - 1, i + 1));
|
||||||
|
|
||||||
|
if (key.return && filtered.length > 0) {
|
||||||
|
const stock = filtered[selectedIndex];
|
||||||
if (stock) navigate('stock-detail', { stockId: stock.id });
|
if (stock) navigate('stock-detail', { stockId: stock.id });
|
||||||
}
|
}
|
||||||
if (input === 'n') navigate('stock-create');
|
if (input === 'n') navigate('stock-create');
|
||||||
if (input === 'r') void fetchStocks();
|
if (input === 'r') void fetchStocks();
|
||||||
|
if (input === 's' || input === '/') {
|
||||||
|
setSearchMode(true);
|
||||||
|
setSelectedIndex(0);
|
||||||
|
}
|
||||||
if (key.backspace || key.escape) back();
|
if (key.backspace || key.escape) back();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -33,27 +86,35 @@ export function StockListScreen() {
|
||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
<Box gap={2}>
|
<Box gap={2}>
|
||||||
<Text color="cyan" bold>Bestände</Text>
|
<Text color="cyan" bold>Bestände</Text>
|
||||||
<Text color="gray" dimColor>({stocks.length})</Text>
|
<Text color="gray" dimColor>({filtered.length}{searchQuery ? ` / ${stocks.length}` : ''})</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{searchMode && (
|
||||||
|
<Box>
|
||||||
|
<Text color="cyan">Suche: </Text>
|
||||||
|
<Text>{searchQuery || '▌'}</Text>
|
||||||
|
<Text color="gray" dimColor> (Escape zum Beenden)</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading && <LoadingSpinner label="Lade Bestände..." />}
|
{loading && <LoadingSpinner label="Lade Bestände..." />}
|
||||||
{error && !loading && <ErrorDisplay message={error} onDismiss={clearError} />}
|
{error && !loading && <ErrorDisplay message={error} onDismiss={clearError} />}
|
||||||
|
|
||||||
{!loading && !error && (
|
{!loading && !error && (
|
||||||
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
|
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
|
||||||
<Box paddingX={1}>
|
<Box paddingX={1}>
|
||||||
<Text color="gray" bold>{' Artikel'.padEnd(20)}</Text>
|
<Text color="gray" bold>{' Artikel'.padEnd(24)}</Text>
|
||||||
<Text color="gray" bold>{'Lagerort'.padEnd(20)}</Text>
|
<Text color="gray" bold>{'Lagerort'.padEnd(22)}</Text>
|
||||||
<Text color="gray" bold>{'Gesamt'.padEnd(14)}</Text>
|
<Text color="gray" bold>{'Gesamt'.padEnd(14)}</Text>
|
||||||
<Text color="gray" bold>{'Verfügbar'.padEnd(14)}</Text>
|
<Text color="gray" bold>{'Verfügbar'.padEnd(14)}</Text>
|
||||||
<Text color="gray" bold>Chargen</Text>
|
<Text color="gray" bold>Chargen</Text>
|
||||||
</Box>
|
</Box>
|
||||||
{stocks.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<Box paddingX={1} paddingY={1}>
|
<Box paddingX={1} paddingY={1}>
|
||||||
<Text color="gray" dimColor>Keine Bestände gefunden.</Text>
|
<Text color="gray" dimColor>{searchQuery ? 'Keine Treffer.' : 'Keine Bestände gefunden.'}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{stocks.map((stock, index) => {
|
{filtered.map((stock, index) => {
|
||||||
const isSelected = index === selectedIndex;
|
const isSelected = index === selectedIndex;
|
||||||
const textColor = isSelected ? 'cyan' : 'white';
|
const textColor = isSelected ? 'cyan' : 'white';
|
||||||
const totalStr = `${stock.totalQuantity}${stock.quantityUnit ? ` ${stock.quantityUnit}` : ''}`;
|
const totalStr = `${stock.totalQuantity}${stock.quantityUnit ? ` ${stock.quantityUnit}` : ''}`;
|
||||||
|
|
@ -62,8 +123,8 @@ export function StockListScreen() {
|
||||||
return (
|
return (
|
||||||
<Box key={stock.id} paddingX={1}>
|
<Box key={stock.id} paddingX={1}>
|
||||||
<Text color={textColor}>{isSelected ? '▶ ' : ' '}</Text>
|
<Text color={textColor}>{isSelected ? '▶ ' : ' '}</Text>
|
||||||
<Text color={textColor}>{stock.articleId.substring(0, 16).padEnd(17)}</Text>
|
<Text color={textColor}>{articleName(stock.articleId).substring(0, 20).padEnd(21)}</Text>
|
||||||
<Text color={isSelected ? 'cyan' : 'gray'}>{stock.storageLocationId.substring(0, 18).padEnd(20)}</Text>
|
<Text color={isSelected ? 'cyan' : 'gray'}>{locationName(stock.storageLocationId).substring(0, 20).padEnd(22)}</Text>
|
||||||
<Text color={isSelected ? 'cyan' : 'gray'}>{totalStr.substring(0, 12).padEnd(14)}</Text>
|
<Text color={isSelected ? 'cyan' : 'gray'}>{totalStr.substring(0, 12).padEnd(14)}</Text>
|
||||||
<Text color={isSelected ? 'cyan' : 'gray'}>{availStr.substring(0, 12).padEnd(14)}</Text>
|
<Text color={isSelected ? 'cyan' : 'gray'}>{availStr.substring(0, 12).padEnd(14)}</Text>
|
||||||
<Text color={isSelected ? 'cyan' : 'gray'}>{String(batchCount)}</Text>
|
<Text color={isSelected ? 'cyan' : 'gray'}>{String(batchCount)}</Text>
|
||||||
|
|
@ -75,7 +136,7 @@ export function StockListScreen() {
|
||||||
|
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text color="gray" dimColor>
|
<Text color="gray" dimColor>
|
||||||
↑↓ nav · Enter Details · [n] Neu · [r] Aktualisieren · Backspace Zurück
|
↑↓ nav · Enter Details · [n] Neu · [r] Aktualisieren · [s] Suche · Backspace Zurück
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
122
frontend/apps/cli/src/components/shared/StockPicker.tsx
Normal file
122
frontend/apps/cli/src/components/shared/StockPicker.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { Box, Text, useInput } from 'ink';
|
||||||
|
import type { StockDTO } from '@effigenix/api-client';
|
||||||
|
|
||||||
|
interface StockPickerProps {
|
||||||
|
stocks: StockDTO[];
|
||||||
|
query: string;
|
||||||
|
onQueryChange: (q: string) => void;
|
||||||
|
onSelect: (stock: StockDTO) => void;
|
||||||
|
focus: boolean;
|
||||||
|
selectedLabel?: string | undefined;
|
||||||
|
maxVisible?: number;
|
||||||
|
articleName: (id: string) => string;
|
||||||
|
locationName: (id: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StockPicker({
|
||||||
|
stocks,
|
||||||
|
query,
|
||||||
|
onQueryChange,
|
||||||
|
onSelect,
|
||||||
|
focus,
|
||||||
|
selectedLabel,
|
||||||
|
maxVisible = 5,
|
||||||
|
articleName,
|
||||||
|
locationName,
|
||||||
|
}: StockPickerProps) {
|
||||||
|
const [cursor, setCursor] = useState(0);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
if (!query) return [];
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
return stocks.filter(
|
||||||
|
(s) =>
|
||||||
|
articleName(s.articleId).toLowerCase().includes(q) ||
|
||||||
|
locationName(s.storageLocationId).toLowerCase().includes(q),
|
||||||
|
).slice(0, maxVisible);
|
||||||
|
}, [stocks, query, maxVisible, articleName, locationName]);
|
||||||
|
|
||||||
|
useInput((input, key) => {
|
||||||
|
if (!focus) return;
|
||||||
|
|
||||||
|
if (key.upArrow) {
|
||||||
|
setCursor((c) => Math.max(0, c - 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.downArrow) {
|
||||||
|
setCursor((c) => Math.min(filtered.length - 1, c + 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.return && filtered.length > 0) {
|
||||||
|
const selected = filtered[cursor];
|
||||||
|
if (selected) onSelect(selected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.backspace || key.delete) {
|
||||||
|
onQueryChange(query.slice(0, -1));
|
||||||
|
setCursor(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.tab || key.escape || key.ctrl || key.meta) return;
|
||||||
|
|
||||||
|
if (input && !key.upArrow && !key.downArrow) {
|
||||||
|
onQueryChange(query + input);
|
||||||
|
setCursor(0);
|
||||||
|
}
|
||||||
|
}, { isActive: focus });
|
||||||
|
|
||||||
|
if (!focus && selectedLabel) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text color="gray">Bestand *</Text>
|
||||||
|
<Box>
|
||||||
|
<Text color="gray"> › </Text>
|
||||||
|
<Text color="green">✓ {selectedLabel}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (focus && selectedLabel && !query) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text color="cyan">Bestand * (tippen zum Ändern)</Text>
|
||||||
|
<Box>
|
||||||
|
<Text color="gray"> › </Text>
|
||||||
|
<Text color="green">✓ {selectedLabel}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text color={focus ? 'cyan' : 'gray'}>Bestand * (Suche)</Text>
|
||||||
|
<Box>
|
||||||
|
<Text color="gray"> › </Text>
|
||||||
|
<Text>{query || (focus ? '▌' : '')}</Text>
|
||||||
|
</Box>
|
||||||
|
{focus && filtered.length > 0 && (
|
||||||
|
<Box flexDirection="column" paddingLeft={2}>
|
||||||
|
{filtered.map((s, i) => {
|
||||||
|
const availStr = `${s.availableQuantity}${s.quantityUnit ? ` ${s.quantityUnit}` : ''}`;
|
||||||
|
return (
|
||||||
|
<Box key={s.id}>
|
||||||
|
<Text color={i === cursor ? 'cyan' : 'white'}>
|
||||||
|
{i === cursor ? '▶ ' : ' '}{articleName(s.articleId)} | {locationName(s.storageLocationId)} | {availStr}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{focus && query && filtered.length === 0 && (
|
||||||
|
<Box paddingLeft={2}>
|
||||||
|
<Text color="yellow">Keine Bestände gefunden.</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
frontend/apps/cli/src/hooks/useStockNameLookup.ts
Normal file
37
frontend/apps/cli/src/hooks/useStockNameLookup.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { useEffect, useMemo, useCallback } from 'react';
|
||||||
|
import { useArticles } from './useArticles.js';
|
||||||
|
import { useStorageLocations } from './useStorageLocations.js';
|
||||||
|
|
||||||
|
export function useStockNameLookup() {
|
||||||
|
const { articles, fetchArticles } = useArticles();
|
||||||
|
const { storageLocations, fetchStorageLocations } = useStorageLocations();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void fetchArticles();
|
||||||
|
void fetchStorageLocations();
|
||||||
|
}, [fetchArticles, fetchStorageLocations]);
|
||||||
|
|
||||||
|
const articleNames = useMemo(() => {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
for (const a of articles) map.set(a.id, a.name);
|
||||||
|
return map;
|
||||||
|
}, [articles]);
|
||||||
|
|
||||||
|
const locationNames = useMemo(() => {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
for (const l of storageLocations) map.set(l.id, l.name);
|
||||||
|
return map;
|
||||||
|
}, [storageLocations]);
|
||||||
|
|
||||||
|
const articleName = useCallback(
|
||||||
|
(id: string) => articleNames.get(id) ?? id.substring(0, 8) + '…',
|
||||||
|
[articleNames],
|
||||||
|
);
|
||||||
|
|
||||||
|
const locationName = useCallback(
|
||||||
|
(id: string) => locationNames.get(id) ?? id.substring(0, 8) + '…',
|
||||||
|
[locationNames],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { articleName, locationName };
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue