mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
Improve product cards/pagination, Farsi locale fonts, and super-admin migrate UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
2e40d5eb4c
commit
f5b2193ba1
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
AlertTriangle,
|
||||
Database,
|
||||
Eraser,
|
||||
FileText,
|
||||
Globe,
|
||||
Lock,
|
||||
@@ -16,7 +18,16 @@ import {
|
||||
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
|
||||
import { ToggleSwitch } from '../components/ToggleSwitch'
|
||||
import { formatCellForDisplay, toE164CellNumber } from '../lib/cellNumber'
|
||||
import type { BusinessesListResponse, BusinessListItem } from '../types/business'
|
||||
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'
|
||||
@@ -24,6 +35,8 @@ import {
|
||||
addBusinessDomain,
|
||||
createBusiness,
|
||||
listBusinesses,
|
||||
migrateBusinessFromOld,
|
||||
purgeBusinessData,
|
||||
removeBusiness,
|
||||
setBusinessActive,
|
||||
updateBusiness,
|
||||
@@ -45,6 +58,7 @@ import {
|
||||
updateBusinessPrimaryColor,
|
||||
} from '../services/businessSettingsService'
|
||||
import { flattenBusinessCategories } from '../utils/categories'
|
||||
import { Pagination } from '@meshkee/dashboard-ui'
|
||||
import pageStyles from '../components/PageContent.module.css'
|
||||
import styles from './BusinessesPage.module.css'
|
||||
|
||||
@@ -56,12 +70,99 @@ function formatDate(value: string) {
|
||||
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit' })
|
||||
}
|
||||
|
||||
function buildPageNumbers(page: number, totalPages: number) {
|
||||
const start = Math.max(1, page - 2)
|
||||
const end = Math.min(totalPages, page + 2)
|
||||
const numbers: number[] = []
|
||||
for (let i = start; i <= end; i++) numbers.push(i)
|
||||
return numbers
|
||||
const MIGRATE_ENTITY_LABELS: Record<MigrateFromOldEntity, string> = {
|
||||
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<MigrateFromOldEntity, boolean> {
|
||||
return {
|
||||
product_categories: false,
|
||||
product: false,
|
||||
customer_categories: false,
|
||||
customer: false,
|
||||
blog_categories: false,
|
||||
blog: false,
|
||||
portfolio_categories: false,
|
||||
portfolio: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function BusinessesPage() {
|
||||
@@ -91,12 +192,14 @@ export function BusinessesPage() {
|
||||
)
|
||||
const [editLoadingSettings, setEditLoadingSettings] = useState(false)
|
||||
const [editSubmitting, setEditSubmitting] = useState(false)
|
||||
const [editError, setEditError] = useState('')
|
||||
|
||||
const [domainOpen, setDomainOpen] = useState(false)
|
||||
const [domainBusiness, setDomainBusiness] = useState<BusinessListItem | null>(null)
|
||||
const [domainId, setDomainId] = useState<number | null>(null)
|
||||
const [domainHost, setDomainHost] = useState('')
|
||||
const [domainSubmitting, setDomainSubmitting] = useState(false)
|
||||
const [domainError, setDomainError] = useState('')
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [createName, setCreateName] = useState('')
|
||||
@@ -108,11 +211,31 @@ export function BusinessesPage() {
|
||||
const [createOwnerCell, setCreateOwnerCell] = useState('')
|
||||
const [createOwnerPassword, setCreateOwnerPassword] = useState('')
|
||||
const [createSubmitting, setCreateSubmitting] = useState(false)
|
||||
const [createError, setCreateError] = useState('')
|
||||
|
||||
const [removeTarget, setRemoveTarget] = useState<BusinessListItem | null>(null)
|
||||
const [togglingId, setTogglingId] = useState<string | null>(null)
|
||||
const [savingColorId, setSavingColorId] = useState<string | null>(null)
|
||||
|
||||
const [migrateOpen, setMigrateOpen] = useState(false)
|
||||
const [migrateBusiness, setMigrateBusiness] = useState<BusinessListItem | null>(null)
|
||||
const [migrateOldId, setMigrateOldId] = useState('')
|
||||
const [migrateEntities, setMigrateEntities] = useState<Record<MigrateFromOldEntity, boolean>>(
|
||||
() => emptyMigrateEntities(),
|
||||
)
|
||||
const [migrateSubmitting, setMigrateSubmitting] = useState(false)
|
||||
const [migrateError, setMigrateError] = useState('')
|
||||
const [migrateResult, setMigrateResult] = useState<MigrateFromOldResponse | null>(null)
|
||||
|
||||
const [purgeOpen, setPurgeOpen] = useState(false)
|
||||
const [purgeBusiness, setPurgeBusiness] = useState<BusinessListItem | null>(null)
|
||||
const [purgeEntities, setPurgeEntities] = useState<Record<PurgeBusinessDataEntity, boolean>>(
|
||||
() => emptyMigrateEntities(),
|
||||
)
|
||||
const [purgeSubmitting, setPurgeSubmitting] = useState(false)
|
||||
const [purgeError, setPurgeError] = useState('')
|
||||
const [purgeResult, setPurgeResult] = useState<PurgeBusinessDataResponse | null>(null)
|
||||
|
||||
async function fetchList() {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
@@ -171,8 +294,6 @@ export function BusinessesPage() {
|
||||
return Math.max(1, Math.ceil(total / pageSize))
|
||||
}, [data?.total, pageSize])
|
||||
|
||||
const pageNumbers = useMemo(() => buildPageNumbers(page, totalPages), [page, totalPages])
|
||||
|
||||
const showingFrom = useMemo(() => {
|
||||
if (!data) return 0
|
||||
return (page - 1) * pageSize + 1
|
||||
@@ -206,6 +327,7 @@ export function BusinessesPage() {
|
||||
setEditBusiness(b)
|
||||
setEditName(b.name)
|
||||
setEditPrimaryColor(normalizeBusinessPrimaryColorId(b.primaryColor))
|
||||
setEditError('')
|
||||
setEditOpen(true)
|
||||
setEditLoadingSettings(true)
|
||||
|
||||
@@ -216,7 +338,7 @@ export function BusinessesPage() {
|
||||
})
|
||||
.catch((err) => {
|
||||
if (isAbortError(err)) return
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to load business theme.')
|
||||
setEditError(err instanceof ApiError ? err.message : 'Unable to load business theme.')
|
||||
})
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) setEditLoadingSettings(false)
|
||||
@@ -286,7 +408,7 @@ export function BusinessesPage() {
|
||||
async function submitEdit() {
|
||||
if (!editBusiness) return
|
||||
setEditSubmitting(true)
|
||||
setError('')
|
||||
setEditError('')
|
||||
try {
|
||||
await updateBusiness(editBusiness.id, { name: editName })
|
||||
await updateBusinessPrimaryColor(editBusiness.id, editPrimaryColor)
|
||||
@@ -306,7 +428,7 @@ export function BusinessesPage() {
|
||||
showToast('Business updated.', 'success')
|
||||
await fetchList()
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to update business.')
|
||||
setEditError(err instanceof ApiError ? err.message : 'Unable to update business.')
|
||||
} finally {
|
||||
setEditSubmitting(false)
|
||||
}
|
||||
@@ -316,16 +438,142 @@ export function BusinessesPage() {
|
||||
setDomainBusiness(b)
|
||||
setDomainId(b.domainId)
|
||||
setDomainHost(b.domain ?? '')
|
||||
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
|
||||
|
||||
setDomainSubmitting(true)
|
||||
setError('')
|
||||
setDomainError('')
|
||||
try {
|
||||
if (domainId) {
|
||||
await updateBusinessDomain(domainBusiness.id, domainId, { host })
|
||||
@@ -366,7 +614,7 @@ export function BusinessesPage() {
|
||||
setDomainBusiness(null)
|
||||
setDomainId(null)
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to save domain.')
|
||||
setDomainError(err instanceof ApiError ? err.message : 'Unable to save domain.')
|
||||
} finally {
|
||||
setDomainSubmitting(false)
|
||||
}
|
||||
@@ -381,6 +629,7 @@ export function BusinessesPage() {
|
||||
setCreateOwnerLastName('')
|
||||
setCreateOwnerCell('')
|
||||
setCreateOwnerPassword('')
|
||||
setCreateError('')
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
@@ -393,7 +642,7 @@ export function BusinessesPage() {
|
||||
if (!ownerCellNumber) return
|
||||
|
||||
setCreateSubmitting(true)
|
||||
setError('')
|
||||
setCreateError('')
|
||||
try {
|
||||
await createBusiness({
|
||||
name: createName.trim(),
|
||||
@@ -410,7 +659,7 @@ export function BusinessesPage() {
|
||||
showToast(`"${createName.trim()}" has been created.`, 'success')
|
||||
await fetchList()
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to create business.')
|
||||
setCreateError(err instanceof ApiError ? err.message : 'Unable to create business.')
|
||||
} finally {
|
||||
setCreateSubmitting(false)
|
||||
}
|
||||
@@ -468,6 +717,13 @@ export function BusinessesPage() {
|
||||
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 (
|
||||
<main className={pageStyles.content}>
|
||||
<div className={pageStyles.pageHeader}>
|
||||
@@ -681,6 +937,32 @@ export function BusinessesPage() {
|
||||
>
|
||||
<Users size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.controlBtn}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
openMigrate(b)
|
||||
}}
|
||||
title="Migrate from old CMS"
|
||||
aria-label="Migrate from old CMS"
|
||||
>
|
||||
<Database size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.controlBtn} ${styles.controlBtnDanger}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
openPurge(b)
|
||||
}}
|
||||
title="Delete data"
|
||||
aria-label="Delete business data"
|
||||
>
|
||||
<Eraser size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.controlBtn}
|
||||
@@ -718,37 +1000,15 @@ export function BusinessesPage() {
|
||||
|
||||
<div className={styles.pagination}>
|
||||
<div>
|
||||
Page {page} / {totalPages}
|
||||
</div>
|
||||
<div className={styles.pagerBtns}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.pageBtn}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page <= 1 || loading}
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
{pageNumbers.map((n) => (
|
||||
<button
|
||||
key={n}
|
||||
type="button"
|
||||
className={`${styles.pageBtn} ${n === page ? styles.pageBtnActive : ''}`}
|
||||
onClick={() => setPage(n)}
|
||||
disabled={loading}
|
||||
>
|
||||
{n}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.pageBtn}
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages || loading}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
Page {page} / {totalPages} · {pageSize} per page · {data?.total ?? 0} total
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setPage}
|
||||
disabled={loading}
|
||||
variant="inline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -769,8 +1029,14 @@ export function BusinessesPage() {
|
||||
onClose={() => {
|
||||
setEditOpen(false)
|
||||
setEditBusiness(null)
|
||||
setEditError('')
|
||||
}}
|
||||
>
|
||||
{editError ? (
|
||||
<p className={styles.alertError} role="alert">
|
||||
{editError}
|
||||
</p>
|
||||
) : null}
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="edit-name">Business name</label>
|
||||
<input
|
||||
@@ -820,8 +1086,14 @@ export function BusinessesPage() {
|
||||
setDomainOpen(false)
|
||||
setDomainBusiness(null)
|
||||
setDomainId(null)
|
||||
setDomainError('')
|
||||
}}
|
||||
>
|
||||
{domainError ? (
|
||||
<p className={styles.alertError} role="alert">
|
||||
{domainError}
|
||||
</p>
|
||||
) : null}
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="domain-host">Domain</label>
|
||||
<input
|
||||
@@ -853,6 +1125,207 @@ export function BusinessesPage() {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={migrateOpen}
|
||||
title={
|
||||
migrateBusiness
|
||||
? `Migrate from old CMS — ${migrateBusiness.name}`
|
||||
: 'Migrate from old CMS'
|
||||
}
|
||||
onClose={closeMigrate}
|
||||
>
|
||||
{migrateSubmitting ? (
|
||||
<p className={styles.alertInfo} role="status" aria-live="polite">
|
||||
Migrating selected data… This can take a moment.
|
||||
</p>
|
||||
) : null}
|
||||
{migrateError ? (
|
||||
<p className={styles.alertError} role="alert">
|
||||
{migrateError}
|
||||
</p>
|
||||
) : null}
|
||||
{migrateResult ? (
|
||||
<div className={styles.alertSuccess} role="status" aria-live="polite">
|
||||
<p className={styles.migrateResultTitle}>{migrateResult.message}</p>
|
||||
{migrateResult.results ? (
|
||||
<ul className={styles.migrateResultList}>
|
||||
{(Object.keys(migrateResult.results) as MigrateFromOldEntity[]).map((key) => {
|
||||
const item = migrateResult.results?.[key]
|
||||
if (!item) return null
|
||||
return (
|
||||
<li key={key}>
|
||||
<strong>{MIGRATE_ENTITY_LABELS[key]}:</strong>{' '}
|
||||
{formatMigrateEntityResult(item)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<p className={styles.helperText}>
|
||||
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.
|
||||
</p>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="migrate-old-id">Old business id</label>
|
||||
<input
|
||||
id="migrate-old-id"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={String(migrateOldId)}
|
||||
onChange={(e) => {
|
||||
setMigrateOldId(e.target.value)
|
||||
setMigrateResult(null)
|
||||
setMigrateError('')
|
||||
}}
|
||||
placeholder="e.g. 2410"
|
||||
disabled={migrateSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.field} style={{ marginTop: 12 }}>
|
||||
<span className={styles.entityGroupLabel}>Data to migrate (one)</span>
|
||||
<div className={styles.entityChecks}>
|
||||
{DATA_ENTITY_GROUPS.map((group) => (
|
||||
<div key={group.title} className={styles.entityGroup}>
|
||||
<span className={styles.entityGroupTitle}>{group.title}</span>
|
||||
<div className={styles.entityNestedChecks}>
|
||||
{group.items.map(([value, label]) => (
|
||||
<label key={value} className={styles.entityCheck}>
|
||||
<input
|
||||
type="radio"
|
||||
name="migrate-entity"
|
||||
checked={migrateEntities[value]}
|
||||
onChange={() => selectMigrateEntity(value)}
|
||||
disabled={migrateSubmitting}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ height: 12 }} />
|
||||
<div className={styles.actionsRow}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.btn} ${styles.btnGhost}`}
|
||||
onClick={closeMigrate}
|
||||
disabled={migrateSubmitting}
|
||||
>
|
||||
{migrateResult ? 'Close' : 'Cancel'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.btn} ${styles.btnPrimary}`}
|
||||
onClick={() => void submitMigrate()}
|
||||
disabled={migrateSubmitting || !canSubmitMigrate}
|
||||
>
|
||||
{migrateSubmitting
|
||||
? 'Migrating…'
|
||||
: migrateResult
|
||||
? 'Migrate again'
|
||||
: 'Save & migrate'}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={purgeOpen}
|
||||
title={
|
||||
purgeBusiness ? `Delete data — ${purgeBusiness.name}` : 'Delete data'
|
||||
}
|
||||
onClose={closePurge}
|
||||
>
|
||||
{purgeSubmitting ? (
|
||||
<p className={styles.alertInfo} role="status" aria-live="polite">
|
||||
Deleting selected data… This can take a moment.
|
||||
</p>
|
||||
) : null}
|
||||
{purgeError ? (
|
||||
<p className={styles.alertError} role="alert">
|
||||
{purgeError}
|
||||
</p>
|
||||
) : null}
|
||||
{purgeResult ? (
|
||||
<div className={styles.alertSuccess} role="status" aria-live="polite">
|
||||
<p className={styles.migrateResultTitle}>{purgeResult.message}</p>
|
||||
{purgeResult.results ? (
|
||||
<ul className={styles.migrateResultList}>
|
||||
{(Object.keys(purgeResult.results) as PurgeBusinessDataEntity[]).map((key) => {
|
||||
const item = purgeResult.results?.[key]
|
||||
if (!item) return null
|
||||
return (
|
||||
<li key={key}>
|
||||
<strong>{MIGRATE_ENTITY_LABELS[key]}:</strong>{' '}
|
||||
{formatPurgeEntityResult(item)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<p className={styles.helperText}>
|
||||
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).
|
||||
</p>
|
||||
<div className={styles.field} style={{ marginTop: 12 }}>
|
||||
<span className={styles.entityGroupLabel}>Data to delete (one)</span>
|
||||
<div className={styles.entityChecks}>
|
||||
{DATA_ENTITY_GROUPS.map((group) => (
|
||||
<div key={group.title} className={styles.entityGroup}>
|
||||
<span className={styles.entityGroupTitle}>{group.title}</span>
|
||||
<div className={styles.entityNestedChecks}>
|
||||
{group.items.map(([value, label]) => (
|
||||
<label key={value} className={styles.entityCheck}>
|
||||
<input
|
||||
type="radio"
|
||||
name="purge-entity"
|
||||
checked={purgeEntities[value]}
|
||||
onChange={() => selectPurgeEntity(value)}
|
||||
disabled={purgeSubmitting}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ height: 12 }} />
|
||||
<div className={styles.actionsRow}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.btn} ${styles.btnGhost}`}
|
||||
onClick={closePurge}
|
||||
disabled={purgeSubmitting}
|
||||
>
|
||||
{purgeResult ? 'Close' : 'Cancel'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.btn} ${styles.btnDanger}`}
|
||||
onClick={() => void submitPurge()}
|
||||
disabled={purgeSubmitting || !canSubmitPurge}
|
||||
>
|
||||
{purgeSubmitting
|
||||
? 'Deleting…'
|
||||
: purgeResult
|
||||
? 'Delete again'
|
||||
: 'Delete selected data'}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={createOpen}
|
||||
title="Add business"
|
||||
@@ -862,6 +1335,11 @@ export function BusinessesPage() {
|
||||
resetCreateForm()
|
||||
}}
|
||||
>
|
||||
{createError ? (
|
||||
<p className={styles.alertError} role="alert">
|
||||
{createError}
|
||||
</p>
|
||||
) : null}
|
||||
<div className={styles.formGrid}>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="create-name-en">Name (EN)</label>
|
||||
|
||||
Reference in New Issue
Block a user