Corp/backend/static/js/components/documentCrudHandler.js

518 lines
18 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.

/**
* @module documentCrudHandler
* @description Обработчик CRUD операций для документов с поддержкой RecipientPicker.
* Управляет созданием, редактированием и удалением документов через API.
*
* Использование:
* import { initDocumentCrud } from './documentCrudHandler.js';
* import { RecipientPicker } from './recipientPickerHandler.js';
*
* initDocumentCrud({
* apiListUrl: '/api/v1/documents/',
* apiDetailBase: '/api/v1/documents/',
* headers: { 'Authorization': 'Bearer TOKEN' },
* createPicker: recipientPickerInstance,
* editPicker: recipientPickerInstance
* });
*/
/**
* Получает CSRF-токен из cookie.
* @returns {string|null} CSRF-токен или null
*/
function getCsrfToken() {
const name = 'csrftoken';
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
/**
* Переключает видимость блоков получателей в зависимости от чекбокса "Отправить всем".
* @param {HTMLInputElement} checkbox - Чекбокс "sent_to_all"
* @param {HTMLElement} deptBlock - Блок с виджетом отделов
* @param {HTMLElement} recipBlock - Блок с виджетом получателей
*/
function toggleRecipientsBlocks(checkbox, deptBlock, recipBlock) {
const hidden = checkbox.checked;
console.log('toggleRecipientsBlocks:', { checked: checkbox.checked, hidden, deptBlock: !!deptBlock, recipBlock: !!recipBlock });
if (deptBlock) deptBlock.hidden = hidden;
if (recipBlock) recipBlock.hidden = hidden;
}
/**
* Добавляет ID получателей в FormData.
* @param {FormData} formData - Объект FormData
* @param {Array<number>} ids - Массив ID получателей
*/
function appendRecipientIds(formData, ids) {
// API ожидает повторяемые recipient_ids
for (const id of ids) {
formData.append('recipient_ids', String(id));
}
}
/**
* Добавляет ID отделов в FormData.
* @param {FormData} formData - Объект FormData
* @param {Array<number>} ids - Массив ID отделов
*/
function appendDepartmentIds(formData, ids) {
// API ожидает повторяемые department_ids
for (const id of ids) {
formData.append('department_ids', String(id));
}
}
/**
* Загружает список отделов из API и заполняет select элементы.
* @param {string} departmentsApi - URL API для получения списка отделов
* @param {Object} headers - HTTP заголовки для запросов
* @param {HTMLSelectElement} createSelect - Select для формы создания
* @param {HTMLSelectElement} editSelect - Select для формы редактирования
*/
async function loadDepartments(departmentsApi, headers, createSelect, editSelect) {
if (!departmentsApi || (!createSelect && !editSelect)) {
console.warn('loadDepartments: отсутствует URL API или select элементы');
return;
}
try {
const response = await fetch(departmentsApi, { headers });
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
const data = await response.json();
const departments = data.results || data || [];
// Заполняем оба select'а
[createSelect, editSelect].forEach(select => {
if (!select) return;
select.innerHTML = '';
departments.forEach(dept => {
const option = document.createElement('option');
option.value = dept.id;
option.textContent = dept.name;
select.appendChild(option);
});
});
console.log(`Загружено отделов: ${departments.length}`);
} catch (error) {
console.error('Ошибка загрузки отделов:', error);
// Показываем сообщение в select'ах
[createSelect, editSelect].forEach(select => {
if (!select) return;
select.innerHTML = '<option disabled>Ошибка загрузки отделов</option>';
});
}
}
/**
* Инициализирует обработчики CRUD операций для документов.
* @param {Object} options - Опции инициализации
* @param {string} options.apiListUrl - URL API для списка документов
* @param {string} options.apiDetailBase - Базовый URL для операций с конкретным документом
* @param {Object} [options.headers={}] - HTTP заголовки для запросов
* @param {Object} options.createPicker - Экземпляр RecipientPicker для формы создания
* @param {Object} options.editPicker - Экземпляр RecipientPicker для формы редактирования
* @param {string} [options.departmentsApi] - URL API для получения списка отделов
* @returns {Object} API с методом destroy
*/
export function initDocumentCrud(options) {
const {
apiListUrl,
apiDetailBase,
headers = {},
createPicker,
editPicker,
departmentsApi
} = options;
// Элементы формы создания
const createForm = document.getElementById('docCreateForm');
const createAllCheckbox = document.getElementById('createSentToAll');
const createDeptBlock = document.getElementById('createDepartmentsBlock');
const createRecipBlock = document.getElementById('createRecipientsBlock');
const createDeptSelect = document.getElementById('createDepartments');
console.log('🔍 Create form elements:', {
createForm: !!createForm,
createAllCheckbox: !!createAllCheckbox,
createDeptBlock: !!createDeptBlock,
createRecipBlock: !!createRecipBlock,
createDeptSelect: !!createDeptSelect
});
// Элементы формы редактирования
const editModal = document.getElementById('docEditModal');
const editForm = document.getElementById('docEditForm');
const editAllCheckbox = document.getElementById('editSentToAll');
const editDeptBlock = document.getElementById('editDepartmentsBlock');
const editRecipBlock = document.getElementById('editRecipientsBlock');
const editDeptSelect = document.getElementById('editDepartments');
console.log('🔍 Edit form elements:', {
editModal: !!editModal,
editForm: !!editForm,
editAllCheckbox: !!editAllCheckbox,
editDeptBlock: !!editDeptBlock,
editRecipBlock: !!editRecipBlock,
editDeptSelect: !!editDeptSelect
});
// Список документов
const listElement = document.getElementById('docList');
console.log('🔍 Required elements check:', {
createForm: !!createForm,
editForm: !!editForm,
listElement: !!listElement
});
if (!createForm || !editForm || !listElement) {
console.warn('❌ initDocumentCrud: обязательные элементы не найдены', {
createForm: !!createForm,
editForm: !!editForm,
listElement: !!listElement
});
return { destroy: () => {} };
}
// Загружаем список отделов
if (departmentsApi) {
loadDepartments(departmentsApi, headers, createDeptSelect, editDeptSelect);
}
// Инициализация переключения блоков получателей
if (createAllCheckbox) {
console.log('✅ Init create checkbox toggle:', {
checkbox: !!createAllCheckbox,
deptBlock: !!createDeptBlock,
recipBlock: !!createRecipBlock,
initialChecked: createAllCheckbox.checked
});
toggleRecipientsBlocks(createAllCheckbox, createDeptBlock, createRecipBlock);
// Добавляем обработчики на change и click для надёжности
const handleToggle = (event) => {
console.log('🔄 Create checkbox changed:', {
checked: createAllCheckbox.checked,
willHide: createAllCheckbox.checked,
eventType: event.type
});
toggleRecipientsBlocks(createAllCheckbox, createDeptBlock, createRecipBlock);
};
createAllCheckbox.addEventListener('change', handleToggle);
createAllCheckbox.addEventListener('click', handleToggle);
} else {
console.warn('❌ createAllCheckbox not found!');
}
if (editAllCheckbox) {
console.log('✅ Init edit checkbox toggle:', {
checkbox: !!editAllCheckbox,
deptBlock: !!editDeptBlock,
recipBlock: !!editRecipBlock,
initialChecked: editAllCheckbox.checked
});
toggleRecipientsBlocks(editAllCheckbox, editDeptBlock, editRecipBlock);
// Добавляем обработчики на change и click для надёжности
const handleToggle = (event) => {
console.log('🔄 Edit checkbox changed:', {
checked: editAllCheckbox.checked,
willHide: editAllCheckbox.checked,
eventType: event.type
});
toggleRecipientsBlocks(editAllCheckbox, editDeptBlock, editRecipBlock);
};
editAllCheckbox.addEventListener('change', handleToggle);
editAllCheckbox.addEventListener('click', handleToggle);
} else {
console.warn('❌ editAllCheckbox not found!');
}
/**
* Обработчик создания документа.
*/
async function handleCreate(e) {
e.preventDefault();
const formData = new FormData(createForm);
formData.set('sent_to_all', createAllCheckbox.checked ? 'true' : 'false');
if (!createAllCheckbox.checked) {
// Собираем выбранные ID отделов
const deptIds = createDeptSelect
? Array.from(createDeptSelect.selectedOptions).map(opt => opt.value)
: [];
// Собираем выбранные ID получателей
const recipientIds = createPicker.getIds();
if (deptIds.length === 0 && recipientIds.length === 0) {
alert('Необходимо выбрать:\n- Хотя бы один отдел, или\n- Хотя бы одного получателя, или\n- Включить «Отправить всем»');
return;
}
// Добавляем отделы
if (deptIds.length > 0) {
appendDepartmentIds(formData, deptIds);
}
// Добавляем получателей
if (recipientIds.length > 0) {
appendRecipientIds(formData, recipientIds);
}
} else {
// При sent_to_all=true удаляем получателей и отделы
formData.delete('recipient_ids');
formData.delete('department_ids');
}
try {
// Добавляем CSRF-токен в заголовки
const csrfToken = getCsrfToken();
const requestHeaders = { ...headers };
if (csrfToken) {
requestHeaders['X-CSRFToken'] = csrfToken;
}
const response = await fetch(apiListUrl, {
method: 'POST',
headers: requestHeaders,
body: formData
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMsg = errorData.detail
|| errorData.non_field_errors?.[0]
|| Object.values(errorData).flat().join(', ')
|| 'HTTP ' + response.status;
throw new Error(errorMsg);
}
await response.json();
window.location.reload();
} catch (error) {
console.error('Ошибка создания документа:', error);
alert('Не удалось создать документ:\n' + error.message);
}
}
/**
* Открывает модальное окно редактирования документа.
* @param {HTMLElement} button - Кнопка с data-атрибутами документа
*/
async function openEditModal(button) {
const docId = button.getAttribute('data-doc-id');
editForm.elements.id.value = docId;
editForm.elements.title.value = button.getAttribute('data-doc-title') || '';
editForm.elements.description.value = button.getAttribute('data-doc-description') || '';
const sentToAll = button.getAttribute('data-doc-sent_to_all') === '1';
editAllCheckbox.checked = sentToAll;
toggleRecipientsBlocks(editAllCheckbox, editDeptBlock, editRecipBlock);
// Загружаем текущие данные документа (получатели и отделы)
try {
const response = await fetch(apiDetailBase + docId + '/', { headers });
if (response.ok) {
const data = await response.json();
// Заполняем получателей
const recipients = (data && data.recipients) ? data.recipients.map(r => ({
id: r.id,
display_name: r.display_name || r.full_name || r.email || ('#' + r.id),
email: r.email || ''
})) : [];
editPicker.setSelected(recipients);
// Заполняем отделы
if (editDeptSelect && data.departments) {
const deptIds = data.departments.map(d => String(d.id));
Array.from(editDeptSelect.options).forEach(option => {
option.selected = deptIds.includes(option.value);
});
}
} else {
editPicker.setSelected([]);
if (editDeptSelect) {
Array.from(editDeptSelect.options).forEach(opt => opt.selected = false);
}
}
} catch (error) {
console.error('Ошибка загрузки данных документа:', error);
editPicker.setSelected([]);
if (editDeptSelect) {
Array.from(editDeptSelect.options).forEach(opt => opt.selected = false);
}
}
bootstrap.Modal.getOrCreateInstance(editModal).show();
}
/**
* Обработчик редактирования документа.
*/
async function handleEdit(e) {
e.preventDefault();
const docId = editForm.elements.id.value;
const formData = new FormData(editForm);
formData.set('sent_to_all', editAllCheckbox.checked ? 'true' : 'false');
if (!editAllCheckbox.checked) {
// Собираем выбранные ID отделов
const deptIds = editDeptSelect
? Array.from(editDeptSelect.selectedOptions).map(opt => opt.value)
: [];
// Собираем выбранные ID получателей
const recipientIds = editPicker.getIds();
if (deptIds.length === 0 && recipientIds.length === 0) {
alert('Выберите отделы, получателей или включите «Отправить всем».');
return;
}
// Добавляем отделы
if (deptIds.length > 0) {
appendDepartmentIds(formData, deptIds);
}
// Добавляем получателей
if (recipientIds.length > 0) {
appendRecipientIds(formData, recipientIds);
}
} else {
formData.delete('recipient_ids');
formData.delete('department_ids');
}
// Если файл не выбран — не отправляем ключ
if (!(editForm.elements.file && editForm.elements.file.files.length)) {
formData.delete('file');
}
try {
// Добавляем CSRF-токен в заголовки
const csrfToken = getCsrfToken();
const requestHeaders = { ...headers };
if (csrfToken) {
requestHeaders['X-CSRFToken'] = csrfToken;
}
const response = await fetch(apiDetailBase + docId + '/', {
method: 'PATCH',
headers: requestHeaders,
body: formData
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMsg = errorData.detail
|| errorData.non_field_errors?.[0]
|| Object.values(errorData).flat().join(', ')
|| 'HTTP ' + response.status;
throw new Error(errorMsg);
}
await response.json();
window.location.reload();
} catch (error) {
console.error('Ошибка редактирования документа:', error);
alert('Не удалось сохранить:\n' + error.message);
}
}
/**
* Удаляет документ после подтверждения.
* @param {HTMLElement} button - Кнопка с data-doc-id
*/
async function handleDelete(button) {
if (!confirm('Удалить документ?')) return;
const docId = button.getAttribute('data-doc-id');
try {
// Добавляем CSRF-токен в заголовки
const csrfToken = getCsrfToken();
const requestHeaders = { ...headers };
if (csrfToken) {
requestHeaders['X-CSRFToken'] = csrfToken;
}
const response = await fetch(apiDetailBase + docId + '/', {
method: 'DELETE',
headers: requestHeaders
});
if (response.status === 204 || response.ok) {
window.location.reload();
} else {
const errorData = await response.json().catch(() => ({}));
const errorMsg = errorData.detail || 'HTTP ' + response.status;
throw new Error(errorMsg);
}
} catch (error) {
console.error('Ошибка удаления документа:', error);
alert('Не удалось удалить документ:\n' + error.message);
}
}
/**
* Делегированный обработчик кликов по списку документов.
*/
function handleListClick(e) {
const button = e.target.closest('button[data-action]');
if (!button) return;
const action = button.getAttribute('data-action');
if (action === 'edit') {
openEditModal(button);
} else if (action === 'delete') {
handleDelete(button);
}
}
// Установка обработчиков
createForm.addEventListener('submit', handleCreate);
editForm.addEventListener('submit', handleEdit);
listElement.addEventListener('click', handleListClick);
/**
* Функция для удаления всех обработчиков.
*/
function destroy() {
createForm.removeEventListener('submit', handleCreate);
editForm.removeEventListener('submit', handleEdit);
listElement.removeEventListener('click', handleListClick);
}
return { destroy };
}
// Экспорт для совместимости с неModular кодом
if (typeof window !== 'undefined') {
window.initDocumentCrud = initDocumentCrud;
}