Add customer profile shortcuts
This commit is contained in:
@@ -169,6 +169,20 @@ def test_operator_ui_loads_backend_customer_history_for_profile():
|
||||
assert "ensureCustomerHistoryLoaded(customer.customer_id).catch(() => {});" in app_js
|
||||
|
||||
|
||||
def test_operator_ui_customer_profile_supports_case_and_call_shortcuts():
|
||||
app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8")
|
||||
styles_css = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "styles.css").read_text(encoding="utf-8")
|
||||
assert "function customerHistoryEventActions(event, context = {})" in app_js
|
||||
assert "function renderCustomerHistoryActions(event, context = {})" in app_js
|
||||
assert 'data-customer-action="interaction"' in app_js
|
||||
assert 'data-customer-action="call"' in app_js
|
||||
assert "Открыть кейс" in app_js
|
||||
assert "Открыть звонок" in app_js
|
||||
assert "focusLiveCall(callId);" in app_js
|
||||
assert ".customer-history-actions {" in styles_css
|
||||
assert ".customer-history-link {" in styles_css
|
||||
|
||||
|
||||
def test_operator_ui_contains_leads_table_front():
|
||||
app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8")
|
||||
index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
+110
-8
@@ -866,6 +866,7 @@ function buildCustomerHistoryEvents(customer) {
|
||||
item.assigned_to ? `владелец ${item.assigned_to}` : '',
|
||||
item.queue_id ? `queue ${item.queue_id}` : '',
|
||||
].filter(Boolean).join(' • '),
|
||||
interaction_id: item.interaction_id,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -880,6 +881,8 @@ function buildCustomerHistoryEvents(customer) {
|
||||
thread.claimed_by_user ? `владелец ${thread.claimed_by_user}` : '',
|
||||
thread.status ? statusMeta(thread.status).label : '',
|
||||
].filter(Boolean).join(' • '),
|
||||
interaction_id: thread.interaction_id,
|
||||
thread_id: thread.thread_id,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -897,6 +900,8 @@ function buildCustomerHistoryEvents(customer) {
|
||||
message.operator_user ? `оператор ${message.operator_user}` : '',
|
||||
message.delivery_status || '',
|
||||
].filter(Boolean).join(' • '),
|
||||
interaction_id: activeThread.interaction_id,
|
||||
thread_id: activeThread.thread_id,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -913,6 +918,8 @@ function buildCustomerHistoryEvents(customer) {
|
||||
item.operator_extension ? `внутр. ${item.operator_extension}` : '',
|
||||
item.claimed_by_user ? `владелец ${item.claimed_by_user}` : '',
|
||||
].filter(Boolean).join(' • '),
|
||||
interaction_id: item.interaction_id,
|
||||
call_id: item.call_id,
|
||||
});
|
||||
const aiMeta = voiceAiStatusMeta(item);
|
||||
if (aiMeta) {
|
||||
@@ -926,6 +933,8 @@ function buildCustomerHistoryEvents(customer) {
|
||||
item.voice_session_id ? `голос ${item.voice_session_id}` : '',
|
||||
item.ai_session_id ? `ai ${item.ai_session_id}` : '',
|
||||
].filter(Boolean).join(' • '),
|
||||
interaction_id: item.interaction_id,
|
||||
call_id: item.call_id,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -941,6 +950,72 @@ function buildCustomerHistoryEvents(customer) {
|
||||
.slice(0, 9);
|
||||
}
|
||||
|
||||
function latestItemByTime(items, fields = []) {
|
||||
const source = Array.isArray(items) ? items.filter(Boolean) : [];
|
||||
if (!source.length) {
|
||||
return null;
|
||||
}
|
||||
return [...source].sort((left, right) => {
|
||||
const rightTime = fields.reduce((max, field) => Math.max(max, customerHistoryTimeValue(right?.[field])), 0);
|
||||
const leftTime = fields.reduce((max, field) => Math.max(max, customerHistoryTimeValue(left?.[field])), 0);
|
||||
if (rightTime !== leftTime) {
|
||||
return rightTime - leftTime;
|
||||
}
|
||||
return String(right?.interaction_id || right?.thread_id || right?.call_id || '').localeCompare(
|
||||
String(left?.interaction_id || left?.thread_id || left?.call_id || ''),
|
||||
);
|
||||
})[0] || null;
|
||||
}
|
||||
|
||||
function customerHistoryEventActions(event, context = {}) {
|
||||
const threadIds = context.threadIds instanceof Set ? context.threadIds : new Set();
|
||||
const callIds = context.callIds instanceof Set ? context.callIds : new Set();
|
||||
const actions = [];
|
||||
if (event?.thread_id && threadIds.has(event.thread_id)) {
|
||||
actions.push({
|
||||
action: 'telegram',
|
||||
label: 'К Telegram',
|
||||
threadId: event.thread_id,
|
||||
});
|
||||
}
|
||||
if (event?.call_id && callIds.has(event.call_id)) {
|
||||
actions.push({
|
||||
action: 'call',
|
||||
label: 'К звонку',
|
||||
callId: event.call_id,
|
||||
});
|
||||
}
|
||||
if (event?.interaction_id) {
|
||||
actions.push({
|
||||
action: 'interaction',
|
||||
label: 'К кейсу',
|
||||
interactionId: event.interaction_id,
|
||||
});
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
function renderCustomerHistoryActions(event, context = {}) {
|
||||
const actions = customerHistoryEventActions(event, context);
|
||||
if (!actions.length) {
|
||||
return '';
|
||||
}
|
||||
return `
|
||||
<div class="customer-history-actions">
|
||||
${actions.map((action) => `
|
||||
<button
|
||||
class="btn ghost customer-history-link"
|
||||
type="button"
|
||||
data-customer-action="${escapeHtml(action.action)}"
|
||||
data-thread-id="${escapeHtml(action.threadId || '')}"
|
||||
data-call-id="${escapeHtml(action.callId || '')}"
|
||||
data-interaction-id="${escapeHtml(action.interactionId || '')}"
|
||||
>${escapeHtml(action.label)}</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function emptyCustomerSpotlightMarkup(title, description) {
|
||||
return `
|
||||
<div class="customer-spotlight-empty">
|
||||
@@ -959,6 +1034,8 @@ function customerSpotlightMarkup(customer, options = {}) {
|
||||
const score = customerLeadScore(customer, index);
|
||||
const historyPayload = customerHistoryPayload(customer.customer_id);
|
||||
const historySummary = historyPayload?.summary || null;
|
||||
const currentThreads = customerTelegramThreads(customer);
|
||||
const currentLiveCalls = customerLiveCalls(customer);
|
||||
const interactions = Array.isArray(historyPayload?.interactions)
|
||||
? historyPayload.interactions
|
||||
: customerInteractions(customer);
|
||||
@@ -983,11 +1060,15 @@ function customerSpotlightMarkup(customer, options = {}) {
|
||||
...(liveCalls.length ? ['voice'] : []),
|
||||
])];
|
||||
const tags = Array.isArray(customer.tags) ? customer.tags.filter(Boolean) : [];
|
||||
const latestInteraction = latestItemByTime(interactions, ['updated_at', 'created_at']);
|
||||
const primaryThread = historySummary?.primary_telegram_thread_id
|
||||
? threads.find((item) => item.thread_id === historySummary.primary_telegram_thread_id) || null
|
||||
: [...threads].sort((left, right) => {
|
||||
return customerHistoryTimeValue(right.last_message_at) - customerHistoryTimeValue(left.last_message_at);
|
||||
})[0] || null;
|
||||
? currentThreads.find((item) => item.thread_id === historySummary.primary_telegram_thread_id) || null
|
||||
: latestItemByTime(currentThreads, ['last_message_at', 'updated_at', 'created_at']);
|
||||
const primaryCall = latestItemByTime(currentLiveCalls, ['last_transition_at', 'ended_at', 'connected_at', 'started_at', 'updated_at']);
|
||||
const historyActionContext = {
|
||||
threadIds: new Set(currentThreads.map((item) => item.thread_id).filter(Boolean)),
|
||||
callIds: new Set(currentLiveCalls.map((item) => item.call_id).filter(Boolean)),
|
||||
};
|
||||
const historyStatusText = customerHistoryPending(customer.customer_id)
|
||||
? 'Обновляем ленту клиента из backend...'
|
||||
: state.customers.historyErrorsById[customer.customer_id]
|
||||
@@ -1025,6 +1106,7 @@ function customerSpotlightMarkup(customer, options = {}) {
|
||||
</div>
|
||||
<div class="customer-history-copy">${escapeHtml(event.body)}</div>
|
||||
${event.note ? `<div class="customer-history-note">${escapeHtml(event.note)}</div>` : ''}
|
||||
${renderCustomerHistoryActions(event, historyActionContext)}
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
@@ -1079,7 +1161,9 @@ function customerSpotlightMarkup(customer, options = {}) {
|
||||
|
||||
<div class="customer-profile-actions">
|
||||
<button class="btn" type="button" data-customer-action="workspace">Открыть рабочий стол</button>
|
||||
<button class="btn ghost" type="button" data-customer-action="interaction" data-interaction-id="${escapeHtml(latestInteraction?.interaction_id || '')}" ${latestInteraction ? '' : 'disabled'}>Открыть кейс</button>
|
||||
<button class="btn ghost" type="button" data-customer-action="telegram" data-thread-id="${escapeHtml(primaryThread?.thread_id || '')}" ${primaryThread ? '' : 'disabled'}>Открыть Telegram</button>
|
||||
<button class="btn ghost" type="button" data-customer-action="call" data-call-id="${escapeHtml(primaryCall?.call_id || '')}" ${primaryCall ? '' : 'disabled'}>Открыть звонок</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3849,15 +3933,16 @@ function focusLiveCall(callId) {
|
||||
if (!callId) {
|
||||
return;
|
||||
}
|
||||
const activeCall = state.liveCalls.items.find((item) => item.call_id === callId) || null;
|
||||
const relatedCall = activeCall || state.liveCalls.recentItems.find((item) => item.call_id === callId) || null;
|
||||
const select = $('liveCallIdSelect');
|
||||
if (select) {
|
||||
select.value = callId;
|
||||
select.value = activeCall ? callId : '';
|
||||
}
|
||||
state.liveCalls.selectedCallId = callId;
|
||||
renderLiveCallsTable(state.liveCalls.items, state.liveCalls.recentItems);
|
||||
syncLiveCallActionButtons();
|
||||
const activeCall = state.liveCalls.items.find((item) => item.call_id === callId) || null;
|
||||
ensureVoiceAiSummaryLoaded(activeCall);
|
||||
ensureVoiceAiSummaryLoaded(relatedCall);
|
||||
}
|
||||
|
||||
async function openUnifiedInboxItem(button) {
|
||||
@@ -5347,7 +5432,7 @@ function updateLiveCallSelector(items) {
|
||||
}
|
||||
|
||||
function renderLiveCallCard(item, { recent = false } = {}) {
|
||||
const isSelected = !recent && item.call_id === state.liveCalls.selectedCallId;
|
||||
const isSelected = item.call_id === state.liveCalls.selectedCallId;
|
||||
const caller = item.caller_number || item.caller_name || 'неизвестно';
|
||||
const aiMeta = voiceAiStatusMeta(item);
|
||||
const badges = [
|
||||
@@ -5664,6 +5749,23 @@ async function handleCustomerActionClick(event) {
|
||||
window.location.hash = '#telegram-page';
|
||||
await selectTelegramThread(threadId);
|
||||
log('Открыт Telegram клиента', { customer_id: customer.customer_id, thread_id: threadId });
|
||||
return;
|
||||
}
|
||||
if (button.dataset.customerAction === 'interaction') {
|
||||
const interactionId = button.dataset.interactionId || '';
|
||||
$('interactionCustomerId').value = customer.customer_id;
|
||||
window.location.hash = '#workspace';
|
||||
log('Открыт кейс клиента', { customer_id: customer.customer_id, interaction_id: interactionId || '-' });
|
||||
return;
|
||||
}
|
||||
if (button.dataset.customerAction === 'call') {
|
||||
const callId = button.dataset.callId || '';
|
||||
if (!callId) {
|
||||
return;
|
||||
}
|
||||
focusLiveCall(callId);
|
||||
window.location.hash = '#calls';
|
||||
log('Открыт звонок клиента', { customer_id: customer.customer_id, call_id: callId });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1291,6 +1291,20 @@ textarea::placeholder {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.customer-history-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.customer-history-link {
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.customer-history-event.voice .customer-history-mark {
|
||||
background: #e8f0ff;
|
||||
border-color: #cfdcff;
|
||||
|
||||
Reference in New Issue
Block a user