1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 17:39:57 +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:
Sebastian Frick 2026-02-24 09:28:56 +01:00
parent 2811836039
commit a77f0ec5df
20 changed files with 1136 additions and 63 deletions

View file

@ -0,0 +1,22 @@
package de.effigenix.application.shared;
import de.effigenix.shared.common.Country;
import de.effigenix.shared.common.CountryRepository;
import java.util.List;
public class ListCountries {
private final CountryRepository countryRepository;
public ListCountries(CountryRepository countryRepository) {
this.countryRepository = countryRepository;
}
public List<Country> execute(String query) {
if (query == null || query.isBlank()) {
return countryRepository.findAll();
}
return countryRepository.search(query);
}
}

View file

@ -0,0 +1,21 @@
package de.effigenix.infrastructure.config;
import de.effigenix.application.shared.ListCountries;
import de.effigenix.infrastructure.shared.InMemoryCountryRepository;
import de.effigenix.shared.common.CountryRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SharedUseCaseConfiguration {
@Bean
public CountryRepository countryRepository() {
return new InMemoryCountryRepository();
}
@Bean
public ListCountries listCountries(CountryRepository countryRepository) {
return new ListCountries(countryRepository);
}
}

View file

@ -0,0 +1,308 @@
package de.effigenix.infrastructure.shared;
import de.effigenix.shared.common.Country;
import de.effigenix.shared.common.CountryRepository;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class InMemoryCountryRepository implements CountryRepository {
private static final Set<String> DACH_CODES = Set.of("DE", "AT", "CH");
private final Map<String, Country> byCode;
private final List<Country> allSorted;
public InMemoryCountryRepository() {
var countries = List.of(
new Country("AF", "Afghanistan"),
new Country("EG", "Ägypten"),
new Country("AX", "Åland-Inseln"),
new Country("AL", "Albanien"),
new Country("DZ", "Algerien"),
new Country("AS", "Amerikanisch-Samoa"),
new Country("VI", "Amerikanische Jungferninseln"),
new Country("AD", "Andorra"),
new Country("AO", "Angola"),
new Country("AI", "Anguilla"),
new Country("AQ", "Antarktis"),
new Country("AG", "Antigua und Barbuda"),
new Country("GQ", "Äquatorialguinea"),
new Country("AR", "Argentinien"),
new Country("AM", "Armenien"),
new Country("AW", "Aruba"),
new Country("AZ", "Aserbaidschan"),
new Country("ET", "Äthiopien"),
new Country("AU", "Australien"),
new Country("BS", "Bahamas"),
new Country("BH", "Bahrain"),
new Country("BD", "Bangladesch"),
new Country("BB", "Barbados"),
new Country("BY", "Belarus"),
new Country("BE", "Belgien"),
new Country("BZ", "Belize"),
new Country("BJ", "Benin"),
new Country("BM", "Bermuda"),
new Country("BT", "Bhutan"),
new Country("BO", "Bolivien"),
new Country("BQ", "Bonaire, Sint Eustatius und Saba"),
new Country("BA", "Bosnien und Herzegowina"),
new Country("BW", "Botsuana"),
new Country("BV", "Bouvetinsel"),
new Country("BR", "Brasilien"),
new Country("VG", "Britische Jungferninseln"),
new Country("IO", "Britisches Territorium im Indischen Ozean"),
new Country("BN", "Brunei Darussalam"),
new Country("BG", "Bulgarien"),
new Country("BF", "Burkina Faso"),
new Country("BI", "Burundi"),
new Country("CV", "Cabo Verde"),
new Country("CL", "Chile"),
new Country("CN", "China"),
new Country("CK", "Cookinseln"),
new Country("CR", "Costa Rica"),
new Country("CI", "Côte d'Ivoire"),
new Country("CW", "Curaçao"),
new Country("DK", "Dänemark"),
new Country("DE", "Deutschland"),
new Country("DM", "Dominica"),
new Country("DO", "Dominikanische Republik"),
new Country("DJ", "Dschibuti"),
new Country("EC", "Ecuador"),
new Country("SV", "El Salvador"),
new Country("ER", "Eritrea"),
new Country("EE", "Estland"),
new Country("SZ", "Eswatini"),
new Country("FK", "Falklandinseln"),
new Country("FO", "Färöer"),
new Country("FJ", "Fidschi"),
new Country("FI", "Finnland"),
new Country("FR", "Frankreich"),
new Country("GF", "Französisch-Guayana"),
new Country("PF", "Französisch-Polynesien"),
new Country("TF", "Französische Süd- und Antarktisgebiete"),
new Country("GA", "Gabun"),
new Country("GM", "Gambia"),
new Country("GE", "Georgien"),
new Country("GH", "Ghana"),
new Country("GI", "Gibraltar"),
new Country("GD", "Grenada"),
new Country("GR", "Griechenland"),
new Country("GL", "Grönland"),
new Country("GP", "Guadeloupe"),
new Country("GU", "Guam"),
new Country("GT", "Guatemala"),
new Country("GG", "Guernsey"),
new Country("GN", "Guinea"),
new Country("GW", "Guinea-Bissau"),
new Country("GY", "Guyana"),
new Country("HT", "Haiti"),
new Country("HM", "Heard und McDonaldinseln"),
new Country("HN", "Honduras"),
new Country("HK", "Hongkong"),
new Country("IN", "Indien"),
new Country("ID", "Indonesien"),
new Country("IQ", "Irak"),
new Country("IR", "Iran"),
new Country("IE", "Irland"),
new Country("IS", "Island"),
new Country("IM", "Isle of Man"),
new Country("IL", "Israel"),
new Country("IT", "Italien"),
new Country("JM", "Jamaika"),
new Country("JP", "Japan"),
new Country("YE", "Jemen"),
new Country("JE", "Jersey"),
new Country("JO", "Jordanien"),
new Country("KY", "Kaimaninseln"),
new Country("KH", "Kambodscha"),
new Country("CM", "Kamerun"),
new Country("CA", "Kanada"),
new Country("KZ", "Kasachstan"),
new Country("QA", "Katar"),
new Country("KE", "Kenia"),
new Country("KG", "Kirgisistan"),
new Country("KI", "Kiribati"),
new Country("CC", "Kokosinseln"),
new Country("CO", "Kolumbien"),
new Country("KM", "Komoren"),
new Country("CD", "Kongo, Demokratische Republik"),
new Country("CG", "Kongo, Republik"),
new Country("KP", "Korea, Demokratische Volksrepublik"),
new Country("KR", "Korea, Republik"),
new Country("HR", "Kroatien"),
new Country("CU", "Kuba"),
new Country("KW", "Kuwait"),
new Country("LA", "Laos"),
new Country("LS", "Lesotho"),
new Country("LV", "Lettland"),
new Country("LB", "Libanon"),
new Country("LR", "Liberia"),
new Country("LY", "Libyen"),
new Country("LI", "Liechtenstein"),
new Country("LT", "Litauen"),
new Country("LU", "Luxemburg"),
new Country("MO", "Macao"),
new Country("MG", "Madagaskar"),
new Country("MW", "Malawi"),
new Country("MY", "Malaysia"),
new Country("MV", "Malediven"),
new Country("ML", "Mali"),
new Country("MT", "Malta"),
new Country("MA", "Marokko"),
new Country("MH", "Marshallinseln"),
new Country("MQ", "Martinique"),
new Country("MR", "Mauretanien"),
new Country("MU", "Mauritius"),
new Country("YT", "Mayotte"),
new Country("MX", "Mexiko"),
new Country("FM", "Mikronesien"),
new Country("MD", "Moldau"),
new Country("MC", "Monaco"),
new Country("MN", "Mongolei"),
new Country("ME", "Montenegro"),
new Country("MS", "Montserrat"),
new Country("MZ", "Mosambik"),
new Country("MM", "Myanmar"),
new Country("NA", "Namibia"),
new Country("NR", "Nauru"),
new Country("NP", "Nepal"),
new Country("NC", "Neukaledonien"),
new Country("NZ", "Neuseeland"),
new Country("NI", "Nicaragua"),
new Country("NL", "Niederlande"),
new Country("NE", "Niger"),
new Country("NG", "Nigeria"),
new Country("NU", "Niue"),
new Country("MK", "Nordmazedonien"),
new Country("MP", "Nördliche Marianen"),
new Country("NF", "Norfolkinsel"),
new Country("NO", "Norwegen"),
new Country("OM", "Oman"),
new Country("AT", "Österreich"),
new Country("TL", "Osttimor"),
new Country("PK", "Pakistan"),
new Country("PW", "Palau"),
new Country("PS", "Palästina"),
new Country("PA", "Panama"),
new Country("PG", "Papua-Neuguinea"),
new Country("PY", "Paraguay"),
new Country("PE", "Peru"),
new Country("PH", "Philippinen"),
new Country("PN", "Pitcairninseln"),
new Country("PL", "Polen"),
new Country("PT", "Portugal"),
new Country("PR", "Puerto Rico"),
new Country("RE", "Réunion"),
new Country("RW", "Ruanda"),
new Country("RO", "Rumänien"),
new Country("RU", "Russland"),
new Country("SB", "Salomonen"),
new Country("ZM", "Sambia"),
new Country("WS", "Samoa"),
new Country("SM", "San Marino"),
new Country("ST", "São Tomé und Príncipe"),
new Country("SA", "Saudi-Arabien"),
new Country("SE", "Schweden"),
new Country("CH", "Schweiz"),
new Country("SN", "Senegal"),
new Country("RS", "Serbien"),
new Country("SC", "Seychellen"),
new Country("SL", "Sierra Leone"),
new Country("ZW", "Simbabwe"),
new Country("SG", "Singapur"),
new Country("SX", "Sint Maarten"),
new Country("SK", "Slowakei"),
new Country("SI", "Slowenien"),
new Country("SO", "Somalia"),
new Country("ES", "Spanien"),
new Country("LK", "Sri Lanka"),
new Country("BL", "St. Barthélemy"),
new Country("SH", "St. Helena"),
new Country("KN", "St. Kitts und Nevis"),
new Country("LC", "St. Lucia"),
new Country("MF", "St. Martin"),
new Country("PM", "St. Pierre und Miquelon"),
new Country("VC", "St. Vincent und die Grenadinen"),
new Country("ZA", "Südafrika"),
new Country("SD", "Sudan"),
new Country("GS", "Südgeorgien und die Südlichen Sandwichinseln"),
new Country("SS", "Südsudan"),
new Country("SR", "Suriname"),
new Country("SJ", "Svalbard und Jan Mayen"),
new Country("SY", "Syrien"),
new Country("TJ", "Tadschikistan"),
new Country("TW", "Taiwan"),
new Country("TZ", "Tansania"),
new Country("TH", "Thailand"),
new Country("TG", "Togo"),
new Country("TK", "Tokelau"),
new Country("TO", "Tonga"),
new Country("TT", "Trinidad und Tobago"),
new Country("TD", "Tschad"),
new Country("CZ", "Tschechien"),
new Country("TN", "Tunesien"),
new Country("TR", "Türkei"),
new Country("TM", "Turkmenistan"),
new Country("TC", "Turks- und Caicosinseln"),
new Country("TV", "Tuvalu"),
new Country("UG", "Uganda"),
new Country("UA", "Ukraine"),
new Country("HU", "Ungarn"),
new Country("UM", "United States Minor Outlying Islands"),
new Country("UY", "Uruguay"),
new Country("UZ", "Usbekistan"),
new Country("VU", "Vanuatu"),
new Country("VA", "Vatikanstadt"),
new Country("VE", "Venezuela"),
new Country("AE", "Vereinigte Arabische Emirate"),
new Country("US", "Vereinigte Staaten"),
new Country("GB", "Vereinigtes Königreich"),
new Country("VN", "Vietnam"),
new Country("WF", "Wallis und Futuna"),
new Country("CX", "Weihnachtsinsel"),
new Country("EH", "Westsahara"),
new Country("CF", "Zentralafrikanische Republik"),
new Country("CY", "Zypern")
);
this.byCode = countries.stream().collect(Collectors.toMap(Country::code, c -> c));
// Sort: DACH first (DE, AT, CH), then rest alphabetically by name
var dach = countries.stream()
.filter(c -> DACH_CODES.contains(c.code()))
.sorted(Comparator.comparing(c -> List.of("DE", "AT", "CH").indexOf(c.code())))
.toList();
var rest = countries.stream()
.filter(c -> !DACH_CODES.contains(c.code()))
.sorted(Comparator.comparing(Country::name))
.toList();
this.allSorted = Stream.concat(dach.stream(), rest.stream()).toList();
}
@Override
public List<Country> findAll() {
return allSorted;
}
@Override
public List<Country> search(String query) {
if (query == null || query.isBlank()) {
return findAll();
}
var q = query.toLowerCase();
var matches = allSorted.stream()
.filter(c -> c.name().toLowerCase().contains(q) || c.code().toLowerCase().contains(q))
.toList();
// DACH countries first in results too
var dach = matches.stream().filter(c -> DACH_CODES.contains(c.code())).toList();
var rest = matches.stream().filter(c -> !DACH_CODES.contains(c.code())).toList();
return Stream.concat(dach.stream(), rest.stream()).toList();
}
@Override
public Optional<Country> findByCode(String code) {
return Optional.ofNullable(byCode.get(code));
}
}

