Files
boursecu_admin/src/pages/menuPages/smsPanel/index.vue
T
2026-07-04 14:21:27 +03:30

1406 lines
48 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="sms-panel-page h-full min-h-0 w-full min-w-0">
<div class="sms-panel-page__scroll h-full min-h-0 overflow-y-auto overflow-x-hidden px-2 pb-8 md:px-4">
<header class="mb-6 md:mb-8">
<h1 class="sms-panel-heading text-xl font-bold md:text-2xl">پنل پیامک</h1>
<p class="sms-panel-subtitle mt-1 text-sm">مدیریت کیف پول، تعرفه و گزارش ارسال پیامک</p>
</header>
<div v-if="pageLoading" class="flex justify-center py-16">
<a-spin size="large" />
</div>
<template v-else-if="overview">
<!-- آمار کلی -->
<section
class="sms-stat-cards mb-6 grid grid-cols-1 gap-2 sm:grid-cols-2 sm:gap-4 xl:grid-cols-4 md:mb-8 md:gap-5"
>
<div
class="sms-stat-card relative overflow-hidden rounded-xl border p-3 shadow-sm transition-shadow hover:shadow-md md:rounded-2xl md:p-6"
:class="balanceCardClass"
>
<div class="sms-stat-inline flex gap-2.5 md:block">
<div
class="sms-stat-icon flex h-9 w-9 shrink-0 items-center justify-center rounded-lg md:mb-4 md:h-11 md:w-11 md:rounded-xl"
:class="balanceIconClass"
>
<Icon icon="vuesax-linear:wallet-3" size="18px" class="md:!text-[22px]" />
</div>
<div class="sms-stat-inline-body sms-balance-stat-body flex min-w-0 flex-1 justify-between gap-2 md:block">
<div class="sms-balance-stat-title flex min-w-0 items-center gap-1 leading-none md:hidden">
<span class="sms-stat-label shrink-0 text-xs font-medium leading-none md:hidden">موجودی کیف پول</span>
<a-tooltip
v-if="overview.wallet.balance <= 0"
trigger="click"
placement="top"
overlay-class-name="sms-balance-info-tooltip"
>
<template #title>{{ balanceAlertHint }}</template>
<button
type="button"
class="sms-balance-info-btn inline-flex shrink-0 items-center justify-center text-[var(--admin-text-secondary)] transition-colors hover:text-[var(--admin-text-muted)]"
aria-label="توضیح هشدار موجودی"
@click.stop
>
<svg class="h-4 w-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="8" cy="8" r="6.25" stroke="currentColor" stroke-width="1.1" />
<circle cx="8" cy="5.2" r="0.55" fill="currentColor" />
<path d="M8 7.2v3.3" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" />
</svg>
</button>
</a-tooltip>
<a-tag
v-if="overview.wallet.is_blocked"
color="error"
class="!m-0 shrink-0 !text-[10px] leading-none"
>
ارسال مسدود
</a-tag>
</div>
<p
class="sms-stat-value shrink-0 whitespace-nowrap text-base font-bold leading-none tracking-tight md:hidden"
:class="balanceTextClass"
>
{{ formatNumber(overview.wallet.balance) }}
<span class="text-xs font-normal opacity-70">ریال</span>
</p>
<div class="hidden w-full md:block">
<div class="mb-2 flex items-center justify-between gap-2">
<div class="sms-balance-stat-title-desktop flex items-center gap-1">
<span class="sms-stat-label text-sm font-medium leading-none">موجودی کیف پول</span>
<a-tooltip
v-if="overview.wallet.balance <= 0"
trigger="click"
placement="top"
overlay-class-name="sms-balance-info-tooltip"
>
<template #title>{{ balanceAlertHint }}</template>
<button
type="button"
class="sms-balance-info-btn inline-flex shrink-0 items-center justify-center text-[var(--admin-text-secondary)] transition-colors hover:text-[var(--admin-text-muted)]"
aria-label="توضیح هشدار موجودی"
@click.stop
>
<svg class="h-4 w-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="8" cy="8" r="6.25" stroke="currentColor" stroke-width="1.1" />
<circle cx="8" cy="5.2" r="0.55" fill="currentColor" />
<path d="M8 7.2v3.3" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" />
</svg>
</button>
</a-tooltip>
</div>
<a-tag
v-if="overview.wallet.is_blocked"
color="error"
class="!m-0 !text-xs"
>
ارسال مسدود
</a-tag>
</div>
<p
class="sms-stat-value whitespace-nowrap text-3xl font-bold leading-normal tracking-tight"
:class="balanceTextClass"
>
{{ formatNumber(overview.wallet.balance) }}
<span class="text-sm font-normal opacity-70">ریال</span>
</p>
</div>
</div>
</div>
</div>
<div
class="sms-stat-card sms-stat-card--sky rounded-xl border p-3 shadow-sm md:rounded-2xl md:p-6"
>
<div class="sms-stat-inline flex gap-2.5 md:block">
<div
class="sms-stat-icon sms-stat-icon--sky flex h-9 w-9 shrink-0 items-center justify-center rounded-lg md:mb-4 md:h-11 md:w-11 md:rounded-xl"
>
<Icon icon="vuesax-linear:tag" Size="18px" class="md:!text-[22px]" />
</div>
<div class="sms-stat-inline-body flex min-w-0 flex-1 justify-between gap-2 md:block">
<p class="sms-stat-label shrink-0 text-xs font-medium leading-none md:mb-2 md:text-sm md:leading-normal">
قیمت هر پارت
</p>
<p class="sms-stat-value sms-stat-value--sky shrink-0 whitespace-nowrap text-base font-bold leading-none md:text-3xl md:leading-normal">
{{ formatNumber(overview.wallet.price_per_part) }}
<span class="sms-stat-unit text-xs font-normal md:text-sm">ریال</span>
</p>
</div>
</div>
</div>
<div
class="sms-stat-card sms-stat-card--emerald rounded-xl border p-3 shadow-sm md:rounded-2xl md:p-6"
>
<div class="sms-stat-inline flex gap-2.5 md:block">
<div
class="sms-stat-icon sms-stat-icon--emerald flex h-9 w-9 shrink-0 items-center justify-center rounded-lg md:mb-4 md:h-11 md:w-11 md:rounded-xl"
>
<Icon icon="vuesax-linear:sms" Size="18px" class="md:!text-[22px]" />
</div>
<div class="sms-stat-inline-body sms-stat-period-body flex min-w-0 flex-1 items-center md:block">
<p class="sms-stat-label shrink-0 text-xs font-medium leading-none md:mb-2 md:text-sm md:leading-normal">
امروز
</p>
<p class="sms-stat-period-mid min-w-0 flex-1 text-center text-sm leading-none md:hidden">
<span class="sms-stat-value sms-stat-value--emerald font-bold">
{{ formatNumber(overview.stats.today.messages) }}
<span class="sms-stat-unit text-xs font-medium">پیامک</span>
</span>
<span class="sms-stat-meta">
· {{ formatNumber(overview.stats.today.parts) }} پارت
</span>
</p>
<p class="sms-stat-meta shrink-0 whitespace-nowrap text-sm leading-none md:hidden">
{{ formatNumber(overview.stats.today.amount) }} ریال
</p>
<p class="sms-stat-value sms-stat-value--emerald hidden text-xl font-bold md:block">
{{ formatNumber(overview.stats.today.messages) }}
<span class="sms-stat-unit text-sm font-medium">پیامک</span>
</p>
<p class="sms-stat-meta hidden text-sm md:mt-2 md:block">
{{ formatNumber(overview.stats.today.parts) }} پارت ·
{{ formatNumber(overview.stats.today.amount) }} ریال
</p>
</div>
</div>
</div>
<div
class="sms-stat-card sms-stat-card--violet rounded-xl border p-3 shadow-sm md:rounded-2xl md:p-6"
>
<div class="sms-stat-inline flex gap-2.5 md:block">
<div
class="sms-stat-icon sms-stat-icon--violet flex h-9 w-9 shrink-0 items-center justify-center rounded-lg md:mb-4 md:h-11 md:w-11 md:rounded-xl"
>
<Icon icon="vuesax-linear:chart" Size="18px" class="md:!text-[22px]" />
</div>
<div class="sms-stat-inline-body sms-stat-period-body flex min-w-0 flex-1 items-center md:block">
<p class="sms-stat-label shrink-0 text-xs font-medium leading-none md:mb-2 md:text-sm md:leading-normal">
ماه جاری
</p>
<p class="sms-stat-period-mid min-w-0 flex-1 text-center text-sm leading-none md:hidden">
<span class="sms-stat-value sms-stat-value--violet font-bold">
{{ formatNumber(overview.stats.month.messages) }}
<span class="sms-stat-unit text-xs font-medium">پیامک</span>
</span>
<span class="sms-stat-meta">
· {{ formatNumber(overview.stats.month.parts) }} پارت
</span>
</p>
<p class="sms-stat-meta shrink-0 whitespace-nowrap text-sm leading-none md:hidden">
{{ formatNumber(overview.stats.month.amount) }} ریال
</p>
<p class="sms-stat-value sms-stat-value--violet hidden text-xl font-bold md:block">
{{ formatNumber(overview.stats.month.messages) }}
<span class="sms-stat-unit text-sm font-medium">پیامک</span>
</p>
<p class="sms-stat-meta hidden text-sm md:mt-2 md:block">
{{ formatNumber(overview.stats.month.parts) }} پارت ·
{{ formatNumber(overview.stats.month.amount) }} ریال
</p>
</div>
</div>
</div>
</section>
<!-- مدیریت و گزارش -->
<section class="sms-panel-mgmt mb-6 flex flex-col gap-4 md:mb-8 md:gap-6">
<div class="sms-panel-surface rounded-2xl p-4 shadow-sm md:p-6">
<div class="sms-panel-section-head mb-4 flex items-center gap-2 pb-3 md:mb-5 md:pb-4">
<Icon icon="vuesax-linear:setting-2" Size="20px" class="sms-panel-section-icon" />
<h2 class="sms-panel-heading text-base font-semibold">مدیریت کیف پول</h2>
</div>
<div class="flex flex-col gap-4 md:gap-6">
<div class="flex flex-col gap-3 md:gap-4 xl:flex-row xl:items-stretch">
<div
class="sms-panel-subcard sms-panel-subcard--sky flex-1 rounded-xl p-3 shadow-sm md:p-5"
>
<div class="mb-3 flex items-center gap-2 md:mb-4">
<span class="sms-stat-icon sms-stat-icon--sky flex h-8 w-8 items-center justify-center rounded-lg">
<Icon icon="vuesax-linear:tag" Size="18px" />
</span>
<div>
<p class="sms-subcard-title text-sm font-semibold">تعرفه پیامک</p>
<p class="sms-panel-subtitle hidden text-xs md:block">
قیمت هر پارت برای ارسال‌های بعدی
</p>
</div>
</div>
<div class="grid grid-cols-[1fr_auto] items-end gap-2 md:gap-3">
<a-form-item label="قیمت هر پارت (ریال)" class="sms-compact-field !mb-0">
<a-input
v-model:value="pricePerPartField"
inputmode="numeric"
placeholder="قیمت پارت"
size="large"
class="!w-full"
/>
</a-form-item>
<BaseButton
type="primary"
size="large"
class="!w-auto shrink-0 whitespace-nowrap"
:loading="actionLoading"
@click="submitPrice"
>
ذخیره قیمت
</BaseButton>
</div>
</div>
<div
class="sms-panel-subcard sms-panel-subcard--emerald flex-1 rounded-xl p-3 shadow-sm md:p-5"
>
<div class="mb-3 flex items-center gap-2 md:mb-4">
<span
class="sms-stat-icon sms-stat-icon--emerald flex h-8 w-8 items-center justify-center rounded-lg"
>
<Icon icon="vuesax-linear:wallet-add" Size="18px" />
</span>
<div>
<p class="sms-subcard-title text-sm font-semibold">شارژ کیف پول</p>
<p class="sms-panel-subtitle hidden text-xs md:block">
افزایش موجودی برای ارسال پیامک
</p>
</div>
</div>
<div
class="grid grid-cols-[1fr_auto] items-end gap-2 md:grid-cols-2 md:gap-3 xl:grid-cols-[1fr_1.2fr_auto]"
>
<a-form-item label="مبلغ شارژ (ریال)" class="sms-compact-field order-1 !mb-0">
<a-input
v-model:value="chargeAmountField"
inputmode="numeric"
placeholder="مبلغ شارژ"
size="large"
class="!w-full"
/>
</a-form-item>
<a-form-item
label="توضیح (اختیاری)"
class="sms-compact-field order-3 col-span-2 !mb-0 md:order-2 md:col-span-1"
>
<BasicInput v-model:value="chargeNote" place-holder="توضیح شارژ" size="large" />
</a-form-item>
<BaseButton
type="primary"
size="large"
class="order-2 !w-auto shrink-0 whitespace-nowrap md:order-3 md:col-span-2 xl:col-span-1"
:loading="actionLoading"
@click="openChargeConfirm"
>
شارژ کیف پول
</BaseButton>
</div>
</div>
</div>
<div
class="sms-panel-block rounded-xl border p-4 md:p-5"
:class="isBlocked ? 'sms-panel-block--blocked' : 'sms-panel-block--active'"
>
<div class="inline-flex max-w-full flex-col gap-2 self-start">
<div class="sms-block-row flex w-full items-center justify-between gap-4">
<span class="sms-panel-heading font-semibold leading-none">
مسدودسازی ارسال پیامک
</span>
<a-switch
v-model:checked="isBlocked"
class="sms-block-switch !m-0 shrink-0"
:class="isBlocked ? 'sms-block-switch--blocked' : 'sms-block-switch--active'"
:loading="blockLoading"
checked-children="مسدود"
un-checked-children="فعال"
@change="onBlockChange"
/>
</div>
<p class="sms-panel-subtitle text-sm leading-relaxed">
در حالت مسدود، هیچ پیامکی از این نسخه ارسال نمی‌شود.
</p>
</div>
</div>
</div>
</div>
<div class="sms-panel-surface sms-report-card rounded-2xl p-4 shadow-sm md:p-6">
<div class="sms-panel-section-head mb-3 flex items-center gap-2 pb-3 md:mb-4 md:pb-4">
<Icon icon="vuesax-linear:calendar" Size="20px" class="sms-panel-section-icon" />
<h2 class="sms-panel-heading text-base font-semibold">گزارش</h2>
</div>
<a-tabs v-model:activeKey="activeReportTab" class="sms-panel-tabs sms-report-tabs">
<a-tab-pane key="sms" tab="گزارش بازه‌ای پیامک">
<div class="flex flex-col gap-3 md:gap-4">
<div
class="sms-report-filter-row grid grid-cols-[1fr_1fr_auto] items-center gap-2 md:flex md:flex-wrap md:items-end md:gap-4"
>
<a-form-item label="از تاریخ" class="sms-compact-field !mb-0 min-w-0 md:min-w-[160px] md:flex-1">
<BaseDatePicker
v-model:value="reportFrom"
format="YYYY-MM-DD"
class="w-full"
placeholder="از تاریخ"
/>
</a-form-item>
<a-form-item label="تا تاریخ" class="sms-compact-field !mb-0 min-w-0 md:min-w-[160px] md:flex-1">
<BaseDatePicker
v-model:value="reportTo"
format="YYYY-MM-DD"
class="w-full"
placeholder="تا تاریخ"
/>
</a-form-item>
<BaseButton
class="sms-report-action-btn !w-auto shrink-0 whitespace-nowrap px-3 md:!w-auto"
:loading="reportLoading"
@click="loadReport"
>
دریافت گزارش
</BaseButton>
</div>
<div
v-if="report"
class="sms-panel-muted-box space-y-2 rounded-xl p-3 md:space-y-3 md:p-4"
>
<div class="grid grid-cols-3 gap-2 md:gap-3">
<div class="sms-panel-kpi rounded-lg px-2 py-1.5 md:px-3 md:py-2.5">
<span class="sms-panel-subtitle text-[10px] md:text-xs">تعداد پیامک</span>
<p class="sms-panel-heading text-sm font-bold md:text-lg">
{{ formatNumber(report.summary.messages) }}
</p>
</div>
<div class="sms-panel-kpi rounded-lg px-2 py-1.5 md:px-3 md:py-2.5">
<span class="sms-panel-subtitle text-[10px] md:text-xs">تعداد پارت</span>
<p class="sms-panel-heading text-sm font-bold md:text-lg">
{{ formatNumber(report.summary.parts) }}
</p>
</div>
<div class="sms-panel-kpi rounded-lg px-2 py-1.5 md:px-3 md:py-2.5">
<span class="sms-panel-subtitle text-[10px] md:text-xs">مبلغ کل</span>
<p class="sms-panel-heading text-sm font-bold md:text-lg">
{{ formatNumber(report.summary.amount) }}
<span class="sms-panel-subtitle text-[10px] font-normal md:text-sm">ریال</span>
</p>
</div>
</div>
<div v-if="report.by_provider?.length" class="sms-panel-divider border-t pt-2 md:pt-3">
<p class="sms-stat-label mb-1 text-[10px] font-semibold md:mb-2 md:text-xs">
بر اساس ارائه‌دهنده
</p>
<ul class="space-y-1 md:space-y-2">
<li
v-for="row in report.by_provider"
:key="row.provider"
class="sms-panel-kpi flex items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-[11px] md:px-3 md:py-2 md:text-xs"
>
<span class="font-medium">{{ row.provider }}</span>
<span class="sms-panel-subtitle shrink-0">
{{ formatNumber(row.messages) }} پیام · {{ formatNumber(row.amount) }} ریال
</span>
</li>
</ul>
</div>
</div>
</div>
</a-tab-pane>
<a-tab-pane key="charge" tab="گزارش شارژ کیف پول">
<div class="flex flex-col gap-3 md:gap-4">
<div
class="sms-report-filter-row grid grid-cols-[1fr_1fr_auto] items-center gap-2 md:flex md:flex-wrap md:items-end md:gap-4"
>
<a-form-item label="از تاریخ" class="sms-compact-field !mb-0 min-w-0 md:min-w-[160px] md:flex-1">
<BaseDatePicker
v-model:value="chargeReportFrom"
format="YYYY-MM-DD"
class="w-full"
placeholder="از تاریخ"
/>
</a-form-item>
<a-form-item label="تا تاریخ" class="sms-compact-field !mb-0 min-w-0 md:min-w-[160px] md:flex-1">
<BaseDatePicker
v-model:value="chargeReportTo"
format="YYYY-MM-DD"
class="w-full"
placeholder="تا تاریخ"
/>
</a-form-item>
<BaseButton
class="sms-report-action-btn !w-auto shrink-0 whitespace-nowrap px-3 md:!w-auto"
:loading="chargeReportLoading"
@click="loadChargeReport"
>
دریافت گزارش
</BaseButton>
</div>
<p class="sms-panel-subtitle text-xs md:text-sm">
در صورت خالی بودن تاریخ، همه شارژها نمایش داده می‌شوند.
</p>
<div
v-if="chargeReport"
class="sms-panel-muted-box space-y-2 rounded-xl p-3 md:space-y-3 md:p-4"
>
<div class="grid grid-cols-2 gap-2 md:gap-3">
<div class="sms-panel-kpi rounded-lg px-2 py-1.5 md:px-3 md:py-2.5">
<span class="sms-panel-subtitle text-[10px] md:text-xs">تعداد شارژ</span>
<p class="sms-panel-heading text-sm font-bold md:text-lg">
{{ formatNumber(chargeReport.summary.charges) }}
</p>
</div>
<div class="sms-panel-kpi rounded-lg px-2 py-1.5 md:px-3 md:py-2.5">
<span class="sms-panel-subtitle text-[10px] md:text-xs">مبلغ کل شارژ</span>
<p class="sms-stat-value sms-stat-value--emerald text-sm font-bold md:text-lg">
{{ formatNumber(chargeReport.summary.amount) }}
<span class="sms-panel-subtitle text-[10px] font-normal md:text-sm">ریال</span>
</p>
</div>
</div>
<div v-if="chargeReport.items.length" class="sms-panel-divider border-t pt-2 md:pt-3">
<p class="sms-stat-label mb-1 text-[10px] font-semibold md:mb-2 md:text-xs">
لیست شارژها
</p>
<ul class="max-h-64 space-y-1 overflow-y-auto md:max-h-80 md:space-y-2">
<li
v-for="item in chargeReport.items"
:key="item.id"
class="sms-panel-kpi rounded-lg px-2 py-2 text-[11px] md:px-3 md:py-2.5 md:text-xs"
>
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="sms-stat-value sms-stat-value--emerald font-semibold">
+{{ formatNumber(item.amount) }} ریال
</p>
<p v-if="item.note" class="sms-panel-subtitle mt-0.5">{{ item.note }}</p>
</div>
<div class="sms-panel-subtitle shrink-0 text-left">
<p>{{ formatReportDate(item.created_at) }}</p>
<p class="mt-0.5">
موجودی:
{{ formatNumber(item.balance_after) }}
</p>
</div>
</div>
</li>
</ul>
</div>
<p v-else class="sms-panel-subtitle text-center text-xs md:text-sm">
شارژی در این بازه یافت نشد.
</p>
</div>
</div>
</a-tab-pane>
</a-tabs>
</div>
</section>
<!-- جداول -->
<section class="sms-panel-surface rounded-2xl p-4 shadow-sm md:p-6">
<a-tabs
v-model:activeKey="activeTab"
class="sms-panel-tabs sms-panel-table-tabs"
:animated="false"
destroy-inactive-tab-pane
>
<a-tab-pane key="logs" tab="لاگ ارسال">
<div
class="sms-panel-muted-box sms-table-date-filter mb-4 grid grid-cols-2 items-end gap-2 rounded-xl p-3 md:mb-5 md:flex md:flex-wrap md:gap-4 md:p-4"
>
<a-form-item label="از تاریخ" class="sms-compact-field !mb-0 min-w-0 md:min-w-[160px] md:flex-1">
<BaseDatePicker
v-model:value="tableFrom"
format="YYYY-MM-DD"
class="w-full"
placeholder="انتخاب تاریخ"
/>
</a-form-item>
<a-form-item label="تا تاریخ" class="sms-compact-field !mb-0 min-w-0 md:min-w-[160px] md:flex-1">
<BaseDatePicker
v-model:value="tableTo"
format="YYYY-MM-DD"
class="w-full"
placeholder="انتخاب تاریخ"
/>
</a-form-item>
</div>
<SmsPanelLogsTable ref="logsTableRef" :from-date="tableFrom" :to-date="tableTo" />
</a-tab-pane>
<a-tab-pane key="ledger" tab="دفترچه کیف پول">
<div
class="sms-panel-muted-box sms-table-date-filter mb-4 grid grid-cols-2 items-end gap-2 rounded-xl p-3 md:mb-5 md:flex md:flex-wrap md:gap-4 md:p-4"
>
<a-form-item label="از تاریخ" class="sms-compact-field !mb-0 min-w-0 md:min-w-[160px] md:flex-1">
<BaseDatePicker
v-model:value="tableFrom"
format="YYYY-MM-DD"
class="w-full"
placeholder="انتخاب تاریخ"
/>
</a-form-item>
<a-form-item label="تا تاریخ" class="sms-compact-field !mb-0 min-w-0 md:min-w-[160px] md:flex-1">
<BaseDatePicker
v-model:value="tableTo"
format="YYYY-MM-DD"
class="w-full"
placeholder="انتخاب تاریخ"
/>
</a-form-item>
</div>
<SmsPanelLedgerTable ref="ledgerTableRef" :from-date="tableFrom" :to-date="tableTo" />
</a-tab-pane>
</a-tabs>
</section>
</template>
</div>
<BasicModal
:visible="chargeModalVisible"
title="تأیید شارژ کیف پول"
width="380px"
ok-text="شارژ"
:loading="actionLoading"
@ok="submitCharge"
@close="chargeModalVisible = false"
>
<p class="sms-panel-subtitle p-3 text-sm">
مبلغ
<strong class="sms-panel-heading">{{ formatNumber(Number(chargeAmount) || 0) }}</strong>
ریال به کیف پول پیامک اضافه شود؟
</p>
</BasicModal>
</div>
</template>
<script setup lang="ts">
import BasicInput from '@/components/BasicInput.vue'
import BasicModal from '@/components/BasicModal.vue'
import BaseDatePicker from '@core/components/BaseDatePicker'
import BaseButton from '@core/components/BaseButton'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import SmsPanelLogsTable from './components/SmsPanelLogsTable.vue'
import SmsPanelLedgerTable from './components/SmsPanelLedgerTable.vue'
import {
chargeSmsWallet,
getSmsPanelChargeReport,
getSmsPanelOverview,
getSmsPanelReport,
updateSmsBlock,
updateSmsPrice,
type SmsPanelChargeReport,
type SmsPanelOverview,
type SmsPanelReport
} from '@/api/smsPanel'
import { toJalaliNoTime } from '@core/utils/useDataTime'
import { message } from 'ant-design-vue'
const pageLoading = ref(true)
const actionLoading = ref(false)
const blockLoading = ref(false)
const isBlocked = ref(false)
const reportLoading = ref(false)
const chargeReportLoading = ref(false)
const overview = ref<SmsPanelOverview | null>(null)
const report = ref<SmsPanelReport | null>(null)
const chargeReport = ref<SmsPanelChargeReport | null>(null)
const chargeAmount = ref<number | null>(null)
const chargeNote = ref('')
const pricePerPart = ref<number | null>(null)
const chargeModalVisible = ref(false)
const reportFrom = ref('')
const reportTo = ref('')
const chargeReportFrom = ref('')
const chargeReportTo = ref('')
const tableFrom = ref('')
const tableTo = ref('')
const activeReportTab = ref('sms')
const activeTab = ref('logs')
const logsTableRef = ref<InstanceType<typeof SmsPanelLogsTable> | null>(null)
const ledgerTableRef = ref<InstanceType<typeof SmsPanelLedgerTable> | null>(null)
const balanceCardClass = computed(() => {
if (!overview.value) return 'sms-stat-card--neutral'
if (overview.value.wallet.balance < 0) return 'sms-stat-card--danger'
if (overview.value.wallet.balance <= 0) return 'sms-stat-card--warn'
return 'sms-stat-card--ok'
})
const balanceIconClass = computed(() => {
if (!overview.value) return 'sms-stat-icon--neutral'
if (overview.value.wallet.balance < 0) return 'sms-stat-icon--danger'
if (overview.value.wallet.balance <= 0) return 'sms-stat-icon--warn'
return 'sms-stat-icon--ok'
})
const balanceTextClass = computed(() => {
if (!overview.value) return 'sms-stat-value--neutral'
if (overview.value.wallet.balance < 0) return 'sms-stat-value--danger'
if (overview.value.wallet.balance <= 0) return 'sms-stat-value--warn'
return 'sms-stat-value--ok'
})
const balanceAlertHint = computed(() => {
const sent = overview.value?.wallet.alert_days_sent ?? 0
return `هشدار روزانه حداکثر ۳ روز (${sent}/۳)`
})
function formatNumber(value: number) {
return Number(value || 0).toLocaleString('fa-IR')
}
function formatReportDate(value: string | null | undefined) {
if (!value) return '—'
return toJalaliNoTime(value)
}
function formatAmountDisplay(value: number | null) {
if (value == null || !Number.isFinite(value)) return ''
return String(Math.trunc(value)).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
function parseAmountDisplay(value: string) {
const digits = value.replace(/[^\d]/g, '')
return digits === '' ? null : Number(digits)
}
function useAmountField(source: Ref<number | null>) {
return computed({
get: () => formatAmountDisplay(source.value),
set: (val: string) => {
source.value = parseAmountDisplay(val)
}
})
}
const chargeAmountField = useAmountField(chargeAmount)
const pricePerPartField = useAmountField(pricePerPart)
async function loadOverview() {
pageLoading.value = true
try {
overview.value = await getSmsPanelOverview()
pricePerPart.value = overview.value.wallet.price_per_part || null
isBlocked.value = overview.value.wallet.is_blocked
} catch {
message.error('خطا در بارگذاری پنل پیامک')
} finally {
pageLoading.value = false
}
}
function openChargeConfirm() {
const amount = Number(chargeAmount.value ?? 0)
if (!amount || amount <= 0) {
message.warning('مبلغ شارژ را وارد کنید')
return
}
chargeModalVisible.value = true
}
async function submitCharge() {
const amount = Number(chargeAmount.value ?? 0)
if (!amount || amount <= 0) return
actionLoading.value = true
try {
const res = await chargeSmsWallet({
amount,
note: chargeNote.value.trim() || undefined
})
message.success(res.message || 'شارژ انجام شد')
chargeModalVisible.value = false
chargeAmount.value = null
chargeNote.value = ''
await loadOverview()
ledgerTableRef.value?.fetch()
if (chargeReport.value) {
await loadChargeReport()
}
} catch (e: any) {
message.error(e?.response?.data?.message || 'خطا در شارژ')
} finally {
actionLoading.value = false
}
}
async function submitPrice() {
const price = Number(pricePerPart.value ?? 0)
if (Number.isNaN(price) || price < 0) {
message.warning('قیمت معتبر وارد کنید')
return
}
actionLoading.value = true
try {
const res = await updateSmsPrice(price)
message.success(res.message || 'قیمت ذخیره شد')
await loadOverview()
} catch (e: any) {
message.error(e?.response?.data?.message || 'خطا در ذخیره قیمت')
} finally {
actionLoading.value = false
}
}
async function onBlockChange(checked: boolean) {
blockLoading.value = true
try {
const res = await updateSmsBlock(checked)
message.success(res.message)
isBlocked.value = res.is_blocked
await loadOverview()
} catch (e: any) {
isBlocked.value = overview.value?.wallet.is_blocked ?? false
message.error(e?.response?.data?.message || 'خطا در تغییر وضعیت')
} finally {
blockLoading.value = false
}
}
async function loadReport() {
reportLoading.value = true
try {
report.value = await getSmsPanelReport({
from_date: reportFrom.value || undefined,
to_date: reportTo.value || undefined
})
} catch {
message.error('خطا در دریافت گزارش')
} finally {
reportLoading.value = false
}
}
async function loadChargeReport() {
chargeReportLoading.value = true
try {
chargeReport.value = await getSmsPanelChargeReport({
from_date: chargeReportFrom.value || undefined,
to_date: chargeReportTo.value || undefined
})
} catch {
message.error('خطا در دریافت گزارش شارژ')
} finally {
chargeReportLoading.value = false
}
}
onMounted(() => {
loadOverview()
})
</script>
<style scoped>
.sms-panel-page__scroll {
-webkit-overflow-scrolling: touch;
}
.sms-panel-heading {
color: var(--admin-text);
}
.sms-panel-subtitle {
color: var(--admin-text-secondary);
}
.sms-panel-section-icon {
color: var(--admin-text-muted);
}
.sms-panel-section-head {
border-bottom: 1px solid var(--admin-border);
}
.sms-panel-surface {
border: 1px solid var(--admin-border);
background: var(--admin-bg-elevated);
color: var(--admin-text);
}
.sms-panel-muted-box {
border: 1px solid var(--admin-border);
background: var(--admin-bg-muted);
}
.sms-panel-kpi {
background: var(--admin-bg-elevated);
color: var(--admin-text);
}
.sms-panel-divider {
border-color: var(--admin-border);
}
.sms-stat-label {
color: var(--admin-text-secondary);
}
.sms-stat-meta {
color: var(--admin-text-muted);
}
.sms-stat-unit {
color: var(--admin-text-secondary);
}
.sms-stat-card--neutral {
border-color: var(--admin-border);
background: var(--admin-bg-elevated);
}
.sms-stat-card--ok {
border-color: rgba(16, 185, 129, 0.28);
background: linear-gradient(to bottom right, rgba(16, 185, 129, 0.1), var(--admin-bg-elevated));
}
.sms-stat-card--warn {
border-color: rgba(245, 158, 11, 0.32);
background: linear-gradient(to bottom right, rgba(245, 158, 11, 0.12), var(--admin-bg-elevated));
}
.sms-stat-card--danger {
border-color: rgba(239, 68, 68, 0.32);
background: linear-gradient(to bottom right, rgba(239, 68, 68, 0.12), var(--admin-bg-elevated));
}
.sms-stat-card--sky {
border-color: rgba(14, 165, 233, 0.28);
background: linear-gradient(to bottom right, rgba(14, 165, 233, 0.1), var(--admin-bg-elevated));
}
.sms-stat-card--emerald {
border-color: rgba(16, 185, 129, 0.28);
background: linear-gradient(to bottom right, rgba(16, 185, 129, 0.1), var(--admin-bg-elevated));
}
.sms-stat-card--violet {
border-color: rgba(139, 92, 246, 0.28);
background: linear-gradient(to bottom right, rgba(139, 92, 246, 0.1), var(--admin-bg-elevated));
}
.sms-stat-icon {
background: var(--admin-bg-muted);
color: var(--admin-text-muted);
}
.sms-stat-icon--neutral {
background: var(--admin-bg-muted);
color: var(--admin-text-muted);
}
.sms-stat-icon--ok,
.sms-stat-icon--emerald {
background: rgba(16, 185, 129, 0.14);
color: #059669;
}
.sms-stat-icon--warn {
background: rgba(245, 158, 11, 0.16);
color: #d97706;
}
.sms-stat-icon--danger {
background: rgba(239, 68, 68, 0.14);
color: #dc2626;
}
.sms-stat-icon--sky {
background: rgba(14, 165, 233, 0.14);
color: #0284c7;
}
.sms-stat-icon--violet {
background: rgba(139, 92, 246, 0.14);
color: #7c3aed;
}
.sms-stat-value--neutral {
color: var(--admin-text);
}
.sms-stat-value--ok {
color: #047857;
}
.sms-stat-value--warn {
color: #b45309;
}
.sms-stat-value--danger {
color: #b91c1c;
}
.sms-stat-value--sky {
color: #0c4a6e;
}
.sms-stat-value--emerald {
color: #047857;
}
.sms-stat-value--violet {
color: #5b21b6;
}
.sms-panel-subcard {
border: 1px solid var(--admin-border);
background: var(--admin-bg-elevated);
}
.sms-panel-subcard--sky {
border-color: rgba(14, 165, 233, 0.24);
background: linear-gradient(to bottom right, rgba(14, 165, 233, 0.08), var(--admin-bg-elevated));
}
.sms-panel-subcard--emerald {
border-color: rgba(16, 185, 129, 0.24);
background: linear-gradient(to bottom right, rgba(16, 185, 129, 0.08), var(--admin-bg-elevated));
}
.sms-subcard-title {
color: var(--admin-text);
}
.sms-panel-block--active {
border-color: rgba(16, 185, 129, 0.28);
background: rgba(16, 185, 129, 0.08);
}
.sms-panel-block--blocked {
border-color: rgba(239, 68, 68, 0.28);
background: rgba(239, 68, 68, 0.1);
}
.sms-panel-page :deep(.ant-btn-primary:not(:disabled)) {
background-color: var(--admin-gold-light);
border-color: var(--admin-gold-light);
}
.sms-panel-page :deep(.ant-btn-primary:not(:disabled):hover),
.sms-panel-page :deep(.ant-btn-primary:not(:disabled):focus) {
background-color: var(--admin-gold-hover);
border-color: var(--admin-gold-hover);
}
.sms-panel-page :deep(.ant-tabs-ink-bar) {
background: var(--admin-gold-light);
}
.sms-panel-page :deep(.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn) {
color: var(--admin-gold-light);
}
.sms-panel-page :deep(.ant-tabs-tab .ant-tabs-tab-btn) {
color: var(--admin-text-muted);
}
.sms-panel-page :deep(.ant-input-lg),
.sms-panel-page :deep(.ant-picker) {
border-radius: 0.5rem;
}
.sms-panel-page :deep(.ant-form-item-label > label) {
font-weight: 500;
color: var(--admin-text-muted);
}
.sms-balance-info-btn {
width: 1.125rem;
height: 1.125rem;
margin: 0;
padding: 0;
border: none;
background: transparent;
line-height: 0;
vertical-align: middle;
cursor: pointer;
}
.sms-balance-stat-title-desktop {
display: flex;
align-items: center;
gap: 0.25rem;
line-height: 1;
}
.sms-balance-stat-title-desktop :deep(.ant-tooltip) {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
}
.sms-panel-tabs :deep(.ant-tabs-nav) {
margin-bottom: 1.25rem;
}
.sms-panel-tabs :deep(.ant-tabs-tab) {
padding: 0.6rem 1rem;
font-weight: 500;
}
.sms-panel-table-tabs :deep(.ant-tabs-content-holder),
.sms-panel-table-tabs :deep(.ant-tabs-content),
.sms-panel-table-tabs :deep(.ant-tabs-tabpane),
.sms-panel-table-tabs :deep(.sms-panel-table),
.sms-panel-table-tabs :deep(.hr-tamin-base-table),
.sms-panel-table-tabs :deep(.scrollable-container) {
width: 100%;
max-width: 100%;
min-width: 0;
}
.sms-report-tabs :deep(.ant-tabs-nav) {
margin-bottom: 1rem;
}
@media (min-width: 768px) {
.sms-report-tabs :deep(.ant-tabs-nav) {
margin-bottom: 1.25rem;
}
}
.sms-panel-page :deep(.sms-report-card .sms-compact-field .ant-picker) {
height: 2rem;
padding-top: 0;
padding-bottom: 0;
font-size: 0.875rem;
}
.sms-panel-page :deep(.sms-report-card .sms-report-action-btn.ant-btn) {
height: 2rem;
padding-top: 0;
padding-bottom: 0;
font-size: 0.8125rem;
line-height: 1.25rem;
}
.sms-panel-page :deep(.sms-report-card .sms-report-filter-row) {
align-items: center;
}
@media (min-width: 768px) {
.sms-panel-page :deep(.sms-report-card .sms-report-filter-row) {
align-items: flex-end;
}
.sms-panel-page :deep(.sms-report-card .sms-compact-field.ant-form-item) {
margin-bottom: 0;
}
}
.sms-panel-page :deep(.sms-block-switch--active:not(.ant-switch-checked)) {
background: #22c55e;
}
.sms-panel-page :deep(.sms-block-switch--active:not(.ant-switch-checked):hover:not(.ant-switch-disabled)) {
background: #16a34a;
}
.sms-panel-page :deep(.sms-block-switch--blocked.ant-switch-checked) {
background: #ef4444;
}
.sms-panel-page :deep(.sms-block-switch--blocked.ant-switch-checked:hover:not(.ant-switch-disabled)) {
background: #dc2626;
}
.sms-panel-page :deep(.sms-block-row .ant-switch) {
margin-top: 0;
margin-bottom: 0;
vertical-align: middle;
}
.sms-panel-page :deep(.sms-block-row .ant-switch-inner) {
display: flex;
align-items: center;
}
@media (max-width: 767px) {
.sms-stat-inline {
align-items: center;
}
.sms-stat-inline-body {
display: flex;
align-items: center;
height: 2.25rem;
min-height: 2.25rem;
}
.sms-stat-inline-body > p {
margin: 0;
}
.sms-balance-stat-body > p {
margin: 0;
}
.sms-balance-stat-title {
display: flex;
align-items: center;
gap: 0.25rem;
line-height: 1;
min-height: 2.25rem;
margin: 0;
}
.sms-balance-stat-title :deep(.ant-tooltip) {
display: inline-flex;
align-items: center;
line-height: 0;
}
.sms-balance-stat-body :deep(.ant-tooltip) {
display: inline-flex;
align-items: center;
line-height: 0;
}
.sms-stat-period-body {
gap: 0.375rem;
}
.sms-stat-period-mid {
padding-inline: 0.25rem;
}
.sms-panel-page :deep(.sms-compact-field .ant-form-item-label) {
display: none;
}
.sms-panel-page :deep(.sms-compact-field .ant-input-lg),
.sms-panel-page :deep(.sms-compact-field .hr-tamin-input .ant-input-lg) {
padding-top: 0.375rem;
padding-bottom: 0.375rem;
font-size: 0.875rem;
}
.sms-panel-page :deep(.sms-panel-table .hr-tamin-base-table .flex.items-center.justify-between.mb-2) {
justify-content: flex-start;
}
.sms-panel-page :deep(.sms-panel-table .hr-tamin-base-table .flex.items-center.justify-between.mb-2 > .w-full) {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: 0.25rem;
width: 100%;
}
.sms-panel-page :deep(.sms-panel-table .hr-tamin-base-table .flex.items-center.justify-between.mb-2 > .w-full > .flex.flex-col) {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
}
.sms-panel-page :deep(.sms-panel-table .hr-tamin-base-table .flex.items-center.justify-between.mb-2 > .w-full > .flex.justify-between.items-center.gap-2) {
flex: 1;
min-width: 0;
}
.sms-panel-page :deep(.sms-panel-table .hr-tamin-base-table .flex.items-center.justify-between.mb-2 > .w-full > .flex.justify-between.items-center.gap-2 > .flex.md\:hidden) {
display: none;
}
.sms-panel-page :deep(.sms-panel-table .hr-tamin-base-table .flex.items-center.justify-between.mb-2 > .w-full > .flex.flex-col > .block.md\:hidden) {
display: none;
}
.sms-panel-page :deep(.sms-panel-table .hr-tamin-base-table .flex.items-center.justify-between.mb-2 > .hidden.md\:block) {
display: none !important;
}
.sms-panel-page :deep(.sms-panel-table .ant-affix) {
width: 100% !important;
}
.sms-panel-page :deep(.sms-panel-table .ant-affix > div) {
width: 100% !important;
}
.sms-panel-page :deep(.sms-panel-table-tabs .ant-tabs-content) {
margin-right: 0 !important;
margin-left: 0 !important;
}
.sms-panel-page :deep(.block.md\:hidden .base-table-mobile-card:not(:has(.base-table-card-header))) {
padding: 0.75rem;
}
.sms-panel-page :deep(.block.md\:hidden .space-y-3) {
gap: 0.625rem;
}
.sms-panel-page :deep(.block.md\:hidden .base-table-mobile-card) {
padding: 0;
border-radius: 0.875rem;
border-color: var(--admin-border);
box-shadow: 0 1px 3px var(--admin-shadow);
}
.sms-panel-page :deep(.block.md\:hidden .base-table-mobile-card:hover) {
box-shadow: 0 2px 8px var(--admin-shadow);
transform: none;
}
.sms-panel-page :deep(.block.md\:hidden .base-table-card-header) {
margin: 0;
padding: 0.625rem 0.875rem;
border: none;
border-bottom: 1px solid var(--admin-border);
border-radius: 0;
background: var(--admin-bg-muted);
}
.sms-panel-page :deep(.block.md\:hidden .base-table-card-row-badge) {
height: 1.625rem;
width: 1.625rem;
background: var(--admin-gold-light);
color: var(--admin-menu-selected-text);
font-size: 0.6875rem;
font-weight: 700;
box-shadow: 0 1px 2px rgb(212 175 55 / 0.35);
}
.sms-panel-page :deep(.block.md\:hidden .base-table-card-header-value) {
font-weight: 600;
color: var(--admin-text-muted);
}
.sms-panel-page :deep(.block.md\:hidden .base-table-card-grid-2) {
margin: 0;
padding: 0.75rem 0.875rem;
gap: 0.5rem 0.875rem;
background: var(--admin-bg-elevated);
border-bottom: 1px solid var(--admin-border);
}
.sms-panel-page :deep(.block.md\:hidden .base-table-card-grid-2 > div) {
min-height: 1.125rem;
padding: 0.25rem 0;
}
.sms-panel-page :deep(.block.md\:hidden .base-table-card-grid-2 .base-table-card-grid-label) {
font-weight: 500;
font-size: 0.6875rem;
color: var(--admin-text-secondary);
}
.sms-panel-page :deep(.block.md\:hidden .base-table-card-grid-2 .base-table-card-grid-value) {
font-weight: 600;
font-size: 0.75rem;
color: var(--admin-text);
}
.sms-panel-page :deep(.block.md\:hidden .base-table-card-stacked) {
margin: 0;
}
.sms-panel-page :deep(.block.md\:hidden .base-table-card-stacked-box) {
margin: 0;
padding: 0.75rem 0.875rem 0.875rem;
border: none;
border-radius: 0;
background: var(--admin-bg-muted);
}
.sms-panel-page :deep(.block.md\:hidden .base-table-card-stacked-label) {
margin: 0 0 0.5rem;
padding-bottom: 0.375rem;
border-bottom: 1px dashed var(--admin-border);
text-align: center;
font-size: 0.6875rem;
font-weight: 600;
color: var(--admin-text-secondary);
letter-spacing: 0.01em;
}
.sms-panel-page :deep(.block.md\:hidden .base-table-card-stacked-value) {
margin-top: 0 !important;
font-size: 0.8125rem;
line-height: 1.55;
color: var(--admin-text-muted);
}
.sms-panel-page :deep(.block.md\:hidden .space-y-3 > div .group\/row.mb-2) {
margin-bottom: 0.125rem;
}
.sms-panel-page :deep(.block.md\:hidden .space-y-3 > div .group\/row .flex) {
align-items: center;
gap: 0.5rem;
}
.sms-panel-page :deep(.block.md\:hidden .space-y-3 > div .w-1\/3) {
width: 30%;
font-size: 0.65rem;
line-height: 1.2;
}
.sms-panel-page :deep(.block.md\:hidden .space-y-3 > div .text-sm) {
font-size: 0.75rem;
line-height: 1.3;
}
}
</style>
<style lang="less">
html.is-dark {
.sms-panel-page {
.sms-stat-value--ok,
.sms-stat-value--emerald {
color: #6ee7b7;
}
.sms-stat-value--warn {
color: #fcd34d;
}
.sms-stat-value--danger {
color: #fda4af;
}
.sms-stat-value--sky {
color: #7dd3fc;
}
.sms-stat-value--violet {
color: #c4b5fd;
}
.sms-stat-icon--ok,
.sms-stat-icon--emerald {
color: #6ee7b7;
}
.sms-stat-icon--warn {
color: #fcd34d;
}
.sms-stat-icon--danger {
color: #fda4af;
}
.sms-stat-icon--sky {
color: #7dd3fc;
}
.sms-stat-icon--violet {
color: #c4b5fd;
}
}
}
</style>