diff --git a/tests/test_gateway_ui.py b/tests/test_gateway_ui.py
index 365bed6..5420b87 100644
--- a/tests/test_gateway_ui.py
+++ b/tests/test_gateway_ui.py
@@ -315,6 +315,10 @@ def test_operator_ui_can_fix_customer_name_from_call_views():
assert 'id="browserPhoneEditNameBtn"' in index_html
assert "function openLiveCallNameEditor" in app_js
assert "function saveLiveCallCustomerName" in app_js
+ assert "function saveCustomerProfileName" in app_js
+ assert "function openCustomerProfileNameEditor" in app_js
+ assert "data-customer-name-form" in app_js
+ assert 'data-customer-action="edit-name"' in app_js
assert "function applyCustomerNamePatchLocally" in app_js
assert "function updateLiveCallNameEditorsUi" in app_js
assert "function handleLiveCallTableClick" in app_js
@@ -323,6 +327,8 @@ def test_operator_ui_can_fix_customer_name_from_call_views():
assert ".live-call-name-editor {" in styles_css
assert ".call-window-name-editor {" in styles_css
assert ".live-call-card-actions {" in styles_css
+ assert ".customer-profile-name-editor {" in styles_css
+ assert ".customer-profile-name-row {" in styles_css
def test_operator_ui_surfaces_voice_name_state_across_call_views():
@@ -343,8 +349,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=track21-voice-name-edit1" in index_html
- assert "/operator/assets/app.js?v=track40-voice-name-edit1" in index_html
+ 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
def test_operator_ui_hides_whatsapp_behind_feature_flag():
diff --git a/ui/operator/app.js b/ui/operator/app.js
index 985bff7..57d1e85 100644
--- a/ui/operator/app.js
+++ b/ui/operator/app.js
@@ -41,6 +41,13 @@ const state = {
historyById: {},
pendingHistoryById: {},
historyErrorsById: {},
+ nameEditor: {
+ open: false,
+ customerId: '',
+ draft: '',
+ saving: false,
+ error: '',
+ },
},
telegram: {
threads: [],
@@ -1067,6 +1074,40 @@ function emptyCustomerSpotlightMarkup(title, description) {
`;
}
+function customerProfileNameEditorMarkup(customer) {
+ const editor = state.customers.nameEditor;
+ const isOpen = editor.open && editor.customerId === customer.customer_id;
+ const statusText = editor.error || (editor.saving ? 'Сохраняем имя клиента...' : 'Имя сохранится в профиле клиента и voice-контуре.');
+ return `
+
+
+
${escapeHtml(customer.display_name || 'Без имени')}
+
${escapeHtml(customerLeadCompany(customer))}
+
+
+
+ ${isOpen ? `
+
+ ` : ''}
+ `;
+}
+
function customerSpotlightMarkup(customer, options = {}) {
const { pageView = false } = options;
const index = Math.max(customerIndex(customer), 0);
@@ -1162,8 +1203,7 @@ function customerSpotlightMarkup(customer, options = {}) {
${escapeHtml(customerInitials(customer.display_name))}
Клиент ${escapeHtml(customer.customer_id)}
-
${escapeHtml(customer.display_name || 'Без имени')}
-
${escapeHtml(customerLeadCompany(customer))}
+ ${customerProfileNameEditorMarkup(customer)}
${escapeHtml(source.label)}
@@ -1374,6 +1414,13 @@ function renderCustomerList() {
}
function selectCustomer(customerId) {
+ if (state.customers.nameEditor.customerId && state.customers.nameEditor.customerId !== customerId) {
+ state.customers.nameEditor.open = false;
+ state.customers.nameEditor.customerId = '';
+ state.customers.nameEditor.draft = '';
+ state.customers.nameEditor.saving = false;
+ state.customers.nameEditor.error = '';
+ }
state.customers.selectedCustomerId = customerId || '';
if (customerId) {
$('interactionCustomerId').value = customerId;
@@ -4122,6 +4169,28 @@ function liveCallCustomerId(item) {
return interactionById(item?.interaction_id || '')?.customer_id || '';
}
+function closeCustomerProfileNameEditor() {
+ state.customers.nameEditor.open = false;
+ state.customers.nameEditor.customerId = '';
+ state.customers.nameEditor.draft = '';
+ state.customers.nameEditor.saving = false;
+ state.customers.nameEditor.error = '';
+ renderCustomerSpotlight();
+}
+
+function openCustomerProfileNameEditor(customerId = state.customers.selectedCustomerId) {
+ const customer = state.customers.items.find((item) => item.customer_id === customerId) || null;
+ if (!customer) {
+ return;
+ }
+ state.customers.nameEditor.open = true;
+ state.customers.nameEditor.customerId = customer.customer_id;
+ state.customers.nameEditor.draft = customer.display_name || '';
+ state.customers.nameEditor.saving = false;
+ state.customers.nameEditor.error = '';
+ renderCustomerSpotlight();
+}
+
function closeLiveCallNameEditor() {
state.liveCalls.nameEditor.open = false;
state.liveCalls.nameEditor.callId = '';
@@ -4209,10 +4278,35 @@ function applyCustomerNamePatchLocally({ customerId, callId, displayName }) {
customer_name_source: 'manual',
};
});
+ if (state.liveCalls.nameEditor.customerId === customerId && !state.liveCalls.nameEditor.saving) {
+ state.liveCalls.nameEditor.draft = normalizedName;
+ }
+ if (state.customers.nameEditor.customerId === customerId && !state.customers.nameEditor.saving) {
+ state.customers.nameEditor.draft = normalizedName;
+ }
renderCustomerList();
refreshVoiceSummaryDependentViews();
}
+async function persistCustomerDisplayName({ customerId, displayName, callId = '' }) {
+ const data = await api('customer', `customers/${encodeURIComponent(customerId)}`, {
+ method: 'PATCH',
+ body: JSON.stringify({ display_name: displayName, source: 'manual' }),
+ });
+ const savedName = data?.display_name || displayName;
+ applyCustomerNamePatchLocally({
+ customerId,
+ callId,
+ displayName: savedName,
+ });
+ ensureCustomerHistoryLoaded(customerId, { force: true }).catch(() => {});
+ if (callId) {
+ loadVoiceAiSummary(callId, { force: true, silent: true }).catch(() => {});
+ }
+ refreshLiveCallsInBackground();
+ return savedName;
+}
+
async function saveLiveCallCustomerName() {
const { customerId, callId } = state.liveCalls.nameEditor;
const displayName = String(state.liveCalls.nameEditor.draft || '').trim().replace(/\s+/g, ' ');
@@ -4228,19 +4322,9 @@ async function saveLiveCallCustomerName() {
state.liveCalls.nameEditor.error = '';
updateLiveCallNameEditorsUi();
try {
- const data = await api('customer', `customers/${encodeURIComponent(customerId)}`, {
- method: 'PATCH',
- body: JSON.stringify({ display_name: displayName, source: 'manual' }),
- });
- applyCustomerNamePatchLocally({
- customerId,
- callId,
- displayName: data?.display_name || displayName,
- });
+ const savedName = await persistCustomerDisplayName({ customerId, callId, displayName });
+ const data = { display_name: savedName };
closeLiveCallNameEditor();
- ensureCustomerHistoryLoaded(customerId, { force: true }).catch(() => {});
- loadVoiceAiSummary(callId, { force: true, silent: true }).catch(() => {});
- refreshLiveCallsInBackground();
log('Имя клиента обновлено оператором', { customer_id: customerId, call_id: callId, name: data?.display_name || displayName });
} catch (err) {
state.liveCalls.nameEditor.saving = false;
@@ -4249,6 +4333,36 @@ async function saveLiveCallCustomerName() {
}
}
+async function saveCustomerProfileName() {
+ const customerId = state.customers.nameEditor.customerId || state.customers.selectedCustomerId;
+ const displayName = String(state.customers.nameEditor.draft || '').trim().replace(/\s+/g, ' ');
+ if (!customerId) {
+ return;
+ }
+ if (displayName.length < 2) {
+ state.customers.nameEditor.error = 'Введите имя клиента минимум из 2 символов.';
+ renderCustomerSpotlight();
+ return;
+ }
+ const currentName = String(selectedCustomer()?.display_name || '').trim();
+ if (displayName === currentName) {
+ closeCustomerProfileNameEditor();
+ return;
+ }
+ state.customers.nameEditor.saving = true;
+ state.customers.nameEditor.error = '';
+ renderCustomerSpotlight();
+ try {
+ const savedName = await persistCustomerDisplayName({ customerId, displayName });
+ closeCustomerProfileNameEditor();
+ log('Имя клиента обновлено из профиля', { customer_id: customerId, name: savedName });
+ } catch (err) {
+ state.customers.nameEditor.saving = false;
+ state.customers.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.';
+ renderCustomerSpotlight();
+ }
+}
+
function liveCallNameEditorMeta(item, customerId) {
if (!item || !customerId) {
return 'Выберите звонок, связанный с клиентом, чтобы исправить имя.';
@@ -6232,6 +6346,14 @@ async function handleCustomerActionClick(event) {
if (!customer) {
return;
}
+ if (button.dataset.customerAction === 'edit-name') {
+ openCustomerProfileNameEditor(customer.customer_id);
+ return;
+ }
+ if (button.dataset.customerAction === 'cancel-name-edit') {
+ closeCustomerProfileNameEditor();
+ return;
+ }
if (button.dataset.customerAction === 'workspace') {
$('interactionCustomerId').value = customer.customer_id;
window.location.hash = '#workspace';
@@ -6266,6 +6388,37 @@ async function handleCustomerActionClick(event) {
}
}
+function handleCustomerProfileNameInput(event) {
+ const input = event.target?.closest?.('[data-customer-name-input]');
+ if (!input) {
+ return;
+ }
+ state.customers.nameEditor.draft = input.value || '';
+ if (!state.customers.nameEditor.error) {
+ return;
+ }
+ state.customers.nameEditor.error = '';
+ const form = input.closest('[data-customer-name-form]');
+ const status = form?.querySelector?.('[data-customer-name-status]');
+ if (status) {
+ status.textContent = 'Имя сохранится в профиле клиента и voice-контуре.';
+ status.classList.remove('error');
+ }
+}
+
+function handleCustomerProfileNameSubmit(event) {
+ const form = event.target?.closest?.('[data-customer-name-form]');
+ if (!form) {
+ return;
+ }
+ event.preventDefault();
+ saveCustomerProfileName().catch((err) => {
+ state.customers.nameEditor.saving = false;
+ state.customers.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.';
+ renderCustomerSpotlight();
+ });
+}
+
function wire() {
$('loginBtn').addEventListener('click', login);
$('corporateLoginBtn').addEventListener('click', startCorporateLogin);
@@ -6313,7 +6466,11 @@ function wire() {
openCustomerProfile(row.dataset.customerId || '');
});
$('customerSpotlight')?.addEventListener('click', handleCustomerActionClick);
+ $('customerSpotlight')?.addEventListener('input', handleCustomerProfileNameInput);
+ $('customerSpotlight')?.addEventListener('submit', handleCustomerProfileNameSubmit);
$('customerProfileContent').addEventListener('click', handleCustomerActionClick);
+ $('customerProfileContent').addEventListener('input', handleCustomerProfileNameInput);
+ $('customerProfileContent').addEventListener('submit', handleCustomerProfileNameSubmit);
$('customerProfileBackBtn').addEventListener('click', () => {
window.location.hash = '#customers-page';
});
diff --git a/ui/operator/index.html b/ui/operator/index.html
index fd89be7..9b05f85 100644
--- a/ui/operator/index.html
+++ b/ui/operator/index.html
@@ -8,7 +8,7 @@
-
+
@@ -640,6 +640,6 @@
-
+