/** * 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.contacts.length > 0 ? this.contacts.map(contact => this.renderRow(contact)).join('') : `` }
Имя Email Телефон Должность
Нет контактов
`; 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); } } } });