309 lines
8.3 KiB
Vue
309 lines
8.3 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 {
|
||
UiBadge,
|
||
UiButton,
|
||
UiCard,
|
||
UiChart,
|
||
UiEmptyState,
|
||
UiPageHeader,
|
||
type UiChartOption,
|
||
} from '@/components/ui';
|
||
import UiIcon from '@/components/UiIcon.vue';
|
||
import type { CommissionEntryDto } from '@mento/shared';
|
||
|
||
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 chartOption = computed<UiChartOption>(() => {
|
||
const userMap = new Map<string, number>();
|
||
for (const r of posted.value) {
|
||
userMap.set(r.userName, (userMap.get(r.userName) || 0) + r.amount);
|
||
}
|
||
const names = Array.from(userMap.keys());
|
||
const values = Array.from(userMap.values());
|
||
|
||
return {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: { type: 'shadow' },
|
||
formatter: (params: unknown) => {
|
||
const p = (params as Array<{ name: string; value: number }>)[0];
|
||
if (!p) return '';
|
||
return `${p.name}: <b>${formatMoney(p.value)}</b>`;
|
||
},
|
||
},
|
||
grid: {
|
||
top: 20,
|
||
right: 20,
|
||
bottom: 40,
|
||
left: 60,
|
||
containLabel: true,
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: names,
|
||
axisLabel: {
|
||
fontSize: 12,
|
||
color: '#64748b',
|
||
},
|
||
axisLine: { lineStyle: { color: '#e2e8f0' } },
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: {
|
||
fontSize: 11,
|
||
color: '#64748b',
|
||
formatter: (val: number) => (val >= 1000000 ? `${toFaDigits(String(val / 1000000))}M` : toFaDigits(String(val))),
|
||
},
|
||
splitLine: { lineStyle: { color: '#f1f5f9' } },
|
||
},
|
||
series: [
|
||
{
|
||
name: 'پورسانت',
|
||
type: 'bar',
|
||
barWidth: '36%',
|
||
data: values,
|
||
itemStyle: {
|
||
color: '#0f766e',
|
||
borderRadius: [4, 4, 0, 0],
|
||
},
|
||
},
|
||
],
|
||
};
|
||
});
|
||
|
||
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>
|
||
<section class="crm-page commissions-page" dir="rtl">
|
||
<UiPageHeader
|
||
title="پورسانت فروش"
|
||
description="گزارش پورسانت ثبتشده برای کارشناسان فروش متصل به فاکتورهای صادره"
|
||
>
|
||
<template #actions>
|
||
<UiButton variant="ghost" :disabled="loading" @click="load">
|
||
<template #icon>
|
||
<UiIcon name="refresh" />
|
||
</template>
|
||
بروزرسانی
|
||
</UiButton>
|
||
</template>
|
||
</UiPageHeader>
|
||
|
||
<div class="kpi-grid">
|
||
<UiCard class="kpi-card">
|
||
<div class="kpi-icon-wrap primary">
|
||
<UiIcon name="dollar" />
|
||
</div>
|
||
<div class="kpi-body">
|
||
<span class="kpi-label">جمع پورسانت ثبتشده</span>
|
||
<strong class="kpi-val">{{ formatMoney(total) }}</strong>
|
||
<small class="kpi-sub">{{ toFaDigits(String(posted.length)) }} فاکتور تایید شده</small>
|
||
</div>
|
||
</UiCard>
|
||
|
||
<UiCard class="kpi-card">
|
||
<div class="kpi-icon-wrap success">
|
||
<UiIcon name="document" />
|
||
</div>
|
||
<div class="kpi-body">
|
||
<span class="kpi-label">تعداد تراکنشها</span>
|
||
<strong class="kpi-val">{{ toFaDigits(String(items.length)) }}</strong>
|
||
<small class="kpi-sub">{{ toFaDigits(String(posted.length)) }} فعال / {{ toFaDigits(String(items.length - posted.length)) }} باطل</small>
|
||
</div>
|
||
</UiCard>
|
||
|
||
<UiCard class="kpi-card">
|
||
<div class="kpi-icon-wrap warning">
|
||
<UiIcon name="trend" />
|
||
</div>
|
||
<div class="kpi-body">
|
||
<span class="kpi-label">میانگین هر فاکتور</span>
|
||
<strong class="kpi-val">{{ formatMoney(avgCommission) }}</strong>
|
||
<small class="kpi-sub">بر اساس موارد فعال</small>
|
||
</div>
|
||
</UiCard>
|
||
</div>
|
||
|
||
<UiCard v-if="posted.length > 0" title="سهم پورسانت به تفکیک کارشناسان" class="chart-card">
|
||
<UiChart :option="chartOption" height="260px" />
|
||
</UiCard>
|
||
|
||
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
|
||
|
||
<UiEmptyState
|
||
v-else-if="!loading && !items.length"
|
||
title="پورسانتی ثبت نشده است"
|
||
description="هنوز هیچ فاکتوری دارای پورسانت در سامانه ثبت نگردیده است."
|
||
icon="document"
|
||
/>
|
||
|
||
<div v-else class="crm-table-container">
|
||
<div class="tc-row">
|
||
<TableColumnsMenu table-key="commissions" align="end" />
|
||
</div>
|
||
<table v-table-cols="'commissions'" class="crm-table">
|
||
<thead>
|
||
<tr>
|
||
<th>کارشناس فروش</th>
|
||
<th>شماره و عنوان فاکتور</th>
|
||
<th>درصد</th>
|
||
<th>مبلغ پورسانت</th>
|
||
<th>وضعیت</th>
|
||
<th>تاریخ ثبت</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in items" :key="row.id">
|
||
<td class="font-medium text-primary">{{ row.userName }}</td>
|
||
<td>
|
||
<RouterLink :to="{ name: 'invoice-detail', params: { id: row.invoiceId } }" class="table-link">
|
||
{{ row.invoiceNumber }}
|
||
</RouterLink>
|
||
<span v-if="row.invoiceTitle" class="invoice-sub-title">{{ row.invoiceTitle }}</span>
|
||
</td>
|
||
<td>
|
||
<UiBadge variant="neutral">{{ toFaDigits(String(row.percent)) }}٪</UiBadge>
|
||
</td>
|
||
<td class="amount-cell">{{ formatMoney(row.amount, row.currency) }}</td>
|
||
<td>
|
||
<UiBadge :variant="row.status === 'posted' ? 'success' : 'neutral'">
|
||
{{ row.status === 'posted' ? 'ثبتشده' : 'باطل' }}
|
||
</UiBadge>
|
||
</td>
|
||
<td>{{ formatJalaliNumeric(new Date(row.createdAt)) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.commissions-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-6);
|
||
}
|
||
|
||
.kpi-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
|
||
gap: var(--sp-4);
|
||
}
|
||
|
||
.kpi-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--sp-4);
|
||
padding: var(--sp-4) var(--sp-5);
|
||
}
|
||
|
||
.kpi-icon-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: var(--radius-lg);
|
||
font-size: 1.25rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.kpi-icon-wrap.primary {
|
||
background: var(--color-brand-50, #f0fdfa);
|
||
color: var(--color-brand-600, #0f766e);
|
||
}
|
||
|
||
.kpi-icon-wrap.success {
|
||
background: var(--color-success-bg, #f0fdf4);
|
||
color: var(--color-success-text, #15803d);
|
||
}
|
||
|
||
.kpi-icon-wrap.warning {
|
||
background: var(--color-warning-bg, #fffbeb);
|
||
color: var(--color-warning-text, #b45309);
|
||
}
|
||
|
||
.kpi-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.kpi-label {
|
||
font-size: var(--fs-xs);
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.kpi-val {
|
||
font-size: var(--fs-lg);
|
||
font-weight: 700;
|
||
color: var(--color-text-primary);
|
||
}
|
||
|
||
.kpi-sub {
|
||
font-size: var(--fs-xs);
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
.chart-card {
|
||
margin-top: var(--sp-2);
|
||
}
|
||
|
||
.font-medium {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.text-primary {
|
||
color: var(--color-text-primary);
|
||
}
|
||
|
||
.amount-cell {
|
||
font-weight: 700;
|
||
color: var(--color-brand-700, #0f766e);
|
||
}
|
||
|
||
.table-link {
|
||
color: var(--color-brand-600, #0f766e);
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.table-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.invoice-sub-title {
|
||
display: block;
|
||
font-size: var(--fs-xs);
|
||
color: var(--color-text-muted);
|
||
margin-top: 2px;
|
||
}
|
||
</style>
|