Files
arcadia-admin/app/components/tenant-detail/localization-tab.tsx
jules 7415b40240 Admin UX overhaul: P0 fixes, error-UX foundations, nav cleanup, tenant detail page
From the 2026-07-14 UI/UX audit (17/40). Four phases:

P1 — Settings route crashed on Agents→Edit (Input/Textarea used but never
imported). Added imports + a shared route-level error boundary
(components/route-error.tsx) re-exported from all shell routes, so one
crashing panel degrades to an explained card with the nav intact instead of
replacing the whole app with a stack trace. Corrected the stale CLAUDE.md
claim that `npm run typecheck` crashes — it works, and would have caught the
missing import.

P2 — Error/empty/feedback foundations. Fixed useSession identity churn that
fired ~3x duplicate fetches per screen and self-inflicted 429s (referentially
stable snapshot). New lib/errors.ts (describeError → plain-language + the fix)
and components/data-state.tsx (DataState renders exactly one of
error/loading/empty/content, so a failed load never shows as "empty";
DialogError for in-dialog failures; 429 auto-retry). Rolled across all 15
list routes; every mutation now toasts. Surfaced+fixed two silent-failure
bugs (sso + buckets-CORS swallowed load errors; the latter could wipe rules
on save).

P3 — Nav IA + trust cleanup. Default-expanded rail; regrouped into 7 coherent
sections; collapsed the Apps/Plan/Entitlements stub triplication into one
honest /billing page; deleted fabricated seeded notifications and the dead
Help menu item; fixed the 403 copy (referenced a tenant switcher that doesn't
exist); removed orphan /assistant + /library routes; gated dev-seed login
hints behind DEV; renamed /activity → /audit-log with a redirect.

P4 — Tenant detail page (routes/tenants.$id.tsx + components/tenant-detail/*),
closing the provision→configure gap. 8 tabs (Overview, Plan & quotas,
Branding, Localization, Email & SMS, Feature flags, IP rules, Inbound
webhooks), each verified saving to the real backend. Row name links to detail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:43:57 +10:00

115 lines
3.6 KiB
TypeScript

import { useState } from "react"
import { useArcadiaClient } from "@crema/arcadia-core-client"
import { useToast } from "@crema/notification-ui"
import { TenantSection, Field } from "~/components/tenant-detail/section"
import { NativeSelect, NativeSelectOption } from "~/components/ui/native-select"
import {
updateLocalization,
TENANT_LOCALES,
TENANT_CURRENCIES,
COMMON_TIMEZONES,
} from "~/lib/arcadia/tenants"
import type { TenantTabProps } from "~/routes/tenants.$id"
export function LocalizationTab({ tenant, reload }: TenantTabProps) {
const arcadia = useArcadiaClient()
const toast = useToast()
const current = tenant.localization
const [locale, setLocale] = useState(current?.locale ?? "")
const [timezone, setTimezone] = useState(current?.timezone ?? "")
const [currency, setCurrency] = useState(current?.currency ?? "")
const [saving, setSaving] = useState(false)
const [error, setError] = useState<unknown>(null)
// The tenant's real timezone might be an IANA name outside our curated list —
// prepend it so the select shows the current value instead of silently
// snapping to the first option.
const timezoneOptions =
current?.timezone && !COMMON_TIMEZONES.includes(current.timezone)
? [current.timezone, ...COMMON_TIMEZONES]
: COMMON_TIMEZONES
const dirty =
locale !== (current?.locale ?? "") ||
timezone !== (current?.timezone ?? "") ||
currency !== (current?.currency ?? "")
const save = async () => {
setSaving(true)
setError(null)
try {
await updateLocalization(arcadia, tenant.id, { locale, timezone, currency })
await reload()
toast.success("Localization updated")
} catch (err) {
setError(err)
} finally {
setSaving(false)
}
}
return (
<TenantSection
title="Localization"
description="Default locale, timezone, and currency for this tenant."
onSubmit={save}
saving={saving}
error={error}
errorContext="update localization"
dirty={dirty}
dataAction="tenant-detail-localization-save"
>
<Field label="Locale" htmlFor="tenant-detail-localization-locale">
<NativeSelect
id="tenant-detail-localization-locale"
className="w-full"
value={locale}
onChange={(e) => setLocale(e.target.value)}
data-action="tenant-detail-localization-locale"
>
{TENANT_LOCALES.map((l) => (
<NativeSelectOption key={l} value={l}>
{l}
</NativeSelectOption>
))}
</NativeSelect>
</Field>
<Field label="Currency" htmlFor="tenant-detail-localization-currency">
<NativeSelect
id="tenant-detail-localization-currency"
className="w-full"
value={currency}
onChange={(e) => setCurrency(e.target.value)}
data-action="tenant-detail-localization-currency"
>
{TENANT_CURRENCIES.map((c) => (
<NativeSelectOption key={c} value={c}>
{c}
</NativeSelectOption>
))}
</NativeSelect>
</Field>
<Field label="Timezone" htmlFor="tenant-detail-localization-timezone">
<NativeSelect
id="tenant-detail-localization-timezone"
className="w-full"
value={timezone}
onChange={(e) => setTimezone(e.target.value)}
data-action="tenant-detail-localization-timezone"
>
{timezoneOptions.map((tz) => (
<NativeSelectOption key={tz} value={tz}>
{tz}
</NativeSelectOption>
))}
</NativeSelect>
</Field>
</TenantSection>
)
}