Improve SMS settings layout and add send-modal variable chips.

Credit/usage divider, total row plus typed counts grid, and insertable template vars on compose.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-09 11:37:35 +03:30
co-authored by Cursor
parent dbc17b2589
commit 29a190f02a
5 changed files with 178 additions and 43 deletions
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { X } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
@@ -12,6 +12,7 @@ import {
} from '../services/customerService'
import { listSmsTemplates, type SmsTemplate } from '../services/smsTemplateService'
import modalStyles from './VariationsModal.module.css'
import settingsStyles from '../pages/SettingsPage.module.css'
export type SmsComposeTarget =
| { mode: 'single'; customer: BusinessCustomerListItem }
@@ -27,6 +28,7 @@ interface SendCustomerSmsModalProps {
const ANIMATION_MS = 220
const MAX_LEN = 700
const SMS_VARS = ['{{user.name}}', '{{user.lastname}}', '{{user.title}}'] as const
export function SendCustomerSmsModal({
open,
@@ -45,6 +47,7 @@ export function SendCustomerSmsModal({
const [templatesLoading, setTemplatesLoading] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('')
const messageRef = useRef<HTMLTextAreaElement>(null)
useEffect(() => {
if (open && target) {
@@ -103,6 +106,21 @@ export function SendCustomerSmsModal({
if (tpl) setMessage(tpl.body.slice(0, MAX_LEN))
}
function insertVariable(token: string) {
if (selectedTemplate?.isVerified) return
const el = messageRef.current
const start = el?.selectionStart ?? message.length
const end = el?.selectionEnd ?? message.length
const next = `${message.slice(0, start)}${token}${message.slice(end)}`.slice(0, MAX_LEN)
const inserted = Math.min(token.length, MAX_LEN - message.slice(0, start).length)
const cursor = start + Math.max(0, inserted)
setMessage(next)
requestAnimationFrame(() => {
el?.focus()
el?.setSelectionRange(cursor, cursor)
})
}
async function handleSubmit() {
if (!target || !canSend) return
setIsSubmitting(true)
@@ -216,8 +234,31 @@ export function SendCustomerSmsModal({
<div className={modalStyles.field}>
<label htmlFor="customer-sms-message">{t('customers.smsModal.message')}</label>
<div
className={settingsStyles.smsVarChips}
role="group"
aria-label={t('settings.sms.variablesHint')}
>
{SMS_VARS.map((token) => (
<button
key={token}
type="button"
className={settingsStyles.smsVarChip}
onClick={() => insertVariable(token)}
disabled={
isSubmitting ||
Boolean(selectedTemplate?.isVerified) ||
message.length >= MAX_LEN
}
dir="ltr"
>
{token}
</button>
))}
</div>
<textarea
id="customer-sms-message"
ref={messageRef}
value={message}
onChange={(e) => setMessage(e.target.value.slice(0, MAX_LEN))}
rows={5}
@@ -288,24 +288,31 @@ 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 className={settingsStyles.smsUsageSection} role="group" aria-label={t('settings.sms.sentCount')}>
<div className={`${settingsStyles.smsCreditRow} ${settingsStyles.smsSentRow}`}>
<span className={settingsStyles.smsCreditLabel}>{t('settings.sms.sentTotal')}</span>
<strong className={settingsStyles.smsCreditValue}>
{loading ? '…' : sentCount.toLocaleString(isFa ? 'fa-IR' : 'en-US')}
</strong>
</div>
<div className={settingsStyles.smsSentTypesRow}>
{(
[
['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.smsSentTypeCell}>
<span className={settingsStyles.smsSentTypeLabel}>{t(labelKey)}</span>
<strong className={settingsStyles.smsSentTypeValue}>
{loading ? '…' : value.toLocaleString(isFa ? 'fa-IR' : 'en-US')}
</strong>
</div>
))}
</div>
</div>
<div className={settingsStyles.smsTemplatesSummary}>
@@ -249,11 +249,13 @@
font-family: var(--font-ui);
}
.smsSentStack {
.smsUsageSection {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 10px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(148, 163, 184, 0.2);
}
.smsSentRow {
@@ -272,6 +274,50 @@
font-size: 14px;
}
.smsSentTypesRow {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 8px;
}
.smsSentTypeCell {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
padding: 10px 12px;
border-radius: var(--radius-sm);
border: 1px solid rgba(15, 23, 42, 0.1);
background: rgba(15, 23, 42, 0.04);
}
.smsSentTypeLabel {
font-size: 11px;
font-weight: 500;
line-height: 1.35;
color: var(--text-secondary);
}
.smsSentTypeValue {
font-size: 14px;
font-weight: 700;
font-variant-numeric: tabular-nums;
font-family: var(--font-ui);
color: var(--text-primary);
}
@media (max-width: 900px) {
.smsSentTypesRow {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 520px) {
.smsSentTypesRow {
grid-template-columns: 1fr;
}
}
.smsTemplatesSummary {
display: flex;
align-items: center;