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
@@ -0,0 +1,40 @@
/**
* Migration: normalize the Liechtenstein country code from the
* colloquial vehicle-plate code `FL` to the ISO 3166-1 alpha-2 code
* `LI`.
*
* Background: the customer create/edit UI used to accept a free-text
* 2-char country code and the placeholder suggested `FL` for
* Liechtenstein. That code isn't ISO — the PDF renderer's locale-aware
* lookup (services/pdfService.js countryName) and the new country
* dropdown both key on ISO, so `FL` rows render as the bare code
* instead of "Liechtenstein". The dropdown now stores `LI`; this
* migration brings existing rows in line so they display correctly and
* match new records.
*
* Scope: customer_accounts.country_code and business_profile.country_code.
* Case-insensitive so a hand-entered `fl` is caught too. The free-text
* country_name override column is left untouched — it exists precisely
* for operators who want a custom display string.
*
* Idempotent: re-runs only touch rows still holding FL, so a second run
* is a no-op.
*/
async function normalizeColumn(knex, table) {
if (!(await knex.schema.hasTable(table))) return;
if (!(await knex.schema.hasColumn(table, 'country_code'))) return;
await knex(table)
.whereRaw('UPPER(country_code) = ?', ['FL'])
.update({ country_code: 'LI' });
}
exports.up = async function(knex) {
await normalizeColumn(knex, 'customer_accounts');
await normalizeColumn(knex, 'business_profile');
};
// Irreversible by design: once normalized to the ISO code there's no
// way to know which `LI` rows were originally `FL`, and reverting would
// reintroduce the non-ISO value the rest of the system can't read.
exports.down = async function() {};