Many fixes

This commit is contained in:
root
2026-01-16 21:58:12 +03:00
parent b810a17649
commit 77f76c8c28
45 changed files with 3392 additions and 1243 deletions
+43 -3
View File
@@ -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()
+399
View File
@@ -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);
}
}
}
});