mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 19:20:23 +01:00
feat(shared): Länderauswahl mit ISO 3166-1 Mapping und CountryPicker
Backend: Country-Record (Shared Kernel), InMemoryCountryRepository mit ~249 Ländern und DACH-Priorisierung, ListCountries-UseCase, GET /api/countries?q= Endpoint. Frontend: CountryPicker-Komponente mit Fuzzy-Suche, DACH-Favoriten bei leerem Query. SupplierCreate-, CustomerCreate- und AddDeliveryAddress- Screens verwenden jetzt den CountryPicker statt Freitext. Detail-Screens zeigen den Ländercode in der Adressanzeige. Closes #71
This commit is contained in:
parent
2811836039
commit
a77f0ec5df
20 changed files with 1136 additions and 63 deletions
129
frontend/apps/cli/src/components/shared/CountryPicker.tsx
Normal file
129
frontend/apps/cli/src/components/shared/CountryPicker.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { useState, useMemo } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import type { CountryDTO } from '@effigenix/api-client';
|
||||
|
||||
interface CountryPickerProps {
|
||||
countries: CountryDTO[];
|
||||
query: string;
|
||||
onQueryChange: (q: string) => void;
|
||||
onSelect: (country: CountryDTO) => void;
|
||||
focus: boolean;
|
||||
selectedName?: string;
|
||||
maxVisible?: number;
|
||||
}
|
||||
|
||||
const DACH_CODES = ['DE', 'AT', 'CH'];
|
||||
|
||||
export function CountryPicker({
|
||||
countries,
|
||||
query,
|
||||
onQueryChange,
|
||||
onSelect,
|
||||
focus,
|
||||
selectedName,
|
||||
maxVisible = 5,
|
||||
}: CountryPickerProps) {
|
||||
const [cursor, setCursor] = useState(0);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!query) {
|
||||
// Show DACH favorites when no query
|
||||
return countries.filter((c) => DACH_CODES.includes(c.code)).slice(0, maxVisible);
|
||||
}
|
||||
const q = query.toLowerCase();
|
||||
return countries.filter(
|
||||
(c) => c.name.toLowerCase().includes(q) || c.code.toLowerCase().includes(q),
|
||||
).slice(0, maxVisible);
|
||||
}, [countries, query, maxVisible]);
|
||||
|
||||
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 && selectedName) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color="gray">Land *</Text>
|
||||
<Box>
|
||||
<Text color="gray"> › </Text>
|
||||
<Text color="green">✓ {selectedName}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (focus && selectedName && !query) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color="cyan">Land * (tippen zum Ändern)</Text>
|
||||
<Box>
|
||||
<Text color="gray"> › </Text>
|
||||
<Text color="green">✓ {selectedName}</Text>
|
||||
</Box>
|
||||
{filtered.length > 0 && (
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
{filtered.map((c, i) => (
|
||||
<Box key={c.code}>
|
||||
<Text color={i === cursor ? 'cyan' : 'white'}>
|
||||
{i === cursor ? '▶ ' : ' '}{c.code} – {c.name}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={focus ? 'cyan' : 'gray'}>Land * (Suche)</Text>
|
||||
<Box>
|
||||
<Text color="gray"> › </Text>
|
||||
<Text>{query || (focus ? '▌' : '')}</Text>
|
||||
</Box>
|
||||
{focus && filtered.length > 0 && (
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
{filtered.map((c, i) => (
|
||||
<Box key={c.code}>
|
||||
<Text color={i === cursor ? 'cyan' : 'white'}>
|
||||
{i === cursor ? '▶ ' : ' '}{c.code} – {c.name}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{focus && query && filtered.length === 0 && (
|
||||
<Box paddingLeft={2}>
|
||||
<Text color="yellow">Kein Land gefunden.</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue