bp/public/assets/js/modules/contacts.js

427 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();
}
/**
* Получить заголовки запроса
*/
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 = `
<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) {
const escapedId = this.escapeJs(contact.id);
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="btn-group contact-actions">
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="contactsManager.edit('${escapedId}')" title="Редактировать">
<i class="fa-solid fa-pen"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-sm"
onclick="contactsManager.remove('${escapedId}')" title="Удалить">
<i class="fa-solid fa-trash"></i>
</button>
</div>
<div class="btn-group edit-actions" style="display: none;">
<button type="button" class="btn btn-outline-success btn-sm"
onclick="contactsManager.save('${escapedId}')" title="Сохранить">
<i class="fa-solid fa-check"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm"
onclick="contactsManager.cancel('${escapedId}')" title="Отмена">
<i class="fa-solid fa-times"></i>
</button>
</div>
</td>`;
}
/**
* Добавить новый контакт
*/
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)}
<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;
}
/**
* Экранирование для 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, '\\x3c')
.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);
}
}
}
});