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:
co-authored by
Cursor
parent
353508a091
commit
a7950f1f13
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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 |
|
||||||
|
|||||||
Reference in New Issue
Block a user