Show SMS sent breakdown by type in admin and business settings.
Data rows for total/mass/singular/auth/auto/admin; balance stays a credit data row. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
6bc57f26ed
commit
dbc17b2589
@@ -43,6 +43,14 @@ export function SmsSettingsSection() {
|
|||||||
const isFa = locale === 'fa'
|
const isFa = locale === 'fa'
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const [balance, setBalance] = useState(0)
|
const [balance, setBalance] = useState(0)
|
||||||
|
const [sentCount, setSentCount] = useState(0)
|
||||||
|
const [sentByType, setSentByType] = useState({
|
||||||
|
mass: 0,
|
||||||
|
singular: 0,
|
||||||
|
auth: 0,
|
||||||
|
autoCustomer: 0,
|
||||||
|
admin: 0,
|
||||||
|
})
|
||||||
const [templates, setTemplates] = useState<SmsTemplate[]>([])
|
const [templates, setTemplates] = useState<SmsTemplate[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
@@ -73,6 +81,14 @@ export function SmsSettingsSection() {
|
|||||||
if (signal?.aborted) return
|
if (signal?.aborted) return
|
||||||
setTemplates(tpl.items)
|
setTemplates(tpl.items)
|
||||||
setBalance(creditRes.balance ?? creditRes.credit ?? 0)
|
setBalance(creditRes.balance ?? creditRes.credit ?? 0)
|
||||||
|
setSentCount(creditRes.sentCount ?? 0)
|
||||||
|
setSentByType({
|
||||||
|
mass: creditRes.sentByType?.mass ?? 0,
|
||||||
|
singular: creditRes.sentByType?.singular ?? 0,
|
||||||
|
auth: creditRes.sentByType?.auth ?? 0,
|
||||||
|
autoCustomer: creditRes.sentByType?.autoCustomer ?? 0,
|
||||||
|
admin: creditRes.sentByType?.admin ?? 0,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isAbortError(err)) return
|
if (isAbortError(err)) return
|
||||||
setError(err instanceof ApiError ? err.message : t('settings.sms.errorLoad'))
|
setError(err instanceof ApiError ? err.message : t('settings.sms.errorLoad'))
|
||||||
@@ -272,6 +288,26 @@ export function SmsSettingsSection() {
|
|||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={settingsStyles.smsSentStack} role="group" aria-label={t('settings.sms.sentCount')}>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
['settings.sms.sentTotal', sentCount],
|
||||||
|
['settings.sms.sentMass', sentByType.mass],
|
||||||
|
['settings.sms.sentSingular', sentByType.singular],
|
||||||
|
['settings.sms.sentAuth', sentByType.auth],
|
||||||
|
['settings.sms.sentAutoCustomer', sentByType.autoCustomer],
|
||||||
|
['settings.sms.sentAdmin', sentByType.admin],
|
||||||
|
] as const
|
||||||
|
).map(([labelKey, value]) => (
|
||||||
|
<div key={labelKey} className={`${settingsStyles.smsCreditRow} ${settingsStyles.smsSentRow}`}>
|
||||||
|
<span className={settingsStyles.smsCreditLabel}>{t(labelKey)}</span>
|
||||||
|
<strong className={settingsStyles.smsCreditValue}>
|
||||||
|
{loading ? '…' : value.toLocaleString(isFa ? 'fa-IR' : 'en-US')}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={settingsStyles.smsTemplatesSummary}>
|
<div className={settingsStyles.smsTemplatesSummary}>
|
||||||
<div className={settingsStyles.smsTemplatesSummaryText}>
|
<div className={settingsStyles.smsTemplatesSummaryText}>
|
||||||
<h4 className={settingsStyles.smsTemplatesTitle}>{t('settings.sms.templates')}</h4>
|
<h4 className={settingsStyles.smsTemplatesTitle}>{t('settings.sms.templates')}</h4>
|
||||||
|
|||||||
@@ -1699,6 +1699,12 @@ const en = {
|
|||||||
'settings.sms.balanceHint':
|
'settings.sms.balanceHint':
|
||||||
'Balance is SMS credits. Each 64 characters costs 1 credit per recipient (login, orders, customer SMS, etc.). Contact Meshkee to top up.',
|
'Balance is SMS credits. Each 64 characters costs 1 credit per recipient (login, orders, customer SMS, etc.). Contact Meshkee to top up.',
|
||||||
'settings.sms.sentCount': 'SMS sent',
|
'settings.sms.sentCount': 'SMS sent',
|
||||||
|
'settings.sms.sentTotal': 'SMS sent (total)',
|
||||||
|
'settings.sms.sentMass': 'Mass send',
|
||||||
|
'settings.sms.sentSingular': 'Singular',
|
||||||
|
'settings.sms.sentAuth': 'Auth service',
|
||||||
|
'settings.sms.sentAutoCustomer': 'Auto customer',
|
||||||
|
'settings.sms.sentAdmin': 'Admin notification',
|
||||||
'settings.sms.credit': 'Account credit',
|
'settings.sms.credit': 'Account credit',
|
||||||
'settings.sms.creditUnavailable': 'Unavailable',
|
'settings.sms.creditUnavailable': 'Unavailable',
|
||||||
'settings.sms.templates': 'Templates',
|
'settings.sms.templates': 'Templates',
|
||||||
@@ -4163,6 +4169,12 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'settings.sms.balanceHint':
|
'settings.sms.balanceHint':
|
||||||
'اعتبار به واحد پیامک است. هر ۶۴ کاراکتر برای هر گیرنده ۱ اعتبار مصرف میکند (ورود، سفارش، پیامک مشتری و غیره). برای افزایش با مشکی تماس بگیرید.',
|
'اعتبار به واحد پیامک است. هر ۶۴ کاراکتر برای هر گیرنده ۱ اعتبار مصرف میکند (ورود، سفارش، پیامک مشتری و غیره). برای افزایش با مشکی تماس بگیرید.',
|
||||||
'settings.sms.sentCount': 'پیامک ارسالشده',
|
'settings.sms.sentCount': 'پیامک ارسالشده',
|
||||||
|
'settings.sms.sentTotal': 'پیامک ارسالشده (کل)',
|
||||||
|
'settings.sms.sentMass': 'ارسال گروهی',
|
||||||
|
'settings.sms.sentSingular': 'ارسال تکی',
|
||||||
|
'settings.sms.sentAuth': 'سرویس احراز هویت',
|
||||||
|
'settings.sms.sentAutoCustomer': 'خودکار مشتری',
|
||||||
|
'settings.sms.sentAdmin': 'اعلان مدیر',
|
||||||
'settings.sms.credit': 'اعتبار حساب',
|
'settings.sms.credit': 'اعتبار حساب',
|
||||||
'settings.sms.creditUnavailable': 'در دسترس نیست',
|
'settings.sms.creditUnavailable': 'در دسترس نیست',
|
||||||
'settings.sms.templates': 'قالبها',
|
'settings.sms.templates': 'قالبها',
|
||||||
|
|||||||
@@ -249,6 +249,29 @@
|
|||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.smsSentStack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smsSentRow {
|
||||||
|
margin-top: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.04);
|
||||||
|
border-color: rgba(15, 23, 42, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.smsSentRow .smsCreditLabel,
|
||||||
|
.smsSentRow .smsCreditValue {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smsSentRow .smsCreditValue {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.smsTemplatesSummary {
|
.smsTemplatesSummary {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ export async function getSmsCredit(signal?: AbortSignal) {
|
|||||||
credit: number | null
|
credit: number | null
|
||||||
/** Lifetime SMS messages billed (one per recipient). */
|
/** Lifetime SMS messages billed (one per recipient). */
|
||||||
sentCount: number
|
sentCount: number
|
||||||
|
sentByType: {
|
||||||
|
mass: number
|
||||||
|
singular: number
|
||||||
|
auth: number
|
||||||
|
autoCustomer: number
|
||||||
|
admin: number
|
||||||
|
}
|
||||||
unit: 'credits'
|
unit: 'credits'
|
||||||
currency: string | null
|
currency: string | null
|
||||||
message: string | null
|
message: string | null
|
||||||
|
|||||||
@@ -88,6 +88,64 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dataRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: var(--radius-sm, 10px);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataRowOk {
|
||||||
|
background: rgba(var(--primary-rgb, 15 118 110) / 0.1);
|
||||||
|
border-color: rgba(var(--primary-rgb, 15 118 110) / 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataRowOk .dataRowLabel,
|
||||||
|
.dataRowOk .dataRowValue {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataRowLow {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border-color: rgba(239, 68, 68, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataRowLow .dataRowLabel,
|
||||||
|
.dataRowLow .dataRowValue {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataRowNeutral {
|
||||||
|
background: rgba(15, 23, 42, 0.04);
|
||||||
|
border-color: rgba(15, 23, 42, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataRowNeutral .dataRowLabel,
|
||||||
|
.dataRowNeutral .dataRowValue {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataRowLabel {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataRowValue {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataRowStack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.entityGroupLabel {
|
.entityGroupLabel {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -2108,13 +2108,20 @@ export function BusinessesPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
<label>Current SMS balance</label>
|
<div
|
||||||
<input
|
className={`${styles.dataRow} ${
|
||||||
value={`${(smsSettingsBusiness?.smsBalance ?? 0).toLocaleString('en-US')} credits`}
|
(smsSettingsBusiness?.smsBalance ?? 0) < 400
|
||||||
readOnly
|
? styles.dataRowLow
|
||||||
disabled
|
: styles.dataRowOk
|
||||||
|
}`}
|
||||||
|
role="status"
|
||||||
aria-label="Current SMS credit balance"
|
aria-label="Current SMS credit balance"
|
||||||
/>
|
>
|
||||||
|
<span className={styles.dataRowLabel}>Current SMS balance</span>
|
||||||
|
<strong className={styles.dataRowValue}>
|
||||||
|
{(smsSettingsBusiness?.smsBalance ?? 0).toLocaleString('en-US')} SMS credits
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
<p className={styles.helperText}>
|
<p className={styles.helperText}>
|
||||||
Prepaid SMS credits. 1 credit = one 64-character segment per recipient (OTP,
|
Prepaid SMS credits. 1 credit = one 64-character segment per recipient (OTP,
|
||||||
customer SMS, partner SMS). New businesses start with 1,000 credits. Values below
|
customer SMS, partner SMS). New businesses start with 1,000 credits. Values below
|
||||||
@@ -2156,16 +2163,43 @@ export function BusinessesPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
<label>SMS sent</label>
|
<div className={styles.dataRowStack} role="group" aria-label="SMS sent by type">
|
||||||
<input
|
<div
|
||||||
value={(smsSettingsBusiness?.smsSentCount ?? 0).toLocaleString('en-US')}
|
className={`${styles.dataRow} ${styles.dataRowNeutral}`}
|
||||||
readOnly
|
role="status"
|
||||||
disabled
|
>
|
||||||
aria-label="SMS messages sent"
|
<span className={styles.dataRowLabel}>SMS sent (total)</span>
|
||||||
/>
|
<strong className={styles.dataRowValue}>
|
||||||
|
{(smsSettingsBusiness?.smsSentCount ?? 0).toLocaleString('en-US')}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
['Mass send', smsSettingsBusiness?.smsSentByType?.mass],
|
||||||
|
['Singular', smsSettingsBusiness?.smsSentByType?.singular],
|
||||||
|
['Auth service', smsSettingsBusiness?.smsSentByType?.auth],
|
||||||
|
[
|
||||||
|
'Auto customer',
|
||||||
|
smsSettingsBusiness?.smsSentByType?.autoCustomer,
|
||||||
|
],
|
||||||
|
['Admin notification', smsSettingsBusiness?.smsSentByType?.admin],
|
||||||
|
] as const
|
||||||
|
).map(([label, value]) => (
|
||||||
|
<div
|
||||||
|
key={label}
|
||||||
|
className={`${styles.dataRow} ${styles.dataRowNeutral}`}
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<span className={styles.dataRowLabel}>{label}</span>
|
||||||
|
<strong className={styles.dataRowValue}>
|
||||||
|
{(value ?? 0).toLocaleString('en-US')}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<p className={styles.helperText}>
|
<p className={styles.helperText}>
|
||||||
Lifetime messages billed for this business (one per recipient). Updated automatically
|
Lifetime messages billed (one per recipient), split by type. Auto customer and
|
||||||
when SMS is charged or refunded.
|
admin notification are reserved for upcoming flows.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: 12 }} />
|
<div style={{ height: 12 }} />
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ export interface BusinessListItem {
|
|||||||
smsBalance: number
|
smsBalance: number
|
||||||
/** Lifetime SMS messages billed (one per recipient; decremented on refund). */
|
/** Lifetime SMS messages billed (one per recipient; decremented on refund). */
|
||||||
smsSentCount: number
|
smsSentCount: number
|
||||||
|
/** Lifetime SMS messages billed by category. */
|
||||||
|
smsSentByType: {
|
||||||
|
mass: number
|
||||||
|
singular: number
|
||||||
|
auth: number
|
||||||
|
autoCustomer: number
|
||||||
|
admin: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MigrateFromOldEntity =
|
export type MigrateFromOldEntity =
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ Products accept optional `brandId` on create/update.
|
|||||||
- Per-business prepaid `businesses.sms_balance` (**SMS credits**) — all Meshkee SMS (OTP login/forgot-password, customer SMS, partner/public SMS for linked domains) costs **1 credit per 64 characters** per recipient (`ceil(len/64)`). Insufficient balance blocks send.
|
- Per-business prepaid `businesses.sms_balance` (**SMS credits**) — all Meshkee SMS (OTP login/forgot-password, customer SMS, partner/public SMS for linked domains) costs **1 credit per 64 characters** per recipient (`ceil(len/64)`). Insufficient balance blocks send.
|
||||||
- Top-up (Super Admin): enter IRT amount → adds `floor(IRT / 500)` credits (e.g. 1,000,000 IRT → 2,000 credits)
|
- Top-up (Super Admin): enter IRT amount → adds `floor(IRT / 500)` credits (e.g. 1,000,000 IRT → 2,000 credits)
|
||||||
- New businesses created in Super Admin start with **1,000** SMS credits
|
- New businesses created in Super Admin start with **1,000** SMS credits
|
||||||
- `businesses.sms_sent_count` — lifetime messages billed (one per recipient)
|
- `businesses.sms_sent_count` — lifetime messages billed (one per recipient), plus typed counters: mass / singular / auth / auto_customer / admin
|
||||||
- External domains (no Meshkee website) still bill via their business row once the domain is linked
|
- External domains (no Meshkee website) still bill via their business row once the domain is linked
|
||||||
- Business Settings → **SMS settings**: credit balance (read-only) + template CRUD
|
- Business Settings → **SMS settings**: credit balance (read-only) + template CRUD
|
||||||
- Super Admin business overflow → **SMS settings** modal: sender number + current credits + IRT top-up
|
- Super Admin business overflow → **SMS settings** modal: sender number + current credits + IRT top-up
|
||||||
|
|||||||
Reference in New Issue
Block a user