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:
@@ -84,6 +84,7 @@ export class ReportsService {
|
||||
status: { in: ['won', 'lost'] },
|
||||
updatedAt: { gte: dealEarliest },
|
||||
},
|
||||
{ createdAt: { gte: dealEarliest } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
@@ -658,6 +659,154 @@ export class ReportsService {
|
||||
const winRatePct = Math.round(winRate * 1000) / 10;
|
||||
const winRatePrevPct = Math.round(winRatePrev * 1000) / 10;
|
||||
|
||||
const incomingCur = deals.filter(
|
||||
(d) => d.createdAt >= start && d.createdAt <= end,
|
||||
);
|
||||
const incomingPrev = deals.filter(
|
||||
(d) => d.createdAt >= prevStart && d.createdAt <= prevEnd,
|
||||
);
|
||||
const convWonCur = incomingCur.filter((d) => d.status === 'won').length;
|
||||
const convWonPrev = incomingPrev.filter((d) => d.status === 'won').length;
|
||||
const conversionPercent = incomingCur.length
|
||||
? Math.round((convWonCur / incomingCur.length) * 1000) / 10
|
||||
: 0;
|
||||
const previousConversionPercent = incomingPrev.length
|
||||
? Math.round((convWonPrev / incomingPrev.length) * 1000) / 10
|
||||
: 0;
|
||||
|
||||
const conversionByTime: ReportsDashboardDto['conversionRate']['byTime'] =
|
||||
[];
|
||||
if (spanDays <= 45) {
|
||||
for (let i = 0; i < spanDays; i += 1) {
|
||||
const dayStart = startOfDay(
|
||||
new Date(start.getFullYear(), start.getMonth(), start.getDate() + i),
|
||||
);
|
||||
const dayEnd = endOfDay(dayStart);
|
||||
if (dayStart > end) break;
|
||||
const dayIncoming = incomingCur.filter(
|
||||
(d) => d.createdAt >= dayStart && d.createdAt <= dayEnd,
|
||||
);
|
||||
const dayWon = dayIncoming.filter((d) => d.status === 'won').length;
|
||||
const j = dateToJalali(dayStart);
|
||||
const key = `${dayStart.getFullYear()}-${String(dayStart.getMonth() + 1).padStart(2, '0')}-${String(dayStart.getDate()).padStart(2, '0')}`;
|
||||
conversionByTime.push({
|
||||
key,
|
||||
label: `${j.jd} ${jalaliMonthName(j.jm)}`,
|
||||
incomingCount: dayIncoming.length,
|
||||
wonCount: dayWon,
|
||||
conversionPercent: dayIncoming.length
|
||||
? Math.round((dayWon / dayIncoming.length) * 1000) / 10
|
||||
: 0,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const buckets = new Map<
|
||||
string,
|
||||
{ label: string; incoming: number; won: number }
|
||||
>();
|
||||
for (const d of incomingCur) {
|
||||
const j = dateToJalali(d.createdAt);
|
||||
const key = `${j.jy}-${String(j.jm).padStart(2, '0')}`;
|
||||
let bucket = buckets.get(key);
|
||||
if (!bucket) {
|
||||
bucket = {
|
||||
label: `${jalaliMonthName(j.jm)} ${j.jy}`,
|
||||
incoming: 0,
|
||||
won: 0,
|
||||
};
|
||||
buckets.set(key, bucket);
|
||||
}
|
||||
bucket.incoming += 1;
|
||||
if (d.status === 'won') bucket.won += 1;
|
||||
}
|
||||
for (const [key, b] of [...buckets.entries()].sort((a, b) =>
|
||||
a[0].localeCompare(b[0]),
|
||||
)) {
|
||||
conversionByTime.push({
|
||||
key,
|
||||
label: b.label,
|
||||
incomingCount: b.incoming,
|
||||
wonCount: b.won,
|
||||
conversionPercent: b.incoming
|
||||
? Math.round((b.won / b.incoming) * 1000) / 10
|
||||
: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const conversionByUserMap = new Map<
|
||||
string,
|
||||
{ label: string; incoming: number; won: number }
|
||||
>();
|
||||
for (const u of users) {
|
||||
conversionByUserMap.set(u.id, {
|
||||
label: u.fullName,
|
||||
incoming: 0,
|
||||
won: 0,
|
||||
});
|
||||
}
|
||||
for (const d of incomingCur) {
|
||||
if (!d.ownerId) continue;
|
||||
const row = conversionByUserMap.get(d.ownerId);
|
||||
if (!row) continue;
|
||||
row.incoming += 1;
|
||||
if (d.status === 'won') row.won += 1;
|
||||
}
|
||||
const conversionByUser: ReportsDashboardDto['conversionRate']['byUser'] = [
|
||||
...conversionByUserMap.entries(),
|
||||
]
|
||||
.map(([key, row]) => ({
|
||||
key,
|
||||
label: row.label,
|
||||
incomingCount: row.incoming,
|
||||
wonCount: row.won,
|
||||
conversionPercent: row.incoming
|
||||
? Math.round((row.won / row.incoming) * 1000) / 10
|
||||
: 0,
|
||||
}))
|
||||
.filter((r) => r.incomingCount > 0)
|
||||
.sort((a, b) => b.conversionPercent - a.conversionPercent);
|
||||
|
||||
const conversionBySourceMap = new Map<
|
||||
string,
|
||||
{ incoming: number; won: number }
|
||||
>();
|
||||
for (const d of incomingCur) {
|
||||
const label = normalizeSource(d.source);
|
||||
const row = conversionBySourceMap.get(label) ?? {
|
||||
incoming: 0,
|
||||
won: 0,
|
||||
};
|
||||
row.incoming += 1;
|
||||
if (d.status === 'won') row.won += 1;
|
||||
conversionBySourceMap.set(label, row);
|
||||
}
|
||||
const conversionBySource: ReportsDashboardDto['conversionRate']['bySource'] =
|
||||
[...conversionBySourceMap.entries()]
|
||||
.map(([label, row]) => ({
|
||||
key: label,
|
||||
label,
|
||||
incomingCount: row.incoming,
|
||||
wonCount: row.won,
|
||||
conversionPercent: row.incoming
|
||||
? Math.round((row.won / row.incoming) * 1000) / 10
|
||||
: 0,
|
||||
}))
|
||||
.sort((a, b) => b.incomingCount - a.incomingCount);
|
||||
|
||||
const conversionRate: ReportsDashboardDto['conversionRate'] = {
|
||||
incomingCount: incomingCur.length,
|
||||
wonCount: convWonCur,
|
||||
conversionPercent,
|
||||
previousIncomingCount: incomingPrev.length,
|
||||
previousWonCount: convWonPrev,
|
||||
previousConversionPercent,
|
||||
changePercent: pctChange(conversionPercent, previousConversionPercent),
|
||||
byTime: conversionByTime,
|
||||
byUser: conversionByUser,
|
||||
bySource: conversionBySource,
|
||||
};
|
||||
|
||||
const periodCompare: ReportsPeriodCompareRowDto[] = [
|
||||
{
|
||||
key: 'sales_amount',
|
||||
@@ -694,6 +843,14 @@ export class ReportsService {
|
||||
previous: winRatePrevPct,
|
||||
changePercent: pctChange(winRatePct, winRatePrevPct),
|
||||
},
|
||||
{
|
||||
key: 'conversion_rate',
|
||||
label: 'نرخ تبدیل',
|
||||
format: 'percent',
|
||||
current: conversionPercent,
|
||||
previous: previousConversionPercent,
|
||||
changePercent: pctChange(conversionPercent, previousConversionPercent),
|
||||
},
|
||||
{
|
||||
key: 'new_customers',
|
||||
label: 'مشتریان جدید',
|
||||
@@ -770,6 +927,11 @@ export class ReportsService {
|
||||
`نرخ برد معاملات در این دوره ${winRatePct}٪ بوده است.`,
|
||||
);
|
||||
}
|
||||
if (incomingCur.length > 0) {
|
||||
insights.push(
|
||||
`نرخ تبدیل ورودیها ${conversionPercent}٪ است (${convWonCur} از ${incomingCur.length} معامله).`,
|
||||
);
|
||||
}
|
||||
if (invoiceCount > 0) {
|
||||
insights.push(
|
||||
`${invoiceCount} فاکتور صادر/پرداختشده با مجموع ${invoiceAmount.toLocaleString('fa-IR')} ثبت شده است.`,
|
||||
@@ -837,6 +999,7 @@ export class ReportsService {
|
||||
winRate: winRatePct,
|
||||
},
|
||||
funnel,
|
||||
conversionRate,
|
||||
periodCompare,
|
||||
tickets: {
|
||||
open: ticketOpen,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -210,6 +210,7 @@ AI پیشنهاد Next Action میدهد؛ اعمال فقط با تأیید
|
||||
|
||||
### گزارشات و داشبورد تحلیلی
|
||||
فروش، قیف، فعالیت، تیم و تیکت با فیلتر تاریخ.
|
||||
نرخ تبدیل (سبک دیدار): از معاملات تازهوارد چند درصد موفق شدهاند — بهتفکیک زمان، کاربر و منبع.
|
||||
|
||||
### تنظیمات تیم
|
||||
دعوت کاربر و مدیریت نقشها.
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
| تاریخ | مورد |
|
||||
|---|---|
|
||||
| ۱۴۰۵/۷/۵ | نرخ تبدیل گزارشات (سبک دیدار): ورودی→موفق + محور زمان/کاربر/منبع |
|
||||
| ۱۴۰۵/۷/۵ | خلاصه خودکار تماس VoIP (ضبط/متن → STT → AI → یادداشت call_result) |
|
||||
| ۱۴۰۵/۷/۵ | همترازی چکلیست: API عمومی/وبهوک، پیامک گروهی، ووکامرس، VoIP؛ درگاه پرداخت عمداً نه |
|
||||
| ۱۴۰۵/۷/۵ | چند مسئول روی معاملهٔ کاریز + ذخیره فیلتر در اشخاص و کاریز |
|
||||
@@ -113,7 +114,7 @@
|
||||
|
||||
| وضعیت | مورد | توضیح |
|
||||
|---|---|---|
|
||||
| [x] | گزارشات و داشبورد | فروش/قیف/فعالیت/تیم/تیکت + فیلتر تاریخ |
|
||||
| [x] | گزارشات و داشبورد | فروش/قیف/فعالیت/تیم/تیکت + فیلتر تاریخ + نرخ تبدیل ورودی→موفق |
|
||||
| [x] | اتوماسیون اگر/آنگاه | deal_won / ticket_resolved → فعالیت، اعلان، امتیاز |
|
||||
| [x] | SMS عملیاتی | OTP auth/پرتال؛ یادآور فعالیت (ترِز) |
|
||||
| [x] | API عمومی + وبهوک خروجی | کلید API؛ `public/v1` افراد/معاملات؛ endpoint وبهوک + تحویل دورهای |
|
||||
|
||||
@@ -2155,6 +2155,33 @@ export interface ReportsFunnelStageDto {
|
||||
shareOfPipelinePercent: number;
|
||||
}
|
||||
|
||||
/** یک ردیف نرخ تبدیل (ورودی → موفق) مثل دیدار */
|
||||
export interface ReportsConversionRateRowDto {
|
||||
key: string;
|
||||
label: string;
|
||||
incomingCount: number;
|
||||
wonCount: number;
|
||||
/** درصد؛ اگر ورودی صفر باشد ۰ */
|
||||
conversionPercent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* نرخ تبدیل به سبک دیدار:
|
||||
* از معاملات تازهساختهشده در بازه، چند درصد الان برنده شدهاند.
|
||||
*/
|
||||
export interface ReportsConversionRateDto {
|
||||
incomingCount: number;
|
||||
wonCount: number;
|
||||
conversionPercent: number;
|
||||
previousIncomingCount: number;
|
||||
previousWonCount: number;
|
||||
previousConversionPercent: number;
|
||||
changePercent: number | null;
|
||||
byTime: ReportsConversionRateRowDto[];
|
||||
byUser: ReportsConversionRateRowDto[];
|
||||
bySource: ReportsConversionRateRowDto[];
|
||||
}
|
||||
|
||||
export interface ReportsPeriodCompareRowDto {
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -2219,6 +2246,8 @@ export interface ReportsDashboardDto {
|
||||
winRate: number;
|
||||
};
|
||||
funnel: ReportsFunnelStageDto[];
|
||||
/** نرخ تبدیل ورودی→موفق (سبک دیدار) */
|
||||
conversionRate: ReportsConversionRateDto;
|
||||
/** مقایسه صریح بازه فعلی با دوره قبل همطول */
|
||||
periodCompare: ReportsPeriodCompareRowDto[];
|
||||
tickets: {
|
||||
|
||||
Reference in New Issue
Block a user