Add Didar-style conversion rate to sales reports.
Show won-over-incoming deals for the selected period with time, user, and source breakdowns alongside the existing win-rate metric. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -712,6 +712,25 @@ function exportReport() {
|
||||
...d.team.map(
|
||||
(t) => `${t.fullName},${t.activitiesDone},${t.wonAmount}`,
|
||||
),
|
||||
'',
|
||||
'نرخ تبدیل (ورودی به موفق)',
|
||||
`کل,${d.conversionRate?.conversionPercent ?? ''}٪`,
|
||||
`ورودی,${d.conversionRate?.incomingCount ?? 0}`,
|
||||
`موفق,${d.conversionRate?.wonCount ?? 0}`,
|
||||
'',
|
||||
'نرخ تبدیل بر اساس زمان',
|
||||
'بازه,ورودی,موفق,نرخ٪',
|
||||
...(d.conversionRate?.byTime ?? []).map(
|
||||
(r) =>
|
||||
`${r.label},${r.incomingCount},${r.wonCount},${r.conversionPercent}`,
|
||||
),
|
||||
'',
|
||||
'نرخ تبدیل بر اساس کاربر',
|
||||
'کاربر,ورودی,موفق,نرخ٪',
|
||||
...(d.conversionRate?.byUser ?? []).map(
|
||||
(r) =>
|
||||
`${r.label},${r.incomingCount},${r.wonCount},${r.conversionPercent}`,
|
||||
),
|
||||
];
|
||||
const blob = new Blob(['\ufeff' + lines.join('\n')], {
|
||||
type: 'text/csv;charset=utf-8',
|
||||
@@ -879,6 +898,86 @@ const monthlyOption = computed<UiChartOption>(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const conversionTimeOption = computed<UiChartOption>(() => {
|
||||
const rows = data.value?.conversionRate?.byTime ?? [];
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (params: any) => {
|
||||
const list = Array.isArray(params) ? params : [params];
|
||||
const name = list[0]?.name ?? '';
|
||||
const row = rows.find((r) => r.label === name);
|
||||
if (!row) return name;
|
||||
return `${name}<br/>نرخ تبدیل: <strong>${toFaDigits(row.conversionPercent)}٪</strong><br/>موفق: ${num(row.wonCount)} از ${num(row.incomingCount)}`;
|
||||
},
|
||||
},
|
||||
grid: { left: '3%', right: '4%', bottom: '8%', top: '14%', containLabel: true },
|
||||
legend: {
|
||||
top: 0,
|
||||
textStyle: { fontFamily: 'IRANSans', fontSize: 11, color: '#475569' },
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: rows.map((r) => r.label),
|
||||
axisLine: { lineStyle: { color: '#cbd5e1' } },
|
||||
axisLabel: { color: '#64748b', fontFamily: 'IRANSans' },
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: 'تعداد',
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { color: '#f1f5f9', type: 'dashed' } },
|
||||
axisLabel: {
|
||||
color: '#64748b',
|
||||
fontFamily: 'IRANSans',
|
||||
formatter: (v: number) => num(v),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: 'نرخ ٪',
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLine: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: {
|
||||
color: '#64748b',
|
||||
fontFamily: 'IRANSans',
|
||||
formatter: (v: number) => `${toFaDigits(v)}٪`,
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'ورودی',
|
||||
type: 'bar',
|
||||
barMaxWidth: 28,
|
||||
itemStyle: { color: '#94a3b8', borderRadius: [6, 6, 0, 0] },
|
||||
data: rows.map((r) => r.incomingCount),
|
||||
},
|
||||
{
|
||||
name: 'موفق',
|
||||
type: 'bar',
|
||||
barMaxWidth: 28,
|
||||
itemStyle: { color: '#14b8a6', borderRadius: [6, 6, 0, 0] },
|
||||
data: rows.map((r) => r.wonCount),
|
||||
},
|
||||
{
|
||||
name: 'نرخ تبدیل',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
smooth: true,
|
||||
showSymbol: true,
|
||||
symbolSize: 7,
|
||||
itemStyle: { color: '#0f766e' },
|
||||
lineStyle: { width: 2, color: '#0f766e' },
|
||||
data: rows.map((r) => r.conversionPercent),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const topProductsOption = computed<UiChartOption>(() => {
|
||||
const prods = (data.value?.topProducts ?? []).slice(0, 5).reverse();
|
||||
return {
|
||||
@@ -1386,6 +1485,113 @@ const activityMax = computed(() =>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="deep-grid conversion-grid">
|
||||
<UiCard class="chart-card span-2" title="نرخ تبدیل">
|
||||
<p class="deep-hint">
|
||||
از معاملات تازهثبتشده در این بازه، چند درصد تا الان به فروش موفق رسیدهاند
|
||||
(متفاوت از «نرخ برد» که فقط معاملات بستهشده را میبیند).
|
||||
</p>
|
||||
<div
|
||||
v-if="data.conversionRate"
|
||||
class="conversion-summary"
|
||||
>
|
||||
<div class="conversion-stat">
|
||||
<span>نرخ تبدیل</span>
|
||||
<strong>
|
||||
{{ toFaDigits(data.conversionRate.conversionPercent) }}٪
|
||||
</strong>
|
||||
<small>
|
||||
{{ formatChange(data.conversionRate.changePercent) }} نسبت به دوره قبل
|
||||
</small>
|
||||
</div>
|
||||
<div class="conversion-stat">
|
||||
<span>ورودی دوره</span>
|
||||
<strong>{{ num(data.conversionRate.incomingCount) }}</strong>
|
||||
<small>
|
||||
دوره قبل {{ num(data.conversionRate.previousIncomingCount) }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="conversion-stat">
|
||||
<span>موفق از همین ورودیها</span>
|
||||
<strong>{{ num(data.conversionRate.wonCount) }}</strong>
|
||||
<small>
|
||||
دوره قبل {{ num(data.conversionRate.previousWonCount) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<UiChart
|
||||
v-if="data.conversionRate?.byTime?.length"
|
||||
:option="conversionTimeOption"
|
||||
height="240px"
|
||||
/>
|
||||
<p v-else class="empty-msg">در این بازه معامله ورودی ثبت نشده است.</p>
|
||||
</UiCard>
|
||||
|
||||
<UiCard class="chart-card" title="نرخ تبدیل بر اساس کاربر">
|
||||
<p class="deep-hint">
|
||||
کیفیت تبدیل فرصتهای هر فروشنده در همین بازه.
|
||||
</p>
|
||||
<ul
|
||||
v-if="data.conversionRate?.byUser?.length"
|
||||
class="conversion-list"
|
||||
>
|
||||
<li
|
||||
v-for="u in data.conversionRate.byUser.slice(0, 10)"
|
||||
:key="u.key"
|
||||
>
|
||||
<div class="conversion-list-top">
|
||||
<strong>{{ u.label }}</strong>
|
||||
<em>{{ toFaDigits(u.conversionPercent) }}٪</em>
|
||||
</div>
|
||||
<div class="hb-track">
|
||||
<div
|
||||
class="hb-fill"
|
||||
:style="{
|
||||
width: `${Math.max(4, Math.min(100, u.conversionPercent))}%`,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<small>
|
||||
{{ num(u.wonCount) }} موفق از {{ num(u.incomingCount) }} ورودی
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="empty-msg">داده کاربری نیست.</p>
|
||||
</UiCard>
|
||||
|
||||
<UiCard class="chart-card" title="نرخ تبدیل بر اساس منبع">
|
||||
<p class="deep-hint">
|
||||
کدام شیوه آشنایی / منبع ورودی بهتر به فروش میرسد.
|
||||
</p>
|
||||
<ul
|
||||
v-if="data.conversionRate?.bySource?.length"
|
||||
class="conversion-list"
|
||||
>
|
||||
<li
|
||||
v-for="s in data.conversionRate.bySource.slice(0, 10)"
|
||||
:key="s.key"
|
||||
>
|
||||
<div class="conversion-list-top">
|
||||
<strong>{{ s.label }}</strong>
|
||||
<em>{{ toFaDigits(s.conversionPercent) }}٪</em>
|
||||
</div>
|
||||
<div class="hb-track">
|
||||
<div
|
||||
class="hb-fill"
|
||||
:style="{
|
||||
width: `${Math.max(4, Math.min(100, s.conversionPercent))}%`,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<small>
|
||||
{{ num(s.wonCount) }} موفق از {{ num(s.incomingCount) }} ورودی
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="empty-msg">منبعی ثبت نشده است.</p>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="charts-grid">
|
||||
<!-- Sales trend (wide) -->
|
||||
<UiCard class="chart-card span-2" title="روند فروش">
|
||||
@@ -1757,6 +1963,68 @@ const activityMax = computed(() =>
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.conversion-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--sp-3);
|
||||
margin-bottom: var(--sp-4);
|
||||
}
|
||||
|
||||
.conversion-stat {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem 0.85rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-subtle, #f8fafc);
|
||||
}
|
||||
|
||||
.conversion-stat span {
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.conversion-stat strong {
|
||||
font-size: 1.35rem;
|
||||
color: var(--accent-deep, #0f766e);
|
||||
}
|
||||
|
||||
.conversion-stat small {
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--ink-secondary);
|
||||
}
|
||||
|
||||
.conversion-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.conversion-list li {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.conversion-list-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.conversion-list-top em {
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
color: var(--accent-deep, #0f766e);
|
||||
}
|
||||
|
||||
.conversion-list small {
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-xs);
|
||||
}
|
||||
|
||||
.funnel-wrap {
|
||||
display: grid;
|
||||
gap: var(--sp-4);
|
||||
@@ -1939,6 +2207,10 @@ const activityMax = computed(() =>
|
||||
.deep-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.conversion-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-toolbar {
|
||||
|
||||
Reference in New Issue
Block a user