Add bilingual user names and optional DNS updates in super-admin.

Edit user now has FA/EN first and last names, and domain save only upserts Arvan records when Update DNS is checked.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-20 11:50:57 +03:30
co-authored by Cursor
parent 353508a091
commit a7950f1f13
8 changed files with 116 additions and 12 deletions
+25 -2
View File
@@ -244,6 +244,7 @@ export function BusinessesPage() {
const [domainId, setDomainId] = useState<number | null>(null) const [domainId, setDomainId] = useState<number | null>(null)
const [domainHost, setDomainHost] = useState('') const [domainHost, setDomainHost] = useState('')
const [domainGitRepoUrl, setDomainGitRepoUrl] = useState('') const [domainGitRepoUrl, setDomainGitRepoUrl] = useState('')
const [domainUpdateDns, setDomainUpdateDns] = useState(false)
const [domainSubmitting, setDomainSubmitting] = useState(false) const [domainSubmitting, setDomainSubmitting] = useState(false)
const [domainError, setDomainError] = useState('') const [domainError, setDomainError] = useState('')
@@ -635,6 +636,7 @@ export function BusinessesPage() {
setDomainId(b.domainId) setDomainId(b.domainId)
setDomainHost(b.domain ?? '') setDomainHost(b.domain ?? '')
setDomainGitRepoUrl(b.gitRepoUrl ?? '') setDomainGitRepoUrl(b.gitRepoUrl ?? '')
setDomainUpdateDns(!b.domainId)
setDomainError('') setDomainError('')
setDomainOpen(true) setDomainOpen(true)
} }
@@ -827,6 +829,7 @@ export function BusinessesPage() {
setDomainId(null) setDomainId(null)
setDomainHost('') setDomainHost('')
setDomainGitRepoUrl('') setDomainGitRepoUrl('')
setDomainUpdateDns(false)
setDomainError('') setDomainError('')
} }
@@ -844,6 +847,7 @@ export function BusinessesPage() {
if (domainId) { if (domainId) {
const updated = (await updateBusinessDomain(businessId, domainId, { const updated = (await updateBusinessDomain(businessId, domainId, {
host, host,
updateDns: domainUpdateDns,
...(gitRepoUrl ? { gitRepoUrl } : {}), ...(gitRepoUrl ? { gitRepoUrl } : {}),
})) as { })) as {
id: number id: number
@@ -892,6 +896,7 @@ export function BusinessesPage() {
const created = (await addBusinessDomain(businessId, { const created = (await addBusinessDomain(businessId, {
host, host,
isPrimary: true, isPrimary: true,
updateDns: domainUpdateDns,
...(gitRepoUrl ? { gitRepoUrl } : {}), ...(gitRepoUrl ? { gitRepoUrl } : {}),
})) as { })) as {
id: number id: number
@@ -946,6 +951,7 @@ export function BusinessesPage() {
setDomainId(null) setDomainId(null)
setDomainHost('') setDomainHost('')
setDomainGitRepoUrl('') setDomainGitRepoUrl('')
setDomainUpdateDns(false)
setDomainError('') setDomainError('')
} catch (err) { } catch (err) {
if (submitId !== domainSubmitGenRef.current) return if (submitId !== domainSubmitGenRef.current) return
@@ -1456,9 +1462,11 @@ export function BusinessesPage() {
{domainError} {domainError}
</p> </p>
) : null} ) : null}
{domainSubmitting && domainGitRepoUrl.trim() ? ( {domainSubmitting && (domainGitRepoUrl.trim() || domainUpdateDns) ? (
<p className={styles.helperText} role="status"> <p className={styles.helperText} role="status">
Saving domain and setting up storefront on the websites server… {domainGitRepoUrl.trim()
? 'Saving domain and setting up storefront on the websites server…'
: 'Saving domain and updating Arvan DNS…'}
</p> </p>
) : null} ) : null}
<div className={styles.field}> <div className={styles.field}>
@@ -1489,6 +1497,21 @@ export function BusinessesPage() {
</p> </p>
</div> </div>
<div style={{ height: 12 }} /> <div style={{ height: 12 }} />
<label className={styles.entityCheck} htmlFor="domain-update-dns">
<input
id="domain-update-dns"
type="checkbox"
checked={domainUpdateDns}
onChange={(e) => setDomainUpdateDns(e.target.checked)}
disabled={domainSubmitting}
/>
<span>Update DNS records</span>
</label>
<p className={styles.subText} style={{ marginTop: 6 }}>
When checked, upserts Arvan records: @ ANAME → ns.meshkee.com, plus www,
business, customer, and api CNAMEs.
</p>
<div style={{ height: 12 }} />
<div className={styles.actionsRow}> <div className={styles.actionsRow}>
<button <button
type="button" type="button"
+45 -4
View File
@@ -37,8 +37,9 @@ function formatDate(value: string) {
} }
function displayName(user: UserListItem) { function displayName(user: UserListItem) {
const name = [user.firstName, user.lastName].filter(Boolean).join(' ').trim() const nameFa = [user.firstName, user.lastName].filter(Boolean).join(' ').trim()
return name || '—' const nameEn = [user.firstNameEn, user.lastNameEn].filter(Boolean).join(' ').trim()
return nameFa || nameEn || '—'
} }
function isSuperAdminUser(user: UserListItem) { function isSuperAdminUser(user: UserListItem) {
@@ -113,6 +114,8 @@ export function UsersPage() {
const [editUser, setEditUser] = useState<UserListItem | null>(null) const [editUser, setEditUser] = useState<UserListItem | null>(null)
const [editFirstName, setEditFirstName] = useState('') const [editFirstName, setEditFirstName] = useState('')
const [editLastName, setEditLastName] = useState('') const [editLastName, setEditLastName] = useState('')
const [editFirstNameEn, setEditFirstNameEn] = useState('')
const [editLastNameEn, setEditLastNameEn] = useState('')
const [editEmail, setEditEmail] = useState('') const [editEmail, setEditEmail] = useState('')
const [editCell, setEditCell] = useState('') const [editCell, setEditCell] = useState('')
const [editSubmitting, setEditSubmitting] = useState(false) const [editSubmitting, setEditSubmitting] = useState(false)
@@ -419,6 +422,8 @@ export function UsersPage() {
setEditUser(user) setEditUser(user)
setEditFirstName(user.firstName ?? '') setEditFirstName(user.firstName ?? '')
setEditLastName(user.lastName ?? '') setEditLastName(user.lastName ?? '')
setEditFirstNameEn(user.firstNameEn ?? '')
setEditLastNameEn(user.lastNameEn ?? '')
setEditEmail('') setEditEmail('')
setEditCell(user.cellNumber) setEditCell(user.cellNumber)
setEditError('') setEditError('')
@@ -433,6 +438,8 @@ export function UsersPage() {
await updateUser(editUser.id, { await updateUser(editUser.id, {
firstName: editFirstName.trim(), firstName: editFirstName.trim(),
lastName: editLastName.trim(), lastName: editLastName.trim(),
firstNameEn: editFirstNameEn.trim(),
lastNameEn: editLastNameEn.trim(),
cellNumber: editCell.trim(), cellNumber: editCell.trim(),
...(editEmail.trim() ? { email: editEmail.trim() } : {}), ...(editEmail.trim() ? { email: editEmail.trim() } : {}),
}) })
@@ -449,6 +456,8 @@ export function UsersPage() {
...item, ...item,
firstName: editFirstName.trim(), firstName: editFirstName.trim(),
lastName: editLastName.trim(), lastName: editLastName.trim(),
firstNameEn: editFirstNameEn.trim() || null,
lastNameEn: editLastNameEn.trim() || null,
cellNumber: editCell.trim(), cellNumber: editCell.trim(),
} }
: item, : item,
@@ -817,20 +826,52 @@ export function UsersPage() {
) : null} ) : null}
<div className={tableStyles.formGrid}> <div className={tableStyles.formGrid}>
<div className={tableStyles.field}> <div className={tableStyles.field}>
<label htmlFor="edit-first-name">First name</label> <label htmlFor="edit-first-name">First name (FA)</label>
<input <input
id="edit-first-name" id="edit-first-name"
className="faText"
value={editFirstName} value={editFirstName}
onChange={(e) => setEditFirstName(e.target.value)} onChange={(e) => setEditFirstName(e.target.value)}
placeholder="نام"
dir="rtl"
lang="fa"
disabled={editSubmitting} disabled={editSubmitting}
/> />
</div> </div>
<div className={tableStyles.field}> <div className={tableStyles.field}>
<label htmlFor="edit-last-name">Last name</label> <label htmlFor="edit-last-name">Last name (FA)</label>
<input <input
id="edit-last-name" id="edit-last-name"
className="faText"
value={editLastName} value={editLastName}
onChange={(e) => setEditLastName(e.target.value)} onChange={(e) => setEditLastName(e.target.value)}
placeholder="نام خانوادگی"
dir="rtl"
lang="fa"
disabled={editSubmitting}
/>
</div>
<div className={tableStyles.field}>
<label htmlFor="edit-first-name-en">First name (EN)</label>
<input
id="edit-first-name-en"
value={editFirstNameEn}
onChange={(e) => setEditFirstNameEn(e.target.value)}
placeholder="First name"
dir="ltr"
lang="en"
disabled={editSubmitting}
/>
</div>
<div className={tableStyles.field}>
<label htmlFor="edit-last-name-en">Last name (EN)</label>
<input
id="edit-last-name-en"
value={editLastNameEn}
onChange={(e) => setEditLastNameEn(e.target.value)}
placeholder="Last name"
dir="ltr"
lang="en"
disabled={editSubmitting} disabled={editSubmitting}
/> />
</div> </div>
+33 -3
View File
@@ -100,6 +100,7 @@ export function WebsitesPage() {
const [editDomain, setEditDomain] = useState<DomainListItem | null>(null) const [editDomain, setEditDomain] = useState<DomainListItem | null>(null)
const [editHost, setEditHost] = useState('') const [editHost, setEditHost] = useState('')
const [editExpiresAt, setEditExpiresAt] = useState('') const [editExpiresAt, setEditExpiresAt] = useState('')
const [editUpdateDns, setEditUpdateDns] = useState(false)
const [editSubmitting, setEditSubmitting] = useState(false) const [editSubmitting, setEditSubmitting] = useState(false)
const [editError, setEditError] = useState('') const [editError, setEditError] = useState('')
@@ -168,6 +169,7 @@ export function WebsitesPage() {
setEditDomain(domain) setEditDomain(domain)
setEditHost(domain.host) setEditHost(domain.host)
setEditExpiresAt(domain.expiresAt ? domain.expiresAt.slice(0, 10) : '') setEditExpiresAt(domain.expiresAt ? domain.expiresAt.slice(0, 10) : '')
setEditUpdateDns(false)
setEditError('') setEditError('')
setEditOpen(true) setEditOpen(true)
} }
@@ -177,13 +179,25 @@ export function WebsitesPage() {
setEditSubmitting(true) setEditSubmitting(true)
setEditError('') setEditError('')
try { try {
await updateDomain(editDomain.id, { const updated = (await updateDomain(editDomain.id, {
host: editHost.trim(), host: editHost.trim(),
updateDns: editUpdateDns,
...(editExpiresAt ? { expiresAt: new Date(editExpiresAt).toISOString() } : {}), ...(editExpiresAt ? { expiresAt: new Date(editExpiresAt).toISOString() } : {}),
}) })) as { dnsError?: string | null }
setEditOpen(false) setEditOpen(false)
setEditDomain(null) setEditDomain(null)
showToast(`"${editHost.trim()}" has been updated.`, 'success') setEditUpdateDns(false)
if (updated.dnsError) {
showToast(`"${editHost.trim()}" was updated, but Arvan DNS failed.`, 'error')
showToast(updated.dnsError, 'error')
} else {
showToast(
editUpdateDns
? `"${editHost.trim()}" has been updated and DNS records synced.`
: `"${editHost.trim()}" has been updated.`,
'success',
)
}
setData((prev) => { setData((prev) => {
if (!prev) return prev if (!prev) return prev
return { return {
@@ -711,6 +725,7 @@ export function WebsitesPage() {
onClose={() => { onClose={() => {
setEditOpen(false) setEditOpen(false)
setEditDomain(null) setEditDomain(null)
setEditUpdateDns(false)
setEditError('') setEditError('')
}} }}
> >
@@ -740,6 +755,21 @@ export function WebsitesPage() {
/> />
</div> </div>
<div style={{ height: 12 }} /> <div style={{ height: 12 }} />
<label className={tableStyles.entityCheck} htmlFor="edit-domain-update-dns">
<input
id="edit-domain-update-dns"
type="checkbox"
checked={editUpdateDns}
onChange={(e) => setEditUpdateDns(e.target.checked)}
disabled={editSubmitting}
/>
<span>Update DNS records</span>
</label>
<p className={tableStyles.subText} style={{ marginTop: 6 }}>
When checked, upserts Arvan records: @ ANAME → ns.meshkee.com, plus www,
business, customer, and api CNAMEs.
</p>
<div style={{ height: 12 }} />
<div className={tableStyles.actionsRow}> <div className={tableStyles.actionsRow}>
<button <button
type="button" type="button"
@@ -28,6 +28,8 @@ export interface UpdateBusinessPayload {
export interface AddDomainPayload { export interface AddDomainPayload {
host: string host: string
isPrimary?: boolean isPrimary?: boolean
/** When true, upsert Meshkee tenant records in Arvan DNS. */
updateDns?: boolean
/** When set, provisions storefront deploy on the websites VM. */ /** When set, provisions storefront deploy on the websites VM. */
gitRepoUrl?: string gitRepoUrl?: string
} }
@@ -38,6 +40,8 @@ export interface DisableBusinessPayload {
export interface UpdateDomainPayload { export interface UpdateDomainPayload {
host: string host: string
/** When true, upsert Meshkee tenant records in Arvan DNS. */
updateDns?: boolean
/** When set, provisions storefront deploy on the websites VM. */ /** When set, provisions storefront deploy on the websites VM. */
gitRepoUrl?: string gitRepoUrl?: string
} }
@@ -16,6 +16,8 @@ export interface ListDomainsParams {
export interface UpdateDomainPayload { export interface UpdateDomainPayload {
host?: string host?: string
expiresAt?: string expiresAt?: string
/** When true, upsert Meshkee tenant records in Arvan DNS. */
updateDns?: boolean
} }
export async function listDomains(params: ListDomainsParams, signal?: AbortSignal) { export async function listDomains(params: ListDomainsParams, signal?: AbortSignal) {
@@ -14,6 +14,8 @@ export interface ListUsersParams {
export interface UpdateUserPayload { export interface UpdateUserPayload {
firstName?: string firstName?: string
lastName?: string lastName?: string
firstNameEn?: string
lastNameEn?: string
email?: string email?: string
cellNumber?: string cellNumber?: string
} }
+2
View File
@@ -15,6 +15,8 @@ export interface UserListItem {
id: number id: number
firstName: string | null firstName: string | null
lastName: string | null lastName: string | null
firstNameEn: string | null
lastNameEn: string | null
cellNumber: string cellNumber: string
roles: string | null roles: string | null
roleSlug: string | null roleSlug: string | null
+3 -3
View File
@@ -3,7 +3,7 @@
> **For AI agents:** Read this file at the start of a new chat before making changes. > **For AI agents:** Read this file at the start of a new chat before making changes.
> Update this document when a major feature is completed or architecture changes. > Update this document when a major feature is completed or architecture changes.
Last updated: August 19, 2026 Last updated: August 20, 2026
--- ---
@@ -138,11 +138,11 @@ Add to `/etc/hosts` (one line per tenant):
|------|------| |------|------|
| `/login` | Login | | `/login` | Login |
| `/` | Home | | `/` | Home |
| `/businesses` | Businesses list (migrate-from-old + delete-data for portfolios/blogs(+news)/customers + categories; single-select). **Add domain** accepts optional Git repo URL → provisions storefront on websites VM and sets `deploy_slug`. Also upserts Arvan DNS (`@` ANAME → `ns.meshkee.com`, `www` CNAME → apex, `business`/`customer`/`api` CNAMEs). | | `/businesses` | Businesses list (migrate-from-old + delete-data for portfolios/blogs(+news)/customers + categories; single-select). **Add/Edit domain** accepts optional Git repo URL → provisions storefront on websites VM and sets `deploy_slug`. Optional **Update DNS records** checkbox upserts Arvan DNS (`@` ANAME → `ns.meshkee.com`, `www` CNAME → apex, `business`/`customer`/`api` CNAMEs). |
| `/businesses/:businessId/invoices` | Business invoices list | | `/businesses/:businessId/invoices` | Business invoices list |
| `/businesses/:businessId/invoices/new` | Issue invoice (full page) | | `/businesses/:businessId/invoices/new` | Issue invoice (full page) |
| `/users` | Users (business filter: Customer vs Manager → Admin/Editor/Viewer; Admin assignable by super-admin only; owners locked) | | `/users` | Users (business filter: Customer vs Manager → Admin/Editor/Viewer; Admin assignable by super-admin only; owners locked) |
| `/websites` | Websites / domains (Deploy when `deploy_slug` set) | | `/websites` | Websites / domains (Deploy when `deploy_slug` set). Edit domain can optionally update Arvan DNS. |
| `/settings` | Platform settings (invoice templates + item templates) | | `/settings` | Platform settings (invoice templates + item templates) |
| `/settings/invoice-templates/new` | Create invoice template | | `/settings/invoice-templates/new` | Create invoice template |
| `/settings/invoice-templates/:templateId` | Edit invoice template | | `/settings/invoice-templates/:templateId` | Edit invoice template |