Files
pn-new-crm/src/components/shared/timezone-combobox.tsx

167 lines
5.5 KiB
TypeScript
Raw Normal View History

feat(i18n): country/phone/timezone/subdivision primitives + form wiring Cross-cutting i18n polish for forms across the marina + residential + company domains. Introduces a single source of truth for country/phone/timezone/ subdivision data and replaces every nationality-as-free-text and timezone- as-string Input with a dedicated combobox. PR1 Countries — ALL_COUNTRY_CODES (~250 ISO-3166-1 alpha-2), Intl.DisplayNames for localized labels, detectDefaultCountry() with navigator-region fallback to US, CountryCombobox with regional-indicator flag glyphs + compact mode for inline use. PR2 Phone — libphonenumber-js wrapper (parsePhone / formatAsYouType / callingCodeFor), PhoneInput with flag dropdown + national-format AsYouType + paste-detect that flips the country dropdown for pasted international strings. PR3 Timezones — country->IANA map (250 entries, multi-zone for AU/BR/CA/CD/ ID/KZ/MN/MX/RU/US), formatTimezoneLabel ("Europe/London (UTC+1)"), TimezoneCombobox with Suggested/All grouping driven by countryHint. PR4 Subdivisions — wraps the iso-3166-2 npm package (~5000 ISO 3166-2 codes for every country), per-country cache, SubdivisionCombobox with "Pick a country first" / "No regions available" empty states. PR5 Schema deltas (migration 0015) — clients.nationality_iso, clientContacts {value_e164, value_country}, clientAddresses {country_iso, subdivision_iso}, residentialClients {phone_e164, phone_country, nationality_iso, timezone, place_of_residence_country_iso, subdivision_iso}, companies {incorporation_ country_iso, incorporation_subdivision_iso}, companyAddresses {country_iso, subdivision_iso}. Plus shared zod validators (validators/i18n.ts) used by every entity validator + route handler. PR6 ClientForm + ClientDetail — CountryCombobox replaces nationality Input, TimezoneCombobox replaces timezone Input (driven by nationalityIso hint), PhoneInput conditionally rendered for phone/whatsapp contacts. Inline editors (InlineCountryField / InlineTimezoneField / InlinePhoneField) for the detail-page overview rows + ContactsEditor. PR7 Residential client form + detail — phone -> PhoneInput, nationality/ timezone/place-of-residence-country/subdivision rows in both create sheet and inline-editable detail view. Subdivision wipes when country flips since codes are country-scoped. PR8 Company form + detail — incorporation country -> CountryCombobox, incorporation region -> SubdivisionCombobox in both modes. PR9 Public inquiry endpoint — accepts pre-normalized phoneE164/phoneCountry and i18n fields from newer website builds, server-side parsePhone() fallback for legacy raw-international submissions. Old Nuxt builds keep working unchanged. Tests: 4 unit suites for the primitives (25 tests), 1 integration spec for the public phone-normalization path (3 tests), 1 smoke spec asserting the combobox triggers render in all three create sheets. Test totals: vitest 713 -> 741 (+28). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:13:08 +02:00
'use client';
import { useMemo, useState } from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { formatTimezoneLabel, listAllTimezones, timezonesForCountry } from '@/lib/i18n/timezones';
import type { CountryCode } from '@/lib/i18n/countries';
interface TimezoneComboboxProps {
value: string | null | undefined;
onChange: (iana: string | null) => void;
/** When set, the dropdown surfaces matching zones first under a "Suggested" group. */
countryHint?: CountryCode;
placeholder?: string;
disabled?: boolean;
className?: string;
clearable?: boolean;
id?: string;
'data-testid'?: string;
/** Open the dropdown on first render. Used by inline-edit wrappers. */
defaultOpen?: boolean;
/** Notified whenever the dropdown opens/closes. Inline-edit wrappers use
* this to auto-exit edit mode when the user dismisses without picking. */
onOpenChange?: (open: boolean) => void;
feat(i18n): country/phone/timezone/subdivision primitives + form wiring Cross-cutting i18n polish for forms across the marina + residential + company domains. Introduces a single source of truth for country/phone/timezone/ subdivision data and replaces every nationality-as-free-text and timezone- as-string Input with a dedicated combobox. PR1 Countries — ALL_COUNTRY_CODES (~250 ISO-3166-1 alpha-2), Intl.DisplayNames for localized labels, detectDefaultCountry() with navigator-region fallback to US, CountryCombobox with regional-indicator flag glyphs + compact mode for inline use. PR2 Phone — libphonenumber-js wrapper (parsePhone / formatAsYouType / callingCodeFor), PhoneInput with flag dropdown + national-format AsYouType + paste-detect that flips the country dropdown for pasted international strings. PR3 Timezones — country->IANA map (250 entries, multi-zone for AU/BR/CA/CD/ ID/KZ/MN/MX/RU/US), formatTimezoneLabel ("Europe/London (UTC+1)"), TimezoneCombobox with Suggested/All grouping driven by countryHint. PR4 Subdivisions — wraps the iso-3166-2 npm package (~5000 ISO 3166-2 codes for every country), per-country cache, SubdivisionCombobox with "Pick a country first" / "No regions available" empty states. PR5 Schema deltas (migration 0015) — clients.nationality_iso, clientContacts {value_e164, value_country}, clientAddresses {country_iso, subdivision_iso}, residentialClients {phone_e164, phone_country, nationality_iso, timezone, place_of_residence_country_iso, subdivision_iso}, companies {incorporation_ country_iso, incorporation_subdivision_iso}, companyAddresses {country_iso, subdivision_iso}. Plus shared zod validators (validators/i18n.ts) used by every entity validator + route handler. PR6 ClientForm + ClientDetail — CountryCombobox replaces nationality Input, TimezoneCombobox replaces timezone Input (driven by nationalityIso hint), PhoneInput conditionally rendered for phone/whatsapp contacts. Inline editors (InlineCountryField / InlineTimezoneField / InlinePhoneField) for the detail-page overview rows + ContactsEditor. PR7 Residential client form + detail — phone -> PhoneInput, nationality/ timezone/place-of-residence-country/subdivision rows in both create sheet and inline-editable detail view. Subdivision wipes when country flips since codes are country-scoped. PR8 Company form + detail — incorporation country -> CountryCombobox, incorporation region -> SubdivisionCombobox in both modes. PR9 Public inquiry endpoint — accepts pre-normalized phoneE164/phoneCountry and i18n fields from newer website builds, server-side parsePhone() fallback for legacy raw-international submissions. Old Nuxt builds keep working unchanged. Tests: 4 unit suites for the primitives (25 tests), 1 integration spec for the public phone-normalization path (3 tests), 1 smoke spec asserting the combobox triggers render in all three create sheets. Test totals: vitest 713 -> 741 (+28). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:13:08 +02:00
}
export function TimezoneCombobox({
value,
onChange,
countryHint,
placeholder = 'Select timezone…',
disabled,
className,
clearable = true,
id,
'data-testid': testId,
defaultOpen = false,
onOpenChange,
feat(i18n): country/phone/timezone/subdivision primitives + form wiring Cross-cutting i18n polish for forms across the marina + residential + company domains. Introduces a single source of truth for country/phone/timezone/ subdivision data and replaces every nationality-as-free-text and timezone- as-string Input with a dedicated combobox. PR1 Countries — ALL_COUNTRY_CODES (~250 ISO-3166-1 alpha-2), Intl.DisplayNames for localized labels, detectDefaultCountry() with navigator-region fallback to US, CountryCombobox with regional-indicator flag glyphs + compact mode for inline use. PR2 Phone — libphonenumber-js wrapper (parsePhone / formatAsYouType / callingCodeFor), PhoneInput with flag dropdown + national-format AsYouType + paste-detect that flips the country dropdown for pasted international strings. PR3 Timezones — country->IANA map (250 entries, multi-zone for AU/BR/CA/CD/ ID/KZ/MN/MX/RU/US), formatTimezoneLabel ("Europe/London (UTC+1)"), TimezoneCombobox with Suggested/All grouping driven by countryHint. PR4 Subdivisions — wraps the iso-3166-2 npm package (~5000 ISO 3166-2 codes for every country), per-country cache, SubdivisionCombobox with "Pick a country first" / "No regions available" empty states. PR5 Schema deltas (migration 0015) — clients.nationality_iso, clientContacts {value_e164, value_country}, clientAddresses {country_iso, subdivision_iso}, residentialClients {phone_e164, phone_country, nationality_iso, timezone, place_of_residence_country_iso, subdivision_iso}, companies {incorporation_ country_iso, incorporation_subdivision_iso}, companyAddresses {country_iso, subdivision_iso}. Plus shared zod validators (validators/i18n.ts) used by every entity validator + route handler. PR6 ClientForm + ClientDetail — CountryCombobox replaces nationality Input, TimezoneCombobox replaces timezone Input (driven by nationalityIso hint), PhoneInput conditionally rendered for phone/whatsapp contacts. Inline editors (InlineCountryField / InlineTimezoneField / InlinePhoneField) for the detail-page overview rows + ContactsEditor. PR7 Residential client form + detail — phone -> PhoneInput, nationality/ timezone/place-of-residence-country/subdivision rows in both create sheet and inline-editable detail view. Subdivision wipes when country flips since codes are country-scoped. PR8 Company form + detail — incorporation country -> CountryCombobox, incorporation region -> SubdivisionCombobox in both modes. PR9 Public inquiry endpoint — accepts pre-normalized phoneE164/phoneCountry and i18n fields from newer website builds, server-side parsePhone() fallback for legacy raw-international submissions. Old Nuxt builds keep working unchanged. Tests: 4 unit suites for the primitives (25 tests), 1 integration spec for the public phone-normalization path (3 tests), 1 smoke spec asserting the combobox triggers render in all three create sheets. Test totals: vitest 713 -> 741 (+28). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:13:08 +02:00
}: TimezoneComboboxProps) {
const [open, setOpen] = useState(defaultOpen);
const handleOpenChange = (next: boolean) => {
setOpen(next);
onOpenChange?.(next);
};
feat(i18n): country/phone/timezone/subdivision primitives + form wiring Cross-cutting i18n polish for forms across the marina + residential + company domains. Introduces a single source of truth for country/phone/timezone/ subdivision data and replaces every nationality-as-free-text and timezone- as-string Input with a dedicated combobox. PR1 Countries — ALL_COUNTRY_CODES (~250 ISO-3166-1 alpha-2), Intl.DisplayNames for localized labels, detectDefaultCountry() with navigator-region fallback to US, CountryCombobox with regional-indicator flag glyphs + compact mode for inline use. PR2 Phone — libphonenumber-js wrapper (parsePhone / formatAsYouType / callingCodeFor), PhoneInput with flag dropdown + national-format AsYouType + paste-detect that flips the country dropdown for pasted international strings. PR3 Timezones — country->IANA map (250 entries, multi-zone for AU/BR/CA/CD/ ID/KZ/MN/MX/RU/US), formatTimezoneLabel ("Europe/London (UTC+1)"), TimezoneCombobox with Suggested/All grouping driven by countryHint. PR4 Subdivisions — wraps the iso-3166-2 npm package (~5000 ISO 3166-2 codes for every country), per-country cache, SubdivisionCombobox with "Pick a country first" / "No regions available" empty states. PR5 Schema deltas (migration 0015) — clients.nationality_iso, clientContacts {value_e164, value_country}, clientAddresses {country_iso, subdivision_iso}, residentialClients {phone_e164, phone_country, nationality_iso, timezone, place_of_residence_country_iso, subdivision_iso}, companies {incorporation_ country_iso, incorporation_subdivision_iso}, companyAddresses {country_iso, subdivision_iso}. Plus shared zod validators (validators/i18n.ts) used by every entity validator + route handler. PR6 ClientForm + ClientDetail — CountryCombobox replaces nationality Input, TimezoneCombobox replaces timezone Input (driven by nationalityIso hint), PhoneInput conditionally rendered for phone/whatsapp contacts. Inline editors (InlineCountryField / InlineTimezoneField / InlinePhoneField) for the detail-page overview rows + ContactsEditor. PR7 Residential client form + detail — phone -> PhoneInput, nationality/ timezone/place-of-residence-country/subdivision rows in both create sheet and inline-editable detail view. Subdivision wipes when country flips since codes are country-scoped. PR8 Company form + detail — incorporation country -> CountryCombobox, incorporation region -> SubdivisionCombobox in both modes. PR9 Public inquiry endpoint — accepts pre-normalized phoneE164/phoneCountry and i18n fields from newer website builds, server-side parsePhone() fallback for legacy raw-international submissions. Old Nuxt builds keep working unchanged. Tests: 4 unit suites for the primitives (25 tests), 1 integration spec for the public phone-normalization path (3 tests), 1 smoke spec asserting the combobox triggers render in all three create sheets. Test totals: vitest 713 -> 741 (+28). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:13:08 +02:00
const allOptions = useMemo(() => {
return listAllTimezones().map((tz) => ({
tz,
label: formatTimezoneLabel(tz),
}));
}, []);
const suggested = useMemo(() => {
if (!countryHint) return [];
const set = new Set(timezonesForCountry(countryHint));
return allOptions.filter((o) => set.has(o.tz));
}, [allOptions, countryHint]);
const rest = useMemo(() => {
if (!suggested.length) return allOptions;
const suggestedSet = new Set(suggested.map((s) => s.tz));
return allOptions.filter((o) => !suggestedSet.has(o.tz));
}, [allOptions, suggested]);
const selectedLabel = value ? formatTimezoneLabel(value) : placeholder;
return (
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
// `modal` is critical for iOS Safari when this combobox is nested
// inside a Sheet (Radix Dialog). Without it, the parent Dialog's
// pointer-events handling can swallow the trigger's touch event,
// so tapping the button does nothing on iPhone. modal=true makes
// Radix isolate the Popover's pointer context from the parent.
<Popover modal open={open} onOpenChange={handleOpenChange}>
feat(i18n): country/phone/timezone/subdivision primitives + form wiring Cross-cutting i18n polish for forms across the marina + residential + company domains. Introduces a single source of truth for country/phone/timezone/ subdivision data and replaces every nationality-as-free-text and timezone- as-string Input with a dedicated combobox. PR1 Countries — ALL_COUNTRY_CODES (~250 ISO-3166-1 alpha-2), Intl.DisplayNames for localized labels, detectDefaultCountry() with navigator-region fallback to US, CountryCombobox with regional-indicator flag glyphs + compact mode for inline use. PR2 Phone — libphonenumber-js wrapper (parsePhone / formatAsYouType / callingCodeFor), PhoneInput with flag dropdown + national-format AsYouType + paste-detect that flips the country dropdown for pasted international strings. PR3 Timezones — country->IANA map (250 entries, multi-zone for AU/BR/CA/CD/ ID/KZ/MN/MX/RU/US), formatTimezoneLabel ("Europe/London (UTC+1)"), TimezoneCombobox with Suggested/All grouping driven by countryHint. PR4 Subdivisions — wraps the iso-3166-2 npm package (~5000 ISO 3166-2 codes for every country), per-country cache, SubdivisionCombobox with "Pick a country first" / "No regions available" empty states. PR5 Schema deltas (migration 0015) — clients.nationality_iso, clientContacts {value_e164, value_country}, clientAddresses {country_iso, subdivision_iso}, residentialClients {phone_e164, phone_country, nationality_iso, timezone, place_of_residence_country_iso, subdivision_iso}, companies {incorporation_ country_iso, incorporation_subdivision_iso}, companyAddresses {country_iso, subdivision_iso}. Plus shared zod validators (validators/i18n.ts) used by every entity validator + route handler. PR6 ClientForm + ClientDetail — CountryCombobox replaces nationality Input, TimezoneCombobox replaces timezone Input (driven by nationalityIso hint), PhoneInput conditionally rendered for phone/whatsapp contacts. Inline editors (InlineCountryField / InlineTimezoneField / InlinePhoneField) for the detail-page overview rows + ContactsEditor. PR7 Residential client form + detail — phone -> PhoneInput, nationality/ timezone/place-of-residence-country/subdivision rows in both create sheet and inline-editable detail view. Subdivision wipes when country flips since codes are country-scoped. PR8 Company form + detail — incorporation country -> CountryCombobox, incorporation region -> SubdivisionCombobox in both modes. PR9 Public inquiry endpoint — accepts pre-normalized phoneE164/phoneCountry and i18n fields from newer website builds, server-side parsePhone() fallback for legacy raw-international submissions. Old Nuxt builds keep working unchanged. Tests: 4 unit suites for the primitives (25 tests), 1 integration spec for the public phone-normalization path (3 tests), 1 smoke spec asserting the combobox triggers render in all three create sheets. Test totals: vitest 713 -> 741 (+28). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:13:08 +02:00
<PopoverTrigger asChild>
<Button
id={id}
variant="outline"
role="combobox"
aria-expanded={open}
disabled={disabled}
className={cn('w-full justify-between', !value && 'text-muted-foreground', className)}
data-testid={testId}
>
<span className="truncate text-sm">{selectedLabel}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
feat(i18n): country/phone/timezone/subdivision primitives + form wiring Cross-cutting i18n polish for forms across the marina + residential + company domains. Introduces a single source of truth for country/phone/timezone/ subdivision data and replaces every nationality-as-free-text and timezone- as-string Input with a dedicated combobox. PR1 Countries — ALL_COUNTRY_CODES (~250 ISO-3166-1 alpha-2), Intl.DisplayNames for localized labels, detectDefaultCountry() with navigator-region fallback to US, CountryCombobox with regional-indicator flag glyphs + compact mode for inline use. PR2 Phone — libphonenumber-js wrapper (parsePhone / formatAsYouType / callingCodeFor), PhoneInput with flag dropdown + national-format AsYouType + paste-detect that flips the country dropdown for pasted international strings. PR3 Timezones — country->IANA map (250 entries, multi-zone for AU/BR/CA/CD/ ID/KZ/MN/MX/RU/US), formatTimezoneLabel ("Europe/London (UTC+1)"), TimezoneCombobox with Suggested/All grouping driven by countryHint. PR4 Subdivisions — wraps the iso-3166-2 npm package (~5000 ISO 3166-2 codes for every country), per-country cache, SubdivisionCombobox with "Pick a country first" / "No regions available" empty states. PR5 Schema deltas (migration 0015) — clients.nationality_iso, clientContacts {value_e164, value_country}, clientAddresses {country_iso, subdivision_iso}, residentialClients {phone_e164, phone_country, nationality_iso, timezone, place_of_residence_country_iso, subdivision_iso}, companies {incorporation_ country_iso, incorporation_subdivision_iso}, companyAddresses {country_iso, subdivision_iso}. Plus shared zod validators (validators/i18n.ts) used by every entity validator + route handler. PR6 ClientForm + ClientDetail — CountryCombobox replaces nationality Input, TimezoneCombobox replaces timezone Input (driven by nationalityIso hint), PhoneInput conditionally rendered for phone/whatsapp contacts. Inline editors (InlineCountryField / InlineTimezoneField / InlinePhoneField) for the detail-page overview rows + ContactsEditor. PR7 Residential client form + detail — phone -> PhoneInput, nationality/ timezone/place-of-residence-country/subdivision rows in both create sheet and inline-editable detail view. Subdivision wipes when country flips since codes are country-scoped. PR8 Company form + detail — incorporation country -> CountryCombobox, incorporation region -> SubdivisionCombobox in both modes. PR9 Public inquiry endpoint — accepts pre-normalized phoneE164/phoneCountry and i18n fields from newer website builds, server-side parsePhone() fallback for legacy raw-international submissions. Old Nuxt builds keep working unchanged. Tests: 4 unit suites for the primitives (25 tests), 1 integration spec for the public phone-normalization path (3 tests), 1 smoke spec asserting the combobox triggers render in all three create sheets. Test totals: vitest 713 -> 741 (+28). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:13:08 +02:00
</Button>
</PopoverTrigger>
<PopoverContent className="w-(--radix-popper-anchor-width) min-w-[360px] p-0" align="start">
feat(i18n): country/phone/timezone/subdivision primitives + form wiring Cross-cutting i18n polish for forms across the marina + residential + company domains. Introduces a single source of truth for country/phone/timezone/ subdivision data and replaces every nationality-as-free-text and timezone- as-string Input with a dedicated combobox. PR1 Countries — ALL_COUNTRY_CODES (~250 ISO-3166-1 alpha-2), Intl.DisplayNames for localized labels, detectDefaultCountry() with navigator-region fallback to US, CountryCombobox with regional-indicator flag glyphs + compact mode for inline use. PR2 Phone — libphonenumber-js wrapper (parsePhone / formatAsYouType / callingCodeFor), PhoneInput with flag dropdown + national-format AsYouType + paste-detect that flips the country dropdown for pasted international strings. PR3 Timezones — country->IANA map (250 entries, multi-zone for AU/BR/CA/CD/ ID/KZ/MN/MX/RU/US), formatTimezoneLabel ("Europe/London (UTC+1)"), TimezoneCombobox with Suggested/All grouping driven by countryHint. PR4 Subdivisions — wraps the iso-3166-2 npm package (~5000 ISO 3166-2 codes for every country), per-country cache, SubdivisionCombobox with "Pick a country first" / "No regions available" empty states. PR5 Schema deltas (migration 0015) — clients.nationality_iso, clientContacts {value_e164, value_country}, clientAddresses {country_iso, subdivision_iso}, residentialClients {phone_e164, phone_country, nationality_iso, timezone, place_of_residence_country_iso, subdivision_iso}, companies {incorporation_ country_iso, incorporation_subdivision_iso}, companyAddresses {country_iso, subdivision_iso}. Plus shared zod validators (validators/i18n.ts) used by every entity validator + route handler. PR6 ClientForm + ClientDetail — CountryCombobox replaces nationality Input, TimezoneCombobox replaces timezone Input (driven by nationalityIso hint), PhoneInput conditionally rendered for phone/whatsapp contacts. Inline editors (InlineCountryField / InlineTimezoneField / InlinePhoneField) for the detail-page overview rows + ContactsEditor. PR7 Residential client form + detail — phone -> PhoneInput, nationality/ timezone/place-of-residence-country/subdivision rows in both create sheet and inline-editable detail view. Subdivision wipes when country flips since codes are country-scoped. PR8 Company form + detail — incorporation country -> CountryCombobox, incorporation region -> SubdivisionCombobox in both modes. PR9 Public inquiry endpoint — accepts pre-normalized phoneE164/phoneCountry and i18n fields from newer website builds, server-side parsePhone() fallback for legacy raw-international submissions. Old Nuxt builds keep working unchanged. Tests: 4 unit suites for the primitives (25 tests), 1 integration spec for the public phone-normalization path (3 tests), 1 smoke spec asserting the combobox triggers render in all three create sheets. Test totals: vitest 713 -> 741 (+28). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:13:08 +02:00
<Command>
<CommandInput placeholder="Search timezones…" />
<CommandList>
<CommandEmpty>No timezone found.</CommandEmpty>
{clearable && value ? (
<CommandGroup>
<CommandItem
value="__clear__"
onSelect={() => {
onChange(null);
setOpen(false);
}}
className="text-muted-foreground"
>
Clear selection
</CommandItem>
</CommandGroup>
) : null}
{suggested.length > 0 ? (
<>
<CommandGroup heading="Suggested">
{suggested.map((opt) => (
<CommandItem
key={opt.tz}
value={opt.tz}
onSelect={() => {
onChange(opt.tz);
setOpen(false);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
value === opt.tz ? 'opacity-100' : 'opacity-0',
)}
/>
<span className="truncate text-sm">{opt.label}</span>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
</>
) : null}
<CommandGroup heading={suggested.length > 0 ? 'All zones' : undefined}>
{rest.map((opt) => (
<CommandItem
key={opt.tz}
value={opt.tz}
onSelect={() => {
onChange(opt.tz);
setOpen(false);
}}
>
<Check
className={cn('mr-2 h-4 w-4', value === opt.tz ? 'opacity-100' : 'opacity-0')}
/>
<span className="truncate text-sm">{opt.label}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}