import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { AlertTriangle, Database, FileText, Globe, Lock, Pencil, Plus, RotateCcw, Search, UserCog, } from 'lucide-react' import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal' import { BusinessActionsOverflowMenu } from '../components/BusinessActionsOverflowMenu' import { ToggleSwitch } from '../components/ToggleSwitch' import { formatCellForDisplay, toE164CellNumber } from '../lib/cellNumber' import type { BusinessesListResponse, BusinessListItem, MigrateEntityResult, MigrateFromOldEntity, MigrateFromOldResponse, PurgeBusinessDataEntity, PurgeBusinessDataResponse, PurgeEntityResult, } from '../types/business' import type { BusinessCategory } from '../types/category' import type { ListBusinessesParams } from '../services/businessService' import { ApiError, isAbortError } from '../lib/api' import { addBusinessDomain, createBusiness, listBusinesses, migrateBusinessFromOld, purgeBusinessData, removeBusiness, setBusinessActive, updateBusiness, updateBusinessDomain, } from '../services/businessService' import { listBusinessCategories } from '../services/categoryService' import { Modal } from '../components/Modal' import { MultiSelectDropdown } from '../components/MultiSelectDropdown' import { PrimaryColorPicker } from '../components/PrimaryColorPicker' import { PrimaryColorSwatchControl } from '../components/PrimaryColorSwatchControl' import { useToast } from '../context/ToastContext' import { DEFAULT_BUSINESS_PRIMARY_COLOR_ID, normalizeBusinessPrimaryColorId, type BusinessPrimaryColorId, } from '../utils/businessPrimaryColors' import { getBusinessSettings, updateBusinessBranding, updateBusinessDefaultLocale, updateBusinessModules, updateBusinessPrimaryColor, type DashboardLocale, } from '../services/businessSettingsService' import { BUSINESS_MODULE_IDS, BUSINESS_MODULE_LABELS, DEFAULT_ENABLED_BUSINESS_MODULES, DEFAULT_HOME_CHARTS, HOME_CHART_IDS, HOME_CHART_LABELS, normalizeEnabledBusinessModules, normalizeHomeCharts, type BusinessModuleId, type HomeChartId, } from '../utils/businessModules' import { flattenBusinessCategories } from '../utils/categories' import { Pagination } from '@meshkee/dashboard-ui' import pageStyles from '../components/PageContent.module.css' import styles from './BusinessesPage.module.css' const DEFAULT_PAGE_SIZE = 10 const DEFAULT_LOCALE: DashboardLocale = 'fa' function normalizeDefaultLocale(value: unknown): DashboardLocale { return value === 'en' || value === 'fa' ? value : DEFAULT_LOCALE } function emptyModuleSelection( enabled: BusinessModuleId[] = DEFAULT_ENABLED_BUSINESS_MODULES, ): Record { const selected = new Set(enabled) return Object.fromEntries( BUSINESS_MODULE_IDS.map((id) => [id, selected.has(id)]), ) as Record } function formatDate(value: string) { const d = new Date(value) if (Number.isNaN(d.getTime())) return value return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit' }) } const MIGRATE_ENTITY_LABELS: Record = { product_categories: 'Product categories', product: 'Products', customer_categories: 'Customer categories', customer: 'Customers', blog_categories: 'Blog categories', blog: 'Blogs', portfolio_categories: 'Portfolio categories', portfolio: 'Portfolios', } const DATA_ENTITY_GROUPS = [ { title: 'Products', items: [ ['product_categories', 'Categories'], ['product', 'Products'], ], }, { title: 'Customers', items: [ ['customer_categories', 'Categories'], ['customer', 'Customers'], ], }, { title: 'Blogs', items: [ ['blog_categories', 'Categories'], ['blog', 'Blogs'], ], }, { title: 'Portfolios', items: [ ['portfolio_categories', 'Categories'], ['portfolio', 'Portfolios'], ], }, ] as const function formatMigrateEntityResult(result: MigrateEntityResult): string { if ('created' in result) { const base = `${result.created} created, ${result.skipped} skipped (${result.total} total)` const extras = [ result.imagesCopied != null ? `${result.imagesCopied} images copied` : null, result.imagesResized != null && result.imagesResized > 0 ? `${result.imagesResized} resized (≤1280px)` : null, result.titlesUpdated != null && result.titlesUpdated > 0 ? `${result.titlesUpdated} titles updated` : null, result.imagesFailed != null && result.imagesFailed > 0 ? `${result.imagesFailed} images failed` : null, result.skippedInvalidCell != null && result.skippedInvalidCell > 0 ? `${result.skippedInvalidCell} invalid/missing cell` : null, result.skippedAlreadyLinked != null && result.skippedAlreadyLinked > 0 ? `${result.skippedAlreadyLinked} already linked` : null, result.skippedCreateFailed != null && result.skippedCreateFailed > 0 ? `${result.skippedCreateFailed} create failed` : null, ].filter(Boolean) return extras.length ? `${base}; ${extras.join(', ')}` : base } return 'Not implemented yet' } function formatPurgeEntityResult(result: PurgeEntityResult): string { if ('deleted' in result) { const extras = [ result.imagesDeleted != null ? `${result.imagesDeleted} images deleted` : null, ].filter(Boolean) const base = `${result.deleted} deleted` return extras.length ? `${base}; ${extras.join(', ')}` : base } return 'Not implemented yet' } function emptyMigrateEntities(): Record { return { product_categories: false, product: false, customer_categories: false, customer: false, blog_categories: false, blog: false, portfolio_categories: false, portfolio: false, } } export function BusinessesPage() { const { showToast } = useToast() const navigate = useNavigate() const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [categories, setCategories] = useState([]) const [appliedFilters, setAppliedFilters] = useState>( {}, ) const [page, setPage] = useState(1) const [pageSize] = useState(DEFAULT_PAGE_SIZE) const [draftName, setDraftName] = useState('') const [draftDomain, setDraftDomain] = useState('') const [draftCategory, setDraftCategory] = useState('') const [editOpen, setEditOpen] = useState(false) const [editBusiness, setEditBusiness] = useState(null) const [editName, setEditName] = useState('') const [editPrimaryColor, setEditPrimaryColor] = useState( DEFAULT_BUSINESS_PRIMARY_COLOR_ID, ) const [editDefaultLocale, setEditDefaultLocale] = useState(DEFAULT_LOCALE) const [editLoadingSettings, setEditLoadingSettings] = useState(false) const [editSubmitting, setEditSubmitting] = useState(false) const [editError, setEditError] = useState('') const [domainOpen, setDomainOpen] = useState(false) const [domainBusiness, setDomainBusiness] = useState(null) const [domainId, setDomainId] = useState(null) const [domainHost, setDomainHost] = useState('') const [domainGitRepoUrl, setDomainGitRepoUrl] = useState('') const [domainSubmitting, setDomainSubmitting] = useState(false) const [domainError, setDomainError] = useState('') const [createOpen, setCreateOpen] = useState(false) const [createName, setCreateName] = useState('') const [createNameFa, setCreateNameFa] = useState('') const [createAbout, setCreateAbout] = useState('') const [createCategoryIds, setCreateCategoryIds] = useState([]) const [createOwnerFirstName, setCreateOwnerFirstName] = useState('') const [createOwnerLastName, setCreateOwnerLastName] = useState('') const [createOwnerFirstNameEn, setCreateOwnerFirstNameEn] = useState('') const [createOwnerLastNameEn, setCreateOwnerLastNameEn] = useState('') const [createOwnerCell, setCreateOwnerCell] = useState('') const [createOwnerPassword, setCreateOwnerPassword] = useState('') const [createSubmitting, setCreateSubmitting] = useState(false) const [createError, setCreateError] = useState('') const [removeTarget, setRemoveTarget] = useState(null) const [togglingId, setTogglingId] = useState(null) const [savingColorId, setSavingColorId] = useState(null) const [savingLocaleId, setSavingLocaleId] = useState(null) const [modulesOpen, setModulesOpen] = useState(false) const [modulesBusiness, setModulesBusiness] = useState(null) const [modulesSelected, setModulesSelected] = useState>( () => emptyModuleSelection(), ) const [modulesCharts, setModulesCharts] = useState<[HomeChartId, HomeChartId]>([ ...DEFAULT_HOME_CHARTS, ]) const [modulesSubmitting, setModulesSubmitting] = useState(false) const [modulesError, setModulesError] = useState('') const [migrateOpen, setMigrateOpen] = useState(false) const [migrateBusiness, setMigrateBusiness] = useState(null) const [migrateOldId, setMigrateOldId] = useState('') const [migrateEntities, setMigrateEntities] = useState>( () => emptyMigrateEntities(), ) const [migrateSubmitting, setMigrateSubmitting] = useState(false) const [migrateError, setMigrateError] = useState('') const [migrateResult, setMigrateResult] = useState(null) const [purgeOpen, setPurgeOpen] = useState(false) const [purgeBusiness, setPurgeBusiness] = useState(null) const [purgeEntities, setPurgeEntities] = useState>( () => emptyMigrateEntities(), ) const [purgeSubmitting, setPurgeSubmitting] = useState(false) const [purgeError, setPurgeError] = useState('') const [purgeResult, setPurgeResult] = useState(null) async function fetchList() { setLoading(true) setError('') try { const result = await listBusinesses({ page, pageSize, ...appliedFilters, }) setData(result) } catch (err) { setError(err instanceof ApiError ? err.message : 'Unable to load businesses.') } finally { setLoading(false) } } useEffect(() => { const controller = new AbortController() async function load() { setLoading(true) setError('') try { const [result, categoryResult] = await Promise.all([ listBusinesses( { page, pageSize, ...appliedFilters, }, controller.signal, ), listBusinessCategories(controller.signal), ]) if (controller.signal.aborted) return setData(result) setCategories(categoryResult.items) } catch (err) { if (isAbortError(err) || controller.signal.aborted) return setError(err instanceof ApiError ? err.message : 'Unable to load businesses.') } finally { if (!controller.signal.aborted) setLoading(false) } } void load() return () => { controller.abort() } }, [page, pageSize, appliedFilters.name, appliedFilters.domain, appliedFilters.category]) const totalPages = useMemo(() => { const total = data?.total ?? 0 return Math.max(1, Math.ceil(total / pageSize)) }, [data?.total, pageSize]) const showingFrom = useMemo(() => { if (!data) return 0 return (page - 1) * pageSize + 1 }, [data, page, pageSize]) const showingTo = useMemo(() => { if (!data) return 0 return Math.min(data.total, page * pageSize) }, [data, page, pageSize]) const categoryOptions = useMemo(() => flattenBusinessCategories(categories), [categories]) function applyFilters() { setPage(1) setAppliedFilters({ ...(draftName.trim() ? { name: draftName.trim() } : {}), ...(draftDomain.trim() ? { domain: draftDomain.trim() } : {}), ...(draftCategory.trim() ? { category: draftCategory.trim() } : {}), }) } function clearFilters() { setDraftName('') setDraftDomain('') setDraftCategory('') setPage(1) setAppliedFilters({}) } function openEdit(b: BusinessListItem) { setEditBusiness(b) setEditName(b.name) setEditPrimaryColor(normalizeBusinessPrimaryColorId(b.primaryColor)) setEditDefaultLocale(normalizeDefaultLocale(b.defaultLocale)) setEditError('') setEditOpen(true) setEditLoadingSettings(true) const controller = new AbortController() void getBusinessSettings(b.id, controller.signal) .then((data) => { setEditPrimaryColor(data.settings.branding.primaryColor) setEditDefaultLocale(normalizeDefaultLocale(data.settings.branding.defaultLocale)) }) .catch((err) => { if (isAbortError(err)) return setEditError(err instanceof ApiError ? err.message : 'Unable to load business theme.') }) .finally(() => { if (!controller.signal.aborted) setEditLoadingSettings(false) }) } function goToBusinessUsers(b: BusinessListItem, membership: 'staff' | 'all') { const params = new URLSearchParams({ businessId: String(b.id), membership, businessName: b.name, }) navigate(`/users?${params.toString()}`) } async function handlePrimaryColorChange( b: BusinessListItem, primaryColor: BusinessPrimaryColorId, ) { const currentColor = normalizeBusinessPrimaryColorId(b.primaryColor) if (currentColor === primaryColor) return setSavingColorId(b.id) setError('') setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => item.id === b.id ? { ...item, primaryColor } : item, ), } }) if (editBusiness?.id === b.id) { setEditPrimaryColor(primaryColor) } try { await updateBusinessPrimaryColor(b.id, primaryColor) showToast(`Theme updated for "${b.name}".`, 'success') } catch (err) { setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => item.id === b.id ? { ...item, primaryColor: currentColor } : item, ), } }) if (editBusiness?.id === b.id) { setEditPrimaryColor(currentColor) } showToast( err instanceof ApiError ? err.message : 'Unable to update theme.', 'error', ) } finally { setSavingColorId(null) } } async function handleDefaultLocaleChange(b: BusinessListItem, defaultLocale: DashboardLocale) { const currentLocale = normalizeDefaultLocale(b.defaultLocale) if (currentLocale === defaultLocale) return setSavingLocaleId(b.id) setError('') setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => item.id === b.id ? { ...item, defaultLocale } : item, ), } }) if (editBusiness?.id === b.id) { setEditDefaultLocale(defaultLocale) } try { await updateBusinessDefaultLocale(b.id, defaultLocale) showToast(`Default language updated for "${b.name}".`, 'success') } catch (err) { setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => item.id === b.id ? { ...item, defaultLocale: currentLocale } : item, ), } }) if (editBusiness?.id === b.id) { setEditDefaultLocale(currentLocale) } showToast( err instanceof ApiError ? err.message : 'Unable to update default language.', 'error', ) } finally { setSavingLocaleId(null) } } function openModules(b: BusinessListItem) { setModulesBusiness(b) setModulesSelected( emptyModuleSelection( normalizeEnabledBusinessModules(b.enabledModules ?? DEFAULT_ENABLED_BUSINESS_MODULES), ), ) setModulesCharts(normalizeHomeCharts(b.homeCharts ?? DEFAULT_HOME_CHARTS)) setModulesError('') setModulesOpen(true) } function closeModules() { if (modulesSubmitting) return setModulesOpen(false) setModulesBusiness(null) setModulesError('') } async function submitModules() { if (!modulesBusiness) return const enabled = BUSINESS_MODULE_IDS.filter((id) => modulesSelected[id]) const charts = normalizeHomeCharts(modulesCharts) setModulesSubmitting(true) setModulesError('') try { await updateBusinessModules(modulesBusiness.id, enabled, charts) setData((prev) => prev ? { ...prev, items: prev.items.map((item) => item.id === modulesBusiness.id ? { ...item, enabledModules: enabled, moduleCount: enabled.length, homeCharts: charts, } : item, ), } : prev, ) showToast('Modules updated.', 'success') setModulesOpen(false) setModulesBusiness(null) } catch (err) { setModulesError( err instanceof ApiError ? err.message : 'Unable to update modules.', ) } finally { setModulesSubmitting(false) } } async function submitEdit() { if (!editBusiness) return setEditSubmitting(true) setEditError('') try { await updateBusiness(editBusiness.id, { name: editName }) await updateBusinessBranding(editBusiness.id, { primaryColor: editPrimaryColor, defaultLocale: editDefaultLocale, }) setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => item.id === editBusiness.id ? { ...item, name: editName.trim(), primaryColor: editPrimaryColor, defaultLocale: editDefaultLocale, } : item, ), } }) setEditOpen(false) setEditBusiness(null) showToast('Business updated.', 'success') await fetchList() } catch (err) { setEditError(err instanceof ApiError ? err.message : 'Unable to update business.') } finally { setEditSubmitting(false) } } function openDomainEdit(b: BusinessListItem) { setDomainBusiness(b) setDomainId(b.domainId) setDomainHost(b.domain ?? '') setDomainGitRepoUrl('') setDomainError('') setDomainOpen(true) } function openMigrate(b: BusinessListItem) { setMigrateBusiness(b) setMigrateOldId(b.oldBusinessId != null ? String(b.oldBusinessId) : '') setMigrateEntities(emptyMigrateEntities()) setMigrateError('') setMigrateResult(null) setMigrateOpen(true) } const migrateSubmittingRef = useRef(false) migrateSubmittingRef.current = migrateSubmitting const closeMigrate = useCallback(() => { if (migrateSubmittingRef.current) return setMigrateOpen(false) setMigrateBusiness(null) setMigrateError('') setMigrateResult(null) }, []) function selectMigrateEntity(entity: MigrateFromOldEntity) { setMigrateResult(null) setMigrateError('') setMigrateEntities({ ...emptyMigrateEntities(), [entity]: true }) } async function submitMigrate() { if (!migrateBusiness) return const oldBusinessId = Number(String(migrateOldId).trim()) if (!Number.isInteger(oldBusinessId) || oldBusinessId <= 0) { setMigrateResult(null) setMigrateError('Enter a valid old business id (positive integer).') return } const entities = (Object.keys(migrateEntities) as MigrateFromOldEntity[]).filter( (key) => migrateEntities[key], ) if (entities.length === 0) { setMigrateResult(null) setMigrateError('Select a data type to migrate.') return } setMigrateSubmitting(true) setMigrateError('') setMigrateResult(null) try { const result = await migrateBusinessFromOld(migrateBusiness.id, { oldBusinessId, entities, }) setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => item.id === migrateBusiness.id ? { ...item, oldBusinessId: String(oldBusinessId) } : item, ), } }) setMigrateResult(result) } catch (err) { setMigrateError( err instanceof ApiError ? err.message : 'Unable to migrate data from old CMS.', ) } finally { setMigrateSubmitting(false) } } function openPurge(b: BusinessListItem) { setPurgeBusiness(b) setPurgeEntities(emptyMigrateEntities()) setPurgeError('') setPurgeResult(null) setPurgeOpen(true) } const purgeSubmittingRef = useRef(false) purgeSubmittingRef.current = purgeSubmitting const closePurge = useCallback(() => { if (purgeSubmittingRef.current) return setPurgeOpen(false) setPurgeBusiness(null) setPurgeError('') setPurgeResult(null) }, []) function selectPurgeEntity(entity: PurgeBusinessDataEntity) { setPurgeResult(null) setPurgeError('') setPurgeEntities({ ...emptyMigrateEntities(), [entity]: true }) } async function submitPurge() { if (!purgeBusiness) return const entities = (Object.keys(purgeEntities) as PurgeBusinessDataEntity[]).filter( (key) => purgeEntities[key], ) if (entities.length === 0) { setPurgeResult(null) setPurgeError('Select a data type to delete.') return } setPurgeSubmitting(true) setPurgeError('') setPurgeResult(null) try { const result = await purgeBusinessData(purgeBusiness.id, { entities }) setPurgeResult(result) showToast(result.message, 'success') } catch (err) { setPurgeError( err instanceof ApiError ? err.message : 'Unable to delete selected business data.', ) } finally { setPurgeSubmitting(false) } } async function submitDomain() { if (!domainBusiness) return const host = domainHost.trim() if (!host) return const gitRepoUrl = domainGitRepoUrl.trim() setDomainSubmitting(true) setDomainError('') try { if (domainId) { await updateBusinessDomain(domainBusiness.id, domainId, { host }) setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => item.id === domainBusiness.id ? { ...item, domain: host } : item, ), } }) showToast(`Domain updated to "${host}".`, 'success') } else { const created = (await addBusinessDomain(domainBusiness.id, { host, isPrimary: true, ...(gitRepoUrl ? { gitRepoUrl } : {}), })) as { id: number host: string sslEnabled: boolean deploySlug?: string | null provisionError?: string | null } setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => item.id === domainBusiness.id ? { ...item, domainId: created.id, domain: created.host, sslEnabled: created.sslEnabled ?? false, } : item, ), } }) if (created.provisionError) { showToast(`Domain "${host}" added, but storefront setup failed.`, 'error') showToast(created.provisionError, 'error') } else if (gitRepoUrl && created.deploySlug) { showToast( `Domain "${host}" added. Storefront deploy is ready on Websites.`, 'success', ) } else { showToast(`Domain "${host}" added.`, 'success') } } setDomainOpen(false) setDomainBusiness(null) setDomainId(null) setDomainGitRepoUrl('') } catch (err) { setDomainError(err instanceof ApiError ? err.message : 'Unable to save domain.') } finally { setDomainSubmitting(false) } } function resetCreateForm() { setCreateName('') setCreateNameFa('') setCreateAbout('') setCreateCategoryIds([]) setCreateOwnerFirstName('') setCreateOwnerLastName('') setCreateOwnerFirstNameEn('') setCreateOwnerLastNameEn('') setCreateOwnerCell('') setCreateOwnerPassword('') setCreateError('') } function openCreate() { resetCreateForm() setCreateOpen(true) } async function submitCreate() { const ownerCellNumber = toE164CellNumber(createOwnerCell.trim()) if (!ownerCellNumber) return setCreateSubmitting(true) setCreateError('') try { await createBusiness({ name: createName.trim(), nameFa: createNameFa.trim(), about: createAbout.trim() || undefined, categoryIds: createCategoryIds, ownerFirstName: createOwnerFirstName.trim(), ownerLastName: createOwnerLastName.trim(), ownerFirstNameEn: createOwnerFirstNameEn.trim(), ownerLastNameEn: createOwnerLastNameEn.trim(), ownerCellNumber, ownerPassword: createOwnerPassword, }) setCreateOpen(false) resetCreateForm() showToast(`"${createName.trim()}" has been created.`, 'success') await fetchList() } catch (err) { setCreateError(err instanceof ApiError ? err.message : 'Unable to create business.') } finally { setCreateSubmitting(false) } } async function handleToggleActive(b: BusinessListItem, isActive: boolean) { setTogglingId(b.id) setError('') setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => (item.id === b.id ? { ...item, isActive } : item)), } }) try { await setBusinessActive(b.id, { isActive }) showToast(`"${b.name}" has been ${isActive ? 'enabled' : 'disabled'}.`, 'success') } catch (err) { setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => item.id === b.id ? { ...item, isActive: !isActive } : item, ), } }) setError(err instanceof ApiError ? err.message : 'Unable to update business status.') } finally { setTogglingId(null) } } async function confirmRemove() { if (!removeTarget) return setError('') try { await removeBusiness(removeTarget.id) setRemoveTarget(null) await fetchList() } catch (err) { setError(err instanceof ApiError ? err.message : 'Unable to remove business.') } } const canSubmitCreate = createName.trim().length >= 2 && createNameFa.trim().length >= 2 && createCategoryIds.length > 0 && createOwnerFirstName.trim().length >= 2 && createOwnerLastName.trim().length >= 2 && createOwnerFirstNameEn.trim().length >= 2 && createOwnerLastNameEn.trim().length >= 2 && toE164CellNumber(createOwnerCell.trim()).length > 0 && createOwnerPassword.length >= 8 const canSubmitMigrate = Number.isInteger(Number(String(migrateOldId).trim())) && Number(String(migrateOldId).trim()) > 0 && Object.values(migrateEntities).some(Boolean) const canSubmitPurge = Object.values(purgeEntities).some(Boolean) return (

Businesses

Manage businesses, domains, and SSL settings.

Filters
setDraftName(e.target.value)} placeholder="Search by business name" />
setDraftDomain(e.target.value)} placeholder="shop-a.local" />
setDraftCategory(e.target.value)} placeholder="e.g. electronics / phones" />
Business list
{data ? ( <> Showing {showingFrom} - {showingTo} of {data.total} ) : ( ' ' )}
{error && (
{error}
)}
{loading && ( )} {!loading && data?.items?.length === 0 && ( )} {!loading && data?.items?.map((b) => ( ))}
Name Date created Domain Owner Theme Lang Modules Actions
Loading...
No results found.
{b.name} {formatDate(b.createdAt)} {b.domain ? (
{b.domain} {b.sslEnabled ? ( ) : ( )}
) : ( No domain )}
{b.ownerNameEn || '—'}
{b.ownerCellNumber ? formatCellForDisplay(b.ownerCellNumber) : '—'}
void handlePrimaryColorChange(b, primaryColor) } />
void handleToggleActive(b, isActive)} /> goToBusinessUsers(b, 'all')} onWipeData={() => openPurge(b)} onRemove={() => setRemoveTarget(b)} />
Page {page} / {totalPages} · {pageSize} per page · {data?.total ?? 0} total
{ setEditOpen(false) setEditBusiness(null) setEditError('') }} > {editError ? (

{editError}

) : null}
setEditName(e.target.value)} disabled={editSubmitting} />
{editLoadingSettings ? (

Loading theme...

) : ( )}
{editLoadingSettings ? (

Loading language...

) : ( )}

