Add customer SMS/Excel tools, selection bar, and sticky import toasts.

Bulk and single SMS from Users, row selection with overflow summary, persistent large import toasts, and business SMS sender in Super Admin.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-08 14:12:57 +03:30
co-authored by Cursor
parent 2a32c46b2e
commit 4c3bf8324e
30 changed files with 1597 additions and 149 deletions
+2 -25
View File
@@ -1,4 +1,5 @@
/* Self-hosted Iran Yekan — served from public/fonts/iranyekan/ so it loads without a local install. */
/* Self-hosted Iran Yekan — served from public/fonts/iranyekan/ so it loads without a local install.
Only real faces are registered. Mapping 500–700 to Regular blocks browser synthetic bold. */
@font-face {
font-family: 'IRANYekan';
src: url('/fonts/iranyekan/IRANYekanWebLight.ttf') format('truetype');
@@ -14,27 +15,3 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'IRANYekan';
src: url('/fonts/iranyekan/IRANYekanWebRegular.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'IRANYekan';
src: url('/fonts/iranyekan/IRANYekanWebRegular.ttf') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'IRANYekan';
src: url('/fonts/iranyekan/IRANYekanWebRegular.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@@ -41,7 +41,16 @@
.field {
display: flex;
flex-direction: column;
gap: 4px;
gap: 6px;
}
/* Stacked fields in modals: keep label glued to its input, space from the field above */
.field + .field {
margin-top: 14px;
}
.formGrid > .field + .field {
margin-top: 0;
}
.fieldCol3 {
@@ -52,11 +61,13 @@
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
margin: 0;
}
.helperText {
margin: 0;
font-size: 13px;
font-size: 12px;
line-height: 1.4;
color: var(--text-muted);
}
@@ -339,6 +339,7 @@ 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('')
@@ -520,6 +521,7 @@ export function BusinessesPage() {
setEditAnnualRenewalAt(
toDateInputValue(b.annualRenewalAt || b.createdAt),
)
setEditSmsSenderNumber(b.smsSenderNumber ?? '')
setEditError('')
setEditOpen(true)
}
@@ -862,12 +864,14 @@ 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
@@ -881,6 +885,7 @@ export function BusinessesPage() {
nameFa,
about: editAbout.trim() || null,
annualRenewalAt,
smsSenderNumber: smsSenderNumber || null,
}
: item,
),
@@ -1961,6 +1966,22 @@ 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
@@ -94,7 +94,7 @@
.promptArea {
width: 100%;
min-height: 220px;
min-height: min(520px, 58vh);
padding: var(--field-padding-y) var(--field-padding-x);
font-size: var(--field-font-size);
font-family: var(--font-ui);
+2 -2
View File
@@ -200,7 +200,7 @@ export function TemplatesPage() {
<Modal
open={editorOpen}
title={editing ? 'Edit template' : 'Add template'}
wide
xl
onClose={() => {
if (submitting) return
setEditorOpen(false)
@@ -230,7 +230,7 @@ export function TemplatesPage() {
onChange={(e) => setDraftBody(e.target.value)}
placeholder="Paste the AI prompt here…"
disabled={submitting}
rows={14}
rows={28}
/>
</div>
<div style={{ height: 12 }} />
@@ -24,6 +24,7 @@ export interface UpdateBusinessPayload {
slug?: string
oldBusinessId?: number | null
annualRenewalAt?: string | null
smsSenderNumber?: string | null
}
export interface AddDomainPayload {
+2
View File
@@ -38,6 +38,8 @@ export interface BusinessListItem {
moduleCount: number
homeCharts: [HomeChartId, HomeChartId]
oldBusinessId: string | null
/** Gama shortcode for customer SMS; null → Meshkee advertising number. */
smsSenderNumber: string | null
}
export type MigrateFromOldEntity =