Show name status in operator customer profile

This commit is contained in:
Yera All
2026-04-08 15:35:37 +05:00
parent 6f25de1f62
commit 5d192621b0
4 changed files with 73 additions and 6 deletions
+8 -2
View File
@@ -317,8 +317,11 @@ def test_operator_ui_can_fix_customer_name_from_call_views():
assert "function saveLiveCallCustomerName" in app_js
assert "function saveCustomerProfileName" in app_js
assert "function openCustomerProfileNameEditor" in app_js
assert "function customerProfileVoiceNameContext" in app_js
assert "data-customer-name-form" in app_js
assert 'data-customer-action="edit-name"' in app_js
assert "customer-profile-name-meta" in app_js
assert "customer-profile-name-flash" in app_js
assert "function applyCustomerNamePatchLocally" in app_js
assert "function updateLiveCallNameEditorsUi" in app_js
assert "function handleLiveCallTableClick" in app_js
@@ -329,6 +332,9 @@ def test_operator_ui_can_fix_customer_name_from_call_views():
assert ".live-call-card-actions {" in styles_css
assert ".customer-profile-name-editor {" in styles_css
assert ".customer-profile-name-row {" in styles_css
assert ".customer-profile-name-meta {" in styles_css
assert ".customer-profile-name-note {" in styles_css
assert ".customer-profile-name-flash {" in styles_css
def test_operator_ui_surfaces_voice_name_state_across_call_views():
@@ -349,8 +355,8 @@ def test_operator_ui_surfaces_voice_name_state_across_call_views():
assert ".micro-badge.name-confirmed {" in styles_css
assert ".micro-badge.name-followup {" in styles_css
assert ".micro-badge.name-missing {" in styles_css
assert "/operator/assets/styles.css?v=track22-customer-profile-name1" in index_html
assert "/operator/assets/app.js?v=track41-customer-profile-name1" in index_html
assert "/operator/assets/styles.css?v=track23-customer-profile-name-meta1" in index_html
assert "/operator/assets/app.js?v=track42-customer-profile-name-meta1" in index_html
def test_operator_ui_hides_whatsapp_behind_feature_flag():
+33 -2
View File
@@ -47,6 +47,7 @@ const state = {
draft: '',
saving: false,
error: '',
flash: '',
},
},
telegram: {
@@ -1074,15 +1075,40 @@ function emptyCustomerSpotlightMarkup(title, description) {
`;
}
function customerProfileNameEditorMarkup(customer) {
function customerProfileVoiceNameContext(liveCalls = []) {
const latestCall = latestItemByTime(liveCalls, ['last_transition_at', 'ended_at', 'connected_at', 'started_at', 'updated_at']);
if (!latestCall) {
return {
statusMeta: null,
sourceLabel: '',
};
}
const voiceContext = voiceSummaryForItem(latestCall) || latestCall || {};
return {
statusMeta: voiceCustomerNameStatusMeta(voiceContext?.customer_name_status),
sourceLabel: formatVoiceCustomerNameSource(voiceContext?.customer_name_source),
};
}
function customerProfileNameEditorMarkup(customer, context = {}) {
const editor = state.customers.nameEditor;
const isOpen = editor.open && editor.customerId === customer.customer_id;
const statusMeta = context.statusMeta || null;
const sourceLabel = String(context.sourceLabel || '').trim();
const flash = editor.flash && editor.customerId === customer.customer_id ? editor.flash : '';
const statusText = editor.error || (editor.saving ? 'Сохраняем имя клиента...' : 'Имя сохранится в профиле клиента и voice-контуре.');
return `
<div class="customer-profile-name-row">
<div class="customer-profile-name-stack">
<h3>${escapeHtml(customer.display_name || 'Без имени')}</h3>
<p>${escapeHtml(customerLeadCompany(customer))}</p>
${(statusMeta || sourceLabel || flash) ? `
<div class="customer-profile-name-meta">
${statusMeta ? `<span class="micro-badge ${escapeHtml(statusMeta.className)}">${escapeHtml(statusMeta.shortLabel)}</span>` : ''}
${sourceLabel ? `<span class="customer-profile-name-note">Источник: ${escapeHtml(sourceLabel)}</span>` : ''}
${flash ? `<span class="customer-profile-name-flash">${escapeHtml(flash)}</span>` : ''}
</div>
` : ''}
</div>
<button class="btn ghost customer-profile-name-trigger" type="button" data-customer-action="edit-name">
${isOpen ? 'Редактирование имени' : 'Изменить имя'}
@@ -1128,6 +1154,7 @@ function customerSpotlightMarkup(customer, options = {}) {
const liveCalls = Array.isArray(historyPayload?.live_calls)
? historyPayload.live_calls
: customerLiveCalls(customer);
const profileNameContext = customerProfileVoiceNameContext(liveCalls);
const historyEvents = Array.isArray(historyPayload?.history) && historyPayload.history.length
? historyPayload.history
: buildCustomerHistoryEvents(customer);
@@ -1203,7 +1230,7 @@ function customerSpotlightMarkup(customer, options = {}) {
<div class="customer-profile-avatar">${escapeHtml(customerInitials(customer.display_name))}</div>
<div class="customer-profile-copy">
<div class="customer-profile-kicker">Клиент ${escapeHtml(customer.customer_id)}</div>
${customerProfileNameEditorMarkup(customer)}
${customerProfileNameEditorMarkup(customer, profileNameContext)}
</div>
<div class="customer-profile-badges">
<span class="${source.className}">${escapeHtml(source.label)}</span>
@@ -1420,6 +1447,7 @@ function selectCustomer(customerId) {
state.customers.nameEditor.draft = '';
state.customers.nameEditor.saving = false;
state.customers.nameEditor.error = '';
state.customers.nameEditor.flash = '';
}
state.customers.selectedCustomerId = customerId || '';
if (customerId) {
@@ -4188,6 +4216,7 @@ function openCustomerProfileNameEditor(customerId = state.customers.selectedCust
state.customers.nameEditor.draft = customer.display_name || '';
state.customers.nameEditor.saving = false;
state.customers.nameEditor.error = '';
state.customers.nameEditor.flash = '';
renderCustomerSpotlight();
}
@@ -4354,6 +4383,7 @@ async function saveCustomerProfileName() {
renderCustomerSpotlight();
try {
const savedName = await persistCustomerDisplayName({ customerId, displayName });
state.customers.nameEditor.flash = 'Сохранено';
closeCustomerProfileNameEditor();
log('Имя клиента обновлено из профиля', { customer_id: customerId, name: savedName });
} catch (err) {
@@ -6394,6 +6424,7 @@ function handleCustomerProfileNameInput(event) {
return;
}
state.customers.nameEditor.draft = input.value || '';
state.customers.nameEditor.flash = '';
if (!state.customers.nameEditor.error) {
return;
}
+2 -2
View File
@@ -8,7 +8,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/operator/assets/styles.css?v=track22-customer-profile-name1" />
<link rel="stylesheet" href="/operator/assets/styles.css?v=track23-customer-profile-name-meta1" />
</head>
<body>
<div class="app-shell">
@@ -640,6 +640,6 @@
<script src="/operator/assets/access-guards.js?v=track16-voice-transcript1"></script>
<script src="/operator/assets/sip-0.21.2.min.js?v=track16-voice-transcript1"></script>
<script src="/operator/assets/app.js?v=track41-customer-profile-name1"></script>
<script src="/operator/assets/app.js?v=track42-customer-profile-name-meta1"></script>
</body>
</html>
+30
View File
@@ -1117,6 +1117,36 @@ textarea::placeholder {
flex: 0 0 auto;
}
.customer-profile-name-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.customer-profile-name-note,
.customer-profile-name-flash {
min-height: 24px;
padding: 0 10px;
border-radius: 999px;
display: inline-flex;
align-items: center;
font-size: 12px;
font-weight: 700;
}
.customer-profile-name-note {
background: #f6f8fc;
border: 1px solid #dce4ef;
color: #52627d;
}
.customer-profile-name-flash {
background: rgba(16, 185, 129, 0.14);
border: 1px solid rgba(16, 185, 129, 0.3);
color: #117c5f;
}
.customer-profile-kicker {
color: var(--text-faint);
font-size: 11px;