518 lines
18 KiB
JavaScript
518 lines
18 KiB
JavaScript
/**
|
||
* @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;
|
||
}
|