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:
@@ -23,7 +23,7 @@ import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Save, Send, X } from 'lucide-react';
|
||||
import { Button, Input } from '../common';
|
||||
import { Button, CountrySelect, Input } from '../common';
|
||||
import {
|
||||
customerAdminService,
|
||||
type CustomerAccountDetail,
|
||||
@@ -136,28 +136,42 @@ export const InlineCustomerCreate: React.FC<Props> = ({ onCreated, onCancel, mod
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
const profileDefaultLocale = profileSnapshot?.profile?.defaultLocale || 'en';
|
||||
const profileCountryCode = profileSnapshot?.profile?.countryCode || '';
|
||||
|
||||
// Seed preferredLanguage with the profile default once the profile
|
||||
// arrives (only if the field is still empty so we don't clobber
|
||||
// explicit user input).
|
||||
// Seed preferredLanguage + countryCode with the profile defaults once
|
||||
// the profile arrives (only if the field is still empty so we don't
|
||||
// clobber explicit user input).
|
||||
React.useEffect(() => {
|
||||
if (profileDefaultLocale && !form.preferredLanguage) {
|
||||
setForm((prev) => prev.preferredLanguage ? prev : { ...prev, preferredLanguage: profileDefaultLocale });
|
||||
}
|
||||
setForm((prev) => {
|
||||
const next = { ...prev };
|
||||
if (profileDefaultLocale && !prev.preferredLanguage) next.preferredLanguage = profileDefaultLocale;
|
||||
if (profileCountryCode && !prev.countryCode) next.countryCode = profileCountryCode.toUpperCase();
|
||||
return next;
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [profileDefaultLocale]);
|
||||
}, [profileDefaultLocale, profileCountryCode]);
|
||||
|
||||
const setField = (key: keyof FormState) =>
|
||||
(e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) =>
|
||||
setForm((prev) => ({ ...prev, [key]: e.target.value }));
|
||||
|
||||
const isValid = !!form.email && /\S+@\S+\.\S+/.test(form.email);
|
||||
const hasEmail = !!form.email && /\S+@\S+\.\S+/.test(form.email);
|
||||
// At least one human-readable identifier so the record isn't a
|
||||
// nameless row that's impossible to recognise in lists later.
|
||||
const hasName = !!(form.companyName.trim() || form.displayName.trim()
|
||||
|| form.firstName.trim() || form.lastName.trim());
|
||||
const isValid = hasEmail && hasName;
|
||||
|
||||
const handleSave = async (mode: 'passive' | 'invite') => {
|
||||
if (!isValid) {
|
||||
if (!hasEmail) {
|
||||
toast.error(t('customers.create.emailRequired', 'A valid email is required.'));
|
||||
return;
|
||||
}
|
||||
if (!hasName) {
|
||||
toast.error(t('customers.create.nameRequired',
|
||||
'Enter at least a company name or a contact name.'));
|
||||
return;
|
||||
}
|
||||
setBusy(mode);
|
||||
try {
|
||||
const customer = await customerAdminService.createDirect(form.email, buildPrefill(form));
|
||||
@@ -298,12 +312,10 @@ export const InlineCustomerCreate: React.FC<Props> = ({ onCreated, onCancel, mod
|
||||
value={form.state}
|
||||
onChange={setField('state')}
|
||||
/>
|
||||
<Input
|
||||
label={t('customers.detail.countryCode', 'Country (ISO code)') as string}
|
||||
<CountrySelect
|
||||
label={t('customers.detail.country', 'Country') as string}
|
||||
value={form.countryCode}
|
||||
onChange={setField('countryCode')}
|
||||
placeholder="CH"
|
||||
maxLength={2}
|
||||
onChange={(code) => setForm((prev) => ({ ...prev, countryCode: code }))}
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||
|
||||
Reference in New Issue
Block a user