View file

@ -0,0 +1,37 @@
package de.effigenix.infrastructure.shared.web;
import de.effigenix.application.shared.ListCountries;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/countries")
@SecurityRequirement(name = "Bearer Authentication")
@Tag(name = "Countries", description = "ISO 3166-1 country reference data")
public class CountryController {
private final ListCountries listCountries;
public CountryController(ListCountries listCountries) {
this.listCountries = listCountries;
}
public record CountryResponse(String code, String name) {}
@GetMapping
public ResponseEntity<List<CountryResponse>> search(
@RequestParam(name = "q", required = false, defaultValue = "") String query) {
var countries = listCountries.execute(query);
var response = countries.stream()
.map(c -> new CountryResponse(c.code(), c.name()))
.toList();
return ResponseEntity.ok(response);
}
}

View file

@ -0,0 +1,13 @@
package de.effigenix.shared.common;
public record Country(String code, String name) {
public Country {
if (code == null || !code.matches("^[A-Z]{2}$")) {
throw new IllegalArgumentException("Country code must be exactly 2 uppercase letters, got: " + code);
}
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Country name must not be blank");
}
}
}

View file

@ -0,0 +1,10 @@
package de.effigenix.shared.common;
import java.util.List;
import java.util.Optional;
public interface CountryRepository {
List<Country> findAll();
List<Country> search(String query);
Optional<Country> findByCode(String code);
}