/**
* Inline-редактирование контактов в карточке клиента
*
* Использование:
*
*
*/
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();
}
/**
* Получить заголовки запроса
*/
getHeaders() {
return {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
};
}
/**
* Загрузить список контактов
*/
async loadContacts() {
try {
const response = await fetch(`${this.apiUrl}/list/${this.clientId}`, {
method: 'POST',
credentials: 'same-origin',
headers: this.getHeaders(),
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 = `
Контактов пока нет
`;
const tableHtml = `
`;
this.container.innerHTML = this.contacts.length > 0 ? tableHtml : emptyState;
}
/**
* Отобразить одну строку контакта
*/
renderRow(contact) {
const escapedId = this.escapeJs(contact.id);
return `
|
${this.escapeHtml(contact.name)}
|
${this.escapeHtml(contact.email || '—')}
|
${this.escapeHtml(contact.phone || '—')}
|
${this.escapeHtml(contact.position || '—')}
|
| `;
}
/**
* Добавить новый контакт
*/
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',
credentials: 'same-origin',
headers: this.getHeaders(),
body: JSON.stringify(data)
});
} else {
// Обновление существующего
response = await fetch(`${this.apiUrl}/update/${contactId}`, {
method: 'POST',
credentials: 'same-origin',
headers: this.getHeaders(),
body: JSON.stringify(data)
});
}
const result = await response.json();
if (result.success) {
// Обновляем локальный массив
// contactId может быть строкой из data-id, а c.id - числом из БД
const contactIdStr = String(contactId);
const index = this.contacts.findIndex(c => String(c.id) === contactIdStr);
if (index !== -1) {
if (result.item) {
// Обновляем с реальным ID от сервера
this.contacts[index] = { ...data, id: result.item.id };
} else {
this.contacts[index] = { ...data, id: contactId };
}
}
this.render();
this.showSuccess(result.message || 'Сохранено');
} else {
this.showError(result.message || 'Ошибка сохранения');
}
} catch (error) {
console.error('Ошибка сохранения контакта:', error);
this.showError('Ошибка соединения с сервером');
}
}
/**
* Отменить редактирование
*/
cancel(contactId) {
const contactIdStr = String(contactId);
if (contactIdStr.startsWith('new_')) {
// Удаляем новую строку
this.contacts = this.contacts.filter(c => String(c.id) !== contactIdStr);
this.render();
} else {
// Перезагружаем данные
this.loadContacts();
}
}
/**
* Удалить контакт
*/
async remove(contactId) {
if (!confirm('Удалить контакт?')) {
return;
}
try {
const response = await fetch(`${this.apiUrl}/delete/${contactId}`, {
method: 'POST',
credentials: 'same-origin',
headers: this.getHeaders(),
body: JSON.stringify({})
});
const result = await response.json();
if (result.success) {
// contactId может быть строкой, а c.id - числом
const contactIdStr = String(contactId);
this.contacts = this.contacts.filter(c => String(c.id) !== contactIdStr);
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)}
`;
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;
}
/**
* Экранирование для JavaScript строки
*/
escapeJs(text) {
if (!text) return '';
// Приводим к строке, так как id может быть числом
const str = String(text);
return str
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(//g, '\\x3e');
}
}
// Инициализация при загрузке страницы
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);
}
}
}
});