feat(crm): country dropdown + name guard for customer create/edit

Replace the free-text 2-char country code field on the inline customer
create form and the customer detail page with a dropdown that shows
localized country names (Intl.DisplayNames, no hardcoded map) while
still storing the ISO 3166-1 alpha-2 code. The create form now seeds the
default country from the business profile instead of leaving it blank or
guessing CH/FL. The free-text countryName override is kept for the rare
case where an operator wants a custom display string.

Standardize Liechtenstein on the ISO code LI instead of the colloquial
plate code FL so it matches the PDF renderer's locale-aware lookup and
the new dropdown. Migration 110 normalizes existing FL rows to LI on
customer_accounts and business_profile (idempotent, case-insensitive).

Require at least one human-readable identifier (company name or a
contact name) at create time so the form can't produce a nameless row
that's impossible to recognise in lists later. Enforced on both the
frontend (isValid + toast) and the backend POST /admin/customers
validator so the API can't be bypassed.

i18n: en + de updated; other locales fall back to inline English
defaults and should get a native review before release.
This commit is contained in:
Luca
2026-06-02 01:28:00 +02:00
parent 840df52581
commit db2c482ae9
9 changed files with 202 additions and 26 deletions
+31
View File
@@ -0,0 +1,31 @@
/**
* Country codes offered in the customer country picker. Stored value is
* always the ISO 3166-1 alpha-2 code; Liechtenstein is `LI` (NOT the
* colloquial `FL` plate code) so it matches ISO + the PDF renderer's
* lookup. Display names are derived at runtime from `Intl.DisplayNames`
* in the active UI language, so the list stays locale-aware without a
* hand-maintained translation map.
*/
export const COUNTRY_CODES = [
'LI', 'CH', 'AT', 'DE', 'FR', 'IT', 'ES', 'PT', 'NL', 'BE', 'LU',
'GB', 'US', 'DK', 'SE', 'NO', 'FI', 'PL', 'CZ', 'SK', 'HU', 'IE',
] as const;
export type CountryCode = (typeof COUNTRY_CODES)[number];
/** Localized country name for an ISO code, falling back to the code. */
export function countryLabel(code: string, lang: string): string {
if (!code) return '';
const upper = code.trim().toUpperCase();
try {
return new Intl.DisplayNames([lang || 'en'], { type: 'region' }).of(upper) || upper;
} catch {
return upper;
}
}
/** Country codes sorted by their localized label for the active language. */
export function sortedCountryOptions(lang: string): { code: string; label: string }[] {
return COUNTRY_CODES.map((code) => ({ code, label: countryLabel(code, lang) }))
.sort((a, b) => a.label.localeCompare(b.label, lang || 'en'));
}