Files
CRM_Montzar/apps/web/src/views/TahesabSalesReportView.vue
T

810 lines
26 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import {
Alert as AAlert,
Button as AButton,
Card as ACard,
Col as ACol,
Collapse as ACollapse,
CollapsePanel as ACollapsePanel,
Empty as AEmpty,
Flex as AFlex,
Form as AForm,
FormItem as AFormItem,
List as AList,
Row as ARow,
Segmented as ASegmented,
Select as ASelect,
Space as ASpace,
Statistic as AStatistic,
Table as ATable,
Tag as ATag,
Typography,
message,
theme,
} from 'ant-design-vue';
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue';
import {
ArrowRightOutlined,
FilterOutlined,
SearchOutlined,
SyncOutlined,
} from '@ant-design/icons-vue';
import axios from 'axios';
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
const AListItem = AList.Item;
const AListItemMeta = AList.Item.Meta;
import {
TAHESAB_ASNAD_KIND_LABELS,
TAHESAB_ASNAD_KINDS,
TAHESAB_JENS_FELEZ_LABELS,
type CustomerGroupDto,
type PersonDto,
type TahesabAsnadKind,
type TahesabAsnadLineDto,
type TahesabAsnadReportPageDto,
type TahesabAsnadSummaryDto,
type TahesabAsnadSyncJobDto,
type TahesabConnectionDto,
type TahesabSalesOverviewDto,
} from '@mento/shared';
import { api } from '@/lib/api';
import {
dateToJalali,
formatLocalDateInput,
jalaliToDate,
parseLocalInput,
toFaDigits,
} from '@/lib/jalali';
import { formatTahesabBedBes } from '@/lib/tahesab';
import { useCustomerGroups } from '@/lib/customer-groups';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import SalesReportBoard from '@/components/tahesab/SalesReportBoard.vue';
import { useAuthStore } from '@/stores/auth';
import { canManageSettings } from '@/lib/roles';
const router = useRouter();
const { token } = theme.useToken();
const { groups: customerGroups, load: loadCustomerGroups } = useCustomerGroups();
const connections = ref<TahesabConnectionDto[]>([]);
const connectionId = ref<string | undefined>();
const fromDate = ref('');
const toDate = ref('');
const groupIds = ref<string[]>([]);
const personId = ref<string | undefined>();
const personOptions = ref<Array<{ value: string; label: string }>>([]);
const kinds = ref<TahesabAsnadKind[]>([]);
const period = ref<'custom' | 'week' | 'month' | 'season'>('month');
const overview = ref<TahesabSalesOverviewDto | null>(null);
const auth = useAuthStore();
const canGroups = computed(() => canManageSettings(auth.user?.role));
const childGroupId = ref<string | undefined>();
const parentGroupId = ref<string | undefined>();
const overviewError = ref('');
let applyingPeriod = false;
const jensFelez = ref<number | undefined>();
const loading = ref(false);
const syncing = ref(false);
const error = ref('');
const syncHint = ref('');
const page = ref(1);
const pageSize = ref(50);
const report = ref<TahesabAsnadReportPageDto | null>(null);
const summary = ref<TahesabAsnadSummaryDto | null>(null);
let pollTimer: ReturnType<typeof setInterval> | null = null;
const kindOptions = TAHESAB_ASNAD_KINDS.map((k) => ({
value: k,
label: TAHESAB_ASNAD_KIND_LABELS[k],
}));
const metalOptions = [
{ value: 0, label: TAHESAB_JENS_FELEZ_LABELS[0] },
{ value: 1, label: TAHESAB_JENS_FELEZ_LABELS[1] },
{ value: 2, label: TAHESAB_JENS_FELEZ_LABELS[2] },
];
function toShamsiYmd(localIso: string): string | null {
const d = parseLocalInput(localIso);
if (!d) return null;
const { jy, jm, jd } = dateToJalali(d);
return `${jy}-${String(jm).padStart(2, '0')}-${String(jd).padStart(2, '0')}`;
}
function defaultRange() {
applyNamedPeriod('month', false);
}
function applyNamedPeriod(key: 'week' | 'month' | 'season' | 'custom', reload: boolean) {
applyingPeriod = true;
period.value = key;
if (key !== 'custom') {
const now = new Date();
const j = dateToJalali(now);
let start = now;
if (key === 'week') {
const delta = (now.getDay() + 1) % 7;
start = new Date(now);
start.setDate(now.getDate() - delta);
} else if (key === 'month') {
start = jalaliToDate(j.jy, j.jm, 1);
} else {
const sm = j.jm <= 3 ? 1 : j.jm <= 6 ? 4 : j.jm <= 9 ? 7 : 10;
start = jalaliToDate(j.jy, sm, 1);
}
fromDate.value = formatLocalDateInput(start);
toDate.value = formatLocalDateInput(now);
}
void nextTick(() => {
applyingPeriod = false;
});
if (reload) applyFilters();
}
function markCustomPeriod() {
if (applyingPeriod) return;
period.value = 'custom';
}
function groupOptionLabel(g: CustomerGroupDto) {
return g.parentName ? `${g.parentName} / ${g.name}` : g.name;
}
function parentChoices(groupId: string) {
return (customerGroups.value as CustomerGroupDto[])
.filter((g) => g.id !== groupId && !g.parentId)
.map((g) => ({ value: g.id, label: g.name }));
}
function groupHasChildren(groupId: string) {
return (customerGroups.value as CustomerGroupDto[]).some((g) => g.parentId === groupId);
}
const childGroupOptions = computed(() =>
(customerGroups.value as CustomerGroupDto[])
.filter((g) => !groupHasChildren(g.id))
.map((g) => ({ value: g.id, label: groupOptionLabel(g) })),
);
const linkedGroups = computed(() =>
(customerGroups.value as CustomerGroupDto[]).filter((g) => g.parentId && g.parentName),
);
function onChildGroup(id: string | undefined) {
childGroupId.value = id;
const current = (customerGroups.value as CustomerGroupDto[]).find((g) => g.id === id);
parentGroupId.value = current?.parentId ?? undefined;
}
async function saveGroupParent() {
const groupId = childGroupId.value;
if (!groupId) return;
const current = (customerGroups.value as CustomerGroupDto[]).find((g) => g.id === groupId);
const next = parentGroupId.value || null;
if ((current?.parentId ?? null) === next) return;
try {
await api.patch(`/customer-groups/${groupId}`, { parentId: next });
await loadCustomerGroups(true);
message.success('گروه اصلی ذخیره شد.');
await loadReport();
} catch (e: unknown) {
message.error(apiErrMessage(e, 'ذخیره گروه اصلی ناموفق بود.'));
}
}
async function clearGroupParent(groupId: string) {
try {
await api.patch(`/customer-groups/${groupId}`, { parentId: null });
if (childGroupId.value === groupId) parentGroupId.value = undefined;
await loadCustomerGroups(true);
message.success('گروه از زیرمجموعه خارج شد.');
await loadReport();
} catch (e: unknown) {
message.error(apiErrMessage(e, 'برداشتن گروه اصلی ناموفق بود.'));
}
}
function kindLabel(k: string) {
return TAHESAB_ASNAD_KIND_LABELS[k as TahesabAsnadKind] ?? k;
}
function metalLabel(v: number | null) {
if (v == null) return '—';
return TAHESAB_JENS_FELEZ_LABELS[v] ?? String(v);
}
function formatNum(n: number | null | undefined) {
if (n == null) return '—';
return toFaDigits(
new Intl.NumberFormat('fa-IR', { maximumFractionDigits: 3 }).format(n),
);
}
function formatMali(n: number | null | undefined) {
return formatTahesabBedBes(n);
}
const columns: TableColumnsType<TahesabAsnadLineDto> = [
{
title: 'تاریخ',
dataIndex: 'tarikh',
key: 'tarikh',
width: 110,
customRender: ({ text }) => toFaDigits(String(text ?? '')),
},
{
title: 'شخص',
key: 'person',
ellipsis: true,
customRender: ({ record }) =>
record.personName
? `${record.personName} (${toFaDigits(String(record.moshtariCode))})`
: toFaDigits(String(record.moshtariCode)),
},
{
title: 'گروه',
key: 'groups',
ellipsis: true,
customRender: ({ record }) =>
record.groups?.map((g) => g.name).join('، ') || '—',
},
{ title: 'نوع سند (NO)', dataIndex: 'noRaw', key: 'noRaw', ellipsis: true },
{
title: 'طبقه‌بندی',
key: 'kind',
width: 100,
customRender: ({ record }) => kindLabel(record.kind),
},
{
title: 'فلز',
key: 'jensFelez',
width: 90,
customRender: ({ record }) => metalLabel(record.jensFelez),
},
{
title: 'وزن',
key: 'vazn',
width: 90,
customRender: ({ record }) => formatNum(record.vazn),
},
{
title: 'مبلغ',
key: 'mali',
width: 140,
customRender: ({ record }) => formatMali(record.mali),
},
{
title: 'ش. فاکتور',
key: 'shFactor',
width: 100,
customRender: ({ record }) =>
record.shFactor != null ? toFaDigits(String(record.shFactor)) : '—',
},
];
const filterParams = computed(() => {
const from = toShamsiYmd(fromDate.value);
const to = toShamsiYmd(toDate.value);
if (!from || !to) return null;
return {
connectionId: connectionId.value,
from,
to,
groupIds: groupIds.value.length ? groupIds.value.join(',') : undefined,
personId: personId.value,
kinds: kinds.value.length ? kinds.value.join(',') : undefined,
jensFelez: jensFelez.value,
};
});
const lastSyncedLabel = computed(() => {
const raw = summary.value?.lastSyncedAt;
if (!raw) return null;
try {
return toFaDigits(new Date(raw).toLocaleString('fa-IR'));
} catch {
return null;
}
});
async function loadConnections() {
const { data } = await api.get<TahesabConnectionDto[]>(
'/integrations/tahesab/connections/public',
);
connections.value = data.filter((c) => c.enabled);
const def =
connections.value.find((c) => c.isDefault) ?? connections.value[0];
if (!connectionId.value && def) connectionId.value = def.id;
}
async function searchPeople(q: string) {
if (!q || q.trim().length < 2) {
personOptions.value = [];
return;
}
try {
const { data } = await api.get<{ items: PersonDto[] }>('/people', {
params: { q: q.trim(), limit: 20 },
});
const items = Array.isArray(data?.items) ? data.items : [];
personOptions.value = items.map((p) => ({
value: p.id,
label: [p.firstName, p.lastName].filter(Boolean).join(' ') || p.id,
}));
} catch {
personOptions.value = [];
}
}
function apiErrMessage(e: unknown, fallback: string): string {
if (!axios.isAxiosError(e)) return fallback;
const raw = e.response?.data as { message?: string | string[] } | undefined;
const msg = raw?.message;
if (Array.isArray(msg)) return msg.filter(Boolean).join(' · ') || fallback;
if (typeof msg === 'string' && msg.trim()) return msg;
return fallback;
}
async function loadReport(opts?: { notifyEmpty?: boolean }) {
const params = filterParams.value;
if (!params) {
error.value = 'تاریخ از و تا را انتخاب کنید.';
message.warning(error.value);
return;
}
loading.value = true;
error.value = '';
try {
const [pageRes, sumRes] = await Promise.all([
api.get<TahesabAsnadReportPageDto>('/integrations/tahesab/reports/asnad', {
params: { ...params, page: page.value, pageSize: pageSize.value },
}),
api.get<TahesabAsnadSummaryDto>(
'/integrations/tahesab/reports/asnad/summary',
{ params },
),
]);
report.value = pageRes.data;
summary.value = sumRes.data;
try {
const overRes = await api.get<TahesabSalesOverviewDto>(
'/integrations/tahesab/reports/asnad/overview',
{ params },
);
overview.value = overRes.data;
overviewError.value = '';
} catch (e: unknown) {
overview.value = null;
const status = axios.isAxiosError(e) ? e.response?.status : undefined;
overviewError.value =
status === 404
? 'خلاصه فروش، گروه و پورسانت بعد از به‌روزرسانی سرور اینجا می‌آید. جدول سندها با دادهٔ فعلی کار می‌کند.'
: apiErrMessage(e, 'خلاصه فروش، گروه و پورسانت بارگذاری نشد.');
}
if (opts?.notifyEmpty && (pageRes.data.total ?? 0) === 0) {
message.info(
'ردیفی نیست. اول «به‌روزرسانی از ته‌حساب» را برای همین بازه بزنید.',
);
}
} catch (e: unknown) {
error.value = apiErrMessage(e, 'بارگذاری گزارش ناموفق بود.');
message.error(error.value);
report.value = null;
summary.value = null;
overview.value = null;
overviewError.value = '';
} finally {
loading.value = false;
}
}
function stopPoll() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
async function pollJob(jobId: string) {
stopPoll();
pollTimer = setInterval(async () => {
try {
const { data } = await api.get<TahesabAsnadSyncJobDto>(
`/integrations/tahesab/reports/asnad/sync/${jobId}`,
);
syncHint.value = `همگام‌سازی: ${toFaDigits(String(data.processed))} از ${toFaDigits(String(data.total))} · ${toFaDigits(String(data.linesUpserted))} ردیف`;
if (data.status === 'done' || data.status === 'failed') {
stopPoll();
syncing.value = false;
if (data.status === 'failed') {
error.value = data.errorMessage || 'همگام‌سازی ناموفق بود.';
} else {
if (data.truncatedPersonIds?.length) {
syncHint.value += ` · هشدار: برای ${toFaDigits(String(data.truncatedPersonIds.length))} مشتری سندهای بازه کامل نیامده است.`;
} else {
syncHint.value = `همگام‌سازی تمام شد · ${toFaDigits(String(data.linesUpserted))} ردیف`;
}
await loadReport();
}
}
} catch {
stopPoll();
syncing.value = false;
error.value = 'وضعیت همگام‌سازی دریافت نشد.';
}
}, 1500);
}
async function startSync() {
const params = filterParams.value;
if (!params) {
error.value = 'ابتدا بازه تاریخ را مشخص کنید، بعد به‌روزرسانی را بزنید.';
message.warning(error.value);
return;
}
syncing.value = true;
error.value = '';
syncHint.value = 'شروع همگام‌سازی…';
try {
const { data } = await api.post<TahesabAsnadSyncJobDto>(
'/integrations/tahesab/reports/asnad/sync',
{
connectionId: params.connectionId,
from: params.from,
to: params.to,
},
{ timeout: 60_000 },
);
message.success('همگام‌سازی شروع شد.');
await pollJob(data.id);
} catch (e: unknown) {
syncing.value = false;
syncHint.value = '';
error.value = apiErrMessage(e, 'شروع همگام‌سازی ناموفق بود.');
message.error(error.value);
}
}
function applyFilters() {
page.value = 1;
void loadReport({ notifyEmpty: true });
}
onMounted(async () => {
defaultRange();
try {
await Promise.all([loadConnections(), loadCustomerGroups()]);
await loadReport();
} catch (e: unknown) {
error.value = apiErrMessage(e, 'بارگذاری اولیه ناموفق بود.');
message.error(error.value);
}
});
function onTableChange(pag: TablePaginationConfig) {
page.value = pag.current ?? 1;
pageSize.value = pag.pageSize ?? 50;
void loadReport();
}
watch([connectionId, groupIds, kinds, jensFelez, personId], () => {
page.value = 1;
});
onUnmounted(() => stopPoll());
</script>
<template>
<a-row :gutter="[24, 24]">
<a-col :span="24">
<a-flex
justify="space-between"
align="center"
wrap="wrap"
:gap="token.marginSM"
>
<div>
<a-typography-title :level="3" :style="{ margin: 0 }">
گزارش فروش ته‌حساب
</a-typography-title>
<a-typography-text type="secondary">
وزن خالص فروش و خرید بعد از مرجوعی، به تفکیک گروه، کارشناس و فروش اینترنتی
</a-typography-text>
</div>
<a-space :size="8" wrap>
<a-button @click="router.push({ name: 'reports' })">
<template #icon><ArrowRightOutlined /></template>
بازگشت
</a-button>
<a-button type="primary" :loading="syncing" @click="startSync">
<template #icon><SyncOutlined /></template>
به‌روزرسانی از ته‌حساب
</a-button>
</a-space>
</a-flex>
</a-col>
<a-col v-if="error" :span="24">
<a-alert type="error" show-icon :message="error" closable @close="error = ''" />
</a-col>
<a-col v-if="syncHint" :span="24">
<a-alert type="info" show-icon :message="syncHint" />
</a-col>
<a-col :span="24">
<a-card :bordered="false">
<template #title>
<a-space :size="8">
<FilterOutlined />
<span>فیلترها</span>
</a-space>
</template>
<a-form layout="vertical" @submit.prevent>
<a-row :gutter="[16, 8]">
<a-col :xs="24" :sm="12" :md="8" :lg="6">
<a-form-item label="اتصال">
<a-select
v-model:value="connectionId"
:options="
connections.map((c) => ({ value: c.id, label: c.label }))
"
placeholder="اتصال ته‌حساب"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :xs="24" :lg="12">
<a-form-item label="بازه">
<a-segmented
v-model:value="period"
block
:options="[
{ label: 'هفته', value: 'week' },
{ label: 'ماه', value: 'month' },
{ label: 'فصل', value: 'season' },
{ label: 'تاریخ دلخواه', value: 'custom' },
]"
@change="(value: string | number) => applyNamedPeriod(value as 'week' | 'month' | 'season' | 'custom', true)"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="8" :lg="6">
<a-form-item label="از تاریخ">
<JalaliDateInput v-model="fromDate" placeholder="از تاریخ" @update:model-value="markCustomPeriod" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="8" :lg="6">
<a-form-item label="تا تاریخ">
<JalaliDateInput v-model="toDate" placeholder="تا تاریخ" @update:model-value="markCustomPeriod" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="8" :lg="6">
<a-form-item label="گروه مشتری">
<a-select
v-model:value="groupIds"
mode="multiple"
allow-clear
placeholder="همه گروه‌ها"
:options="
(customerGroups as CustomerGroupDto[]).map((g) => ({
value: g.id,
label: groupOptionLabel(g),
}))
"
:max-tag-count="2"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="8" :lg="6">
<a-form-item label="نوع سند">
<a-select
v-model:value="kinds"
mode="multiple"
:options="kindOptions"
placeholder="همه انواع"
:max-tag-count="2"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="8" :lg="6">
<a-form-item label="فلز">
<a-select
v-model:value="jensFelez"
allow-clear
placeholder="همه"
:options="metalOptions"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12" :md="8" :lg="6">
<a-form-item label="شخص">
<a-select
v-model:value="personId"
show-search
allow-clear
placeholder="جستجوی نام…"
:filter-option="false"
:options="personOptions"
@search="searchPeople"
>
<template #suffixIcon><SearchOutlined /></template>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
<a-typography-text v-if="lastSyncedLabel" type="secondary">
آخرین همگام‌سازی: {{ lastSyncedLabel }}
</a-typography-text>
<a-typography-text v-else type="secondary">
بعد از انتخاب بازه، فیلتر را اعمال کنید یا از ته‌حساب به‌روز کنید.
</a-typography-text>
<a-button type="primary" :loading="loading" @click="applyFilters">
اعمال فیلتر
</a-button>
</a-flex>
</a-form>
</a-card>
</a-col>
<a-col v-if="canGroups" :span="24">
<a-collapse :bordered="false">
<a-collapse-panel key="parents" header="چیدمان گروه اصلی و فرعی">
<a-typography-paragraph type="secondary">
گروه فرعی را به یک گروه اصلی وصل کنید تا فیلتر فروش، زیرگروه‌ها را هم بیاورد.
</a-typography-paragraph>
<a-row :gutter="[12, 12]" align="bottom">
<a-col :xs="24" :md="9">
<a-form-item label="گروه فرعی">
<a-select
:value="childGroupId"
show-search
allow-clear
placeholder="جستجوی گروه"
option-filter-prop="label"
:options="childGroupOptions"
@change="(value: string | undefined) => onChildGroup(value)"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="9">
<a-form-item label="گروه اصلی">
<a-select
v-model:value="parentGroupId"
show-search
allow-clear
placeholder="بدون گروه اصلی"
option-filter-prop="label"
:disabled="!childGroupId"
:options="childGroupId ? parentChoices(childGroupId) : []"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="6">
<a-form-item label=" ">
<a-button type="primary" block :disabled="!childGroupId" @click="saveGroupParent">
ذخیره ارتباط
</a-button>
</a-form-item>
</a-col>
</a-row>
<a-list
v-if="linkedGroups.length"
:data-source="linkedGroups"
:locale="{ emptyText: 'هنوز گروهی زیرمجموعه نشده.' }"
>
<template #renderItem="{ item }">
<a-list-item>
<a-typography-text>{{ item.parentName }} / {{ item.name }}</a-typography-text>
<template #actions>
<a-button type="link" @click="clearGroupParent(item.id)">جدا کردن</a-button>
</template>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
</a-collapse>
</a-col>
<a-col v-if="overviewError" :span="24">
<a-alert type="warning" show-icon :message="overviewError" />
</a-col>
<a-col :span="24">
<SalesReportBoard :overview="overview" />
</a-col>
<a-col v-if="summary" :xs="24" :sm="8">
<a-card :bordered="false">
<a-statistic
title="تعداد ردیف"
:value="toFaDigits(String(summary.totalLines))"
/>
</a-card>
</a-col>
<a-col v-if="summary" :xs="24" :sm="8">
<a-card :bordered="false">
<a-statistic title="جمع مبلغ" :value="formatMali(summary.maliSum)" />
</a-card>
</a-col>
<a-col v-if="summary" :xs="24" :sm="8">
<a-card :bordered="false">
<a-statistic title="جمع وزن" :value="formatNum(summary.vaznSum)" />
</a-card>
</a-col>
<a-col v-if="summary?.byKind?.length" :xs="24" :lg="12">
<a-card :bordered="false" title="به تفکیک نوع">
<a-list :data-source="summary.byKind" item-layout="horizontal">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :title="item.label">
<template #description>
{{ toFaDigits(String(item.lineCount)) }} ردیف ·
{{ formatMali(item.maliSum) }} · وزن {{ formatNum(item.vaznSum) }}
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col v-if="summary?.byGroup?.length" :xs="24" :lg="12">
<a-card :bordered="false" title="به تفکیک گروه">
<a-list
:data-source="summary.byGroup.slice(0, 8)"
item-layout="horizontal"
>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :title="item.label">
<template #description>
{{ toFaDigits(String(item.lineCount)) }} ردیف ·
{{ formatMali(item.maliSum) }}
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col v-if="summary?.distinctNos?.length" :span="24">
<a-card :bordered="false" title="انواع سند خام (NO)">
<a-space wrap>
<a-tag v-for="no in summary.distinctNos" :key="no">{{ no }}</a-tag>
</a-space>
</a-card>
</a-col>
<a-col :span="24">
<a-card :bordered="false" title="ردیف‌های گزارش">
<a-table
:columns="columns"
:data-source="report?.items ?? []"
:loading="loading"
:pagination="{
current: page,
pageSize,
total: report?.total ?? 0,
showSizeChanger: true,
showTotal: (total) => `${toFaDigits(String(total))} ردیف`,
pageSizeOptions: ['20', '50', '100'],
}"
row-key="id"
:scroll="{ x: 1100 }"
@change="onTableChange"
>
<template #emptyText>
<a-empty description="برای این بازه ردیفی نیست. بازه را اعمال کنید یا از ته‌حساب به‌روز کنید." />
</template>
</a-table>
</a-card>
</a-col>
</a-row>
</template>