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:
Alireza Hassani
2026-09-09 11:31:18 +03:30
co-authored by Cursor
parent 6bc57f26ed
commit dbc17b2589
8 changed files with 194 additions and 16 deletions
@@ -43,6 +43,14 @@ export function SmsSettingsSection() {
const isFa = locale === 'fa'
const { showToast } = useToast()
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 [loading, setLoading] = useState(true)
const [error, setError] = useState('')
@@ -73,6 +81,14 @@ export function SmsSettingsSection() {
if (signal?.aborted) return
setTemplates(tpl.items)
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) {
if (isAbortError(err)) return
setError(err instanceof ApiError ? err.message : t('settings.sms.errorLoad'))
@@ -272,6 +288,26 @@ export function SmsSettingsSection() {
</strong>
</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.smsTemplatesSummaryText}>
<h4 className={settingsStyles.smsTemplatesTitle}>{t('settings.sms.templates')}</h4>
+12
View File
@@ -1699,6 +1699,12 @@ const en = {
'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.',
'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.creditUnavailable': 'Unavailable',
'settings.sms.templates': 'Templates',
@@ -4163,6 +4169,12 @@ const fa: Record<MessageKey, string> = {
'settings.sms.balanceHint':
'اعتبار به واحد پیامک است. هر ۶۴ کاراکتر برای هر گیرنده ۱ اعتبار مصرف می‌کند (ورود، سفارش، پیامک مشتری و غیره). برای افزایش با مشکی تماس بگیرید.',
'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.creditUnavailable': 'در دسترس نیست',
'settings.sms.templates': 'قالب‌ها',
@@ -249,6 +249,29 @@
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 {
display: flex;
align-items: center;
@@ -71,6 +71,13 @@ export async function getSmsCredit(signal?: AbortSignal) {
credit: number | null
/** Lifetime SMS messages billed (one per recipient). */
sentCount: number
sentByType: {
mass: number
singular: number
auth: number
autoCustomer: number
admin: number
}
unit: 'credits'
currency: string | null
message: string | null
@@ -88,6 +88,64 @@
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 {
font-size: 12px;
font-weight: 500;
+49 -15
View File
@@ -2108,13 +2108,20 @@ export function BusinessesPage() {
</p>
</div>
<div className={styles.field}>
<label>Current SMS balance</label>
<input
value={`${(smsSettingsBusiness?.smsBalance ?? 0).toLocaleString('en-US')} credits`}
readOnly
disabled
<div
className={`${styles.dataRow} ${
(smsSettingsBusiness?.smsBalance ?? 0) < 400
? styles.dataRowLow
: styles.dataRowOk
}`}
role="status"
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}>
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
@@ -2156,16 +2163,43 @@ export function BusinessesPage() {
</p>
</div>
<div className={styles.field}>
<label>SMS sent</label>
<input
value={(smsSettingsBusiness?.smsSentCount ?? 0).toLocaleString('en-US')}
readOnly
disabled
aria-label="SMS messages sent"
/>
<div className={styles.dataRowStack} role="group" aria-label="SMS sent by type">
<div
className={`${styles.dataRow} ${styles.dataRowNeutral}`}
role="status"
>
<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}>
Lifetime messages billed for this business (one per recipient). Updated automatically
when SMS is charged or refunded.
Lifetime messages billed (one per recipient), split by type. Auto customer and
admin notification are reserved for upcoming flows.
</p>
</div>
<div style={{ height: 12 }} />
+8
View File
@@ -44,6 +44,14 @@ export interface BusinessListItem {
smsBalance: number
/** Lifetime SMS messages billed (one per recipient; decremented on refund). */
smsSentCount: number
/** Lifetime SMS messages billed by category. */
smsSentByType: {
mass: number
singular: number
auth: number
autoCustomer: number
admin: number
}
}
export type MigrateFromOldEntity =
+1 -1
View File
@@ -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.
- 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
- `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
- Business Settings → **SMS settings**: credit balance (read-only) + template CRUD
- Super Admin business overflow → **SMS settings** modal: sender number + current credits + IRT top-up