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:
@@ -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() {};
|
||||
@@ -245,6 +245,17 @@ router.post('/', [
|
||||
body('prefill.country_code').optional({ nullable: true }).isString().isLength({ max: 2 }),
|
||||
body('prefill.country_name').optional({ nullable: true }).isString().isLength({ max: 120 }),
|
||||
body('prefill.preferred_language').optional({ nullable: true }).isString().isLength({ min: 2, max: 8 }),
|
||||
// At least one human-readable identifier so the record isn't a
|
||||
// nameless row that's impossible to recognise in lists later.
|
||||
body('prefill').custom((prefill) => {
|
||||
const p = prefill || {};
|
||||
const hasName = ['company_name', 'display_name', 'first_name', 'last_name']
|
||||
.some((k) => typeof p[k] === 'string' && p[k].trim());
|
||||
if (!hasName) {
|
||||
throw new Error('At least a company name or a contact name is required');
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
], handleAsync(async (req, res) => {
|
||||
validateRequest(req);
|
||||
const { id } = await customerAccountsService.createDirect({
|
||||
|
||||
Reference in New Issue
Block a user