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:
co-authored by
Cursor
parent
8f0b4cef85
commit
0eb19bbebd
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user