Replace the inline SVG icon set with the vuesax linear pack, add skeleton primitives for loading states, and keep page scroll inside the app shell. Co-authored-by: Cursor <cursoragent@cursor.com>
239 lines
8.5 KiB
Vue
239 lines
8.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
|
|
import { RouterLink } from 'vue-router';
|
|
import { api } from '@/lib/api';
|
|
import { formatMoney } from '@/lib/money';
|
|
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
|
import { registerTableColumns, useTableColumns } from '@/lib/tableColumns';
|
|
import {
|
|
Alert as AAlert,
|
|
Button as AButton,
|
|
Card as ACard,
|
|
Col as ACol,
|
|
Empty as AEmpty,
|
|
Flex as AFlex,
|
|
Progress as AProgress,
|
|
Row as ARow,
|
|
Statistic as AStatistic,
|
|
Table as ATable,
|
|
Tag as ATag,
|
|
Typography,
|
|
} from 'ant-design-vue';
|
|
import type { TableColumnsType } from 'ant-design-vue';
|
|
import {
|
|
FileTextOutlined,
|
|
ReloadOutlined,
|
|
RiseOutlined,
|
|
WalletOutlined,
|
|
} from '@/icons/ant-bridge';
|
|
import type { CommissionEntryDto } from '@mento/shared';
|
|
import { CrmSkeleton, CrmSkeletonTable } from '@/components/skeleton';
|
|
|
|
const { Title: ATypographyTitle, Text: ATypographyText, Link: ATypographyLink } = Typography;
|
|
|
|
const COMMISSION_COL_LABELS = [
|
|
'کارشناس فروش',
|
|
'شماره و عنوان فاکتور',
|
|
'درصد',
|
|
'مبلغ پورسانت',
|
|
'وضعیت',
|
|
'تاریخ ثبت',
|
|
] as const;
|
|
|
|
registerTableColumns('commissions', [...COMMISSION_COL_LABELS]);
|
|
const tableCols = useTableColumns(() => 'commissions');
|
|
|
|
const loading = ref(false);
|
|
const error = ref('');
|
|
const items = ref<CommissionEntryDto[]>([]);
|
|
|
|
const posted = computed(() => items.value.filter((r) => r.status === 'posted'));
|
|
const total = computed(() => posted.value.reduce((sum, r) => sum + r.amount, 0));
|
|
const avgCommission = computed(() => (posted.value.length ? Math.round(total.value / posted.value.length) : 0));
|
|
|
|
const chartData = computed(() => {
|
|
const userMap = new Map<string, number>();
|
|
for (const r of posted.value) {
|
|
userMap.set(r.userName, (userMap.get(r.userName) || 0) + r.amount);
|
|
}
|
|
const rows = Array.from(userMap.entries())
|
|
.map(([name, value]) => ({ name, value }))
|
|
.sort((a, b) => b.value - a.value);
|
|
const max = rows.length ? rows[0].value : 0;
|
|
return rows.map((row) => ({
|
|
...row,
|
|
percent: max > 0 ? Math.round((row.value / max) * 100) : 0,
|
|
}));
|
|
});
|
|
|
|
const columns = computed<TableColumnsType<CommissionEntryDto>>(() => {
|
|
const all: TableColumnsType<CommissionEntryDto> = [
|
|
{ title: 'کارشناس فروش', key: 'کارشناس فروش' },
|
|
{ title: 'شماره و عنوان فاکتور', key: 'شماره و عنوان فاکتور' },
|
|
{
|
|
title: 'درصد',
|
|
key: 'درصد',
|
|
customRender: ({ record }) => `${toFaDigits(String(record.percent))}٪`,
|
|
},
|
|
{ title: 'مبلغ پورسانت', key: 'مبلغ پورسانت' },
|
|
{ title: 'وضعیت', key: 'وضعیت' },
|
|
{
|
|
title: 'تاریخ ثبت',
|
|
key: 'تاریخ ثبت',
|
|
customRender: ({ record }) => formatJalaliNumeric(new Date(record.createdAt)),
|
|
},
|
|
];
|
|
return all.filter((col) => {
|
|
const key = String(col.key ?? '');
|
|
if (!COMMISSION_COL_LABELS.includes(key as (typeof COMMISSION_COL_LABELS)[number])) {
|
|
return true;
|
|
}
|
|
return tableCols.isVisible(key);
|
|
});
|
|
});
|
|
|
|
async function load() {
|
|
loading.value = true;
|
|
error.value = '';
|
|
try {
|
|
const { data } = await api.get<CommissionEntryDto[]>('/commissions');
|
|
items.value = data;
|
|
} catch {
|
|
error.value = 'بارگذاری پورسانتها ناموفق بود.';
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(load);
|
|
</script>
|
|
|
|
<template>
|
|
<a-flex vertical :gap="16">
|
|
<a-flex justify="space-between" align="center" wrap="wrap" :gap="12">
|
|
<div>
|
|
<a-typography-title :level="3" :style="{ margin: 0 }">پورسانت فروش</a-typography-title>
|
|
</div>
|
|
<a-button :disabled="loading" :loading="loading" @click="load">
|
|
<template #icon><ReloadOutlined /></template>
|
|
بروزرسانی
|
|
</a-button>
|
|
</a-flex>
|
|
|
|
<a-row :gutter="[16, 16]">
|
|
<a-col :xs="24" :md="8">
|
|
<a-card :bordered="false">
|
|
<a-statistic title="جمع پورسانت ثبتشده" :value="formatMoney(total)">
|
|
<template #prefix><WalletOutlined /></template>
|
|
</a-statistic>
|
|
<a-typography-text type="secondary">
|
|
{{ toFaDigits(String(posted.length)) }} فاکتور تایید شده
|
|
</a-typography-text>
|
|
</a-card>
|
|
</a-col>
|
|
<a-col :xs="24" :md="8">
|
|
<a-card :bordered="false">
|
|
<a-statistic title="تعداد تراکنشها" :value="toFaDigits(String(items.length))">
|
|
<template #prefix><FileTextOutlined /></template>
|
|
</a-statistic>
|
|
<a-typography-text type="secondary">
|
|
{{ toFaDigits(String(posted.length)) }} فعال /
|
|
{{ toFaDigits(String(items.length - posted.length)) }} باطل
|
|
</a-typography-text>
|
|
</a-card>
|
|
</a-col>
|
|
<a-col :xs="24" :md="8">
|
|
<a-card :bordered="false">
|
|
<a-statistic title="میانگین هر فاکتور" :value="formatMoney(avgCommission)">
|
|
<template #prefix><RiseOutlined /></template>
|
|
</a-statistic>
|
|
<a-typography-text type="secondary">بر اساس موارد فعال</a-typography-text>
|
|
</a-card>
|
|
</a-col>
|
|
</a-row>
|
|
|
|
<a-card v-if="posted.length > 0" title="سهم پورسانت به تفکیک کارشناسان" :bordered="false">
|
|
<a-flex vertical :gap="16">
|
|
<div v-for="row in chartData" :key="row.name">
|
|
<a-flex justify="space-between" align="center" :gap="12">
|
|
<a-typography-text strong>{{ row.name }}</a-typography-text>
|
|
<a-typography-text>{{ formatMoney(row.value) }}</a-typography-text>
|
|
</a-flex>
|
|
<a-progress
|
|
:percent="row.percent"
|
|
:format="(p?: number) => `${toFaDigits(String(p ?? 0))}٪`"
|
|
/>
|
|
</div>
|
|
</a-flex>
|
|
</a-card>
|
|
|
|
<a-card :bordered="false">
|
|
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
|
|
<a-typography-text type="secondary">
|
|
{{ toFaDigits(String(items.length)) }} پورسانت
|
|
</a-typography-text>
|
|
<TableColumnsMenu table-key="commissions" align="end" />
|
|
</a-flex>
|
|
|
|
<a-alert
|
|
v-if="error"
|
|
type="error"
|
|
show-icon
|
|
:message="error"
|
|
:style="{ marginTop: '16px' }"
|
|
/>
|
|
|
|
<CrmSkeleton :loading="loading">
|
|
<CrmSkeletonTable :rows="8" :columns="6" :card="false">
|
|
<a-table
|
|
:columns="columns"
|
|
:data-source="items"
|
|
:pagination="false"
|
|
row-key="id"
|
|
:scroll="{ x: 'max-content' }"
|
|
:style="{ marginTop: '16px' }"
|
|
>
|
|
<template #emptyText>
|
|
<a-empty
|
|
description="پورسانتی ثبت نشده است؛ هنوز هیچ فاکتوری دارای پورسانت در سامانه ثبت نگردیده است."
|
|
/>
|
|
</template>
|
|
<template #bodyCell="{ column, record }">
|
|
<template v-if="column.key === 'کارشناس فروش'">
|
|
<a-typography-text strong>{{ record.userName }}</a-typography-text>
|
|
</template>
|
|
<template v-else-if="column.key === 'شماره و عنوان فاکتور'">
|
|
<a-flex vertical :gap="0">
|
|
<RouterLink
|
|
:to="{ name: 'invoice-detail', params: { id: record.invoiceId } }"
|
|
custom
|
|
v-slot="{ href, navigate }"
|
|
>
|
|
<a-typography-link :href="href" @click="navigate">
|
|
{{ record.invoiceNumber }}
|
|
</a-typography-link>
|
|
</RouterLink>
|
|
<a-typography-text v-if="record.invoiceTitle" type="secondary">
|
|
{{ record.invoiceTitle }}
|
|
</a-typography-text>
|
|
</a-flex>
|
|
</template>
|
|
<template v-else-if="column.key === 'مبلغ پورسانت'">
|
|
<a-typography-text strong>
|
|
{{ formatMoney(record.amount, record.currency) }}
|
|
</a-typography-text>
|
|
</template>
|
|
<template v-else-if="column.key === 'وضعیت'">
|
|
<a-tag :color="record.status === 'posted' ? 'success' : 'default'" :bordered="false">
|
|
{{ record.status === 'posted' ? 'ثبتشده' : 'باطل' }}
|
|
</a-tag>
|
|
</template>
|
|
</template>
|
|
</a-table>
|
|
</CrmSkeletonTable>
|
|
</CrmSkeleton>
|
|
</a-card>
|
|
</a-flex>
|
|
</template>
|