Files
CRM_Montzar/apps/web/src/views/CommissionsView.vue
T
2026-10-05 11:56:49 +03:30

309 lines
8.3 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.
<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>