Add customer profile name editing for operator

This commit is contained in:
Yera All
2026-04-08 15:13:05 +05:00
parent fa2ea1ef03
commit 6f25de1f62
4 changed files with 234 additions and 19 deletions
+171 -14
View File
@@ -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';
});
+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=track21-voice-name-edit1" />
<link rel="stylesheet" href="/operator/assets/styles.css?v=track22-customer-profile-name1" />
</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=track40-voice-name-edit1"></script>
<script src="/operator/assets/app.js?v=track41-customer-profile-name1"></script>
</body>
</html>
+53 -1
View File
@@ -1100,6 +1100,23 @@ textarea::placeholder {
min-width: 0;
}
.customer-profile-name-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
margin-top: 8px;
}
.customer-profile-name-stack {
min-width: 0;
flex: 1;
}
.customer-profile-name-trigger {
flex: 0 0 auto;
}
.customer-profile-kicker {
color: var(--text-faint);
font-size: 11px;
@@ -1124,6 +1141,35 @@ textarea::placeholder {
line-height: 1.5;
}
.customer-profile-name-editor {
display: grid;
gap: 10px;
margin-top: 14px;
padding: 14px 16px;
border-radius: 18px;
border: 1px solid rgba(163, 183, 222, 0.55);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(244, 248, 255, 0.96));
}
.customer-profile-name-editor-label {
color: var(--text-faint);
font-size: 11px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.customer-profile-name-editor-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.customer-profile-name-editor-form input {
min-width: 220px;
flex: 1 1 260px;
}
.customer-profile-badges {
display: flex;
flex-wrap: wrap;
@@ -5189,6 +5235,11 @@ body.analytics-drilldown-open {
flex-direction: column;
}
.customer-profile-name-row {
flex-direction: column;
align-items: stretch;
}
.customer-profile-badges {
justify-content: flex-start;
max-width: none;
@@ -5225,7 +5276,8 @@ body.analytics-drilldown-open {
}
.call-window-name-form,
.live-call-name-editor-form {
.live-call-name-editor-form,
.customer-profile-name-editor-form {
grid-template-columns: 1fr;
}