Add customer profile name editing for operator
This commit is contained in:
+171
-14
@@ -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 `
|
||||
<div class="customer-profile-name-row">
|
||||
<div class="customer-profile-name-stack">
|
||||
<h3>${escapeHtml(customer.display_name || 'Без имени')}</h3>
|
||||
<p>${escapeHtml(customerLeadCompany(customer))}</p>
|
||||
</div>
|
||||
<button class="btn ghost customer-profile-name-trigger" type="button" data-customer-action="edit-name">
|
||||
${isOpen ? 'Редактирование имени' : 'Изменить имя'}
|
||||
</button>
|
||||
</div>
|
||||
${isOpen ? `
|
||||
<form class="customer-profile-name-editor" data-customer-name-form>
|
||||
<div class="customer-profile-name-editor-label">Как сохранить имя клиента в профиле</div>
|
||||
<div class="customer-profile-name-editor-form">
|
||||
<input
|
||||
type="text"
|
||||
value="${escapeHtml(editor.draft)}"
|
||||
placeholder="Введите имя клиента"
|
||||
data-customer-name-input
|
||||
${editor.saving ? 'disabled' : ''}
|
||||
/>
|
||||
<button class="btn" type="submit" ${editor.saving ? 'disabled' : ''}>${editor.saving ? 'Сохраняем...' : 'Сохранить имя'}</button>
|
||||
<button class="btn ghost" type="button" data-customer-action="cancel-name-edit" ${editor.saving ? 'disabled' : ''}>Отмена</button>
|
||||
</div>
|
||||
<p class="hint${editor.error ? ' error' : ''}" data-customer-name-status>${escapeHtml(statusText)}</p>
|
||||
</form>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
function customerSpotlightMarkup(customer, options = {}) {
|
||||
const { pageView = false } = options;
|
||||
const index = Math.max(customerIndex(customer), 0);
|
||||
@@ -1162,8 +1203,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>
|
||||
<h3>${escapeHtml(customer.display_name || 'Без имени')}</h3>
|
||||
<p>${escapeHtml(customerLeadCompany(customer))}</p>
|
||||
${customerProfileNameEditorMarkup(customer)}
|
||||
</div>
|
||||
<div class="customer-profile-badges">
|
||||
<span class="${source.className}">${escapeHtml(source.label)}</span>
|
||||
@@ -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';
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user