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:
parent
797f435a49
commit
d27dbaa843
30 changed files with 3882 additions and 1 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue