400 lines
15 KiB
JavaScript
400 lines
15 KiB
JavaScript
/**
|
||
* Inline-редактирование контактов в карточке клиента
|
||
*
|
||
* Использование:
|
||
* <div id="contacts-container"
|
||
* data-client-id="123"
|
||
* data-api-url="/crm/contacts"
|
||
* data-csrf-token="...">
|
||
* </div>
|
||
*/
|
||
|
||
class ContactsManager {
|
||
constructor(container) {
|
||
this.container = container;
|
||
this.clientId = container.dataset.clientId;
|
||
this.apiUrl = container.dataset.apiUrl;
|
||
this.csrfToken = container.dataset.csrfToken;
|
||
this.contacts = [];
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.loadContacts();
|
||
}
|
||
|
||
/**
|
||
* Загрузить список контактов
|
||
*/
|
||
async loadContacts() {
|
||
try {
|
||
const response = await fetch(`${this.apiUrl}/list/${this.clientId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
this.contacts = data.items || [];
|
||
this.render();
|
||
} else {
|
||
this.showError(data.message || 'Ошибка загрузки контактов');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки контактов:', error);
|
||
this.showError('Ошибка соединения с сервером');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отобразить таблицу контактов
|
||
*/
|
||
render() {
|
||
// Обновляем счётчик
|
||
const countBadge = document.getElementById('contacts-count');
|
||
if (countBadge) {
|
||
countBadge.textContent = this.contacts.length;
|
||
}
|
||
|
||
// Формируем HTML
|
||
const emptyState = `
|
||
<div class="text-center py-4 text-muted">
|
||
<i class="fa-solid fa-users fa-3x mb-3 text-muted opacity-50"></i>
|
||
<p>Контактов пока нет</p>
|
||
<button type="button" class="btn btn-primary btn-sm" onclick="contactsManager.addNew()">
|
||
<i class="fa-solid fa-plus me-1"></i>Добавить контакт
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
const tableHtml = `
|
||
<div class="d-flex justify-content-end mb-3">
|
||
<button type="button" class="btn btn-primary btn-sm" onclick="contactsManager.addNew()">
|
||
<i class="fa-solid fa-plus me-1"></i>Добавить контакт
|
||
</button>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-hover" id="contacts-table">
|
||
<thead class="bg-light">
|
||
<tr>
|
||
<th style="width: 30%;">Имя</th>
|
||
<th style="width: 25%;">Email</th>
|
||
<th style="width: 25%;">Телефон</th>
|
||
<th style="width: 20%;">Должность</th>
|
||
<th style="width: 60px;"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="contacts-tbody">
|
||
${this.contacts.length > 0
|
||
? this.contacts.map(contact => this.renderRow(contact)).join('')
|
||
: `<tr><td colspan="5" class="text-center py-4 text-muted">Нет контактов</td></tr>`
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
|
||
this.container.innerHTML = this.contacts.length > 0 ? tableHtml : emptyState;
|
||
}
|
||
|
||
/**
|
||
* Отобразить одну строку контакта
|
||
*/
|
||
renderRow(contact) {
|
||
return `
|
||
<tr data-id="${contact.id}" class="contact-row">
|
||
<td>
|
||
<span class="contact-display contact-name">${this.escapeHtml(contact.name)}</span>
|
||
<input type="text" class="form-control form-control-sm contact-edit contact-name-input"
|
||
value="${this.escapeHtml(contact.name)}" style="display: none;" placeholder="Имя">
|
||
</td>
|
||
<td>
|
||
<span class="contact-display contact-email">${this.escapeHtml(contact.email || '—')}</span>
|
||
<input type="email" class="form-control form-control-sm contact-edit contact-email-input"
|
||
value="${this.escapeHtml(contact.email || '')}" style="display: none;" placeholder="Email">
|
||
</td>
|
||
<td>
|
||
<span class="contact-display contact-phone">${this.escapeHtml(contact.phone || '—')}</span>
|
||
<input type="text" class="form-control form-control-sm contact-edit contact-phone-input"
|
||
value="${this.escapeHtml(contact.phone || '')}" style="display: none;" placeholder="Телефон">
|
||
</td>
|
||
<td>
|
||
<span class="contact-display contact-position">${this.escapeHtml(contact.position || '—')}</span>
|
||
<input type="text" class="form-control form-control-sm contact-edit contact-position-input"
|
||
value="${this.escapeHtml(contact.position || '')}" style="display: none;" placeholder="Должность">
|
||
</td>
|
||
<td class="text-end">
|
||
<div class="contact-actions">
|
||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||
onclick="contactsManager.edit(${contact.id})" title="Редактировать">
|
||
<i class="fa-solid fa-pen"></i>
|
||
</button>
|
||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||
onclick="contactsManager.remove(${contact.id})" title="Удалить">
|
||
<i class="fa-solid fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
<div class="edit-actions" style="display: none;">
|
||
<button type="button" class="btn btn-outline-success btn-sm"
|
||
onclick="contactsManager.save(${contact.id})" title="Сохранить">
|
||
<i class="fa-solid fa-check"></i>
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||
onclick="contactsManager.cancel(${contact.id})" title="Отмена">
|
||
<i class="fa-solid fa-times"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Добавить новый контакт
|
||
*/
|
||
addNew() {
|
||
const newId = 'new_' + Date.now();
|
||
const emptyRow = {
|
||
id: newId,
|
||
name: '',
|
||
email: '',
|
||
phone: '',
|
||
position: '',
|
||
};
|
||
|
||
this.contacts.push(emptyRow);
|
||
this.render();
|
||
|
||
// Переключаем новую строку в режим редактирования
|
||
this.edit(newId);
|
||
}
|
||
|
||
/**
|
||
* Начать редактирование контакта
|
||
*/
|
||
edit(contactId) {
|
||
const row = this.container.querySelector(`tr[data-id="${contactId}"]`);
|
||
if (!row) return;
|
||
|
||
// Показываем инпуты, скрываем текст
|
||
row.querySelectorAll('.contact-display').forEach(el => el.style.display = 'none');
|
||
row.querySelectorAll('.contact-edit').forEach(el => el.style.display = 'block');
|
||
|
||
// Скрываем кнопки действий, показываем кнопки редактирования
|
||
row.querySelector('.contact-actions').style.display = 'none';
|
||
row.querySelector('.edit-actions').style.display = 'inline-flex';
|
||
|
||
// Фокус на поле имени
|
||
const nameInput = row.querySelector('.contact-name-input');
|
||
if (nameInput) {
|
||
nameInput.focus();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Сохранить изменения контакта
|
||
*/
|
||
async save(contactId) {
|
||
const row = this.container.querySelector(`tr[data-id="${contactId}"]`);
|
||
if (!row) return;
|
||
|
||
const data = {
|
||
customer_id: this.clientId,
|
||
name: row.querySelector('.contact-name-input').value.trim(),
|
||
email: row.querySelector('.contact-email-input').value.trim(),
|
||
phone: row.querySelector('.contact-phone-input').value.trim(),
|
||
position: row.querySelector('.contact-position-input').value.trim(),
|
||
};
|
||
|
||
// Валидация
|
||
if (!data.name) {
|
||
this.showError('Имя контакта обязательно');
|
||
row.querySelector('.contact-name-input').focus();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
let response;
|
||
|
||
if (contactId.toString().startsWith('new_')) {
|
||
// Создание нового
|
||
response = await fetch(`${this.apiUrl}/store`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(data)
|
||
});
|
||
} else {
|
||
// Обновление существующего
|
||
response = await fetch(`${this.apiUrl}/update/${contactId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(data)
|
||
});
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
// Обновляем локальный массив
|
||
const index = this.contacts.findIndex(c => c.id === contactId);
|
||
if (index !== -1) {
|
||
if (result.item) {
|
||
// Обновляем с реальным ID от сервера
|
||
this.contacts[index] = { ...data, id: result.item.id };
|
||
} else {
|
||
this.contacts[index] = data;
|
||
}
|
||
}
|
||
|
||
this.render();
|
||
this.showSuccess(result.message || 'Сохранено');
|
||
} else {
|
||
this.showError(result.message || 'Ошибка сохранения');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка сохранения контакта:', error);
|
||
this.showError('Ошибка соединения с сервером');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отменить редактирование
|
||
*/
|
||
cancel(contactId) {
|
||
if (contactId.toString().startsWith('new_')) {
|
||
// Удаляем новую строку
|
||
this.contacts = this.contacts.filter(c => c.id !== contactId);
|
||
this.render();
|
||
} else {
|
||
// Перезагружаем данные
|
||
this.loadContacts();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Удалить контакт
|
||
*/
|
||
async remove(contactId) {
|
||
if (!confirm('Удалить контакт?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${this.apiUrl}/delete/${contactId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
this.contacts = this.contacts.filter(c => c.id !== contactId);
|
||
this.render();
|
||
this.showSuccess(result.message || 'Контакт удалён');
|
||
} else {
|
||
this.showError(result.message || 'Ошибка удаления');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка удаления контакта:', error);
|
||
this.showError('Ошибка соединения с сервером');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Показать сообщение об ошибке
|
||
*/
|
||
showError(message) {
|
||
this.showNotification(message, 'danger');
|
||
}
|
||
|
||
/**
|
||
* Показать сообщение об успехе
|
||
*/
|
||
showSuccess(message) {
|
||
this.showNotification(message, 'success');
|
||
}
|
||
|
||
/**
|
||
* Показать уведомление
|
||
*/
|
||
showNotification(message, type) {
|
||
// Удаляем предыдущие уведомления
|
||
const existing = this.container.querySelector('.contacts-alert');
|
||
if (existing) existing.remove();
|
||
|
||
const alert = document.createElement('div');
|
||
alert.className = `contacts-alert alert alert-${type} alert-dismissible fade show mt-3`;
|
||
alert.role = 'alert';
|
||
alert.innerHTML = `
|
||
${this.escapeHtml(message)}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||
`;
|
||
|
||
this.container.insertBefore(alert, this.container.firstChild);
|
||
|
||
// Автоудаление через 3 секунды
|
||
setTimeout(() => {
|
||
if (alert.parentNode) {
|
||
alert.remove();
|
||
}
|
||
}, 3000);
|
||
}
|
||
|
||
/**
|
||
* Экранирование HTML
|
||
*/
|
||
escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
}
|
||
|
||
// Инициализация при загрузке страницы
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const container = document.getElementById('contacts-container');
|
||
if (container) {
|
||
window.contactsManager = new ContactsManager(container);
|
||
}
|
||
});
|
||
|
||
// Обработка Enter в полях редактирования
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') {
|
||
const target = e.target;
|
||
if (target.classList.contains('contact-edit')) {
|
||
e.preventDefault();
|
||
const row = target.closest('tr');
|
||
if (row) {
|
||
const contactId = parseInt(row.dataset.id) || row.dataset.id;
|
||
window.contactsManager.save(contactId);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (e.key === 'Escape') {
|
||
const target = e.target;
|
||
if (target.classList.contains('contact-edit')) {
|
||
e.preventDefault();
|
||
const row = target.closest('tr');
|
||
if (row) {
|
||
const contactId = parseInt(row.dataset.id) || row.dataset.id;
|
||
window.contactsManager.cancel(contactId);
|
||
}
|
||
}
|
||
}
|
||
});
|