From dbc17b25894abfaaf49404bd43536d44c3b39f8d Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Wed, 9 Sep 2026 11:31:18 +0330 Subject: [PATCH] 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 --- .../src/components/SmsSettingsSection.tsx | 36 +++++++++++ apps/business/src/i18n/messages.ts | 12 ++++ .../src/pages/SettingsPage.module.css | 23 +++++++ .../src/services/smsTemplateService.ts | 7 ++ .../src/pages/BusinessesPage.module.css | 58 +++++++++++++++++ apps/super-admin/src/pages/BusinessesPage.tsx | 64 ++++++++++++++----- apps/super-admin/src/types/business.ts | 8 +++ docs/PROJECT_CONTEXT.md | 2 +- 8 files changed, 194 insertions(+), 16 deletions(-) diff --git a/apps/business/src/components/SmsSettingsSection.tsx b/apps/business/src/components/SmsSettingsSection.tsx index 3873dcb..ef8f2fc 100644 --- a/apps/business/src/components/SmsSettingsSection.tsx +++ b/apps/business/src/components/SmsSettingsSection.tsx @@ -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([]) 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() { +
+ {( + [ + ['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]) => ( +
+ {t(labelKey)} + + {loading ? '…' : value.toLocaleString(isFa ? 'fa-IR' : 'en-US')} + +
+ ))} +
+

{t('settings.sms.templates')}

diff --git a/apps/business/src/i18n/messages.ts b/apps/business/src/i18n/messages.ts index 02c418f..88bc03f 100644 --- a/apps/business/src/i18n/messages.ts +++ b/apps/business/src/i18n/messages.ts @@ -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 = { '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': 'قالب‌ها', diff --git a/apps/business/src/pages/SettingsPage.module.css b/apps/business/src/pages/SettingsPage.module.css index eddf568..fbd5193 100644 --- a/apps/business/src/pages/SettingsPage.module.css +++ b/apps/business/src/pages/SettingsPage.module.css @@ -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; diff --git a/apps/business/src/services/smsTemplateService.ts b/apps/business/src/services/smsTemplateService.ts index 1e5ada6..3ce5220 100644 --- a/apps/business/src/services/smsTemplateService.ts +++ b/apps/business/src/services/smsTemplateService.ts @@ -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 diff --git a/apps/super-admin/src/pages/BusinessesPage.module.css b/apps/super-admin/src/pages/BusinessesPage.module.css index 8235243..c9688c3 100644 --- a/apps/super-admin/src/pages/BusinessesPage.module.css +++ b/apps/super-admin/src/pages/BusinessesPage.module.css @@ -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; diff --git a/apps/super-admin/src/pages/BusinessesPage.tsx b/apps/super-admin/src/pages/BusinessesPage.tsx index e3b4fe8..24b6e10 100644 --- a/apps/super-admin/src/pages/BusinessesPage.tsx +++ b/apps/super-admin/src/pages/BusinessesPage.tsx @@ -2108,13 +2108,20 @@ export function BusinessesPage() {

- - + > + Current SMS balance + + {(smsSettingsBusiness?.smsBalance ?? 0).toLocaleString('en-US')} SMS credits + +

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() {

- - +
+
+ SMS sent (total) + + {(smsSettingsBusiness?.smsSentCount ?? 0).toLocaleString('en-US')} + +
+ {( + [ + ['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]) => ( +
+ {label} + + {(value ?? 0).toLocaleString('en-US')} + +
+ ))} +

- 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.

diff --git a/apps/super-admin/src/types/business.ts b/apps/super-admin/src/types/business.ts index e90f3b3..b8ed392 100644 --- a/apps/super-admin/src/types/business.ts +++ b/apps/super-admin/src/types/business.ts @@ -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 = diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index 52e525b..f1d07b4 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -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