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

309 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 {
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>