Many fixes
This commit is contained in:
@@ -98,21 +98,61 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Получение CSRF токена из мета-тега
|
||||
* Получение CSRF токена из нескольких источников (с fallback)
|
||||
*/
|
||||
function getCsrfToken() {
|
||||
// 1. Пробуем из мета-тега
|
||||
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||
return meta ? meta.getAttribute('content') : '';
|
||||
if (meta && meta.getAttribute('content')) {
|
||||
return meta.getAttribute('content');
|
||||
}
|
||||
|
||||
// 2. Пробуем из data-атрибута body
|
||||
if (document.body && document.body.dataset.csrfToken) {
|
||||
return document.body.dataset.csrfToken;
|
||||
}
|
||||
|
||||
// 3. Пробуем из скрытого input на странице
|
||||
const csrfInput = document.querySelector('input[name*="csrf"]');
|
||||
if (csrfInput && csrfInput.value) {
|
||||
return csrfInput.value;
|
||||
}
|
||||
|
||||
// 4. Пробуем из cookie - CodeIgniter 4 использует 'csrf_cookie_name'
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
const [name, value] = cookie.trim().split('=');
|
||||
if (name === 'csrf_cookie_name' && value) {
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('CSRF token not found anywhere');
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление CSRF токена в мета-теге
|
||||
* Обновление CSRF токена во всех источниках
|
||||
*/
|
||||
function updateCsrfToken(token, hash) {
|
||||
// Обновляем мета-теги
|
||||
const tokenMeta = document.querySelector('meta[name="csrf-token"]');
|
||||
const hashMeta = document.querySelector('meta[name="csrf-hash"]');
|
||||
if (tokenMeta) tokenMeta.setAttribute('content', token);
|
||||
if (hashMeta) hashMeta.setAttribute('content', hash);
|
||||
|
||||
// Обновляем data-атрибут body
|
||||
if (document.body) {
|
||||
document.body.dataset.csrfToken = token;
|
||||
}
|
||||
|
||||
// Обновляем все input поля с CSRF
|
||||
document.querySelectorAll('input[name*="csrf"]').forEach(input => {
|
||||
input.value = token;
|
||||
});
|
||||
|
||||
// Обновляем cookie
|
||||
document.cookie = 'csrf_cookie_name=' + encodeURIComponent(token) + '; path=/; SameSite=Lax';
|
||||
}
|
||||
|
||||
// Перехват fetch()
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user