Fill new voice name-flow mock analytics data

This commit is contained in:
Yera All
2026-04-05 10:46:09 +05:00
parent d959c2b2f0
commit 6d8f6e67f0
2 changed files with 150 additions and 45 deletions
+91 -44
View File
@@ -1410,6 +1410,27 @@ function mockVoiceNameSeed() {
return seed && typeof seed === 'object' ? seed : {};
}
function mockVoiceNameLabels() {
const labels = analyticsMockData().voice_name_labels;
return labels && typeof labels === 'object' ? labels : {};
}
function mockVoiceNameLabel(key, fallback) {
return mockVoiceNameLabels()[key] || fallback;
}
function mockVoiceNameProfile(metric, fallback) {
const profile = mockVoiceNameSeed().trend_profiles?.[metric];
if (!profile || typeof profile !== 'object') {
return fallback;
}
return {
base: Number.isFinite(Number(profile.base)) ? Number(profile.base) : fallback.base,
step: Number.isFinite(Number(profile.step)) ? Number(profile.step) : fallback.step,
cycle: Math.max(1, Number.isFinite(Number(profile.cycle)) ? Number(profile.cycle) : fallback.cycle),
};
}
function voiceNameMetricRatesFromTotals(totals) {
const scenarioCalls = Number(totals.scenario_calls || 0);
const neededDownstream = Number(totals.needed_downstream || Math.max(0, scenarioCalls - Number(totals.start_obtained || 0)));
@@ -1439,13 +1460,15 @@ function buildMockVoiceNameOverview(rangeMeta, options = {}) {
const factor = mockAnalyticsScale(previous ? rangeMeta.previous : rangeMeta.current)
* (previous ? 0.91 : 1)
* (queueId === 'all' ? 1 : Math.max(0.48, mockAnalyticsQueueFactor(queueId)));
const scenarioCalls = Math.max(0, Math.round(Number(totalSeed.scenario_calls || 180) * factor));
const startRatio = Number(totalSeed.start_obtained || 110) / Math.max(Number(totalSeed.scenario_calls || 180), 1);
const downstreamRatio = Number(totalSeed.downstream_ai_obtained || 36) / Math.max(Number(totalSeed.scenario_calls || 180), 1);
const followupRatio = Number(totalSeed.followup_required || 20) / Math.max(Number(totalSeed.scenario_calls || 180), 1);
const handoffConfirmedRatio = Number(totalSeed.handoff_confirmed_name || 12) / Math.max(Number(totalSeed.scenario_calls || 180), 1);
const handoffUnconfirmedRatio = Number(totalSeed.handoff_unconfirmed_name || 26) / Math.max(Number(totalSeed.scenario_calls || 180), 1);
const manualRateSeed = Number(totalSeed.manual_corrected || 10) / Math.max((Number(totalSeed.handoff_confirmed_name || 12) + Number(totalSeed.handoff_unconfirmed_name || 26)), 1);
const baseScenarioCalls = Math.max(Number(totalSeed.scenario_calls || 180), 1);
const scenarioCalls = Math.max(0, Math.round(baseScenarioCalls * factor));
const startRatio = Number(totalSeed.start_obtained || 110) / baseScenarioCalls;
const downstreamRatio = Number(totalSeed.downstream_ai_obtained || 36) / baseScenarioCalls;
const followupRatio = Number(totalSeed.followup_required || 20) / baseScenarioCalls;
const handoffConfirmedRatio = Number(totalSeed.handoff_confirmed_name || 12) / baseScenarioCalls;
const handoffUnconfirmedRatio = Number(totalSeed.handoff_unconfirmed_name || 26) / baseScenarioCalls;
const manualRateSeed = Number(totalSeed.manual_corrected || 10)
/ Math.max((Number(totalSeed.handoff_confirmed_name || 12) + Number(totalSeed.handoff_unconfirmed_name || 26)), 1);
const startObtained = Math.min(scenarioCalls, Math.round(scenarioCalls * startRatio));
const downstreamAiObtained = Math.max(0, Math.round(scenarioCalls * downstreamRatio));
@@ -1505,21 +1528,28 @@ function buildMockVoiceNameOverview(rangeMeta, options = {}) {
};
}).filter((item) => item.scenario_calls > 0);
const queueRows = mockAnalyticsQueueOptions()
const queueSeeds = Array.isArray(seed.queues) ? seed.queues : [];
const queueRows = (queueSeeds.length ? queueSeeds : mockAnalyticsQueueOptions().map((item, index) => ({
queue_id: item.queue_id,
share: 0.2 + index * 0.12,
start_capture_rate: 69 - index * 4,
downstream_rescue_rate: 45 - index * 5,
handoff_unconfirmed_rate: 38 + index * 5,
manual_correction_rate: 17 + index * 3,
})))
.filter((item) => queueId === 'all' || item.queue_id === queueId)
.map((item, index) => {
const queueFactor = Math.max(0.28, mockAnalyticsQueueFactor(item.queue_id));
const calls = Math.max(0, Math.round(scenarioCalls * (0.2 + index * 0.12) * queueFactor));
const startCaptured = Math.min(calls, Math.round(calls * (0.69 - index * 0.04)));
.map((item) => {
const calls = Math.max(0, Math.round(scenarioCalls * Number(item.share || 0)));
const startCaptured = Math.min(calls, Math.round(calls * Number(item.start_capture_rate || 0) / 100));
const downstreamNeeded = Math.max(0, calls - startCaptured);
const downstreamCaptured = Math.min(downstreamNeeded, Math.round(downstreamNeeded * (0.45 - index * 0.05)));
const downstreamCaptured = Math.min(downstreamNeeded, Math.round(downstreamNeeded * Number(item.downstream_rescue_rate || 0) / 100));
const unresolved = Math.max(0, calls - startCaptured - downstreamCaptured);
const followup = Math.round(unresolved * 0.57);
const followup = Math.round(unresolved * 0.58);
const missing = Math.max(0, unresolved - followup);
const handoffs = Math.round(unresolved * (0.64 + index * 0.04));
const unconfirmed = Math.min(handoffs, Math.round(handoffs * (0.38 + index * 0.05)));
const handoffs = Math.round(unresolved * 0.72);
const unconfirmed = Math.min(handoffs, Math.round(handoffs * Number(item.handoff_unconfirmed_rate || 0) / 100));
const confirmed = Math.max(0, handoffs - unconfirmed);
const manual = Math.min(handoffs, Math.round(handoffs * (0.17 + index * 0.03)));
const manual = Math.min(handoffs, Math.round(handoffs * Number(item.manual_correction_rate || 0) / 100));
return {
queue_id: item.queue_id,
scenario_calls: calls,
@@ -1543,6 +1573,23 @@ function buildMockVoiceNameOverview(rangeMeta, options = {}) {
})
.filter((item) => item.scenario_calls > 0);
const coverageSeed = seed.coverage || {};
const coverage = {
sessions_with_start_decision: Math.min(
scenarioCalls,
Math.round(scenarioCalls * (Number(coverageSeed.sessions_with_start_decision || baseScenarioCalls) / baseScenarioCalls)),
),
sessions_with_final_ai_state: Math.min(
scenarioCalls,
Math.round(scenarioCalls * (Number(coverageSeed.sessions_with_final_ai_state || baseScenarioCalls) / baseScenarioCalls)),
),
sessions_with_manual_overlay: Math.min(
scenarioCalls,
Math.round(scenarioCalls * (Number(coverageSeed.sessions_with_manual_overlay || manualCorrected) / baseScenarioCalls)),
),
note: coverageSeed.note || analyticsMockData().coverage_note || 'Показаны демонстрационные данные по name-flow.',
};
return {
window: {
from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(),
@@ -1558,56 +1605,56 @@ function buildMockVoiceNameOverview(rangeMeta, options = {}) {
metrics,
breakdowns: {
funnel: [
{ stage: 'scenario_calls', label: 'Звонки в сценарии', sessions: scenarioCalls, share: 100 },
{ stage: 'start_obtained', label: 'Имя взято сразу', sessions: startObtained, share: scenarioCalls ? Number(((startObtained / scenarioCalls) * 100).toFixed(2)) : 0 },
{ stage: 'needed_downstream', label: 'Потребовался AI после старта', sessions: neededDownstream, share: scenarioCalls ? Number(((neededDownstream / scenarioCalls) * 100).toFixed(2)) : 0 },
{ stage: 'downstream_ai_obtained', label: 'Имя добрал AI после follow-up', sessions: downstreamAiObtained, share: scenarioCalls ? Number(((downstreamAiObtained / scenarioCalls) * 100).toFixed(2)) : 0 },
{ stage: 'handoff_confirmed_name', label: 'Передача с подтверждённым именем', sessions: handoffConfirmedName, share: scenarioCalls ? Number(((handoffConfirmedName / scenarioCalls) * 100).toFixed(2)) : 0 },
{ stage: 'handoff_unconfirmed_name', label: 'Передача без подтверждённого имени', sessions: handoffUnconfirmedName, share: scenarioCalls ? Number(((handoffUnconfirmedName / scenarioCalls) * 100).toFixed(2)) : 0 },
{ stage: 'scenario_calls', label: mockVoiceNameLabel('scenario_calls', 'Звонки в сценарии'), sessions: scenarioCalls, share: 100 },
{ stage: 'start_obtained', label: mockVoiceNameLabel('start_obtained', 'Имя взято сразу'), sessions: startObtained, share: scenarioCalls ? Number(((startObtained / scenarioCalls) * 100).toFixed(2)) : 0 },
{ stage: 'needed_downstream', label: mockVoiceNameLabel('needed_downstream', 'Потребовался AI после старта'), sessions: neededDownstream, share: scenarioCalls ? Number(((neededDownstream / scenarioCalls) * 100).toFixed(2)) : 0 },
{ stage: 'downstream_ai_obtained', label: mockVoiceNameLabel('downstream_ai_obtained', 'Имя добрал AI после follow-up'), sessions: downstreamAiObtained, share: scenarioCalls ? Number(((downstreamAiObtained / scenarioCalls) * 100).toFixed(2)) : 0 },
{ stage: 'handoff_confirmed_name', label: mockVoiceNameLabel('handoff_confirmed_name', 'Передача с подтверждённым именем'), sessions: handoffConfirmedName, share: scenarioCalls ? Number(((handoffConfirmedName / scenarioCalls) * 100).toFixed(2)) : 0 },
{ stage: 'handoff_unconfirmed_name', label: mockVoiceNameLabel('handoff_unconfirmed_name', 'Передача без подтверждённого имени'), sessions: handoffUnconfirmedName, share: scenarioCalls ? Number(((handoffUnconfirmedName / scenarioCalls) * 100).toFixed(2)) : 0 },
],
by_language: languageRows,
by_queue: queueRows,
handoff: [
{ outcome: 'confirmed_name', label: 'Передача с подтверждённым именем', sessions: handoffConfirmedName, share: allHandoffs ? Number(((handoffConfirmedName / allHandoffs) * 100).toFixed(2)) : 0 },
{ outcome: 'unconfirmed_name', label: 'Передача без подтверждённого имени', sessions: handoffUnconfirmedName, share: allHandoffs ? Number(((handoffUnconfirmedName / allHandoffs) * 100).toFixed(2)) : 0 },
{ outcome: 'confirmed_name', label: mockVoiceNameLabel('confirmed_name', 'Передача с подтверждённым именем'), sessions: handoffConfirmedName, share: allHandoffs ? Number(((handoffConfirmedName / allHandoffs) * 100).toFixed(2)) : 0 },
{ outcome: 'unconfirmed_name', label: mockVoiceNameLabel('unconfirmed_name', 'Передача без подтверждённого имени'), sessions: handoffUnconfirmedName, share: allHandoffs ? Number(((handoffUnconfirmedName / allHandoffs) * 100).toFixed(2)) : 0 },
],
},
coverage: {
sessions_with_start_decision: Math.round(scenarioCalls * 0.98),
sessions_with_final_ai_state: Math.round(scenarioCalls * 0.94),
sessions_with_manual_overlay: manualCorrected,
note: analyticsMockData().coverage_note || 'Показаны демонстрационные данные по name-flow.',
},
coverage,
};
}
function buildMockVoiceNameTrend(rangeMeta, metric) {
const interval = analyticsTimeseriesIntervalForRange(rangeMeta);
const baseOverview = buildMockVoiceNameOverview(rangeMeta);
const scenarioProfile = mockVoiceNameProfile('scenario_calls', { base: 24, step: 2.4, cycle: 4 });
const metricProfile = metric === 'scenario_calls'
? scenarioProfile
: mockVoiceNameProfile(metric, { base: 60, step: 2.1, cycle: 4 });
const queueFactor = Math.max(0.48, mockAnalyticsQueueFactor(state.analytics.queueId === 'all' ? 'sales_kz' : state.analytics.queueId));
const points = [];
let cursor = new Date(interval === 'hour'
? rangeMeta.current.from.getTime()
: startOfDay(rangeMeta.current.from).getTime());
while (cursor < rangeMeta.current.to) {
const index = points.length;
const scenarioCalls = Math.max(4, Math.round((baseOverview.totals.scenario_calls || 0) / Math.max(5, index + 5) * (0.92 + (index % 4) * 0.06)));
const neededDownstream = Math.max(0, Math.round(scenarioCalls * 0.35));
const scenarioCalls = Math.max(
4,
Math.round((scenarioProfile.base + (index % scenarioProfile.cycle) * scenarioProfile.step) * queueFactor),
);
const neededDownstream = Math.max(1, Math.round(scenarioCalls * 0.35));
const handoffs = Math.max(1, Math.round(scenarioCalls * 0.18));
let value = 0;
let denominator = scenarioCalls;
if (metric === 'start_capture_rate') {
value = 61 + (index % 4) * 2.4;
if (metric === 'scenario_calls') {
value = scenarioCalls;
denominator = scenarioCalls;
} else if (metric === 'downstream_rescue_rate') {
value = 42 + (index % 5) * 2.1;
value = metricProfile.base + (index % metricProfile.cycle) * metricProfile.step;
denominator = neededDownstream;
} else if (metric === 'handoff_unconfirmed_rate') {
value = 34 + (index % 4) * 2.6;
denominator = handoffs;
} else if (metric === 'manual_correction_rate') {
value = 15 + (index % 3) * 1.8;
} else if (metric === 'handoff_unconfirmed_rate' || metric === 'manual_correction_rate') {
value = metricProfile.base + (index % metricProfile.cycle) * metricProfile.step;
denominator = handoffs;
} else {
value = scenarioCalls;
value = metricProfile.base + (index % metricProfile.cycle) * metricProfile.step;
denominator = scenarioCalls;
}
points.push({
@@ -3716,7 +3763,7 @@ function renderVoiceNameAnalyticsEmptyState() {
const total = Number(state.analytics.voiceNameOverview?.totals?.scenario_calls || 0);
if (!state.analytics.loading && total === 0) {
box.hidden = false;
box.textContent = 'За выбранный период не найдено звонков, прошедших через сценарий сбора имени.';
box.textContent = mockVoiceNameSeed().empty_note || 'За выбранный период не найдено звонков, прошедших через сценарий сбора имени.';
return;
}
box.hidden = true;
+59 -1
View File
@@ -115,7 +115,65 @@
"handoff_unconfirmed_rate": 46.5,
"manual_correction_rate": 23.0
}
]
],
"queues": [
{
"queue_id": "sales_kz",
"share": 0.31,
"start_capture_rate": 68.4,
"downstream_rescue_rate": 49.6,
"handoff_unconfirmed_rate": 24.1,
"manual_correction_rate": 11.2
},
{
"queue_id": "support_kz",
"share": 0.39,
"start_capture_rate": 61.2,
"downstream_rescue_rate": 46.9,
"handoff_unconfirmed_rate": 31.8,
"manual_correction_rate": 15.4
},
{
"queue_id": "vip_kz",
"share": 0.11,
"start_capture_rate": 73.5,
"downstream_rescue_rate": 52.8,
"handoff_unconfirmed_rate": 18.6,
"manual_correction_rate": 8.9
},
{
"queue_id": "retention_kz",
"share": 0.19,
"start_capture_rate": 55.1,
"downstream_rescue_rate": 39.4,
"handoff_unconfirmed_rate": 37.9,
"manual_correction_rate": 19.7
}
],
"coverage": {
"sessions_with_start_decision": 192,
"sessions_with_final_ai_state": 184,
"sessions_with_manual_overlay": 12,
"note": "Мок-данные по voice name-flow показывают стартовый захват имени, добор AI и ручное исправление оператором."
},
"trend_profiles": {
"scenario_calls": { "base": 24, "step": 2.4, "cycle": 4 },
"start_capture_rate": { "base": 60.5, "step": 2.2, "cycle": 4 },
"downstream_rescue_rate": { "base": 40.2, "step": 1.9, "cycle": 5 },
"handoff_unconfirmed_rate": { "base": 33.1, "step": 2.4, "cycle": 4 },
"manual_correction_rate": { "base": 14.4, "step": 1.5, "cycle": 3 }
},
"empty_note": "За выбранный период нет звонков, прошедших через сценарий сбора имени."
},
"voice_name_labels": {
"scenario_calls": "Звонки в сценарии",
"start_obtained": "Имя взято сразу",
"needed_downstream": "Потребовался AI после старта",
"downstream_ai_obtained": "Имя добрал AI после follow-up",
"handoff_confirmed_name": "Передача с подтверждённым именем",
"handoff_unconfirmed_name": "Передача без подтверждённого имени",
"confirmed_name": "Передача с подтверждённым именем",
"unconfirmed_name": "Передача без подтверждённого имени"
},
"agent_shift_rows": [
{