From d0d621502f0a69b7382d149ad9b32c7ff84963a3 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sat, 8 Aug 2026 11:20:26 +0330 Subject: [PATCH] Add support panel UI, search filters, and money thousand separators. Co-authored-by: Cursor --- src/components/MoneyInput.vue | 54 +++ src/pages/HomePage.vue | 63 ++- src/pages/PluginPanelPage.vue | 33 +- src/pages/PluginTenantPage.vue | 31 +- src/pages/ServerPanelPage.vue | 38 +- src/pages/ServerTenantPage.vue | 23 +- src/pages/SmsPanelPage.vue | 42 +- src/pages/SmsTenantPage.vue | 75 +--- src/pages/SupportPanelPage.vue | 234 +++++++++++ src/pages/SupportTenantPage.vue | 665 ++++++++++++++++++++++++++++++++ src/router/index.ts | 10 + src/styles.css | 4 +- src/utils/money.ts | 29 ++ 13 files changed, 1169 insertions(+), 132 deletions(-) create mode 100644 src/components/MoneyInput.vue create mode 100644 src/pages/SupportPanelPage.vue create mode 100644 src/pages/SupportTenantPage.vue create mode 100644 src/utils/money.ts diff --git a/src/components/MoneyInput.vue b/src/components/MoneyInput.vue new file mode 100644 index 0000000..03c2b8b --- /dev/null +++ b/src/components/MoneyInput.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/pages/HomePage.vue b/src/pages/HomePage.vue index 097a23d..0047339 100644 --- a/src/pages/HomePage.vue +++ b/src/pages/HomePage.vue @@ -5,6 +5,7 @@ import { MessageOutlined, CloudServerOutlined, AppstoreAddOutlined, + CustomerServiceOutlined, TeamOutlined, HomeOutlined, UserOutlined, @@ -52,12 +53,15 @@ type ExpiringPlugin = { } } +type ExpiringSupport = ExpiringPlugin + const router = useRouter() const loading = ref(true) const tenants = ref([]) const smsTenants = ref([]) const expiringServers = ref([]) const expiringPlugins = ref([]) +const expiringSupports = ref([]) const hamkarCount = computed(() => tenants.value.filter((t) => t.product === 'hamkar').length) const khanegiCount = computed(() => tenants.value.filter((t) => t.product === 'khanegi').length) @@ -96,16 +100,19 @@ const modules = computed(() => [ async function load() { loading.value = true try { - const [tenantsRes, lowestRes, expiringRes, pluginExpiringRes] = await Promise.all([ - api.get('/tenants'), - api.get('/sms/lowest', { params: { limit: 5 } }), - api.get('/servers/expiring', { params: { limit: 5 } }), - api.get('/plugins/expiring', { params: { limit: 5 } }), - ]) + const [tenantsRes, lowestRes, expiringRes, pluginExpiringRes, supportExpiringRes] = + await Promise.all([ + api.get('/tenants'), + api.get('/sms/lowest', { params: { limit: 5 } }), + api.get('/servers/expiring', { params: { limit: 5 } }), + api.get('/plugins/expiring', { params: { limit: 5 } }), + api.get('/support/expiring', { params: { limit: 5 } }), + ]) tenants.value = tenantsRes.data smsTenants.value = lowestRes.data expiringServers.value = expiringRes.data expiringPlugins.value = pluginExpiringRes.data + expiringSupports.value = supportExpiringRes.data } catch (e: any) { message.error(e?.response?.data?.message || 'خطا در دریافت آمار') } finally { @@ -137,6 +144,14 @@ function openPluginPanel(tenantId?: string) { router.push({ name: 'plugin-panel' }) } +function openSupportPanel(tenantId?: string) { + if (tenantId) { + router.push({ name: 'support-tenant', params: { tenantId } }) + return + } + router.push({ name: 'support-panel' }) +} + function smsTone(level?: BalanceLevel) { switch (level) { case 'healthy': @@ -173,7 +188,7 @@ onMounted(load)

سلام، خوش آمدید

- از اینجا به پنل پیامکی، سرور، افزونه، مدیران و نسخه‌های همکار / خانگی دسترسی دارید. + از اینجا به پنل پیامکی، سرور، افزونه، پشتیبانی، مدیران و نسخه‌های همکار / خانگی دسترسی دارید.

@@ -285,6 +300,40 @@ onMounted(load) +
+
+
+ +
+

پنل پشتیبانی

+

پشتیبانی‌هایی که اعتبارشان رو به پایان است (قابل ثبت رایگان)

+
+
+ +
+ پشتیبانی نزدیک‌انقضا یا بدون تاریخ ثبت‌شده نیست +
+
+
+
+
+ {{ expiringSupports.length }} مورد پیگیری + ورود ← +
+
+
+ +
نسخه‌ای نیست
@@ -121,6 +148,7 @@ onMounted(loadTenants) {{ t.sms?.balanceLabel }} {{ t.isBlocked ? 'مسدود' : t.domain }} +
نسخه‌ای نیست
@@ -198,4 +226,10 @@ onMounted(loadTenants) box-shadow: 0 10px 22px rgba(23, 50, 77, 0.08); } .tenant-card small { direction: ltr; text-align: right; } +.empty-hint { + grid-column: 1 / -1; + color: #94a3b8; + padding: 12px; + font-size: 0.85rem; +} diff --git a/src/pages/SmsTenantPage.vue b/src/pages/SmsTenantPage.vue index 6cabd8f..0cbade3 100644 --- a/src/pages/SmsTenantPage.vue +++ b/src/pages/SmsTenantPage.vue @@ -6,7 +6,9 @@ import { ArrowRightOutlined } from '@ant-design/icons-vue' import type { TableColumnsType } from 'ant-design-vue' import api from '@/api/http' import JalaliDatePicker from '@/components/JalaliDatePicker.vue' +import MoneyInput from '@/components/MoneyInput.vue' import { formatIranDateTime, todayIranDateInput } from '@/utils/datetime' +import { formatMoney, formatNumber } from '@/utils/money' type BalanceLevel = 'healthy' | 'warning' | 'critical' | 'unknown' @@ -96,42 +98,6 @@ const pageSize = 20 const tenantId = computed(() => String(route.params.tenantId || '')) -function toGroupedDisplay(value: number | null | undefined) { - if (value == null || Number.isNaN(Number(value))) return '' - const n = Math.trunc(Number(value)) - const neg = n < 0 - const digits = String(Math.abs(n)) - const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, ',') - return neg ? `-${grouped}` : grouped -} - -function fromGroupedInput(raw: string, allowNegative = false) { - const text = String(raw ?? '') - const neg = allowNegative && text.trim().startsWith('-') - const digits = text.replace(/[^\d]/g, '') - if (!digits) return 0 - const n = Number(digits) - return neg ? -n : n -} - -function onGroupedMoneyInput( - apply: (n: number) => void, - value: string | number, - allowNegative = false, -) { - apply(fromGroupedInput(String(value ?? ''), allowNegative)) -} - -function formatNumber(value: number | null | undefined) { - if (value == null) return '—' - return new Intl.NumberFormat('fa-IR').format(value) -} - -function formatMoney(value: number | null | undefined) { - if (value == null) return '—' - return `${formatNumber(value)} ریال` -} - function toneClass(level?: BalanceLevel) { return level || 'unknown' } @@ -478,13 +444,7 @@ onMounted(async () => {

تعرفه پیامک

- + ذخیره قیمت
@@ -492,13 +452,10 @@ onMounted(async () => {

شارژ کیف پول

- @@ -542,23 +499,11 @@ onMounted(async () => {
ذخیره آستانه‌ها
@@ -1007,10 +952,6 @@ onMounted(async () => { font-size: 0.78rem; } -.money-input :deep(input) { - font-variant-numeric: tabular-nums; -} - .money-input, .filter-row :deep(.jalali-date-field), .filter-row :deep(.ant-input), diff --git a/src/pages/SupportPanelPage.vue b/src/pages/SupportPanelPage.vue new file mode 100644 index 0000000..2668637 --- /dev/null +++ b/src/pages/SupportPanelPage.vue @@ -0,0 +1,234 @@ + + + + + diff --git a/src/pages/SupportTenantPage.vue b/src/pages/SupportTenantPage.vue new file mode 100644 index 0000000..099308c --- /dev/null +++ b/src/pages/SupportTenantPage.vue @@ -0,0 +1,665 @@ + + + + + diff --git a/src/router/index.ts b/src/router/index.ts index 40b93e7..6bf483a 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -49,6 +49,16 @@ const router = createRouter({ name: 'plugin-tenant', component: () => import('@/pages/PluginTenantPage.vue'), }, + { + path: 'support', + name: 'support-panel', + component: () => import('@/pages/SupportPanelPage.vue'), + }, + { + path: 'support/:tenantId', + name: 'support-tenant', + component: () => import('@/pages/SupportTenantPage.vue'), + }, { path: 'admins', name: 'admins', diff --git a/src/styles.css b/src/styles.css index 7f09cb4..5e8224c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -143,7 +143,7 @@ button, input, textarea, select, @media (min-width: 1100px) { .dash-modules { - grid-template-columns: repeat(4, 1fr); + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); } } @@ -185,6 +185,7 @@ button, input, textarea, select, .module-card.sms .icon-wrap { background: linear-gradient(145deg, #9b7cff, #7c5cfc); } .module-card.server .icon-wrap { background: linear-gradient(145deg, #ff8a8a, #ff6b6b); } .module-card.plugin .icon-wrap { background: linear-gradient(145deg, #5ec8e8, #2aa8cc); } +.module-card.support .icon-wrap { background: linear-gradient(145deg, #60a5fa, #3b82f6); } .module-card.admins .icon-wrap { background: linear-gradient(145deg, #5b8def, #3b6fd4); } .module-card.hamkar .icon-wrap { background: linear-gradient(145deg, #3ad4b0, #20c997); } .module-card.khanegi .icon-wrap { background: linear-gradient(145deg, #ffc86b, #ffb84d); } @@ -231,6 +232,7 @@ button, input, textarea, select, .module-card.sms .progress > span { background: #7c5cfc; width: 72%; } .module-card.server .progress > span { background: #ff6b6b; width: 54%; } .module-card.plugin .progress > span { background: #2aa8cc; width: 58%; } +.module-card.support .progress > span { background: #3b82f6; width: 62%; } .module-card.admins .progress > span { background: #3b6fd4; width: 66%; } .module-card.hamkar .progress > span { background: #20c997; width: 80%; } .module-card.khanegi .progress > span { background: #ffb84d; width: 68%; } diff --git a/src/utils/money.ts b/src/utils/money.ts new file mode 100644 index 0000000..006cdb3 --- /dev/null +++ b/src/utils/money.ts @@ -0,0 +1,29 @@ +/** Display/parse helpers for rial money inputs with thousand separators. */ + +export function toGroupedDisplay(value: number | null | undefined) { + if (value == null || Number.isNaN(Number(value))) return '' + const n = Math.trunc(Number(value)) + const neg = n < 0 + const digits = String(Math.abs(n)) + const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + return neg ? `-${grouped}` : grouped +} + +export function fromGroupedInput(raw: string, allowNegative = false) { + const text = String(raw ?? '') + const neg = allowNegative && text.trim().startsWith('-') + const digits = text.replace(/[^\d]/g, '') + if (!digits) return 0 + const n = Number(digits) + return neg ? -n : n +} + +export function formatNumber(value: number | null | undefined) { + if (value == null) return '—' + return new Intl.NumberFormat('fa-IR').format(value) +} + +export function formatMoney(value: number | null | undefined) { + if (value == null) return '—' + return `${formatNumber(value)} ریال` +}