2026-07-23 19:42:07 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-07-30 18:19:54 +03:30
|
|
|
|
import { Modal, message } from 'ant-design-vue'
|
2026-07-23 19:42:07 +03:30
|
|
|
|
import { ArrowRightOutlined } from '@ant-design/icons-vue'
|
2026-07-30 18:19:54 +03:30
|
|
|
|
import type { TableColumnsType } from 'ant-design-vue'
|
2026-07-23 19:42:07 +03:30
|
|
|
|
import api from '@/api/http'
|
2026-07-30 18:19:54 +03:30
|
|
|
|
import JalaliDatePicker from '@/components/JalaliDatePicker.vue'
|
2026-08-08 11:20:26 +03:30
|
|
|
|
import MoneyInput from '@/components/MoneyInput.vue'
|
2026-08-08 12:35:59 +03:30
|
|
|
|
import TenantPanelShortcuts from '@/components/TenantPanelShortcuts.vue'
|
2026-07-30 18:19:54 +03:30
|
|
|
|
import { formatIranDateTime, todayIranDateInput } from '@/utils/datetime'
|
2026-08-08 11:20:26 +03:30
|
|
|
|
import { formatMoney, formatNumber } from '@/utils/money'
|
2026-07-23 19:42:07 +03:30
|
|
|
|
|
|
|
|
|
|
type BalanceLevel = 'healthy' | 'warning' | 'critical' | 'unknown'
|
|
|
|
|
|
|
|
|
|
|
|
type SmsSummary = {
|
|
|
|
|
|
balanceRials: number
|
|
|
|
|
|
balanceLabel: string
|
|
|
|
|
|
balanceLevel: BalanceLevel
|
|
|
|
|
|
perPartRials: number
|
|
|
|
|
|
todayMessages: number
|
|
|
|
|
|
todayParts: number
|
|
|
|
|
|
todayCostRials: number
|
|
|
|
|
|
monthMessages: number
|
|
|
|
|
|
monthParts: number
|
|
|
|
|
|
monthCostRials: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const saving = ref(false)
|
|
|
|
|
|
const charging = ref(false)
|
2026-07-30 18:19:54 +03:30
|
|
|
|
const blockLoading = ref(false)
|
|
|
|
|
|
const chargeInfoSaving = ref(false)
|
|
|
|
|
|
const reportLoading = ref(false)
|
|
|
|
|
|
const chargeReportLoading = ref(false)
|
|
|
|
|
|
const logsLoading = ref(false)
|
|
|
|
|
|
const ledgerLoading = ref(false)
|
|
|
|
|
|
|
2026-07-23 19:42:07 +03:30
|
|
|
|
const tenant = ref<{
|
|
|
|
|
|
id: string
|
|
|
|
|
|
product: 'hamkar' | 'khanegi'
|
|
|
|
|
|
slug: string
|
|
|
|
|
|
nameFa: string
|
|
|
|
|
|
domain: string
|
|
|
|
|
|
} | null>(null)
|
2026-07-30 18:19:54 +03:30
|
|
|
|
const summary = ref<SmsSummary | null>(null)
|
2026-07-23 19:42:07 +03:30
|
|
|
|
|
|
|
|
|
|
const form = reactive({
|
|
|
|
|
|
pricePerPartRials: 2500,
|
|
|
|
|
|
isBlocked: false,
|
2026-08-08 14:12:30 +03:30
|
|
|
|
autoBlockBelowRials: null as number | null,
|
2026-07-23 19:42:07 +03:30
|
|
|
|
alertCriticalRials: 10000,
|
|
|
|
|
|
alertWarningRials: 100000,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const chargeForm = reactive({
|
|
|
|
|
|
amountRials: 100000,
|
|
|
|
|
|
note: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
const chargeInfo = reactive({
|
|
|
|
|
|
card: '',
|
|
|
|
|
|
iban: '',
|
|
|
|
|
|
accountHolder: '',
|
|
|
|
|
|
instruction: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const reportTab = ref('sms')
|
|
|
|
|
|
const tablesTab = ref('logs')
|
|
|
|
|
|
const reportFrom = ref<string | null>(null)
|
|
|
|
|
|
const reportTo = ref<string | null>(null)
|
|
|
|
|
|
const chargeFrom = ref<string | null>(null)
|
|
|
|
|
|
const chargeTo = ref<string | null>(null)
|
|
|
|
|
|
const logsFrom = ref<string | null>(null)
|
|
|
|
|
|
const logsTo = ref<string | null>(null)
|
|
|
|
|
|
const ledgerFrom = ref<string | null>(null)
|
|
|
|
|
|
const ledgerTo = ref<string | null>(null)
|
|
|
|
|
|
const logsSearch = ref('')
|
|
|
|
|
|
const ledgerSearch = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
const periodReport = ref<{
|
|
|
|
|
|
summary: { messages: number; parts: number; amount: number }
|
|
|
|
|
|
byProvider: { provider: string; messages: number; parts: number; amount: number }[]
|
|
|
|
|
|
} | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const chargeReport = ref<{
|
|
|
|
|
|
summary: { charges: number; amount: number }
|
|
|
|
|
|
items: { id: string; amount: number; balanceAfter: number | null; note: string | null; createdAt: string }[]
|
|
|
|
|
|
} | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const logsPage = ref(1)
|
|
|
|
|
|
const logsTotal = ref(0)
|
|
|
|
|
|
const logsRows = ref<any[]>([])
|
|
|
|
|
|
const ledgerPage = ref(1)
|
|
|
|
|
|
const ledgerTotal = ref(0)
|
|
|
|
|
|
const ledgerRows = ref<any[]>([])
|
|
|
|
|
|
const pageSize = 20
|
|
|
|
|
|
|
2026-07-23 19:42:07 +03:30
|
|
|
|
const tenantId = computed(() => String(route.params.tenantId || ''))
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
function toneClass(level?: BalanceLevel) {
|
|
|
|
|
|
return level || 'unknown'
|
2026-07-23 19:42:07 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadDetail() {
|
|
|
|
|
|
if (!tenantId.value) return
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.get(`/sms/tenants/${tenantId.value}`)
|
|
|
|
|
|
tenant.value = data.tenant
|
|
|
|
|
|
summary.value = data.summary
|
|
|
|
|
|
Object.assign(form, {
|
|
|
|
|
|
pricePerPartRials: data.account.pricePerPartRials,
|
|
|
|
|
|
isBlocked: data.account.isBlocked,
|
2026-08-08 14:12:30 +03:30
|
|
|
|
autoBlockBelowRials:
|
|
|
|
|
|
data.account.autoBlockBelowRials == null ? null : data.account.autoBlockBelowRials,
|
2026-07-23 19:42:07 +03:30
|
|
|
|
alertCriticalRials: data.account.alertCriticalRials,
|
|
|
|
|
|
alertWarningRials: data.account.alertWarningRials,
|
2026-07-30 18:19:54 +03:30
|
|
|
|
})
|
|
|
|
|
|
Object.assign(chargeInfo, {
|
|
|
|
|
|
card: data.chargeInfo?.card || '',
|
|
|
|
|
|
iban: data.chargeInfo?.iban || '',
|
|
|
|
|
|
accountHolder: data.chargeInfo?.accountHolder || '',
|
|
|
|
|
|
instruction: data.chargeInfo?.instruction || '',
|
2026-07-23 19:42:07 +03:30
|
|
|
|
})
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'خطا در دریافت حساب پیامک')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
async function savePrice() {
|
2026-07-23 19:42:07 +03:30
|
|
|
|
saving.value = true
|
|
|
|
|
|
try {
|
2026-07-30 18:19:54 +03:30
|
|
|
|
await api.patch(`/sms/tenants/${tenantId.value}`, {
|
|
|
|
|
|
pricePerPartRials: form.pricePerPartRials,
|
|
|
|
|
|
})
|
|
|
|
|
|
message.success('قیمت ذخیره شد')
|
2026-07-23 19:42:07 +03:30
|
|
|
|
await loadDetail()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'ذخیره ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
async function saveThresholds() {
|
|
|
|
|
|
saving.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.patch(`/sms/tenants/${tenantId.value}`, {
|
|
|
|
|
|
alertCriticalRials: form.alertCriticalRials,
|
|
|
|
|
|
alertWarningRials: form.alertWarningRials,
|
2026-08-08 14:12:30 +03:30
|
|
|
|
autoBlockBelowRials: form.autoBlockBelowRials,
|
2026-07-30 18:19:54 +03:30
|
|
|
|
})
|
|
|
|
|
|
message.success('آستانهها ذخیره شد')
|
|
|
|
|
|
await loadDetail()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'ذخیره ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openChargeConfirm() {
|
|
|
|
|
|
if (!chargeForm.amountRials) {
|
|
|
|
|
|
message.warning('مبلغ را وارد کنید')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: 'تأیید شارژ کیف پول',
|
|
|
|
|
|
content: `مبلغ ${formatMoney(chargeForm.amountRials)} روی کیف پول ثبت شود؟`,
|
|
|
|
|
|
okText: 'ثبت',
|
|
|
|
|
|
cancelText: 'انصراف',
|
|
|
|
|
|
onOk: () => chargeWallet(),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 19:42:07 +03:30
|
|
|
|
async function chargeWallet() {
|
|
|
|
|
|
charging.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.post(`/sms/tenants/${tenantId.value}/charge`, {
|
|
|
|
|
|
amountRials: Number(chargeForm.amountRials),
|
|
|
|
|
|
note: chargeForm.note || undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
message.success('موجودی بهروز شد')
|
|
|
|
|
|
chargeForm.note = ''
|
|
|
|
|
|
await loadDetail()
|
2026-07-30 18:19:54 +03:30
|
|
|
|
if (tablesTab.value === 'ledger') await loadLedger()
|
|
|
|
|
|
if (reportTab.value === 'charges') await loadChargeReport()
|
2026-07-23 19:42:07 +03:30
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'شارژ ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
charging.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
async function onBlockChange(checked: boolean | string | number) {
|
|
|
|
|
|
const next = Boolean(checked)
|
|
|
|
|
|
blockLoading.value = true
|
2026-07-23 19:42:07 +03:30
|
|
|
|
try {
|
2026-07-30 18:19:54 +03:30
|
|
|
|
await api.post(`/sms/tenants/${tenantId.value}/${next ? 'block' : 'unblock'}`)
|
|
|
|
|
|
form.isBlocked = next
|
|
|
|
|
|
message.success(next ? 'پیامک مسدود شد' : 'پیامک آزاد شد')
|
2026-07-23 19:42:07 +03:30
|
|
|
|
await loadDetail()
|
|
|
|
|
|
} catch (e: any) {
|
2026-07-30 18:19:54 +03:30
|
|
|
|
form.isBlocked = !next
|
2026-07-23 19:42:07 +03:30
|
|
|
|
message.error(e?.response?.data?.message || 'عملیات ناموفق')
|
2026-07-30 18:19:54 +03:30
|
|
|
|
} finally {
|
|
|
|
|
|
blockLoading.value = false
|
2026-07-23 19:42:07 +03:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
async function saveChargeInfo() {
|
|
|
|
|
|
chargeInfoSaving.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.put(`/sms/tenants/${tenantId.value}/charge-info`, {
|
|
|
|
|
|
chargeCard: chargeInfo.card,
|
|
|
|
|
|
chargeIban: chargeInfo.iban,
|
|
|
|
|
|
chargeHolder: chargeInfo.accountHolder,
|
|
|
|
|
|
chargeInstruction: chargeInfo.instruction,
|
|
|
|
|
|
})
|
|
|
|
|
|
message.success('اطلاعات واریز ذخیره شد')
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'ذخیره ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
chargeInfoSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadPeriodReport() {
|
|
|
|
|
|
reportLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.get(`/sms/tenants/${tenantId.value}/report`, {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
fromDate: reportFrom.value || undefined,
|
|
|
|
|
|
toDate: reportTo.value || undefined,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
periodReport.value = data
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'خطا در گزارش')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
reportLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadChargeReport() {
|
|
|
|
|
|
chargeReportLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.get(`/sms/tenants/${tenantId.value}/report/charges`, {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
fromDate: chargeFrom.value || undefined,
|
|
|
|
|
|
toDate: chargeTo.value || undefined,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
chargeReport.value = data
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'خطا در گزارش شارژ')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
chargeReportLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadLogs() {
|
|
|
|
|
|
logsLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.post(`/sms/tenants/${tenantId.value}/logs/table`, {
|
|
|
|
|
|
page: logsPage.value,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
fromDate: logsFrom.value || undefined,
|
|
|
|
|
|
toDate: logsTo.value || undefined,
|
|
|
|
|
|
search: logsSearch.value || undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
logsRows.value = data.rows || []
|
|
|
|
|
|
logsTotal.value = data.total || 0
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'خطا در لاگ ارسال')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
logsLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadLedger() {
|
|
|
|
|
|
ledgerLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.post(`/sms/tenants/${tenantId.value}/ledger/table`, {
|
|
|
|
|
|
page: ledgerPage.value,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
fromDate: ledgerFrom.value || undefined,
|
|
|
|
|
|
toDate: ledgerTo.value || undefined,
|
|
|
|
|
|
search: ledgerSearch.value || undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
ledgerRows.value = data.rows || []
|
|
|
|
|
|
ledgerTotal.value = data.total || 0
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'خطا در دفترچه')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
ledgerLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const logColumns: TableColumnsType = [
|
|
|
|
|
|
{ title: 'گیرنده', dataIndex: 'receiver', key: 'receiver', width: 130 },
|
|
|
|
|
|
{ title: 'متن', dataIndex: 'body', key: 'body', ellipsis: true },
|
|
|
|
|
|
{ title: 'پارت', dataIndex: 'parts', key: 'parts', width: 70 },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'مبلغ',
|
|
|
|
|
|
key: 'totalAmount',
|
|
|
|
|
|
width: 120,
|
2026-07-30 21:03:19 +03:30
|
|
|
|
customRender: ({ record }: { record: any }) => formatMoney(record.totalAmount),
|
2026-07-30 18:19:54 +03:30
|
|
|
|
},
|
|
|
|
|
|
{ title: 'ارائهدهنده', dataIndex: 'provider', key: 'provider', width: 110 },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'زمان',
|
|
|
|
|
|
key: 'createdAt',
|
|
|
|
|
|
width: 150,
|
2026-07-30 21:03:19 +03:30
|
|
|
|
customRender: ({ record }: { record: any }) => formatIranDateTime(record.createdAt),
|
2026-07-30 18:19:54 +03:30
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const ledgerColumns: TableColumnsType = [
|
|
|
|
|
|
{ title: 'نوع', dataIndex: 'type', key: 'type', width: 90 },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'مبلغ',
|
|
|
|
|
|
key: 'amount',
|
|
|
|
|
|
width: 130,
|
2026-07-30 21:03:19 +03:30
|
|
|
|
customRender: ({ record }: { record: any }) => formatMoney(record.amount),
|
2026-07-30 18:19:54 +03:30
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'مانده',
|
|
|
|
|
|
key: 'balanceAfter',
|
|
|
|
|
|
width: 130,
|
2026-07-30 21:03:19 +03:30
|
|
|
|
customRender: ({ record }: { record: any }) => formatMoney(record.balanceAfter),
|
2026-07-30 18:19:54 +03:30
|
|
|
|
},
|
|
|
|
|
|
{ title: 'توضیح', dataIndex: 'note', key: 'note', ellipsis: true },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'زمان',
|
|
|
|
|
|
key: 'createdAt',
|
|
|
|
|
|
width: 150,
|
2026-07-30 21:03:19 +03:30
|
|
|
|
customRender: ({ record }: { record: any }) => formatIranDateTime(record.createdAt),
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const providerReportColumns: TableColumnsType = [
|
|
|
|
|
|
{ title: 'ارائهدهنده', dataIndex: 'provider', key: 'provider' },
|
|
|
|
|
|
{ title: 'پیامک', dataIndex: 'messages', key: 'messages' },
|
|
|
|
|
|
{ title: 'پارت', dataIndex: 'parts', key: 'parts' },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'مبلغ',
|
|
|
|
|
|
key: 'amount',
|
|
|
|
|
|
customRender: ({ record }: { record: any }) => formatMoney(record.amount),
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const chargeReportColumns: TableColumnsType = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'مبلغ',
|
|
|
|
|
|
key: 'amount',
|
|
|
|
|
|
customRender: ({ record }: { record: any }) => formatMoney(record.amount),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'مانده',
|
|
|
|
|
|
key: 'balanceAfter',
|
|
|
|
|
|
customRender: ({ record }: { record: any }) => formatMoney(record.balanceAfter),
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: 'توضیح', dataIndex: 'note', key: 'note' },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'زمان',
|
|
|
|
|
|
key: 'createdAt',
|
|
|
|
|
|
customRender: ({ record }: { record: any }) => formatIranDateTime(record.createdAt),
|
2026-07-30 18:19:54 +03:30
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
watch(tenantId, async () => {
|
|
|
|
|
|
await loadDetail()
|
|
|
|
|
|
await Promise.all([loadPeriodReport(), loadChargeReport(), loadLogs(), loadLedger()])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(reportTab, (tab) => {
|
|
|
|
|
|
if (tab === 'sms' && !periodReport.value) loadPeriodReport()
|
|
|
|
|
|
if (tab === 'charges' && !chargeReport.value) loadChargeReport()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(tablesTab, (tab) => {
|
|
|
|
|
|
if (tab === 'logs') loadLogs()
|
|
|
|
|
|
if (tab === 'ledger') loadLedger()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
const today = todayIranDateInput()
|
|
|
|
|
|
reportFrom.value = `${today.slice(0, 8)}01`
|
|
|
|
|
|
reportTo.value = today
|
|
|
|
|
|
await loadDetail()
|
|
|
|
|
|
await Promise.all([loadPeriodReport(), loadLogs(), loadLedger()])
|
|
|
|
|
|
})
|
2026-07-23 19:42:07 +03:30
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-07-30 18:19:54 +03:30
|
|
|
|
<div class="page-shell sms-page">
|
|
|
|
|
|
<div class="page-head">
|
2026-07-23 19:42:07 +03:30
|
|
|
|
<a-button type="text" @click="router.push({ name: 'sms-panel' })">
|
|
|
|
|
|
<ArrowRightOutlined />
|
|
|
|
|
|
</a-button>
|
2026-07-30 18:19:54 +03:30
|
|
|
|
<div class="page-head-text">
|
|
|
|
|
|
<h1 class="page-title">{{ tenant?.nameFa || 'پنل پیامک' }}</h1>
|
|
|
|
|
|
<p class="page-sub">مدیریت کیف پول، تعرفه و گزارش ارسال پیامک</p>
|
2026-08-08 10:22:35 +03:30
|
|
|
|
<div v-if="tenant" class="page-head-meta">
|
|
|
|
|
|
<a-tag :color="tenant.product === 'hamkar' ? 'cyan' : 'orange'">
|
|
|
|
|
|
{{ tenant.product === 'hamkar' ? 'همکار' : 'خانگی' }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
<span class="domain">{{ tenant.domain }}</span>
|
|
|
|
|
|
</div>
|
2026-07-23 19:42:07 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-08 12:35:59 +03:30
|
|
|
|
<TenantPanelShortcuts :tenant-id="tenantId" current="sms" />
|
|
|
|
|
|
|
2026-07-23 19:42:07 +03:30
|
|
|
|
<a-spin :spinning="loading">
|
2026-07-30 18:19:54 +03:30
|
|
|
|
<template v-if="tenant && summary">
|
|
|
|
|
|
<section class="stat-grid">
|
|
|
|
|
|
<div class="stat-card" :class="toneClass(summary.balanceLevel)">
|
|
|
|
|
|
<div class="stat-label-row">
|
|
|
|
|
|
<span>موجودی کیف پول</span>
|
|
|
|
|
|
<a-tag v-if="form.isBlocked" color="error">ارسال مسدود</a-tag>
|
|
|
|
|
|
</div>
|
2026-08-08 13:27:31 +03:30
|
|
|
|
<strong>{{ formatMoney(summary.balanceRials) }}</strong>
|
2026-07-30 18:19:54 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card sky">
|
|
|
|
|
|
<span>قیمت هر پارت</span>
|
2026-08-08 13:27:31 +03:30
|
|
|
|
<strong>{{ formatMoney(summary.perPartRials) }}</strong>
|
2026-07-30 18:19:54 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card emerald">
|
|
|
|
|
|
<span>امروز</span>
|
|
|
|
|
|
<strong>{{ formatNumber(summary.todayMessages) }} <small>پیامک</small></strong>
|
|
|
|
|
|
<em>{{ formatNumber(summary.todayParts) }} پارت · {{ formatMoney(summary.todayCostRials) }}</em>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card violet">
|
|
|
|
|
|
<span>ماه جاری</span>
|
|
|
|
|
|
<strong>{{ formatNumber(summary.monthMessages) }} <small>پیامک</small></strong>
|
|
|
|
|
|
<em>{{ formatNumber(summary.monthParts) }} پارت · {{ formatMoney(summary.monthCostRials) }}</em>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel-card mgmt">
|
|
|
|
|
|
<h2>مدیریت کیف پول</h2>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="mgmt-row">
|
|
|
|
|
|
<div class="subcard sky">
|
|
|
|
|
|
<h3>تعرفه پیامک</h3>
|
|
|
|
|
|
<div class="inline-actions">
|
2026-08-08 11:20:26 +03:30
|
|
|
|
<MoneyInput v-model:value="form.pricePerPartRials" placeholder="قیمت هر پارت" />
|
2026-07-30 18:19:54 +03:30
|
|
|
|
<a-button type="primary" size="large" :loading="saving" @click="savePrice">ذخیره قیمت</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="subcard emerald">
|
|
|
|
|
|
<h3>شارژ کیف پول</h3>
|
|
|
|
|
|
<div class="charge-grid">
|
2026-08-08 11:20:26 +03:30
|
|
|
|
<MoneyInput
|
|
|
|
|
|
v-model:value="chargeForm.amountRials"
|
|
|
|
|
|
allow-negative
|
2026-08-08 13:27:31 +03:30
|
|
|
|
placeholder="مبلغ (تومان)"
|
2026-07-30 18:19:54 +03:30
|
|
|
|
/>
|
|
|
|
|
|
<a-input v-model:value="chargeForm.note" size="large" placeholder="توضیح (اختیاری)" />
|
|
|
|
|
|
<a-button type="primary" size="large" :loading="charging" @click="openChargeConfirm">
|
|
|
|
|
|
شارژ کیف پول
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
2026-07-23 19:42:07 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
<div class="block-row" :class="{ blocked: form.isBlocked }">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>مسدودسازی ارسال پیامک</strong>
|
|
|
|
|
|
<p>در حالت مسدود، ارسال از این نسخه متوقف میشود (پس از اتصال محصولات).</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-switch
|
|
|
|
|
|
:checked="form.isBlocked"
|
|
|
|
|
|
:loading="blockLoading"
|
|
|
|
|
|
checked-children="مسدود"
|
|
|
|
|
|
un-checked-children="فعال"
|
|
|
|
|
|
@change="onBlockChange"
|
|
|
|
|
|
/>
|
2026-07-23 19:42:07 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
<div class="subcard violet charge-info">
|
|
|
|
|
|
<h3>اطلاعات واریز شارژ</h3>
|
|
|
|
|
|
<div class="fields-2">
|
|
|
|
|
|
<a-input v-model:value="chargeInfo.card" size="large" dir="ltr" placeholder="شماره کارت" />
|
|
|
|
|
|
<a-input v-model:value="chargeInfo.iban" size="large" dir="ltr" placeholder="شبا" />
|
|
|
|
|
|
<a-input v-model:value="chargeInfo.accountHolder" size="large" placeholder="نام صاحب حساب" />
|
|
|
|
|
|
<a-textarea
|
|
|
|
|
|
v-model:value="chargeInfo.instruction"
|
|
|
|
|
|
:rows="2"
|
|
|
|
|
|
placeholder="راهنمای واریز"
|
2026-07-23 19:42:07 +03:30
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-07-30 18:19:54 +03:30
|
|
|
|
<a-button type="primary" :loading="chargeInfoSaving" @click="saveChargeInfo">ذخیره اطلاعات واریز</a-button>
|
2026-07-23 19:42:07 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
<div class="subcard thresholds">
|
|
|
|
|
|
<h3>آستانههای رنگ موجودی</h3>
|
|
|
|
|
|
<div class="threshold-grid">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span class="tone-label"><i class="dot critical" /> آستانه قرمز</span>
|
2026-08-08 11:20:26 +03:30
|
|
|
|
<MoneyInput v-model:value="form.alertCriticalRials" />
|
2026-07-30 18:19:54 +03:30
|
|
|
|
</label>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span class="tone-label"><i class="dot warning" /> آستانه زرد</span>
|
2026-08-08 11:20:26 +03:30
|
|
|
|
<MoneyInput v-model:value="form.alertWarningRials" />
|
2026-07-30 18:19:54 +03:30
|
|
|
|
</label>
|
|
|
|
|
|
<a-button type="primary" size="large" :loading="saving" @click="saveThresholds">ذخیره آستانهها</a-button>
|
2026-07-23 19:42:07 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-08-08 14:12:30 +03:30
|
|
|
|
|
|
|
|
|
|
<div class="subcard auto-block">
|
|
|
|
|
|
<h3>مسدودسازی خودکار ارسال</h3>
|
|
|
|
|
|
<p class="hint">
|
|
|
|
|
|
اگر مبلغی بگذارید و موجودی از آن کمتر شود، ارسال پیامک خودکار قطع میشود (کلید مسدودسازی
|
|
|
|
|
|
روشن نمیشود). خالی بگذارید تا فقط با کلید دستی مسدود شود.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="threshold-grid">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span class="tone-label">آستانه مسدودسازی خودکار (تومان)</span>
|
|
|
|
|
|
<MoneyInput
|
|
|
|
|
|
v-model:value="form.autoBlockBelowRials"
|
|
|
|
|
|
nullable
|
|
|
|
|
|
placeholder="خالی = بدون مسدودسازی خودکار"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<a-button type="primary" size="large" :loading="saving" @click="saveThresholds">
|
|
|
|
|
|
ذخیره آستانه
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-07-30 18:19:54 +03:30
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel-card">
|
|
|
|
|
|
<h2>گزارش</h2>
|
|
|
|
|
|
<a-tabs v-model:activeKey="reportTab">
|
|
|
|
|
|
<a-tab-pane key="sms" tab="گزارش بازهای پیامک">
|
|
|
|
|
|
<div class="filter-row">
|
|
|
|
|
|
<JalaliDatePicker v-model="reportFrom" value-type="ymd" placeholder="از تاریخ" />
|
|
|
|
|
|
<JalaliDatePicker v-model="reportTo" value-type="ymd" placeholder="تا تاریخ" />
|
|
|
|
|
|
<a-button type="primary" :loading="reportLoading" @click="loadPeriodReport">دریافت گزارش</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="periodReport" class="report-summary">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span>پیامک</span>
|
|
|
|
|
|
<strong>{{ formatNumber(periodReport.summary.messages) }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span>پارت</span>
|
|
|
|
|
|
<strong>{{ formatNumber(periodReport.summary.parts) }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span>مبلغ</span>
|
|
|
|
|
|
<strong>{{ formatMoney(periodReport.summary.amount) }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-empty
|
|
|
|
|
|
v-if="periodReport && !periodReport.byProvider.length"
|
|
|
|
|
|
description="هنوز لاگ ارسالی ثبت نشده (پس از اتصال consume پر میشود)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
v-else-if="periodReport?.byProvider?.length"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:pagination="false"
|
2026-08-08 10:22:35 +03:30
|
|
|
|
:scroll="{ x: true }"
|
2026-07-30 18:19:54 +03:30
|
|
|
|
:data-source="periodReport.byProvider"
|
2026-07-30 21:03:19 +03:30
|
|
|
|
:columns="providerReportColumns"
|
2026-07-30 18:19:54 +03:30
|
|
|
|
row-key="provider"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<a-tab-pane key="charges" tab="گزارش شارژ کیف پول">
|
|
|
|
|
|
<div class="filter-row">
|
|
|
|
|
|
<JalaliDatePicker v-model="chargeFrom" value-type="ymd" placeholder="از تاریخ" />
|
|
|
|
|
|
<JalaliDatePicker v-model="chargeTo" value-type="ymd" placeholder="تا تاریخ" />
|
|
|
|
|
|
<a-button type="primary" :loading="chargeReportLoading" @click="loadChargeReport">
|
|
|
|
|
|
دریافت گزارش
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="chargeReport" class="report-summary">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span>تعداد شارژ</span>
|
|
|
|
|
|
<strong>{{ formatNumber(chargeReport.summary.charges) }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span>جمع مبلغ</span>
|
|
|
|
|
|
<strong>{{ formatMoney(chargeReport.summary.amount) }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
v-if="chargeReport"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:loading="chargeReportLoading"
|
|
|
|
|
|
:pagination="false"
|
2026-08-08 10:22:35 +03:30
|
|
|
|
:scroll="{ x: true }"
|
2026-07-30 18:19:54 +03:30
|
|
|
|
:data-source="chargeReport.items"
|
2026-07-30 21:03:19 +03:30
|
|
|
|
:columns="chargeReportColumns"
|
2026-07-30 18:19:54 +03:30
|
|
|
|
row-key="id"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
</a-tabs>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel-card">
|
|
|
|
|
|
<h2>جداول</h2>
|
|
|
|
|
|
<a-tabs v-model:activeKey="tablesTab" destroy-inactive-tab-pane>
|
|
|
|
|
|
<a-tab-pane key="logs" tab="لاگ ارسال">
|
|
|
|
|
|
<div class="filter-row">
|
|
|
|
|
|
<JalaliDatePicker v-model="logsFrom" value-type="ymd" placeholder="از تاریخ" />
|
|
|
|
|
|
<JalaliDatePicker v-model="logsTo" value-type="ymd" placeholder="تا تاریخ" />
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="logsSearch"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
placeholder="جستجو"
|
|
|
|
|
|
style="max-width: 200px"
|
|
|
|
|
|
@pressEnter="() => { logsPage = 1; loadLogs() }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-button type="primary" :loading="logsLoading" @click="() => { logsPage = 1; loadLogs() }">
|
|
|
|
|
|
اعمال
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
:loading="logsLoading"
|
|
|
|
|
|
:columns="logColumns"
|
|
|
|
|
|
:data-source="logsRows"
|
2026-08-08 10:22:35 +03:30
|
|
|
|
:scroll="{ x: true }"
|
2026-07-30 18:19:54 +03:30
|
|
|
|
row-key="id"
|
|
|
|
|
|
:pagination="{
|
|
|
|
|
|
current: logsPage,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
total: logsTotal,
|
|
|
|
|
|
showSizeChanger: false,
|
|
|
|
|
|
onChange: (p: number) => { logsPage = p; loadLogs() },
|
|
|
|
|
|
}"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<a-tab-pane key="ledger" tab="دفترچه کیف پول">
|
|
|
|
|
|
<div class="filter-row">
|
|
|
|
|
|
<JalaliDatePicker v-model="ledgerFrom" value-type="ymd" placeholder="از تاریخ" />
|
|
|
|
|
|
<JalaliDatePicker v-model="ledgerTo" value-type="ymd" placeholder="تا تاریخ" />
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="ledgerSearch"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
placeholder="جستجو در توضیح"
|
|
|
|
|
|
style="max-width: 200px"
|
|
|
|
|
|
@pressEnter="() => { ledgerPage = 1; loadLedger() }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-button type="primary" :loading="ledgerLoading" @click="() => { ledgerPage = 1; loadLedger() }">
|
|
|
|
|
|
اعمال
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
:loading="ledgerLoading"
|
|
|
|
|
|
:columns="ledgerColumns"
|
|
|
|
|
|
:data-source="ledgerRows"
|
2026-08-08 10:22:35 +03:30
|
|
|
|
:scroll="{ x: true }"
|
2026-07-30 18:19:54 +03:30
|
|
|
|
row-key="id"
|
|
|
|
|
|
:pagination="{
|
|
|
|
|
|
current: ledgerPage,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
total: ledgerTotal,
|
|
|
|
|
|
showSizeChanger: false,
|
|
|
|
|
|
onChange: (p: number) => { ledgerPage = p; loadLedger() },
|
|
|
|
|
|
}"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
</a-tabs>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
2026-07-23 19:42:07 +03:30
|
|
|
|
<a-empty v-else-if="!loading" description="نسخه پیدا نشد" />
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-07-30 18:19:54 +03:30
|
|
|
|
.sms-page {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 14px;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sms-page :deep(.ant-spin-nested-loading),
|
|
|
|
|
|
.sms-page :deep(.ant-spin-container) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sms-page :deep(.ant-table-wrapper) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sms-page :deep(.ant-tabs) {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sms-page :deep(.ant-tabs-nav) {
|
|
|
|
|
|
margin-bottom: 12px;
|
2026-07-23 19:42:07 +03:30
|
|
|
|
}
|
2026-07-30 18:19:54 +03:30
|
|
|
|
|
|
|
|
|
|
.page-head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
margin-bottom: 4px;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-width: 0;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-head-text {
|
2026-08-08 10:22:35 +03:30
|
|
|
|
flex: 1 1 160px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-head-meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
min-width: 0;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.domain {
|
|
|
|
|
|
color: #8b95a8;
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
direction: ltr;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
|
word-break: break-all;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-grid {
|
|
|
|
|
|
display: grid;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
grid-template-columns: 1fr;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
gap: 12px;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-width: 0;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #e8eef5;
|
|
|
|
|
|
border-radius: 16px;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
padding: 14px;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
2026-08-08 10:22:35 +03:30
|
|
|
|
min-width: 0;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
overflow-wrap: anywhere;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card span {
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card strong {
|
2026-08-08 10:22:35 +03:30
|
|
|
|
font-size: 1.25rem;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
color: #0f172a;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
overflow-wrap: anywhere;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card small,
|
|
|
|
|
|
.stat-card em {
|
|
|
|
|
|
font-style: normal;
|
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #64748b;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
overflow-wrap: anywhere;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-label-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
min-width: 0;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card.healthy {
|
|
|
|
|
|
background: linear-gradient(160deg, #ecfdf5, #fff);
|
|
|
|
|
|
border-color: #bbf7d0;
|
|
|
|
|
|
color: #166534;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card.healthy strong { color: #166534; }
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card.warning {
|
|
|
|
|
|
background: linear-gradient(160deg, #fffbeb, #fff);
|
|
|
|
|
|
border-color: #fde68a;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card.warning strong { color: #92400e; }
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card.critical {
|
|
|
|
|
|
background: linear-gradient(160deg, #fef2f2, #fff);
|
|
|
|
|
|
border-color: #fecaca;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card.critical strong { color: #991b1b; }
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card.sky {
|
|
|
|
|
|
background: linear-gradient(160deg, #f0f9ff, #fff);
|
|
|
|
|
|
border-color: #bae6fd;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card.emerald {
|
|
|
|
|
|
background: linear-gradient(160deg, #ecfdf5, #fff);
|
|
|
|
|
|
border-color: #a7f3d0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card.violet {
|
|
|
|
|
|
background: linear-gradient(160deg, #f5f3ff, #fff);
|
|
|
|
|
|
border-color: #ddd6fe;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #e8eef5;
|
|
|
|
|
|
border-radius: 18px;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
padding: 14px;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
2026-08-08 10:22:35 +03:30
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow-x: clip;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-card h2 {
|
|
|
|
|
|
margin: 0 0 14px;
|
|
|
|
|
|
padding-bottom: 10px;
|
|
|
|
|
|
border-bottom: 1px solid #f1f5f9;
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mgmt-row {
|
|
|
|
|
|
display: grid;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
grid-template-columns: 1fr;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subcard {
|
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
padding: 14px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 12px;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
min-width: 0;
|
|
|
|
|
|
max-width: 100%;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subcard h3 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 0.92rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-08 14:12:30 +03:30
|
|
|
|
.subcard .hint {
|
|
|
|
|
|
margin: 8px 0 12px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
.subcard.sky {
|
|
|
|
|
|
background: linear-gradient(160deg, #f0f9ff, #fff);
|
|
|
|
|
|
border-color: #bae6fd;
|
|
|
|
|
|
}
|
|
|
|
|
|
.subcard.emerald {
|
|
|
|
|
|
background: linear-gradient(160deg, #ecfdf5, #fff);
|
|
|
|
|
|
border-color: #a7f3d0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.subcard.violet {
|
|
|
|
|
|
background: linear-gradient(160deg, #f5f3ff, #fff);
|
|
|
|
|
|
border-color: #ddd6fe;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-actions,
|
|
|
|
|
|
.charge-grid,
|
|
|
|
|
|
.filter-row,
|
|
|
|
|
|
.threshold-grid,
|
|
|
|
|
|
.fields-2 {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 10px;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-width: 0;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-row {
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-08 10:22:35 +03:30
|
|
|
|
.inline-actions,
|
|
|
|
|
|
.charge-grid,
|
|
|
|
|
|
.filter-row,
|
|
|
|
|
|
.threshold-grid,
|
|
|
|
|
|
.fields-2,
|
|
|
|
|
|
.mgmt-row {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.block-row {
|
2026-07-23 19:42:07 +03:30
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
align-items: center;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
flex-wrap: wrap;
|
2026-07-23 19:42:07 +03:30
|
|
|
|
border-radius: 14px;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
border: 1px solid #bbf7d0;
|
|
|
|
|
|
background: #f0fdf4;
|
|
|
|
|
|
padding: 14px;
|
|
|
|
|
|
margin-bottom: 12px;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.block-row > div {
|
|
|
|
|
|
flex: 1 1 200px;
|
|
|
|
|
|
min-width: 0;
|
2026-07-23 19:42:07 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
.block-row.blocked {
|
|
|
|
|
|
border-color: #fecaca;
|
|
|
|
|
|
background: #fef2f2;
|
2026-07-23 19:42:07 +03:30
|
|
|
|
}
|
2026-07-30 18:19:54 +03:30
|
|
|
|
|
|
|
|
|
|
.block-row p {
|
|
|
|
|
|
margin: 4px 0 0;
|
|
|
|
|
|
color: #64748b;
|
2026-07-23 19:42:07 +03:30
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
}
|
2026-07-30 18:19:54 +03:30
|
|
|
|
|
|
|
|
|
|
.charge-info {
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tone-label {
|
|
|
|
|
|
display: inline-flex;
|
2026-07-23 19:42:07 +03:30
|
|
|
|
align-items: center;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
gap: 6px;
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
color: #334155;
|
2026-07-23 19:42:07 +03:30
|
|
|
|
}
|
2026-07-30 18:19:54 +03:30
|
|
|
|
|
|
|
|
|
|
.dot {
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dot.critical { background: #ef4444; }
|
|
|
|
|
|
.dot.warning { background: #eab308; }
|
|
|
|
|
|
|
|
|
|
|
|
.report-summary {
|
|
|
|
|
|
display: grid;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
grid-template-columns: 1fr;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
gap: 10px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.report-summary > div {
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 4px;
|
2026-08-08 10:22:35 +03:30
|
|
|
|
min-width: 0;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.report-summary span {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 0.78rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-08 10:22:35 +03:30
|
|
|
|
.money-input,
|
|
|
|
|
|
.filter-row :deep(.jalali-date-field),
|
|
|
|
|
|
.filter-row :deep(.ant-input),
|
|
|
|
|
|
.filter-row :deep(.ant-btn),
|
|
|
|
|
|
.inline-actions :deep(.ant-input),
|
|
|
|
|
|
.charge-grid :deep(.ant-input),
|
|
|
|
|
|
.charge-grid :deep(.ant-btn) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 640px) {
|
|
|
|
|
|
.stat-grid {
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
}
|
2026-07-30 18:19:54 +03:30
|
|
|
|
.report-summary {
|
2026-08-08 10:22:35 +03:30
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
.fields-2 {
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card strong {
|
|
|
|
|
|
font-size: 1.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 900px) {
|
|
|
|
|
|
.mgmt-row {
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
|
|
|
|
|
.inline-actions {
|
2026-08-08 10:22:35 +03:30
|
|
|
|
grid-template-columns: 1fr auto;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.charge-grid {
|
|
|
|
|
|
grid-template-columns: 1fr 1.2fr auto;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.filter-row {
|
|
|
|
|
|
grid-template-columns: 1fr 1fr auto auto;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.threshold-grid {
|
|
|
|
|
|
grid-template-columns: 1fr 1fr auto;
|
|
|
|
|
|
align-items: end;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 1100px) {
|
|
|
|
|
|
.stat-grid {
|
|
|
|
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card strong {
|
|
|
|
|
|
font-size: 1.45rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.panel-card {
|
|
|
|
|
|
padding: 16px;
|
2026-07-30 18:19:54 +03:30
|
|
|
|
}
|
2026-07-23 19:42:07 +03:30
|
|
|
|
}
|
|
|
|
|
|
</style>
|