Add SMS templates UI, prepaid balance settings, and list filter layout.

Business settings manage templates and balance; Super Admin verifies templates and tops up via SMS settings; filters use a separate panel with even field gaps.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-08 16:24:26 +03:30
co-authored by Cursor
parent 8f0b4cef85
commit 0eb19bbebd
27 changed files with 2324 additions and 527 deletions
+143 -24
View File
@@ -339,10 +339,16 @@ export function BusinessesPage() {
const [editNameFa, setEditNameFa] = useState('')
const [editAbout, setEditAbout] = useState('')
const [editAnnualRenewalAt, setEditAnnualRenewalAt] = useState('')
const [editSmsSenderNumber, setEditSmsSenderNumber] = useState('')
const [editSubmitting, setEditSubmitting] = useState(false)
const [editError, setEditError] = useState('')
const [smsSettingsOpen, setSmsSettingsOpen] = useState(false)
const [smsSettingsBusiness, setSmsSettingsBusiness] = useState<BusinessListItem | null>(null)
const [smsSenderNumber, setSmsSenderNumber] = useState('')
const [smsBalance, setSmsBalance] = useState('0')
const [smsSettingsSubmitting, setSmsSettingsSubmitting] = useState(false)
const [smsSettingsError, setSmsSettingsError] = useState('')
const [domainOpen, setDomainOpen] = useState(false)
const [domainBusiness, setDomainBusiness] = useState<BusinessListItem | null>(null)
const [domainId, setDomainId] = useState<number | null>(null)
@@ -521,11 +527,18 @@ export function BusinessesPage() {
setEditAnnualRenewalAt(
toDateInputValue(b.annualRenewalAt || b.createdAt),
)
setEditSmsSenderNumber(b.smsSenderNumber ?? '')
setEditError('')
setEditOpen(true)
}
function openSmsSettings(b: BusinessListItem) {
setSmsSettingsBusiness(b)
setSmsSenderNumber(b.smsSenderNumber ?? '')
setSmsBalance(String(b.smsBalance ?? 0))
setSmsSettingsError('')
setSmsSettingsOpen(true)
}
async function openAiPrompts(b: BusinessListItem) {
setPromptsBusiness(b)
setPromptsOpen(true)
@@ -864,14 +877,12 @@ export function BusinessesPage() {
: editBusiness.createdAt
? new Date(editBusiness.createdAt).toISOString()
: null
const smsSenderNumber = editSmsSenderNumber.replace(/\D/g, '')
try {
await updateBusiness(editBusiness.id, {
name,
nameFa,
about: editAbout.trim(),
annualRenewalAt,
smsSenderNumber: smsSenderNumber || null,
})
setData((prev) => {
if (!prev) return prev
@@ -885,7 +896,6 @@ export function BusinessesPage() {
nameFa,
about: editAbout.trim() || null,
annualRenewalAt,
smsSenderNumber: smsSenderNumber || null,
}
: item,
),
@@ -903,6 +913,50 @@ export function BusinessesPage() {
}
}
async function submitSmsSettings() {
if (!smsSettingsBusiness) return
const sender = smsSenderNumber.replace(/\D/g, '')
const balanceParsed = Number.parseInt(smsBalance.replace(/\D/g, '') || '0', 10)
if (!Number.isFinite(balanceParsed) || balanceParsed < 0) {
setSmsSettingsError('SMS balance must be a non-negative integer.')
return
}
setSmsSettingsSubmitting(true)
setSmsSettingsError('')
try {
await updateBusiness(smsSettingsBusiness.id, {
smsSenderNumber: sender || null,
smsBalance: balanceParsed,
})
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === smsSettingsBusiness.id
? {
...item,
smsSenderNumber: sender || null,
smsBalance: balanceParsed,
}
: item,
),
}
})
setSmsSettingsOpen(false)
setSmsSettingsBusiness(null)
showToast('SMS settings saved.', 'success')
} catch (err) {
setSmsSettingsError(
err instanceof ApiError ? err.message : 'Unable to save SMS settings.',
)
} finally {
setSmsSettingsSubmitting(false)
}
}
async function restartRenewalCountdown(b: BusinessListItem) {
const contractAt = b.annualRenewalAt || b.createdAt
const annualRenewalAt = addOneYearToDueIso(contractAt)
@@ -1406,7 +1460,7 @@ export function BusinessesPage() {
<form className={styles.filtersGrid} onSubmit={(e) => { e.preventDefault(); applyFilters() }}>
<div className={styles.filtersInputs}>
<div className={`${styles.field} ${styles.fieldCol3}`}>
<div className={styles.field}>
<label htmlFor="filter-name">Name</label>
<input
id="filter-name"
@@ -1416,7 +1470,7 @@ export function BusinessesPage() {
/>
</div>
<div className={`${styles.field} ${styles.fieldCol3}`}>
<div className={styles.field}>
<label htmlFor="filter-domain">Domain</label>
<input
id="filter-domain"
@@ -1426,7 +1480,7 @@ export function BusinessesPage() {
/>
</div>
<div className={`${styles.field} ${styles.fieldCol3}`}>
<div className={styles.field}>
<label htmlFor="filter-category">Category</label>
<input
id="filter-category"
@@ -1708,6 +1762,7 @@ export function BusinessesPage() {
businessName={b.name}
host={b.domain}
onAiPrompts={() => void openAiPrompts(b)}
onSmsSettings={() => openSmsSettings(b)}
onAllUsers={() => goToBusinessUsers(b, 'all')}
onWipeData={() => openPurge(b)}
onRemove={() => setRemoveTarget(b)}
@@ -1966,22 +2021,6 @@ export function BusinessesPage() {
placeholder="Select renewal date"
/>
</div>
<div className={styles.field}>
<label htmlFor="edit-sms-sender">SMS sender number</label>
<input
id="edit-sms-sender"
inputMode="numeric"
value={editSmsSenderNumber}
onChange={(e) => setEditSmsSenderNumber(e.target.value.replace(/\D/g, '').slice(0, 16))}
placeholder="e.g. 5000123456 (empty = 5000990009)"
disabled={editSubmitting}
autoComplete="off"
/>
<p className={styles.helperText}>
Used for customer SMS from this business. Leave empty to use Meshkee advertising line
5000990009.
</p>
</div>
<div className={styles.field}>
<label htmlFor="edit-about">About business</label>
<textarea
@@ -2013,6 +2052,82 @@ export function BusinessesPage() {
</div>
</Modal>
<Modal
open={smsSettingsOpen}
title={
smsSettingsBusiness
? `SMS settings · ${smsSettingsBusiness.name}`
: 'SMS settings'
}
onClose={() => {
if (smsSettingsSubmitting) return
setSmsSettingsOpen(false)
setSmsSettingsBusiness(null)
setSmsSettingsError('')
}}
>
{smsSettingsError ? (
<p className={styles.alertError} role="alert">
{smsSettingsError}
</p>
) : null}
<div className={styles.field}>
<label htmlFor="sms-settings-sender">SMS sender number</label>
<input
id="sms-settings-sender"
inputMode="numeric"
value={smsSenderNumber}
onChange={(e) => setSmsSenderNumber(e.target.value.replace(/\D/g, '').slice(0, 16))}
placeholder="e.g. 5000123456 (empty = 9000590009)"
disabled={smsSettingsSubmitting}
autoComplete="off"
/>
<p className={styles.helperText}>
Used for free-text customer SMS. Leave empty for Meshkee advertising line
9000590009. Verified templates always use 5000110005.
</p>
</div>
<div className={styles.field}>
<label htmlFor="sms-settings-balance">SMS balance (IRT)</label>
<input
id="sms-settings-balance"
inputMode="numeric"
value={smsBalance}
onChange={(e) => setSmsBalance(e.target.value.replace(/\D/g, '').slice(0, 12))}
placeholder="0"
disabled={smsSettingsSubmitting}
autoComplete="off"
/>
<p className={styles.helperText}>
Prepaid SMS balance in IRT. Billing: 400 IRT per 64 characters per recipient
(OTP, customer SMS, partner SMS). New businesses start with 500,000 IRT. Values
below 200,000 IRT show as low balance in the business dashboard.
</p>
</div>
<div style={{ height: 12 }} />
<div className={styles.actionsRow}>
<button
type="button"
className={`${styles.btn} ${styles.btnGhost}`}
onClick={() => {
setSmsSettingsOpen(false)
setSmsSettingsBusiness(null)
}}
disabled={smsSettingsSubmitting}
>
Cancel
</button>
<button
type="button"
className={`${styles.btn} ${styles.btnPrimary}`}
onClick={() => void submitSmsSettings()}
disabled={smsSettingsSubmitting}
>
Save
</button>
</div>
</Modal>
<Modal
open={domainOpen}
title={domainId ? 'Edit domain' : 'Add domain'}
@@ -2491,6 +2606,10 @@ export function BusinessesPage() {
{createError}
</p>
) : null}
<p className={styles.helperText} style={{ marginTop: 0, marginBottom: 12 }}>
New businesses receive an initial SMS balance of 500,000 IRT (400 IRT per 64 characters
per recipient).
</p>
<div className={styles.formGrid}>
<div className={styles.field}>
<label htmlFor="create-name-en">Name (EN)</label>