Fill new voice name-flow mock analytics data
This commit is contained in:
+91
-44
@@ -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;
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user