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:
co-authored by
Cursor
parent
2a32c46b2e
commit
4c3bf8324e
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user