Business and customer dashboards open in this language.

{ setDomainOpen(false) setDomainBusiness(null) setDomainId(null) setDomainGitRepoUrl('') setDomainError('') }} > {domainError ? (

{domainError}

) : null}
setDomainHost(e.target.value)} placeholder="oaktasty.com" disabled={domainSubmitting} />
{!domainId ? ( <>
setDomainGitRepoUrl(e.target.value)} placeholder="git@git.meshkee.com:Meshkee-Websites/oaktasty.git" disabled={domainSubmitting} autoComplete="off" spellCheck={false} />

Fill to enable storefront Deploy on Websites. Leave empty for domain-only (dashboards).

) : null}
{modulesError ? (

{modulesError}

) : null}

Choose which optional modules this business can use. Customers, website, and other general sections stay available for every business.

{BUSINESS_MODULE_IDS.map((id) => ( ))}

Home charts

{migrateSubmitting ? (

Migrating selected data… This can take a moment.

) : null} {migrateError ? (

{migrateError}

) : null} {migrateResult ? (

{migrateResult.message}

{migrateResult.results ? (
    {(Object.keys(migrateResult.results) as MigrateFromOldEntity[]).map((key) => { const item = migrateResult.results?.[key] if (!item) return null return (
  • {MIGRATE_ENTITY_LABELS[key]}:{' '} {formatMigrateEntityResult(item)}
  • ) })}
) : null}
) : null}

