Move analyst mock data to external JSON

This commit is contained in:
Yera All
2026-04-03 23:59:12 +05:00
parent 78918a38fd
commit 3598373627
3 changed files with 222 additions and 60 deletions
+108 -60
View File
@@ -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) {
+110
View File
@@ -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
}
]
}