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

237 lines
8.3 KiB
Vue
Raw Normal View History

<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,
Spin as ASpin,
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 '@ant-design/icons-vue';
import type { CommissionEntryDto } from '@mento/shared';
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' }"
/>
<a-spin :spinning="loading">
<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>
</a-spin>
</a-card>
</a-flex>
</template>