1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 10:09:35 +01:00

feat(cli): Stammdaten-TUI mit Master Data API-Anbindung

- Neue Screens: Kategorien, Lieferanten, Artikel, Kunden (jeweils
  Liste, Detail, Anlegen + Detailaktionen wie Bewertung, Zertifikate,
  Verkaufseinheiten, Lieferadressen, Präferenzen)
- API-Client: Resources für alle 4 Stammdaten-Aggregate implementiert
  (categories, suppliers, articles, customers) mit Mapping von
  verschachtelten Domain-VOs auf flache DTOs
- Lieferant, Artikel, Kategorie: echte HTTP-Calls gegen Backend
  (/api/suppliers, /api/articles, /api/categories, /api/customers)
- 204-No-Content-Endpoints (removeSalesUnit, removeSupplier,
  removeCertificate, removeDeliveryAddress, removeFrameContract)
  lösen Re-Fetch des Aggregats aus
- MasterdataMenu, Navigation-Erweiterung, App.tsx-Routing
This commit is contained in:
Sebastian Frick 2026-02-18 13:35:20 +01:00
parent 797f435a49
commit d27dbaa843
30 changed files with 3882 additions and 1 deletions

View file

@ -0,0 +1,89 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Box, Text, useInput } from 'ink';
import type { CustomerPreference } from '@effigenix/api-client';
import { CUSTOMER_PREFERENCE_LABELS } from '@effigenix/api-client';
import { useNavigation } from '../../../state/navigation-context.js';
import { useCustomers } from '../../../hooks/useCustomers.js';
import { LoadingSpinner } from '../../shared/LoadingSpinner.js';
import { ErrorDisplay } from '../../shared/ErrorDisplay.js';
import { client } from '../../../utils/api-client.js';
const ALL_PREFERENCES: CustomerPreference[] = [
'BIO', 'REGIONAL', 'TIERWOHL', 'HALAL', 'KOSHER', 'GLUTENFREI', 'LAKTOSEFREI',
];
function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : 'Unbekannter Fehler';
}
export function SetPreferencesScreen() {
const { params, navigate, back } = useNavigation();
const customerId = params['customerId'] ?? '';
const { setPreferences, loading, error, clearError } = useCustomers();
const [selectedIndex, setSelectedIndex] = useState(0);
const [checked, setChecked] = useState<Set<CustomerPreference>>(new Set());
const [initLoading, setInitLoading] = useState(true);
const [initError, setInitError] = useState<string | null>(null);
useEffect(() => {
client.customers.getById(customerId)
.then((c) => { setChecked(new Set(c.preferences)); setInitLoading(false); })
.catch((err: unknown) => { setInitError(errorMessage(err)); setInitLoading(false); });
}, [customerId]);
useInput((_input, key) => {
if (initLoading || loading) return;
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
if (key.downArrow) setSelectedIndex((i) => Math.min(ALL_PREFERENCES.length - 1, i + 1));
if (_input === ' ') {
const pref = ALL_PREFERENCES[selectedIndex];
if (pref) {
setChecked((prev) => {
const next = new Set(prev);
if (next.has(pref)) next.delete(pref);
else next.add(pref);
return next;
});
}
}
if (key.return) void handleSave();
if (key.escape) back();
});
const handleSave = useCallback(async () => {
const updated = await setPreferences(customerId, Array.from(checked));
if (updated) navigate('customer-detail', { customerId });
}, [customerId, checked, setPreferences, navigate]);
if (initLoading) return <LoadingSpinner label="Lade Präferenzen..." />;
if (initError) return <ErrorDisplay message={initError} onDismiss={back} />;
if (loading) return <LoadingSpinner label="Speichere Präferenzen..." />;
return (
<Box flexDirection="column" gap={1}>
<Text color="cyan" bold>Präferenzen setzen</Text>
{error && <ErrorDisplay message={error} onDismiss={clearError} />}
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={2} paddingY={1} gap={0}>
{ALL_PREFERENCES.map((pref, i) => {
const isSelected = i === selectedIndex;
const isChecked = checked.has(pref);
return (
<Box key={pref} gap={1}>
<Text color={isSelected ? 'cyan' : 'gray'}>{isSelected ? '▶' : ' '}</Text>
<Text color={isChecked ? 'green' : 'gray'}>{isChecked ? '[✓]' : '[ ]'}</Text>
<Text color={isSelected ? 'cyan' : 'white'}>{CUSTOMER_PREFERENCE_LABELS[pref]}</Text>
</Box>
);
})}
</Box>
<Box marginTop={1}>
<Text color="gray" dimColor>
navigieren · Leertaste togglen · Enter speichern · Escape Abbrechen
</Text>
</Box>
</Box>
);
}