Add e-payment UI, checkout payment result, and legacy migrate domain lookup.

Wire Mellat settings in business dashboard, customer bank return page with payment details, and super-admin migrate modal domain confirmation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-13 09:51:22 +03:30
co-authored by Cursor
parent a11f9643bb
commit cc94b929e0
54 changed files with 6415 additions and 96 deletions
+121 -1
View File
@@ -33,12 +33,14 @@ import {
addBusinessDomain,
createBusiness,
listBusinesses,
lookupLegacyMigrate,
migrateBusinessFromOld,
purgeBusinessData,
removeBusiness,
setBusinessActive,
updateBusiness,
updateBusinessDomain,
type LegacyMigrateLookupResponse,
} from '../services/businessService'
import { listBusinessCategories } from '../services/categoryService'
import { Modal } from '../components/Modal'
@@ -111,6 +113,8 @@ const MIGRATE_ENTITY_LABELS: Record<MigrateFromOldEntity, string> = {
customer: 'Customers',
blog_categories: 'Blog categories',
blog: 'Blogs',
video_categories: 'Video categories',
video: 'Videos',
portfolio_categories: 'Portfolio categories',
portfolio: 'Portfolios',
}
@@ -137,6 +141,13 @@ const DATA_ENTITY_GROUPS = [
['blog', 'Blogs'],
],
},
{
title: 'Videos',
items: [
['video_categories', 'Categories'],
['video', 'Videos'],
],
},
{
title: 'Portfolios',
items: [
@@ -194,6 +205,8 @@ function emptyMigrateEntities(): Record<MigrateFromOldEntity, boolean> {
customer: false,
blog_categories: false,
blog: false,
video_categories: false,
video: false,
portfolio_categories: false,
portfolio: false,
}
@@ -273,6 +286,9 @@ export function BusinessesPage() {
const [migrateSubmitting, setMigrateSubmitting] = useState(false)
const [migrateError, setMigrateError] = useState('')
const [migrateResult, setMigrateResult] = useState<MigrateFromOldResponse | null>(null)
const [migrateLookup, setMigrateLookup] = useState<LegacyMigrateLookupResponse | null>(null)
const [migrateLookupLoading, setMigrateLookupLoading] = useState(false)
const [migrateLookupError, setMigrateLookupError] = useState('')
const [purgeOpen, setPurgeOpen] = useState(false)
const [purgeBusiness, setPurgeBusiness] = useState<BusinessListItem | null>(null)
@@ -629,6 +645,8 @@ export function BusinessesPage() {
setMigrateEntities(emptyMigrateEntities())
setMigrateError('')
setMigrateResult(null)
setMigrateLookup(null)
setMigrateLookupError('')
setMigrateOpen(true)
}
@@ -641,8 +659,59 @@ export function BusinessesPage() {
setMigrateBusiness(null)
setMigrateError('')
setMigrateResult(null)
setMigrateLookup(null)
setMigrateLookupError('')
}, [])
useEffect(() => {
if (!migrateOpen || !migrateBusiness) return
const trimmed = String(migrateOldId).trim()
const explicitId = Number(trimmed)
const hasExplicitId = trimmed !== '' && Number.isInteger(explicitId) && explicitId > 0
const controller = new AbortController()
const timer = window.setTimeout(() => {
void (async () => {
setMigrateLookupLoading(true)
setMigrateLookupError('')
try {
const result = await lookupLegacyMigrate(
migrateBusiness.id,
hasExplicitId ? explicitId : undefined,
controller.signal,
)
if (controller.signal.aborted) return
setMigrateLookup(result)
// Auto-fill from Meshkee domain match when the field is empty
if (
!hasExplicitId &&
result.source === 'meshkee_domain' &&
result.oldBusinessId
) {
setMigrateOldId(result.oldBusinessId)
}
} catch (err) {
if (isAbortError(err)) return
setMigrateLookup(null)
setMigrateLookupError(
err instanceof ApiError
? err.message
: 'Unable to look up old CMS domain.',
)
} finally {
if (!controller.signal.aborted) setMigrateLookupLoading(false)
}
})()
}, hasExplicitId ? 350 : 0)
return () => {
controller.abort()
window.clearTimeout(timer)
}
}, [migrateOpen, migrateBusiness, migrateOldId])
function selectMigrateEntity(entity: MigrateFromOldEntity) {
setMigrateResult(null)
setMigrateError('')
@@ -1593,7 +1662,7 @@ export function BusinessesPage() {
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.
needed. The old id is resolved from the Meshkee domain when possible.
</p>
<div className={styles.field}>
<label htmlFor="migrate-old-id">Old business id</label>
@@ -1612,6 +1681,57 @@ export function BusinessesPage() {
disabled={migrateSubmitting}
/>
</div>
{migrateLookupLoading ? (
<p className={styles.alertInfo} role="status">
Looking up old CMS domain…
</p>
) : null}
{migrateLookupError ? (
<p className={styles.alertError} role="alert">
{migrateLookupError}
</p>
) : null}
{migrateLookup && !migrateLookupLoading ? (
migrateLookup.oldBusinessId && migrateLookup.domains.length > 0 ? (
<div className={styles.alertInfo} role="status">
<strong>Old CMS match</strong>
<div style={{ marginTop: 6 }}>
Domain{migrateLookup.domains.length > 1 ? 's' : ''}:{' '}
<strong dir="ltr">{migrateLookup.domains.join(', ')}</strong>
</div>
{(migrateLookup.brandEn || migrateLookup.brandFa) && (
<div style={{ marginTop: 4 }}>
Brand:{' '}
<strong>
{[migrateLookup.brandEn, migrateLookup.brandFa]
.filter(Boolean)
.join(' / ')}
</strong>
</div>
)}
{migrateLookup.matchedHost ? (
<div style={{ marginTop: 4 }}>
Matched from Meshkee host:{' '}
<strong dir="ltr">{migrateLookup.matchedHost}</strong>
</div>
) : null}
{migrateLookup.meshkeeHosts.length > 0 ? (
<div style={{ marginTop: 4, opacity: 0.85 }}>
This business hosts:{' '}
<span dir="ltr">{migrateLookup.meshkeeHosts.join(', ')}</span>
</div>
) : null}
</div>
) : (
<p className={styles.alertInfo} role="status">
{migrateOldId.trim()
? 'No old CMS business found for this id. Check the number before migrating.'
: migrateLookup.meshkeeHosts.length
? `No old CMS domain match for ${migrateLookup.meshkeeHosts.join(', ')}. Enter the old business id manually.`
: 'This Meshkee business has no domain yet. Enter the old business id manually.'}
</p>
)
) : null}
<div className={styles.field} style={{ marginTop: 12 }}>
<span className={styles.entityGroupLabel}>Data to migrate (one)</span>
<div className={styles.entityChecks}>
@@ -117,6 +117,32 @@ export async function migrateBusinessFromOld(
})
}
export interface LegacyMigrateLookupResponse {
meshkeeHosts: string[]
meshkeeBusinessName: string
oldBusinessId: string | null
domains: string[]
brandEn: string | null
brandFa: string | null
slug: string | null
matchedHost: string | null
source: 'old_business_id' | 'meshkee_domain' | 'none'
}
export async function lookupLegacyMigrate(
businessId: string,
oldBusinessId?: number,
signal?: AbortSignal,
) {
const q = new URLSearchParams()
if (oldBusinessId != null) q.set('oldBusinessId', String(oldBusinessId))
const query = q.toString()
return apiRequest<LegacyMigrateLookupResponse>(
`/businesses/${businessId}/migrate-from-old/lookup${query ? `?${query}` : ''}`,
{ auth: true, signal },
)
}
export async function purgeBusinessData(
businessId: string,
payload: PurgeBusinessDataPayload,
+2
View File
@@ -46,6 +46,8 @@ export type MigrateFromOldEntity =
| 'customer'
| 'blog_categories'
| 'blog'
| 'video_categories'
| 'video'
| 'portfolio_categories'
| 'portfolio'
@@ -6,6 +6,7 @@ export const BUSINESS_DASHBOARD_MODULE_IDS = [
'blog',
'warehouse',
'videos',
'instructions',
'finance',
] as const
@@ -33,6 +34,7 @@ export const BUSINESS_DASHBOARD_MODULE_LABELS: Record<
blog: 'Blog',
warehouse: 'Warehouse',
videos: 'Videos',
instructions: 'Instructions',
finance: 'Finance',
}