Move analyst mock data to external JSON
This commit is contained in:
@@ -386,6 +386,7 @@ def test_supervisor_ui_no_longer_renders_analytics_dashboard():
|
||||
def test_analyst_ui_contains_analytics_dashboard_contract():
|
||||
app_js = (Path(__file__).resolve().parents[1] / "ui" / "analyst" / "app.js").read_text(encoding="utf-8")
|
||||
index_html = (Path(__file__).resolve().parents[1] / "ui" / "analyst" / "index.html").read_text(encoding="utf-8")
|
||||
mock_json = (Path(__file__).resolve().parents[1] / "ui" / "analyst" / "mock-analytics.json")
|
||||
assert 'id="analyticsPanel"' in index_html
|
||||
assert 'id="analyticsPreset"' in index_html
|
||||
assert 'id="analyticsFrom"' in index_html
|
||||
@@ -488,6 +489,9 @@ def test_analyst_ui_contains_analytics_dashboard_contract():
|
||||
assert "function syncAnalyticsUrlState" in app_js
|
||||
assert "function parseAnalyticsDeepLinkState" in app_js
|
||||
assert "function restoreAnalyticsDrilldownFromUrlState" in app_js
|
||||
assert "const ANALYTICS_MOCK_DATA_URL = '/analyst/assets/mock-analytics.json';" in app_js
|
||||
assert "await ensureAnalyticsMockDataLoaded();" in app_js
|
||||
assert mock_json.exists()
|
||||
assert "window.history.replaceState(null, '', nextUrl);" in app_js
|
||||
assert "url.searchParams.set('dd_mode', drilldown.mode);" in app_js
|
||||
assert "url.searchParams.set('dd_selected', drilldown.selectedId);" in app_js
|
||||
|
||||
+108
-60
@@ -34,6 +34,8 @@ const state = {
|
||||
trendInterval: 'day',
|
||||
agentTrendInterval: 'day',
|
||||
statusMessage: '',
|
||||
mockData: null,
|
||||
mockDataError: '',
|
||||
loading: false,
|
||||
error: '',
|
||||
aiError: '',
|
||||
@@ -93,20 +95,7 @@ const AGENT_ANALYTICS_TREND_OPTIONS = [
|
||||
'fcr_rate',
|
||||
];
|
||||
const ENABLE_ANALYTICS_MOCK_DATA = true;
|
||||
const ANALYTICS_MOCK_QUEUE_OPTIONS = [
|
||||
{ queue_id: 'sales_kz', name: 'Продажи KZ' },
|
||||
{ queue_id: 'support_kz', name: 'Поддержка KZ' },
|
||||
{ queue_id: 'vip_kz', name: 'VIP-линия' },
|
||||
{ queue_id: 'retention_kz', name: 'Удержание' },
|
||||
];
|
||||
const ANALYTICS_MOCK_AGENTS = [
|
||||
'agent.aigerim',
|
||||
'agent.dias',
|
||||
'agent.nurdaulet',
|
||||
'agent.madina',
|
||||
'agent.ruslan',
|
||||
'agent.tomiris',
|
||||
];
|
||||
const ANALYTICS_MOCK_DATA_URL = '/analyst/assets/mock-analytics.json';
|
||||
const ANALYTICS_DEEP_LINK_KEYS = [
|
||||
'preset',
|
||||
'from',
|
||||
@@ -803,6 +792,33 @@ function analyticsMockEnabled() {
|
||||
return ENABLE_ANALYTICS_MOCK_DATA === true;
|
||||
}
|
||||
|
||||
function analyticsMockData() {
|
||||
return state.analytics.mockData || {};
|
||||
}
|
||||
|
||||
async function ensureAnalyticsMockDataLoaded() {
|
||||
if (!analyticsMockEnabled()) {
|
||||
return null;
|
||||
}
|
||||
if (state.analytics.mockData) {
|
||||
return state.analytics.mockData;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(ANALYTICS_MOCK_DATA_URL, { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const payload = await response.json();
|
||||
state.analytics.mockData = payload && typeof payload === 'object' ? payload : {};
|
||||
state.analytics.mockDataError = '';
|
||||
return state.analytics.mockData;
|
||||
} catch (error) {
|
||||
state.analytics.mockData = {};
|
||||
state.analytics.mockDataError = error?.message || 'Не удалось загрузить mock-analytics.json';
|
||||
return state.analytics.mockData;
|
||||
}
|
||||
}
|
||||
|
||||
function mockAnalyticsScale(range) {
|
||||
if (!range?.from || !range?.to) {
|
||||
return 1;
|
||||
@@ -813,16 +829,20 @@ function mockAnalyticsScale(range) {
|
||||
}
|
||||
|
||||
function mockAnalyticsQueueOptions() {
|
||||
return ANALYTICS_MOCK_QUEUE_OPTIONS.map((item) => ({ ...item }));
|
||||
const queueOptions = Array.isArray(analyticsMockData().queue_options)
|
||||
? analyticsMockData().queue_options
|
||||
: [];
|
||||
return queueOptions.map((item) => ({ ...item }));
|
||||
}
|
||||
|
||||
function mockAnalyticsAgents() {
|
||||
return Array.isArray(analyticsMockData().agents)
|
||||
? analyticsMockData().agents.slice()
|
||||
: [];
|
||||
}
|
||||
|
||||
function mockAnalyticsQueueFactor(queueId = state.analytics.queueId) {
|
||||
const factors = {
|
||||
sales_kz: 1.08,
|
||||
support_kz: 0.96,
|
||||
vip_kz: 0.58,
|
||||
retention_kz: 0.72,
|
||||
};
|
||||
const factors = analyticsMockData().queue_factors || {};
|
||||
if (!queueId || queueId === 'all') {
|
||||
return 1;
|
||||
}
|
||||
@@ -830,32 +850,25 @@ function mockAnalyticsQueueFactor(queueId = state.analytics.queueId) {
|
||||
}
|
||||
|
||||
function mockAnalyticsChannelBase() {
|
||||
return {
|
||||
voice: { total: 620, answered: 584, abandoned: 36 },
|
||||
telegram: { total: 214, answered: 205, abandoned: 9 },
|
||||
whatsapp: { total: 182, answered: 171, abandoned: 11 },
|
||||
webchat: { total: 141, answered: 132, abandoned: 9 },
|
||||
email: { total: 108, answered: 94, abandoned: 14 },
|
||||
};
|
||||
return analyticsMockData().channel_base || {};
|
||||
}
|
||||
|
||||
function mockAiReasonLabel(reasonKey) {
|
||||
const labels = {
|
||||
requested_human: 'Запрос оператора',
|
||||
knowledge_or_tool_gap: 'Не хватило знаний или инструмента',
|
||||
policy_or_sensitive: 'Чувствительный сценарий',
|
||||
delivery_or_runtime_error: 'Ошибка доставки или runtime',
|
||||
manual_claim: 'Ручной takeover',
|
||||
other: 'Другая причина',
|
||||
};
|
||||
const labels = analyticsMockData().ai_reason_labels || {};
|
||||
return labels[reasonKey] || reasonKey || 'Другая причина';
|
||||
}
|
||||
|
||||
function mockInteractionStatus(index = 0) {
|
||||
return ['closed', 'in_progress', 'closed', 'new', 'closed', 'escalated'][index % 6];
|
||||
const statuses = Array.isArray(analyticsMockData().interaction_statuses)
|
||||
? analyticsMockData().interaction_statuses
|
||||
: [];
|
||||
if (!statuses.length) {
|
||||
return 'closed';
|
||||
}
|
||||
return statuses[index % statuses.length];
|
||||
}
|
||||
|
||||
function mockAnalyticsCoverage(totalRows, note = 'Показаны демонстрационные данные для временного наполнения витрины.') {
|
||||
function mockAnalyticsCoverage(totalRows, note = analyticsMockData().coverage_note || 'Показаны демонстрационные данные для временного наполнения витрины.') {
|
||||
const metricNames = ['total', 'answered', 'SL', 'ASA', 'AHT', 'Abandon', 'FCR', 'DigitalShare'];
|
||||
const metricStatus = {};
|
||||
const supportedChannels = {};
|
||||
@@ -1007,8 +1020,12 @@ function buildMockAgentOverview(rangeMeta, options = {}) {
|
||||
const previous = Boolean(options.previous);
|
||||
const queueId = options.queueId ?? state.analytics.queueId;
|
||||
const queueOptions = mockAnalyticsQueueOptions();
|
||||
const rows = ANALYTICS_MOCK_AGENTS.map((agentId, index) => {
|
||||
const queue = queueOptions[index % queueOptions.length];
|
||||
const agents = mockAnalyticsAgents();
|
||||
const stateCycle = Array.isArray(analyticsMockData().agent_state_cycle)
|
||||
? analyticsMockData().agent_state_cycle
|
||||
: [];
|
||||
const rows = agents.map((agentId, index) => {
|
||||
const queue = queueOptions[index % Math.max(queueOptions.length, 1)] || { queue_id: 'sales_kz', name: 'Продажи KZ' };
|
||||
const dominantQueue = queueId !== 'all' ? queueId : queue.queue_id;
|
||||
const interactionsTotal = Math.round((24 - index * 2) * (previous ? 0.88 : 1));
|
||||
const answeredTotal = Math.max(0, Math.round(interactionsTotal * (0.88 - index * 0.015)));
|
||||
@@ -1019,7 +1036,7 @@ function buildMockAgentOverview(rangeMeta, options = {}) {
|
||||
closed_total: Math.max(0, answeredTotal - (index % 3)),
|
||||
avg_handle_seconds: Number((290 + index * 24 + (previous ? 18 : 0)).toFixed(2)),
|
||||
fcr_rate: Number((78 - index * 3 + (previous ? -2 : 0)).toFixed(2)),
|
||||
current_state: ['READY', 'BUSY', 'READY', 'BREAK', 'BUSY', 'OFFLINE'][index % 6],
|
||||
current_state: stateCycle.length ? stateCycle[index % stateCycle.length] : 'READY',
|
||||
current_queue_id: dominantQueue,
|
||||
dominant_queue_id: dominantQueue,
|
||||
last_activity_at: new Date(rangeMeta.current.to.getTime() - index * 37 * 60 * 1000).toISOString(),
|
||||
@@ -1053,11 +1070,18 @@ function buildMockAgentOverview(rangeMeta, options = {}) {
|
||||
};
|
||||
})
|
||||
.filter((item) => item.agents_total > 0);
|
||||
const byShift = [
|
||||
{ shift_key: 'night', label: 'Ночная смена', agents_with_activity: Math.max(2, Math.round(agentsWithActivity * 0.3)), interactions_total: Math.round(interactionsTotal * 0.18), answered_total: Math.round(answeredTotal * 0.17), avg_handle_seconds: 356, fcr_rate: 69.5 },
|
||||
{ shift_key: 'day', label: 'Дневная смена', agents_with_activity: Math.max(4, Math.round(agentsWithActivity * 0.9)), interactions_total: Math.round(interactionsTotal * 0.52), answered_total: Math.round(answeredTotal * 0.53), avg_handle_seconds: 318, fcr_rate: 76.8 },
|
||||
{ shift_key: 'evening', label: 'Вечерняя смена', agents_with_activity: Math.max(3, Math.round(agentsWithActivity * 0.55)), interactions_total: Math.round(interactionsTotal * 0.30), answered_total: Math.round(answeredTotal * 0.30), avg_handle_seconds: 334, fcr_rate: 73.4 },
|
||||
];
|
||||
const shiftSeeds = Array.isArray(analyticsMockData().agent_shift_rows)
|
||||
? analyticsMockData().agent_shift_rows
|
||||
: [];
|
||||
const byShift = shiftSeeds.map((seed) => ({
|
||||
shift_key: seed.shift_key,
|
||||
label: seed.label,
|
||||
agents_with_activity: Math.max(1, Math.round(Number(seed.agents_with_activity || 0) * (previous ? 0.92 : 1))),
|
||||
interactions_total: Math.max(0, Math.round(Number(seed.interactions_total || 0) * (previous ? 0.9 : 1))),
|
||||
answered_total: Math.max(0, Math.round(Number(seed.answered_total || 0) * (previous ? 0.9 : 1))),
|
||||
avg_handle_seconds: Number((Number(seed.avg_handle_seconds || 0) + (previous ? 18 : 0)).toFixed(2)),
|
||||
fcr_rate: Number((Number(seed.fcr_rate || 0) + (previous ? -2 : 0)).toFixed(2)),
|
||||
}));
|
||||
return {
|
||||
window: {
|
||||
from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(),
|
||||
@@ -1156,10 +1180,15 @@ function buildMockAiOverview(rangeMeta, options = {}) {
|
||||
);
|
||||
}
|
||||
const scale = mockAnalyticsScale(previous ? rangeMeta.previous : rangeMeta.current) * (previous ? 0.91 : 1);
|
||||
const baseRows = [
|
||||
{ channel: 'telegram', sessions_started: Math.round(176 * scale), ai_only_sessions: Math.round(106 * scale), human_touched_sessions: Math.round(58 * scale), handoff_rate: 24.4, ai_latency_avg_ms: 1380, containment_rate: 60.2, closed_without_operator_rate: 52.1 },
|
||||
{ channel: 'whatsapp', sessions_started: Math.round(142 * scale), ai_only_sessions: Math.round(80 * scale), human_touched_sessions: Math.round(54 * scale), handoff_rate: 28.9, ai_latency_avg_ms: 1524, containment_rate: 56.4, closed_without_operator_rate: 48.6 },
|
||||
].filter((item) => channel === 'all' || item.channel === channel);
|
||||
const aiChannelRows = Array.isArray(analyticsMockData().ai_channel_rows)
|
||||
? analyticsMockData().ai_channel_rows
|
||||
: [];
|
||||
const baseRows = aiChannelRows.map((item) => ({
|
||||
...item,
|
||||
sessions_started: Math.round(Number(item.sessions_started || 0) * scale),
|
||||
ai_only_sessions: Math.round(Number(item.ai_only_sessions || 0) * scale),
|
||||
human_touched_sessions: Math.round(Number(item.human_touched_sessions || 0) * scale),
|
||||
})).filter((item) => channel === 'all' || item.channel === channel);
|
||||
const sessionsStarted = baseRows.reduce((acc, item) => acc + item.sessions_started, 0);
|
||||
const sessionsContained = Math.round(baseRows.reduce((acc, item) => acc + item.sessions_started * (item.containment_rate / 100), 0));
|
||||
const sessionsHandoff = Math.round(baseRows.reduce((acc, item) => acc + item.sessions_started * (item.handoff_rate / 100), 0));
|
||||
@@ -1270,9 +1299,11 @@ function buildMockAiTrend(rangeMeta, metric) {
|
||||
|
||||
function buildMockInteractionDrilldownData(filters, limit, offset, mode) {
|
||||
const normalized = normalizeAnalyticsDrilldownFilters(filters);
|
||||
const queueOptions = mockAnalyticsQueueOptions();
|
||||
const agents = mockAnalyticsAgents();
|
||||
const total = 18;
|
||||
const rows = Array.from({ length: total }, (_, index) => {
|
||||
const queueId = normalized.queue_id || mockAnalyticsQueueOptions()[index % mockAnalyticsQueueOptions().length].queue_id;
|
||||
const queueId = normalized.queue_id || queueOptions[index % Math.max(queueOptions.length, 1)]?.queue_id || 'sales_kz';
|
||||
let channel = normalized.channel || ['voice', 'telegram', 'whatsapp', 'webchat', 'email'][index % 5];
|
||||
if (mode === 'metric' && normalized.metric === 'DigitalShare') {
|
||||
channel = ['telegram', 'whatsapp', 'webchat', 'email'][index % 4];
|
||||
@@ -1288,7 +1319,7 @@ function buildMockInteractionDrilldownData(filters, limit, offset, mode) {
|
||||
channel,
|
||||
status: abandoned ? 'closed' : mockInteractionStatus(index),
|
||||
queue_id: queueId,
|
||||
assigned_to: normalized.agent_id || (abandoned ? null : ANALYTICS_MOCK_AGENTS[index % ANALYTICS_MOCK_AGENTS.length]),
|
||||
assigned_to: normalized.agent_id || (abandoned ? null : (agents[index % Math.max(agents.length, 1)] || null)),
|
||||
created_at: createdAt.toISOString(),
|
||||
updated_at: new Date(createdAt.getTime() + 42 * 60 * 1000).toISOString(),
|
||||
answered,
|
||||
@@ -1323,13 +1354,15 @@ function buildMockInteractionDrilldownData(filters, limit, offset, mode) {
|
||||
}
|
||||
|
||||
function buildMockInteractionDetail(interactionId) {
|
||||
const queueOptions = mockAnalyticsQueueOptions();
|
||||
const agents = mockAnalyticsAgents();
|
||||
const interaction = {
|
||||
interaction_id: interactionId,
|
||||
subject: `Карточка ${interactionId}`,
|
||||
channel: ['voice', 'telegram', 'whatsapp'][interactionId.length % 3],
|
||||
status: 'closed',
|
||||
queue_id: mockAnalyticsQueueOptions()[interactionId.length % mockAnalyticsQueueOptions().length].queue_id,
|
||||
assigned_to: ANALYTICS_MOCK_AGENTS[interactionId.length % ANALYTICS_MOCK_AGENTS.length],
|
||||
queue_id: queueOptions[interactionId.length % Math.max(queueOptions.length, 1)]?.queue_id || 'sales_kz',
|
||||
assigned_to: agents[interactionId.length % Math.max(agents.length, 1)] || null,
|
||||
created_at: new Date(Date.now() - 8 * 60 * 60 * 1000).toISOString(),
|
||||
updated_at: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
|
||||
wait_seconds: 18,
|
||||
@@ -1350,9 +1383,11 @@ function buildMockInteractionDetail(interactionId) {
|
||||
|
||||
function buildMockAiDrilldownData(filters, limit, offset) {
|
||||
const normalized = normalizeAnalyticsDrilldownFilters(filters);
|
||||
const queueOptions = mockAnalyticsQueueOptions();
|
||||
const agents = mockAnalyticsAgents();
|
||||
const total = 14;
|
||||
const allItems = Array.from({ length: total }, (_, index) => {
|
||||
const queue = mockAnalyticsQueueOptions()[index % mockAnalyticsQueueOptions().length];
|
||||
const queue = queueOptions[index % Math.max(queueOptions.length, 1)] || { queue_id: 'sales_kz' };
|
||||
const channel = normalized.channel && normalized.channel !== 'all'
|
||||
? normalized.channel
|
||||
: ['telegram', 'whatsapp'][index % 2];
|
||||
@@ -1373,7 +1408,7 @@ function buildMockAiDrilldownData(filters, limit, offset) {
|
||||
reason_key: handoff ? reasonKey : '',
|
||||
reason_label: handoff ? mockAiReasonLabel(reasonKey) : '',
|
||||
raw_handoff_reason: handoff ? `raw:${reasonKey}` : '',
|
||||
assigned_to: handoff ? ANALYTICS_MOCK_AGENTS[index % ANALYTICS_MOCK_AGENTS.length] : null,
|
||||
assigned_to: handoff ? (agents[index % Math.max(agents.length, 1)] || null) : null,
|
||||
claimed_by_user: handoff ? `sup-${(index % 3) + 1}` : null,
|
||||
assistant_turns: 6 + index,
|
||||
user_turns: 3 + (index % 4),
|
||||
@@ -1464,9 +1499,15 @@ function buildMockAnalyticsDashboard(rangeMeta) {
|
||||
overview,
|
||||
compare: buildMockAnalyticsOverview(rangeMeta, { previous: true }),
|
||||
coverage: {
|
||||
implemented_metrics: ['SL', 'ASA', 'AHT', 'Abandon', 'FCR', 'DigitalShare'],
|
||||
dimensions: ['queue', 'channel', 'agent', 'shift'],
|
||||
supported_filters: ['preset', 'from_ts', 'to_ts', 'queue_id', 'channel'],
|
||||
implemented_metrics: Array.isArray(analyticsMockData().dashboard_coverage?.implemented_metrics)
|
||||
? analyticsMockData().dashboard_coverage.implemented_metrics
|
||||
: [],
|
||||
dimensions: Array.isArray(analyticsMockData().dashboard_coverage?.dimensions)
|
||||
? analyticsMockData().dashboard_coverage.dimensions
|
||||
: [],
|
||||
supported_filters: Array.isArray(analyticsMockData().dashboard_coverage?.supported_filters)
|
||||
? analyticsMockData().dashboard_coverage.supported_filters
|
||||
: [],
|
||||
},
|
||||
aiOverview: buildMockAiOverview(rangeMeta),
|
||||
aiCompare: buildMockAiOverview(rangeMeta, { previous: true }),
|
||||
@@ -5554,6 +5595,7 @@ async function loadAiAnalyticsDrilldownDetailsState(sessionId) {
|
||||
syncAnalyticsUrlState();
|
||||
|
||||
if (analyticsMockEnabled()) {
|
||||
await ensureAnalyticsMockDataLoaded();
|
||||
const detail = buildMockAiDetail(sessionId);
|
||||
state.drilldown.selectedInteraction = {
|
||||
...(state.drilldown.selectedInteraction || {}),
|
||||
@@ -5608,6 +5650,7 @@ loadAnalyticsDrilldownDetails = async function loadAnalyticsDrilldownDetails(int
|
||||
syncAnalyticsUrlState();
|
||||
|
||||
if (analyticsMockEnabled()) {
|
||||
await ensureAnalyticsMockDataLoaded();
|
||||
const detail = buildMockInteractionDetail(interactionId);
|
||||
state.drilldown.selectedInteraction = {
|
||||
...(state.drilldown.selectedInteraction || {}),
|
||||
@@ -5657,6 +5700,7 @@ async function loadAiAnalyticsDrilldownPageState(options = {}) {
|
||||
renderAnalyticsDrilldown();
|
||||
|
||||
if (analyticsMockEnabled()) {
|
||||
await ensureAnalyticsMockDataLoaded();
|
||||
const data = buildMockAiDrilldownData(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset);
|
||||
state.drilldown.items = Array.isArray(data?.items) ? data.items : [];
|
||||
state.drilldown.total = Number(data?.total || 0);
|
||||
@@ -5760,6 +5804,7 @@ loadAnalyticsDrilldownPage = async function loadAnalyticsDrilldownPage(options =
|
||||
renderAnalyticsDrilldown();
|
||||
|
||||
if (analyticsMockEnabled()) {
|
||||
await ensureAnalyticsMockDataLoaded();
|
||||
const data = buildMockInteractionDrilldownData(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset, state.drilldown.mode);
|
||||
state.drilldown.items = Array.isArray(data?.items) ? data.items : [];
|
||||
state.drilldown.total = Number(data?.total || 0);
|
||||
@@ -5988,6 +6033,7 @@ async function loadAnalyticsDashboard(_silent = false) {
|
||||
renderAnalyticsDashboard(rangeMeta);
|
||||
|
||||
if (analyticsMockEnabled()) {
|
||||
await ensureAnalyticsMockDataLoaded();
|
||||
const payload = buildMockAnalyticsDashboard(rangeMeta);
|
||||
if (requestId !== state.analytics.requestId) {
|
||||
return;
|
||||
@@ -6008,7 +6054,9 @@ async function loadAnalyticsDashboard(_silent = false) {
|
||||
state.analytics.aiTrend = payload.aiTrend;
|
||||
state.analytics.agentTrend = payload.agentTrend;
|
||||
state.analytics.loading = false;
|
||||
state.analytics.statusMessage = 'Показаны демонстрационные данные.';
|
||||
state.analytics.statusMessage = state.analytics.mockDataError
|
||||
? `Показаны демонстрационные данные. Не удалось загрузить внешний mock-файл: ${state.analytics.mockDataError}`
|
||||
: 'Показаны демонстрационные данные из внешнего mock-файла.';
|
||||
renderAnalyticsDashboard(rangeMeta);
|
||||
syncAnalyticsUrlState();
|
||||
if (state.drilldown.open) {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"queue_options": [
|
||||
{ "queue_id": "sales_kz", "name": "Продажи KZ" },
|
||||
{ "queue_id": "support_kz", "name": "Поддержка KZ" },
|
||||
{ "queue_id": "vip_kz", "name": "VIP-линия" },
|
||||
{ "queue_id": "retention_kz", "name": "Удержание" }
|
||||
],
|
||||
"agents": [
|
||||
"agent.aigerim",
|
||||
"agent.dias",
|
||||
"agent.nurdaulet",
|
||||
"agent.madina",
|
||||
"agent.ruslan",
|
||||
"agent.tomiris"
|
||||
],
|
||||
"queue_factors": {
|
||||
"sales_kz": 1.08,
|
||||
"support_kz": 0.96,
|
||||
"vip_kz": 0.58,
|
||||
"retention_kz": 0.72
|
||||
},
|
||||
"channel_base": {
|
||||
"voice": { "total": 620, "answered": 584, "abandoned": 36 },
|
||||
"telegram": { "total": 214, "answered": 205, "abandoned": 9 },
|
||||
"whatsapp": { "total": 182, "answered": 171, "abandoned": 11 },
|
||||
"webchat": { "total": 141, "answered": 132, "abandoned": 9 },
|
||||
"email": { "total": 108, "answered": 94, "abandoned": 14 }
|
||||
},
|
||||
"ai_reason_labels": {
|
||||
"requested_human": "Запрос оператора",
|
||||
"knowledge_or_tool_gap": "Не хватило знаний или инструмента",
|
||||
"policy_or_sensitive": "Чувствительный сценарий",
|
||||
"delivery_or_runtime_error": "Ошибка доставки или runtime",
|
||||
"manual_claim": "Ручной takeover",
|
||||
"other": "Другая причина"
|
||||
},
|
||||
"interaction_statuses": [
|
||||
"closed",
|
||||
"in_progress",
|
||||
"closed",
|
||||
"new",
|
||||
"closed",
|
||||
"escalated"
|
||||
],
|
||||
"agent_state_cycle": [
|
||||
"READY",
|
||||
"BUSY",
|
||||
"READY",
|
||||
"BREAK",
|
||||
"BUSY",
|
||||
"OFFLINE"
|
||||
],
|
||||
"coverage_note": "Показаны демонстрационные данные для временного наполнения витрины.",
|
||||
"dashboard_coverage": {
|
||||
"implemented_metrics": ["SL", "ASA", "AHT", "Abandon", "FCR", "DigitalShare"],
|
||||
"dimensions": ["queue", "channel", "agent", "shift"],
|
||||
"supported_filters": ["preset", "from_ts", "to_ts", "queue_id", "channel"]
|
||||
},
|
||||
"ai_channel_rows": [
|
||||
{
|
||||
"channel": "telegram",
|
||||
"sessions_started": 176,
|
||||
"ai_only_sessions": 106,
|
||||
"human_touched_sessions": 58,
|
||||
"handoff_rate": 24.4,
|
||||
"ai_latency_avg_ms": 1380,
|
||||
"containment_rate": 60.2,
|
||||
"closed_without_operator_rate": 52.1
|
||||
},
|
||||
{
|
||||
"channel": "whatsapp",
|
||||
"sessions_started": 142,
|
||||
"ai_only_sessions": 80,
|
||||
"human_touched_sessions": 54,
|
||||
"handoff_rate": 28.9,
|
||||
"ai_latency_avg_ms": 1524,
|
||||
"containment_rate": 56.4,
|
||||
"closed_without_operator_rate": 48.6
|
||||
}
|
||||
],
|
||||
"agent_shift_rows": [
|
||||
{
|
||||
"shift_key": "night",
|
||||
"label": "Ночная смена",
|
||||
"agents_with_activity": 2,
|
||||
"interactions_total": 18,
|
||||
"answered_total": 17,
|
||||
"avg_handle_seconds": 356,
|
||||
"fcr_rate": 69.5
|
||||
},
|
||||
{
|
||||
"shift_key": "day",
|
||||
"label": "Дневная смена",
|
||||
"agents_with_activity": 5,
|
||||
"interactions_total": 52,
|
||||
"answered_total": 53,
|
||||
"avg_handle_seconds": 318,
|
||||
"fcr_rate": 76.8
|
||||
},
|
||||
{
|
||||
"shift_key": "evening",
|
||||
"label": "Вечерняя смена",
|
||||
"agents_with_activity": 3,
|
||||
"interactions_total": 30,
|
||||
"answered_total": 30,
|
||||
"avg_handle_seconds": 334,
|
||||
"fcr_rate": 73.4
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user