Files
minicrm/public/document-fields.js

1478 lines
56 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
// document-fields.js - Скрипт для управления полями документа в задачах
// Показывает кнопку "Реквизиты" только для задач с типом "document",
// только когда задача раскрыта и только для пользователей из групп "Подписант" или "Секретарь"
(function() {
'use strict';
// Конфигурация
const CONFIG = {
modalId: 'documentFieldsModal',
signerGroup: 'Подписант',
secretaryGroup: 'Секретарь',
apiEndpoint: '/api2/idusers',
usersEndpoint: '/api/users',
modalStyles: `
<style>
.document-fields-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 10000;
justify-content: center;
align-items: center;
}
.document-fields-modal.active {
display: flex;
}
.document-fields-content {
background: white;
padding: 25px;
border-radius: 8px;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.document-fields-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.document-fields-header h3 {
margin: 0;
color: #333;
font-size: 1.3em;
}
.document-fields-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0 5px;
}
.document-fields-close:hover {
color: #333;
}
.document-fields-form {
margin-bottom: 20px;
}
.document-field-group {
margin-bottom: 15px;
position: relative;
}
.document-field-group label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 600;
font-size: 0.9em;
}
.document-field-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1em;
box-sizing: border-box;
}
.document-field-group input:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 2px rgba(76,175,80,0.1);
}
.document-field-group input[readonly] {
background-color: #f9f9f9;
cursor: not-allowed;
}
/* Стили для поля с календарем */
.date-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.date-input-wrapper input {
padding-right: 40px;
}
.calendar-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
background-color: #4CAF50;
color: white;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s;
user-select: none;
z-index: 2;
}
.calendar-icon:hover {
background-color: #45a049;
}
/* Календарь */
.inline-calendar {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
z-index: 1000;
margin-top: 5px;
padding: 10px;
width: 280px;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #f0f0f0;
}
.calendar-month-year {
font-weight: bold;
color: #333;
}
.calendar-nav {
display: flex;
gap: 5px;
}
.calendar-nav button {
background: none;
border: 1px solid #ddd;
border-radius: 4px;
width: 30px;
height: 30px;
cursor: pointer;
font-size: 16px;
color: #555;
transition: all 0.2s;
}
.calendar-nav button:hover {
background-color: #f0f0f0;
border-color: #999;
}
.calendar-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
margin-bottom: 5px;
}
.calendar-weekday {
font-size: 12px;
font-weight: bold;
color: #666;
padding: 5px;
}
.calendar-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
}
.calendar-day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
border: none;
background: none;
padding: 5px;
}
.calendar-day:hover {
background-color: #e8f5e9;
}
.calendar-day.selected {
background-color: #4CAF50;
color: white;
font-weight: bold;
}
.calendar-day.today {
border: 1px solid #4CAF50;
}
.calendar-day.empty {
cursor: default;
color: #ccc;
}
.calendar-day.empty:hover {
background-color: transparent;
}
.document-fields-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
}
.document-fields-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 0.95em;
cursor: pointer;
transition: all 0.2s;
}
.document-fields-btn.save {
background-color: #4CAF50;
color: white;
}
.document-fields-btn.save:hover {
background-color: #45a049;
transform: translateY(-1px);
}
.document-fields-btn.cancel {
background-color: #f44336;
color: white;
}
.document-fields-btn.cancel:hover {
background-color: #da190b;
transform: translateY(-1px);
}
.document-fields-btn:active {
transform: translateY(0);
}
.document-fields-loading {
text-align: center;
padding: 30px;
color: #666;
}
.document-fields-error {
background-color: #ffebee;
color: #c62828;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
border: 1px solid #ef9a9a;
}
.document-fields-success {
background-color: #e8f5e9;
color: #2e7d32;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
border: 1px solid #a5d6a7;
}
.document-fields-info {
font-size: 0.9em;
color: #666;
margin-bottom: 10px;
padding: 8px;
background-color: #f5f5f5;
border-radius: 4px;
}
.document-fields-info strong {
color: #333;
}
</style>
`,
buttonStyles: `
<style>
.document-fields-btn-icon {
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
margin: 0 5px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
display: inline-flex;
align-items: center;
gap: 4px;
}
.document-fields-btn-icon:hover {
background-color: #1976D2;
}
.document-fields-btn-icon.document {
background-color: #9C27B0;
}
.document-fields-btn-icon.document:hover {
background-color: #7B1FA2;
}
.document-fields-btn-icon.document.loading {
opacity: 0.7;
cursor: wait;
}
.document-fields-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
background-color: #e3f2fd;
color: #1976d2;
margin-left: 8px;
}
.document-fields-placeholder {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #9C27B0;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 4px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
`
};
// Текущий пользователь
let currentUser = null;
// Группы пользователя (все группы из разных источников)
let userGroups = [];
// Кэш для типов задач
const taskTypeCache = new Map();
// Множество задач, для которых уже выполняется проверка
const pendingChecks = new Set();
// Селекторы для поиска раскрытых задач
const EXPANDED_TASK_SELECTORS = [
'.task-card.expanded',
'.task-item.expanded',
'[data-expanded="true"]',
'.task-details',
'.task-content.expanded'
];
// Состояние календаря
let calendarState = {
currentDate: new Date(),
selectedDate: null,
isOpen: false,
inputElement: null
};
// Получение текущего пользователя
async function getCurrentUser() {
try {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.user) {
currentUser = data.user;
console.log('✅ DocumentFields: текущий пользователь', currentUser.login);
// Получаем все группы пользователя
await getAllUserGroups(currentUser.login || currentUser.id);
}
return currentUser;
} catch (error) {
console.error('❌ DocumentFields: ошибка получения пользователя', error);
return null;
}
}
// Получение ВСЕХ групп пользователя из разных источников
async function getAllUserGroups(userLogin) {
const allGroups = new Set();
try {
// 1. Пробуем получить через API /api2/idusers
await fetchGroupsFromApi2(userLogin, allGroups);
// 2. Пробуем получить через API /api/users
await fetchGroupsFromUsersApi(userLogin, allGroups);
// 3. Проверяем, есть ли группы в currentUser
if (currentUser) {
addGroupsFromCurrentUser(allGroups);
}
// Преобразуем Set в массив и сохраняем
userGroups = Array.from(allGroups);
console.log('✅ DocumentFields: ВСЕ группы пользователя:', userGroups);
// Детальный вывод для отладки
console.log('🔍 Проверка доступа:');
console.log(' - Группы:', userGroups);
console.log(' - Ищем "Подписант":', userGroups.some(g =>
g.toLowerCase().includes(CONFIG.signerGroup.toLowerCase())));
console.log(' - Ищем "Секретарь":', userGroups.some(g =>
g.toLowerCase().includes(CONFIG.secretaryGroup.toLowerCase())));
return userGroups;
} catch (error) {
console.error('❌ DocumentFields: ошибка получения групп', error);
return [];
}
}
// Получение групп из /api2/idusers
async function fetchGroupsFromApi2(userLogin, groupsSet) {
try {
const response = await fetch(`${CONFIG.apiEndpoint}?login=${userLogin}`);
if (!response.ok) {
console.warn('⚠️ DocumentFields: API /api2/idusers вернул ошибку', response.status);
return;
}
const data = await response.json();
console.log('📦 Данные из /api2/idusers:', data);
// Обрабатываем массив пользователей
if (Array.isArray(data)) {
const userData = data.find(u =>
u.login === userLogin ||
u.user_login === userLogin ||
u.name === userLogin ||
u.user_name === userLogin
);
if (userData) {
extractGroupsFromUserData(userData, groupsSet);
}
}
// Обрабатываем объект пользователя
else if (data && typeof data === 'object') {
extractGroupsFromUserData(data, groupsSet);
}
} catch (error) {
console.error('❌ DocumentFields: ошибка fetchGroupsFromApi2', error);
}
}
// Получение групп из /api/users
async function fetchGroupsFromUsersApi(userLogin, groupsSet) {
try {
const response = await fetch(`${CONFIG.usersEndpoint}?login=${userLogin}`);
if (!response.ok) {
console.warn('⚠️ DocumentFields: API /api/users вернул ошибку', response.status);
return;
}
const data = await response.json();
console.log('📦 Данные из /api/users:', data);
// Обрабатываем данные в зависимости от формата
if (Array.isArray(data)) {
const userData = data.find(u =>
u.login === userLogin ||
u.username === userLogin ||
u.email === userLogin
);
if (userData) {
extractGroupsFromUserData(userData, groupsSet);
}
} else if (data && typeof data === 'object') {
extractGroupsFromUserData(data, groupsSet);
}
} catch (error) {
console.error('❌ DocumentFields: ошибка fetchGroupsFromUsersApi', error);
}
}
// Извлечение групп из данных пользователя
function extractGroupsFromUserData(userData, groupsSet) {
if (!userData) return;
console.log('🔍 Извлекаем группы из:', userData);
// 1. Проверяем group_name (может быть строкой или массивом)
if (userData.group_name) {
if (Array.isArray(userData.group_name)) {
userData.group_name.forEach(g => {
if (g) groupsSet.add(String(g).trim());
});
} else if (typeof userData.group_name === 'string') {
// Может быть строкой с разделителями
const groups = userData.group_name.split(/[,;|]/).map(g => g.trim());
groups.forEach(g => {
if (g) groupsSet.add(g);
});
}
}
// 2. Проверяем ldap_group
if (userData.ldap_group) {
if (Array.isArray(userData.ldap_group)) {
userData.ldap_group.forEach(g => {
if (g) groupsSet.add(String(g).trim());
});
} else if (typeof userData.ldap_group === 'string') {
groupsSet.add(userData.ldap_group.trim());
}
}
// 3. Проверяем metadata.groups
if (userData.metadata?.groups) {
if (Array.isArray(userData.metadata.groups)) {
userData.metadata.groups.forEach(g => {
if (g) groupsSet.add(String(g).trim());
});
}
}
// 4. Проверяем просто groups (если есть)
if (userData.groups) {
if (Array.isArray(userData.groups)) {
userData.groups.forEach(g => {
if (g) groupsSet.add(String(g).trim());
});
} else if (typeof userData.groups === 'string') {
const groups = userData.groups.split(/[,;|]/).map(g => g.trim());
groups.forEach(g => {
if (g) groupsSet.add(g);
});
}
}
// 5. Проверяем roles (если есть)
if (userData.roles) {
if (Array.isArray(userData.roles)) {
userData.roles.forEach(r => {
if (r) groupsSet.add(String(r).trim());
});
}
}
// 6. Проверяем department (может содержать группу)
if (userData.department) {
groupsSet.add(String(userData.department).trim());
}
// 7. Проверяем position (может содержать группу)
if (userData.position) {
groupsSet.add(String(userData.position).trim());
}
}
// Добавление групп из currentUser
function addGroupsFromCurrentUser(groupsSet) {
if (!currentUser) return;
// Проверяем различные поля в currentUser
const fieldsToCheck = [
'group',
'groups',
'role',
'roles',
'department',
'position',
'user_group',
'user_groups'
];
fieldsToCheck.forEach(field => {
if (currentUser[field]) {
if (Array.isArray(currentUser[field])) {
currentUser[field].forEach(g => {
if (g) groupsSet.add(String(g).trim());
});
} else if (typeof currentUser[field] === 'string') {
groupsSet.add(currentUser[field].trim());
}
}
});
}
// Проверка, имеет ли пользователь доступ (Подписант или Секретарь)
function hasUserAccess() {
if (!userGroups || userGroups.length === 0) {
console.log('❌ DocumentFields: у пользователя нет групп');
return false;
}
// Приводим искомые группы к нижнему регистру
const signerGroupLower = CONFIG.signerGroup.toLowerCase();
const secretaryGroupLower = CONFIG.secretaryGroup.toLowerCase();
// Проверяем КАЖДУЮ группу пользователя
for (const group of userGroups) {
if (!group) continue;
const groupLower = String(group).toLowerCase().trim();
// Точное совпадение
if (groupLower === signerGroupLower || groupLower === secretaryGroupLower) {
console.log(`✅ Найдено точное совпадение: "${group}"`);
return true;
}
// Частичное совпадение (содержит подстроку)
if (groupLower.includes(signerGroupLower) || groupLower.includes(secretaryGroupLower)) {
console.log(`✅ Найдено частичное совпадение: "${group}" содержит искомую группу`);
return true;
}
// Совпадение по словам (если группа состоит из нескольких слов)
const groupWords = groupLower.split(/[\s\-_]/);
const signerWords = signerGroupLower.split(/[\s\-_]/);
const secretaryWords = secretaryGroupLower.split(/[\s\-_]/);
// Проверяем, содержит ли группа все слова из искомой группы
const matchesSigner = signerWords.every(word =>
groupLower.includes(word) || groupWords.some(gw => gw.includes(word))
);
const matchesSecretary = secretaryWords.every(word =>
groupLower.includes(word) || groupWords.some(gw => gw.includes(word))
);
if (matchesSigner || matchesSecretary) {
console.log(`✅ Найдено совпадение по словам: "${group}"`);
return true;
}
}
console.log('❌ DocumentFields: пользователь НЕ имеет доступа');
console.log(' Группы пользователя:', userGroups);
console.log(' Нужные группы:', [CONFIG.signerGroup, CONFIG.secretaryGroup]);
return false;
}
// Функция для отладки - показывает все группы пользователя
function debugUserGroups() {
console.log('=== ОТЛАДКА ГРУПП ПОЛЬЗОВАТЕЛЯ ===');
console.log('Текущий пользователь:', currentUser);
console.log('Все группы:', userGroups);
console.log('Ищем "Подписант":', userGroups.filter(g =>
g.toLowerCase().includes(CONFIG.signerGroup.toLowerCase())
));
console.log('Ищем "Секретарь":', userGroups.filter(g =>
g.toLowerCase().includes(CONFIG.secretaryGroup.toLowerCase())
));
console.log('Доступ:', hasUserAccess());
console.log('================================');
return {
user: currentUser,
groups: userGroups,
hasAccess: hasUserAccess()
};
}
// Получение типа задачи по ID
async function getTaskType(taskId) {
// Проверяем кэш
if (taskTypeCache.has(taskId)) {
return taskTypeCache.get(taskId);
}
// Проверяем, не выполняется ли уже проверка для этой задачи
if (pendingChecks.has(taskId)) {
// Ждем завершения проверки
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (taskTypeCache.has(taskId)) {
clearInterval(checkInterval);
resolve(taskTypeCache.get(taskId));
}
}, 100);
// Таймаут на случай ошибки
setTimeout(() => {
clearInterval(checkInterval);
resolve('regular');
}, 5000);
});
}
pendingChecks.add(taskId);
try {
const response = await fetch(`/api/tasks/${taskId}/type`);
if (!response.ok) {
if (response.status === 404) {
taskTypeCache.set(taskId, 'regular');
return 'regular';
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const taskType = data.task_type || 'regular';
// Сохраняем в кэш
taskTypeCache.set(taskId, taskType);
return taskType;
} catch (error) {
console.error(`❌ Ошибка получения типа задачи ${taskId}:`, error);
taskTypeCache.set(taskId, 'regular');
return 'regular';
} finally {
pendingChecks.delete(taskId);
}
}
// Проверка, раскрыта ли задача
function isTaskExpanded(taskCard) {
// Проверяем по различным селекторам
for (const selector of EXPANDED_TASK_SELECTORS) {
if (taskCard.matches(selector) || taskCard.querySelector(selector)) {
return true;
}
}
// Проверяем наличие классов раскрытия
if (taskCard.classList.contains('expanded') ||
taskCard.classList.contains('open') ||
taskCard.classList.contains('active')) {
return true;
}
// Проверяем атрибуты
if (taskCard.getAttribute('data-expanded') === 'true' ||
taskCard.getAttribute('aria-expanded') === 'true') {
return true;
}
return false;
}
// Функции для работы с календарем
function formatDate(date) {
if (!date) return '';
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
}
function parseDate(dateStr) {
if (!dateStr) return null;
const parts = dateStr.split('.');
if (parts.length === 3) {
const day = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1;
const year = parseInt(parts[2], 10);
return new Date(year, month, day);
}
return null;
}
function getMonthName(month, year) {
const date = new Date(year, month, 1);
return date.toLocaleString('ru-RU', { month: 'long' });
}
function generateCalendar(year, month, selectedDate) {
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
// Корректировка первого дня (0 - воскресенье, делаем понедельник первым)
let startOffset = firstDay === 0 ? 6 : firstDay - 1;
const today = new Date();
const todayStr = formatDate(today);
let html = '';
let dayCount = 1;
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 7; j++) {
if (i === 0 && j < startOffset) {
html += '<div class="calendar-day empty"></div>';
} else if (dayCount <= daysInMonth) {
const currentDate = new Date(year, month, dayCount);
const dateStr = formatDate(currentDate);
const isSelected = selectedDate && dateStr === formatDate(selectedDate);
const isToday = dateStr === todayStr;
html += `<div class="calendar-day ${isSelected ? 'selected' : ''} ${isToday ? 'today' : ''}" onclick="documentFields.selectDate('${dateStr}')">${dayCount}</div>`;
dayCount++;
} else {
html += '<div class="calendar-day empty"></div>';
}
}
}
return html;
}
function renderCalendar() {
const calendar = document.getElementById('inlineCalendar');
if (!calendar) return;
const year = calendarState.currentDate.getFullYear();
const month = calendarState.currentDate.getMonth();
const monthName = getMonthName(month, year);
let html = `
<div class="calendar-header">
<div class="calendar-month-year">${monthName} ${year}</div>
<div class="calendar-nav">
<button onclick="documentFields.prevMonth()">←</button>
<button onclick="documentFields.nextMonth()">→</button>
</div>
</div>
<div class="calendar-weekdays">
<div class="calendar-weekday">Пн</div>
<div class="calendar-weekday">Вт</div>
<div class="calendar-weekday">Ср</div>
<div class="calendar-weekday">Чт</div>
<div class="calendar-weekday">Пт</div>
<div class="calendar-weekday">Сб</div>
<div class="calendar-weekday">Вс</div>
</div>
<div class="calendar-days">
${generateCalendar(year, month, calendarState.selectedDate)}
</div>
`;
calendar.innerHTML = html;
}
function toggleCalendar() {
if (calendarState.isOpen) {
closeCalendar();
} else {
openCalendar();
}
}
function openCalendar() {
const calendar = document.getElementById('inlineCalendar');
if (!calendar) return;
const dateInput = document.getElementById('documentDate');
if (dateInput && dateInput.value) {
const parsedDate = parseDate(dateInput.value);
if (parsedDate && !isNaN(parsedDate.getTime())) {
calendarState.selectedDate = parsedDate;
calendarState.currentDate = new Date(parsedDate);
}
}
calendarState.isOpen = true;
calendar.style.display = 'block';
renderCalendar();
}
function closeCalendar() {
const calendar = document.getElementById('inlineCalendar');
if (calendar) {
calendar.style.display = 'none';
}
calendarState.isOpen = false;
}
function selectDate(dateStr) {
const dateInput = document.getElementById('documentDate');
if (dateInput) {
dateInput.value = dateStr;
calendarState.selectedDate = parseDate(dateStr);
}
closeCalendar();
}
function prevMonth() {
calendarState.currentDate.setMonth(calendarState.currentDate.getMonth() - 1);
renderCalendar();
}
function nextMonth() {
calendarState.currentDate.setMonth(calendarState.currentDate.getMonth() + 1);
renderCalendar();
}
// Создание модального окна
function createModal() {
if (!document.getElementById('document-fields-styles')) {
const styleElement = document.createElement('div');
styleElement.id = 'document-fields-styles';
styleElement.innerHTML = CONFIG.modalStyles + CONFIG.buttonStyles;
document.head.appendChild(styleElement);
}
if (document.getElementById(CONFIG.modalId)) {
return;
}
const modalHTML = `
<div id="${CONFIG.modalId}" class="document-fields-modal">
<div class="document-fields-content">
<div class="document-fields-header">
<h3>📄 Реквизиты документа</h3>
<button class="document-fields-close" onclick="documentFields.closeModal()">&times;</button>
</div>
<div id="documentFieldsMessage" class="document-fields-success" style="display: none;"></div>
<div id="documentFieldsError" class="document-fields-error" style="display: none;"></div>
<div id="documentFieldsLoading" class="document-fields-loading" style="display: none;">
Загрузка...
</div>
<div id="documentFieldsForm" class="document-fields-form" style="display: none;">
<div class="document-fields-info">
<strong>ID задачи:</strong> <span id="documentTaskId"></span>
</div>
<div class="document-field-group">
<label for="documentNumber">Номер документа:</label>
<input type="text" id="documentNumber" placeholder="Введите номер документа" maxlength="100">
</div>
<div class="document-field-group">
<label for="documentDate">Дата документа:</label>
<div class="date-input-wrapper">
<input type="text" id="documentDate" placeholder="ДД.ММ.ГГГГ" maxlength="10" readonly>
<div class="calendar-icon" onclick="documentFields.toggleCalendar()">📅</div>
<div id="inlineCalendar" class="inline-calendar" style="display: none;"></div>
</div>
</div>
<div class="document-field-group">
<label for="documentAuthor">Автор (логин):</label>
<input type="text" id="documentAuthor" placeholder="Логин автора" readonly>
<small style="color: #666;">Автоматически устанавливается ваш логин</small>
</div>
<div class="document-fields-buttons">
<button class="document-fields-btn cancel" onclick="documentFields.closeModal()">Отмена</button>
<button class="document-fields-btn save" onclick="documentFields.saveFields()">Сохранить</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Добавляем обработчик клика вне календаря для его закрытия
document.addEventListener('click', function(event) {
const calendar = document.getElementById('inlineCalendar');
const calendarIcon = document.querySelector('.calendar-icon');
if (calendar && calendarState.isOpen) {
if (!calendar.contains(event.target) && !calendarIcon.contains(event.target)) {
closeCalendar();
}
}
});
}
// Открытие модального окна
async function openModal(taskId) {
// Проверяем тип задачи
const taskType = await getTaskType(taskId);
if (taskType !== 'document') {
alert('Эта функция доступна только для задач типа "Документ"');
return;
}
const modal = document.getElementById(CONFIG.modalId);
if (!modal) {
createModal();
}
const modalElement = document.getElementById(CONFIG.modalId);
const form = document.getElementById('documentFieldsForm');
const loading = document.getElementById('documentFieldsLoading');
const errorDiv = document.getElementById('documentFieldsError');
const messageDiv = document.getElementById('documentFieldsMessage');
if (errorDiv) errorDiv.style.display = 'none';
if (messageDiv) messageDiv.style.display = 'none';
if (form) form.style.display = 'none';
if (loading) loading.style.display = 'block';
modalElement.classList.add('active');
const taskIdSpan = document.getElementById('documentTaskId');
if (taskIdSpan) taskIdSpan.textContent = taskId;
const authorInput = document.getElementById('documentAuthor');
if (authorInput && currentUser) {
authorInput.value = currentUser.login || '';
}
try {
const response = await fetch(`/api/tasks/${taskId}/document-fields`);
if (!response.ok) {
throw new Error('Ошибка загрузки данных');
}
const result = await response.json();
if (result.success) {
const numberInput = document.getElementById('documentNumber');
const dateInput = document.getElementById('documentDate');
if (numberInput) numberInput.value = result.data.document_n || '';
if (dateInput) {
dateInput.value = result.data.document_d || '';
// Устанавливаем выбранную дату в календарь
if (result.data.document_d) {
const parsedDate = parseDate(result.data.document_d);
if (parsedDate && !isNaN(parsedDate.getTime())) {
calendarState.selectedDate = parsedDate;
calendarState.currentDate = new Date(parsedDate);
}
} else {
calendarState.selectedDate = null;
calendarState.currentDate = new Date();
}
}
if (result.data.document_a && authorInput && !authorInput.value) {
authorInput.value = result.data.document_a;
}
}
} catch (error) {
console.error('❌ Ошибка загрузки полей документа:', error);
showError('Ошибка загрузки данных: ' + error.message);
} finally {
if (loading) loading.style.display = 'none';
if (form) form.style.display = 'block';
}
}
// Закрытие модального окна
function closeModal() {
const modal = document.getElementById(CONFIG.modalId);
if (modal) {
modal.classList.remove('active');
const numberInput = document.getElementById('documentNumber');
const dateInput = document.getElementById('documentDate');
const authorInput = document.getElementById('documentAuthor');
if (numberInput) numberInput.value = '';
if (dateInput) dateInput.value = '';
if (authorInput) authorInput.value = '';
// Закрываем календарь
closeCalendar();
// Сбрасываем состояние календаря
calendarState = {
currentDate: new Date(),
selectedDate: null,
isOpen: false,
inputElement: null
};
}
}
// Показать ошибку
function showError(message) {
const errorDiv = document.getElementById('documentFieldsError');
if (!errorDiv) return;
errorDiv.textContent = message;
errorDiv.style.display = 'block';
setTimeout(() => {
errorDiv.style.display = 'none';
}, 5000);
}
// Показать успех
function showSuccess(message) {
const messageDiv = document.getElementById('documentFieldsMessage');
if (!messageDiv) return;
messageDiv.textContent = message;
messageDiv.style.display = 'block';
setTimeout(() => {
messageDiv.style.display = 'none';
closeModal();
}, 2000);
}
// Валидация даты
function validateDate(dateStr) {
if (!dateStr) return true;
const datePattern = /^(\d{2})\.(\d{2})\.(\d{4})$/;
if (!datePattern.test(dateStr)) {
return 'Неверный формат даты. Используйте ДД.ММ.ГГГГ';
}
const [, day, month, year] = dateStr.match(datePattern);
const date = new Date(year, month - 1, day);
if (date.getDate() != day || date.getMonth() + 1 != month || date.getFullYear() != year) {
return 'Некорректная дата';
}
return true;
}
// Сохранение полей
async function saveFields() {
const taskId = document.getElementById('documentTaskId')?.textContent;
if (!taskId) {
showError('ID задачи не найден');
return;
}
const document_n = document.getElementById('documentNumber')?.value.trim() || '';
let document_d = document.getElementById('documentDate')?.value.trim() || '';
const document_a = document.getElementById('documentAuthor')?.value.trim() || currentUser?.login;
if (document_d) {
const dateValidation = validateDate(document_d);
if (dateValidation !== true) {
showError(dateValidation);
return;
}
}
const saveBtn = document.querySelector('.document-fields-btn.save');
if (!saveBtn) return;
const originalText = saveBtn.textContent;
saveBtn.textContent = 'Сохранение...';
saveBtn.disabled = true;
try {
const response = await fetch(`/api/tasks/${taskId}/document-fields`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
document_n: document_n || null,
document_d: document_d || null,
document_a: document_a || null
})
});
const result = await response.json();
if (response.ok && result.success) {
showSuccess('✅ Поля документа сохранены');
updateTaskCardDisplay(taskId, result.data);
} else {
showError(result.error || 'Ошибка сохранения');
}
} catch (error) {
console.error('❌ Ошибка сохранения:', error);
showError('Ошибка сети: ' + error.message);
} finally {
saveBtn.textContent = originalText;
saveBtn.disabled = false;
}
}
// Обновление отображения в карточке задачи
function updateTaskCardDisplay(taskId, data) {
const taskCards = document.querySelectorAll(`[data-task-id="${taskId}"]`);
taskCards.forEach(card => {
let docContainer = card.querySelector('.document-fields-display');
if (!docContainer) {
docContainer = document.createElement('div');
docContainer.className = 'document-fields-display';
const header = card.querySelector('.task-header');
if (header) {
header.after(docContainer);
} else {
card.prepend(docContainer);
}
}
let displayHTML = '<div style="margin: 8px 0; padding: 5px; background-color: #f5f5f5; border-radius: 4px; font-size: 0.9em;">';
if (data.document_n) {
displayHTML += `<span style="margin-right: 15px;"><strong>№:</strong> ${data.document_n}</span>`;
}
if (data.document_d) {
displayHTML += `<span style="margin-right: 15px;"><strong>Дата:</strong> ${data.document_d}</span>`;
}
if (data.document_a) {
displayHTML += `<span><strong>Автор:</strong> ${data.document_a}</span>`;
}
if (!data.document_n && !data.document_d) {
displayHTML += '<span style="color: #999;">Нет данных документа</span>';
}
displayHTML += '</div>';
docContainer.innerHTML = displayHTML;
});
}
// Добавление кнопки в раскрытую задачу
async function addButtonToExpandedTask(taskCard) {
if (!currentUser) return;
// Проверяем, имеет ли пользователь доступ (Подписант или Секретарь)
if (!hasUserAccess()) {
return;
}
const taskId = taskCard.dataset.taskId;
if (!taskId) return;
// Проверяем, есть ли уже кнопка
if (taskCard.querySelector('.document-fields-btn-icon')) return;
// Проверяем, раскрыта ли задача
if (!isTaskExpanded(taskCard)) return;
// Показываем индикатор загрузки
const actionsContainer = taskCard.querySelector('.action-buttons, .task-actions');
if (!actionsContainer) return;
const loadingIndicator = document.createElement('span');
loadingIndicator.className = 'document-fields-placeholder';
loadingIndicator.title = 'Проверка типа задачи...';
actionsContainer.appendChild(loadingIndicator);
try {
// Получаем тип задачи
const taskType = await getTaskType(taskId);
// Удаляем индикатор загрузки
loadingIndicator.remove();
// Добавляем кнопку только для задач с типом "document"
if (taskType === 'document') {
const btn = document.createElement('button');
btn.className = 'document-fields-btn-icon document';
btn.innerHTML = '📄 Реквизиты';
btn.onclick = (e) => {
e.stopPropagation();
window.documentFields.openModal(taskId);
};
btn.title = 'Редактировать реквизиты документа';
actionsContainer.appendChild(btn);
console.log(`✅ Добавлена кнопка для раскрытой задачи ${taskId} (тип: document)`);
}
} catch (error) {
console.error(`❌ Ошибка проверки задачи ${taskId}:`, error);
loadingIndicator.remove();
}
}
// Обработчик раскрытия задачи
function handleTaskExpand(mutations) {
for (const mutation of mutations) {
if (mutation.type === 'attributes' &&
(mutation.attributeName === 'class' || mutation.attributeName === 'data-expanded')) {
const target = mutation.target;
if (target.dataset?.taskId && isTaskExpanded(target)) {
addButtonToExpandedTask(target);
}
}
if (mutation.type === 'childList' && mutation.addedNodes.length) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) { // Element node
// Проверяем сам элемент
if (node.dataset?.taskId && isTaskExpanded(node)) {
addButtonToExpandedTask(node);
}
// Проверяем дочерние элементы
const expandedTasks = node.querySelectorAll('[data-task-id]');
expandedTasks.forEach(task => {
if (isTaskExpanded(task)) {
addButtonToExpandedTask(task);
}
});
}
}
}
}
}
// Наблюдатель за изменениями DOM
function observeDOM() {
// Наблюдатель за атрибутами и появлением новых элементов
const observer = new MutationObserver(handleTaskExpand);
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class', 'data-expanded', 'aria-expanded'],
childList: true,
subtree: true
});
console.log('👀 Наблюдатель за раскрытием задач запущен');
// Также проверяем уже раскрытые задачи при загрузке
setTimeout(() => {
document.querySelectorAll('[data-task-id]').forEach(task => {
if (isTaskExpanded(task)) {
addButtonToExpandedTask(task);
}
});
}, 2000);
}
// Очистка кэша
function clearCache() {
taskTypeCache.clear();
pendingChecks.clear();
console.log('🗑️ Кэш типов задач очищен');
}
// Функция для ручного обновления групп пользователя
async function refreshUserGroups() {
if (currentUser) {
await getAllUserGroups(currentUser.login || currentUser.id);
return hasUserAccess();
}
return false;
}
// Инициализация
async function init() {
console.log('🔄 DocumentFields module initializing...');
try {
await getCurrentUser();
// Проверяем доступ пользователя
if (hasUserAccess()) {
createModal();
observeDOM();
console.log('✅ DocumentFields module loaded (with access)');
// Для отладки показываем группы
setTimeout(debugUserGroups, 1000);
} else {
console.log(' DocumentFields module loaded (no access - user is not Signer or Secretary)');
debugUserGroups(); // Показываем отладку даже при отсутствии доступа
}
} catch (error) {
console.error('❌ Ошибка инициализации:', error);
}
}
// Экспортируем функции
window.documentFields = {
openModal,
closeModal,
saveFields,
getTaskType,
clearCache,
init,
addButtonToExpandedTask,
isTaskExpanded,
hasUserAccess,
getAllUserGroups,
refreshUserGroups,
debugUserGroups,
// Функции календаря
toggleCalendar,
selectDate,
prevMonth,
nextMonth
};
// Запускаем инициализацию
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();