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/masterdata/articles/ArticleDetailScreen.tsx
Sebastian Frick c89ee359d1 fix(tui): TypeScript-Fehler durch strikte generierte OpenAPI-Typen beheben
RoleDTO auf generierten Typ umgestellt, exactOptionalPropertyTypes-Konflikte
gelöst, Null-Checks für nullable AddressResponse ergänzt und Enum-Casts
für string-basierte SalesUnit-Felder hinzugefügt.
2026-02-25 17:34:14 +01:00

212 lines
8.2 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, { useCallback, useEffect, useState } from 'react';
import { Box, Text, useInput } from 'ink';
import type { ArticleDTO, SalesUnitDTO, Unit, PriceModel } from '@effigenix/api-client';
import { UNIT_LABELS, PRICE_MODEL_LABELS } from '@effigenix/api-client';
import { useNavigation } from '../../../state/navigation-context.js';
import { useArticles } from '../../../hooks/useArticles.js';
import { LoadingSpinner } from '../../shared/LoadingSpinner.js';
import { ErrorDisplay } from '../../shared/ErrorDisplay.js';
import { SuccessDisplay } from '../../shared/SuccessDisplay.js';
import { ConfirmDialog } from '../../shared/ConfirmDialog.js';
import { client } from '../../../utils/api-client.js';
type MenuAction = 'toggle-status' | 'add-sales-unit' | 'remove-sales-unit' | 'back';
type Mode = 'menu' | 'confirm-status' | 'select-sales-unit';
const MENU_ITEMS: { id: MenuAction; label: (a: ArticleDTO) => string }[] = [
{ id: 'toggle-status', label: (a) => a.status === 'ACTIVE' ? '[Deaktivieren]' : '[Aktivieren]' },
{ id: 'add-sales-unit', label: () => '[Verkaufseinheit hinzufügen]' },
{ id: 'remove-sales-unit', label: (a) => a.salesUnits.length > 1 ? '[Verkaufseinheit entfernen]' : '[Entfernen (min. 1 erforderlich)]' },
{ id: 'back', label: () => '[Zurück]' },
];
function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : 'Unbekannter Fehler';
}
export function ArticleDetailScreen() {
const { params, navigate, back } = useNavigation();
const articleId = params['articleId'] ?? '';
const { activateArticle, deactivateArticle, removeSalesUnit } = useArticles();
const [article, setArticle] = useState<ArticleDTO | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedAction, setSelectedAction] = useState(0);
const [mode, setMode] = useState<Mode>('menu');
const [actionLoading, setActionLoading] = useState(false);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [selectedSuIndex, setSelectedSuIndex] = useState(0);
const loadArticle = useCallback(() => {
setLoading(true);
setError(null);
client.articles.getById(articleId)
.then((a) => { setArticle(a); setLoading(false); })
.catch((err: unknown) => { setError(errorMessage(err)); setLoading(false); });
}, [articleId]);
useEffect(() => { if (articleId) loadArticle(); }, [loadArticle, articleId]);
useInput((_input, key) => {
if (loading || actionLoading) return;
if (mode === 'select-sales-unit' && article) {
if (key.upArrow) setSelectedSuIndex((i) => Math.max(0, i - 1));
if (key.downArrow) setSelectedSuIndex((i) => Math.min(article.salesUnits.length - 1, i + 1));
if (key.return) {
const su = article.salesUnits[selectedSuIndex];
if (su && article.salesUnits.length > 1) void handleRemoveSalesUnit(su);
}
if (key.escape) setMode('menu');
return;
}
if (mode !== 'menu') return;
if (key.upArrow) setSelectedAction((i) => Math.max(0, i - 1));
if (key.downArrow) setSelectedAction((i) => Math.min(MENU_ITEMS.length - 1, i + 1));
if (key.return) void handleAction();
if (key.backspace || key.escape) back();
});
const handleAction = async () => {
if (!article) return;
const item = MENU_ITEMS[selectedAction];
if (!item) return;
switch (item.id) {
case 'toggle-status':
setMode('confirm-status');
break;
case 'add-sales-unit':
navigate('article-add-sales-unit', { articleId: article.id });
break;
case 'remove-sales-unit':
if (article.salesUnits.length > 1) {
setSelectedSuIndex(0);
setMode('select-sales-unit');
}
break;
case 'back':
back();
break;
}
};
const handleToggleStatus = useCallback(async () => {
if (!article) return;
setMode('menu');
setActionLoading(true);
const fn = article.status === 'ACTIVE' ? deactivateArticle : activateArticle;
const updated = await fn(article.id);
setActionLoading(false);
if (updated) {
setArticle(updated);
setSuccessMessage(article.status === 'ACTIVE' ? 'Artikel deaktiviert.' : 'Artikel aktiviert.');
}
}, [article, activateArticle, deactivateArticle]);
const handleRemoveSalesUnit = useCallback(async (su: SalesUnitDTO) => {
if (!article) return;
setMode('menu');
setActionLoading(true);
const updated = await removeSalesUnit(article.id, su.id);
setActionLoading(false);
if (updated) {
setArticle(updated);
setSuccessMessage(`Verkaufseinheit "${UNIT_LABELS[su.unit as Unit]}" entfernt.`);
}
}, [article, removeSalesUnit]);
if (loading) return <LoadingSpinner label="Lade Artikel..." />;
if (error && !article) return <ErrorDisplay message={error} onDismiss={back} />;
if (!article) return <Text color="red">Artikel nicht gefunden.</Text>;
const statusColor = article.status === 'ACTIVE' ? 'green' : 'red';
return (
<Box flexDirection="column" gap={1}>
<Text color="cyan" bold>Artikel: {article.articleNumber} {article.name}</Text>
{error && <ErrorDisplay message={error} onDismiss={() => setError(null)} />}
{successMessage && <SuccessDisplay message={successMessage} onDismiss={() => setSuccessMessage(null)} />}
{/* Info-Box */}
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={2} paddingY={1}>
<Box gap={2}>
<Text color="gray">Status:</Text>
<Text color={statusColor} bold>{article.status}</Text>
</Box>
<Box gap={2}>
<Text color="gray">Artikelnummer:</Text>
<Text>{article.articleNumber}</Text>
</Box>
<Box gap={2}>
<Text color="gray">Kategorie-ID:</Text>
<Text>{article.categoryId}</Text>
</Box>
<Box flexDirection="column" marginTop={1}>
<Text color="gray">Verkaufseinheiten ({article.salesUnits.length}):</Text>
{article.salesUnits.map((su) => (
<Box key={su.id} paddingLeft={2} gap={1}>
<Text color="yellow"></Text>
<Text>{UNIT_LABELS[su.unit as Unit]}</Text>
<Text color="gray">({PRICE_MODEL_LABELS[su.priceModel as PriceModel]})</Text>
<Text color="green">{su.price.toFixed(2)} </Text>
</Box>
))}
</Box>
{article.supplierIds.length > 0 && (
<Box gap={2} marginTop={1}>
<Text color="gray">Lieferanten:</Text>
<Text>{article.supplierIds.length} zugewiesen</Text>
</Box>
)}
</Box>
{/* Confirm Status */}
{mode === 'confirm-status' && (
<ConfirmDialog
message={article.status === 'ACTIVE' ? 'Artikel deaktivieren?' : 'Artikel aktivieren?'}
onConfirm={() => void handleToggleStatus()}
onCancel={() => setMode('menu')}
/>
)}
{/* Select Sales Unit to Remove */}
{mode === 'select-sales-unit' && (
<Box flexDirection="column" gap={1}>
<Text color="yellow">Verkaufseinheit entfernen:</Text>
{article.salesUnits.map((su, i) => (
<Box key={su.id}>
<Text color={i === selectedSuIndex ? 'cyan' : 'white'}>
{i === selectedSuIndex ? '▶ ' : ' '}
{UNIT_LABELS[su.unit as Unit]} {su.price.toFixed(2)}
</Text>
</Box>
))}
<Text color="gray" dimColor> auswählen · Enter Entfernen · Escape Abbrechen</Text>
</Box>
)}
{/* Action Menu */}
{mode === 'menu' && (
<Box flexDirection="column">
<Text color="gray" bold>Aktionen:</Text>
{actionLoading && <LoadingSpinner label="Aktion wird ausgeführt..." />}
{!actionLoading && MENU_ITEMS.map((item, index) => (
<Box key={item.id}>
<Text color={index === selectedAction ? 'cyan' : 'white'}>
{index === selectedAction ? '▶ ' : ' '}{item.label(article)}
</Text>
</Box>
))}
</Box>
)}
<Box marginTop={1}>
<Text color="gray" dimColor> navigieren · Enter ausführen · Backspace Zurück</Text>
</Box>
</Box>
);
}