календарь для документов

This commit is contained in:
2026-02-23 23:40:10 +05:00
parent 384469aa2c
commit d62abb6cef
3 changed files with 678 additions and 18 deletions

View File

@@ -89,7 +89,7 @@ function showMainInterface() {
// Функция для перезагрузки всех скриптов // Функция для перезагрузки всех скриптов
function reloadAllScripts() { function reloadAllScripts() {
console.log('🔄 Перезагрузка всех скриптов после авторизации...'); //console.log('🔄 Перезагрузка всех скриптов после авторизации...');
// Список скриптов для перезагрузки (в правильном порядке) // Список скриптов для перезагрузки (в правильном порядке)
const scriptsToReload = [ const scriptsToReload = [
@@ -134,7 +134,7 @@ function loadScriptsSequentially(scripts, index) {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = scripts[index]; script.src = scripts[index];
script.onload = () => { script.onload = () => {
console.log(`✅ Загружен: ${scripts[index]}`); // console.log(`✅ Загружен: ${scripts[index]}`);
loadScriptsSequentially(scripts, index + 1); loadScriptsSequentially(scripts, index + 1);
}; };
script.onerror = (error) => { script.onerror = (error) => {

View File

@@ -1,5 +1,6 @@
// document-fields.js - Скрипт для управления полями документа в задачах // document-fields.js - Скрипт для управления полями документа в задачах
// Показывает кнопку "Реквизиты" только для задач с типом "document" и только когда задача раскрыта // Показывает кнопку "Реквизиты" только для задач с типом "document",
// только когда задача раскрыта и только для пользователей из групп "Подписант" или "Секретарь"
(function() { (function() {
'use strict'; 'use strict';
@@ -7,6 +8,10 @@
// Конфигурация // Конфигурация
const CONFIG = { const CONFIG = {
modalId: 'documentFieldsModal', modalId: 'documentFieldsModal',
signerGroup: 'Подписант',
secretaryGroup: 'Секретарь',
apiEndpoint: '/api2/idusers',
usersEndpoint: '/api/users',
modalStyles: ` modalStyles: `
<style> <style>
.document-fields-modal { .document-fields-modal {
@@ -81,6 +86,7 @@
.document-field-group { .document-field-group {
margin-bottom: 15px; margin-bottom: 15px;
position: relative;
} }
.document-field-group label { .document-field-group label {
@@ -111,6 +117,150 @@
cursor: not-allowed; 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 { .document-fields-buttons {
display: flex; display: flex;
gap: 10px; gap: 10px;
@@ -257,6 +407,9 @@
// Текущий пользователь // Текущий пользователь
let currentUser = null; let currentUser = null;
// Группы пользователя (все группы из разных источников)
let userGroups = [];
// Кэш для типов задач // Кэш для типов задач
const taskTypeCache = new Map(); const taskTypeCache = new Map();
@@ -272,6 +425,14 @@
'.task-content.expanded' '.task-content.expanded'
]; ];
// Состояние календаря
let calendarState = {
currentDate: new Date(),
selectedDate: null,
isOpen: false,
inputElement: null
};
// Получение текущего пользователя // Получение текущего пользователя
async function getCurrentUser() { async function getCurrentUser() {
try { try {
@@ -283,6 +444,9 @@
if (data.user) { if (data.user) {
currentUser = data.user; currentUser = data.user;
console.log('✅ DocumentFields: текущий пользователь', currentUser.login); console.log('✅ DocumentFields: текущий пользователь', currentUser.login);
// Получаем все группы пользователя
await getAllUserGroups(currentUser.login || currentUser.id);
} }
return currentUser; return currentUser;
} catch (error) { } catch (error) {
@@ -291,6 +455,291 @@
} }
} }
// Получение ВСЕХ групп пользователя из разных источников
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 // Получение типа задачи по ID
async function getTaskType(taskId) { async function getTaskType(taskId) {
// Проверяем кэш // Проверяем кэш
@@ -371,6 +820,153 @@
return false; 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() { function createModal() {
if (!document.getElementById('document-fields-styles')) { if (!document.getElementById('document-fields-styles')) {
@@ -411,7 +1007,11 @@
<div class="document-field-group"> <div class="document-field-group">
<label for="documentDate">Дата документа:</label> <label for="documentDate">Дата документа:</label>
<input type="text" id="documentDate" placeholder="ДД.ММ.ГГГГ" maxlength="10"> <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>
<div class="document-field-group"> <div class="document-field-group">
@@ -430,6 +1030,18 @@
`; `;
document.body.insertAdjacentHTML('beforeend', modalHTML); 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();
}
}
});
} }
// Открытие модального окна // Открытие модального окна
@@ -482,7 +1094,21 @@
const dateInput = document.getElementById('documentDate'); const dateInput = document.getElementById('documentDate');
if (numberInput) numberInput.value = result.data.document_n || ''; if (numberInput) numberInput.value = result.data.document_n || '';
if (dateInput) dateInput.value = result.data.document_d || ''; 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) { if (result.data.document_a && authorInput && !authorInput.value) {
authorInput.value = result.data.document_a; authorInput.value = result.data.document_a;
@@ -510,6 +1136,17 @@
if (numberInput) numberInput.value = ''; if (numberInput) numberInput.value = '';
if (dateInput) dateInput.value = ''; if (dateInput) dateInput.value = '';
if (authorInput) authorInput.value = ''; if (authorInput) authorInput.value = '';
// Закрываем календарь
closeCalendar();
// Сбрасываем состояние календаря
calendarState = {
currentDate: new Date(),
selectedDate: null,
isOpen: false,
inputElement: null
};
} }
} }
@@ -663,6 +1300,11 @@
async function addButtonToExpandedTask(taskCard) { async function addButtonToExpandedTask(taskCard) {
if (!currentUser) return; if (!currentUser) return;
// Проверяем, имеет ли пользователь доступ (Подписант или Секретарь)
if (!hasUserAccess()) {
return;
}
const taskId = taskCard.dataset.taskId; const taskId = taskCard.dataset.taskId;
if (!taskId) return; if (!taskId) return;
@@ -772,16 +1414,34 @@
console.log('🗑️ Кэш типов задач очищен'); console.log('🗑️ Кэш типов задач очищен');
} }
// Функция для ручного обновления групп пользователя
async function refreshUserGroups() {
if (currentUser) {
await getAllUserGroups(currentUser.login || currentUser.id);
return hasUserAccess();
}
return false;
}
// Инициализация // Инициализация
async function init() { async function init() {
console.log('🔄 DocumentFields module initializing...'); console.log('🔄 DocumentFields module initializing...');
try { try {
await getCurrentUser(); await getCurrentUser();
// Проверяем доступ пользователя
if (hasUserAccess()) {
createModal(); createModal();
observeDOM(); observeDOM();
console.log('✅ DocumentFields module loaded (with access)');
console.log('✅ DocumentFields module loaded'); // Для отладки показываем группы
setTimeout(debugUserGroups, 1000);
} else {
console.log(' DocumentFields module loaded (no access - user is not Signer or Secretary)');
debugUserGroups(); // Показываем отладку даже при отсутствии доступа
}
} catch (error) { } catch (error) {
console.error('❌ Ошибка инициализации:', error); console.error('❌ Ошибка инициализации:', error);
} }
@@ -796,7 +1456,16 @@
clearCache, clearCache,
init, init,
addButtonToExpandedTask, addButtonToExpandedTask,
isTaskExpanded isTaskExpanded,
hasUserAccess,
getAllUserGroups,
refreshUserGroups,
debugUserGroups,
// Функции календаря
toggleCalendar,
selectDate,
prevMonth,
nextMonth
}; };
// Запускаем инициализацию // Запускаем инициализацию

View File

@@ -82,15 +82,6 @@ navButtons.push(
id: "create-task-btn" id: "create-task-btn"
} }
); );
if (currentUser && navbar_checkUserGroup('1Секретарь') || currentUser && currentUser.role === 'admin') {
navButtons.push({
onclick: "TasksType.show('document')",
className: "nav-btn tasks",
icon: "fas fa-list",
text: "Согласование",
id: "create-task-btn"
});
}
navButtons.push( navButtons.push(
{ {
onclick: "showKanbanSection()", onclick: "showKanbanSection()",