Link this business to a legacy WillaEngine business id, then choose one data type to migrate. Selecting blogs also migrates news under a top-level News category. Selecting customers also migrates client categories when needed.

{ setMigrateOldId(e.target.value) setMigrateResult(null) setMigrateError('') }} placeholder="e.g. 2410" disabled={migrateSubmitting} />
Data to migrate (one)
{DATA_ENTITY_GROUPS.map((group) => (
{group.title}
{group.items.map(([value, label]) => ( ))}
))}
{purgeSubmitting ? (

Deleting selected data… This can take a moment.

) : null} {purgeError ? (

{purgeError}

) : null} {purgeResult ? (

{purgeResult.message}

{purgeResult.results ? (
    {(Object.keys(purgeResult.results) as PurgeBusinessDataEntity[]).map((key) => { const item = purgeResult.results?.[key] if (!item) return null return (
  • {MIGRATE_ENTITY_LABELS[key]}:{' '} {formatPurgeEntityResult(item)}
  • ) })}
) : null}
) : null}

Permanently remove one selected data type from this business so you can migrate again. Deleting blogs or portfolios also removes their images from storage. Deleting customers removes business links (not global user accounts).

Data to delete (one)
{DATA_ENTITY_GROUPS.map((group) => (
{group.title}
{group.items.map(([value, label]) => ( ))}
))}
{ setCreateOpen(false) resetCreateForm() }} > {createError ? (

{createError}

) : null}
setCreateName(e.target.value)} placeholder="New Shop" disabled={createSubmitting} />
setCreateNameFa(e.target.value)} placeholder="نام فارسی" dir="rtl" disabled={createSubmitting} />
Owner
setCreateOwnerFirstNameEn(e.target.value)} placeholder="First name" disabled={createSubmitting} />
setCreateOwnerLastNameEn(e.target.value)} placeholder="Last name" disabled={createSubmitting} />
setCreateOwnerFirstName(e.target.value)} placeholder="نام" dir="rtl" lang="fa" disabled={createSubmitting} />
setCreateOwnerLastName(e.target.value)} placeholder="نام خانوادگی" dir="rtl" lang="fa" disabled={createSubmitting} />
setCreateOwnerCell(e.target.value)} placeholder="0912..." name="owner-cell" autoComplete="off" disabled={createSubmitting} />
setCreateOwnerPassword(e.target.value)} placeholder="Min. 8 characters" name="owner-password" autoComplete="new-password" disabled={createSubmitting} />