Add files via upload

This commit is contained in:
kalugin66
2025-11-07 14:16:25 +05:00
committed by GitHub
parent 881155ca5b
commit 8489bba63b
7 changed files with 3159 additions and 0 deletions

195
public/index.html Normal file
View File

@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>School CRM - Управление задачами</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Форма логина -->
<div id="login-modal" class="modal">
<div class="modal-content">
<h2>Вход в School CRM</h2>
<form id="login-form">
<div class="form-group">
<label for="login">Логин:</label>
<input type="text" id="login" name="login" required>
</div>
<div class="form-group">
<label for="password">Пароль:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Войти</button>
</form>
<div class="test-users">
<h3>Тестовый пользователь:</h3>
<ul>
<li><strong>teacher</strong> / teacher123</li>
</ul>
</div>
</div>
</div>
<div class="container">
<header>
<h1>School CRM - Управление задачами</h1>
<div class="user-info">
<span id="current-user"></span>
<button onclick="logout()">Выйти</button>
</div>
<nav>
<button onclick="showSection('tasks')">Задачи</button>
<button onclick="showSection('create-task')">Создать задачу</button>
<button onclick="showSection('logs')">Лог активности</button>
</nav>
</header>
<main>
<!-- Секция списка задач -->
<section id="tasks-section" class="section">
<h2>Все задачи</h2>
<div id="tasks-controls">
<label>
<input type="checkbox" id="show-deleted" onchange="loadTasks()">
Показать удаленные задачи
</label>
</div>
<div id="tasks-list"></div>
</section>
<!-- Секция создания задачи -->
<section id="create-task-section" class="section">
<h2>Создать новую задачу</h2>
<form id="create-task-form" enctype="multipart/form-data">
<div class="form-group">
<label for="title">Название задачи:</label>
<input type="text" id="title" name="title" required>
</div>
<div class="form-group">
<label for="description">Описание:</label>
<textarea id="description" name="description" rows="4"></textarea>
</div>
<div class="form-group">
<label for="start-date">Дата и время начала (необязательно):</label>
<input type="datetime-local" id="start-date" name="startDate">
</div>
<div class="form-group">
<label for="due-date">Дата и время выполнения (необязательно):</label>
<input type="datetime-local" id="due-date" name="dueDate">
</div>
<div class="form-group">
<label>Исполнители:</label>
<div id="users-checklist" class="checkbox-group"></div>
</div>
<div class="form-group">
<label for="files">Прикрепить файлы (до 15 файлов, максимум 300MB):</label>
<input type="file" id="files" name="files" multiple>
<div id="file-list"></div>
</div>
<button type="submit">Создать задачу</button>
</form>
</section>
<!-- Секция логов -->
<section id="logs-section" class="section">
<h2>Лог активности</h2>
<div id="logs-list"></div>
</section>
</main>
</div>
<!-- Модальное окно для редактирования задачи -->
<div id="edit-task-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditModal()">&times;</span>
<h3>Редактировать задачу</h3>
<form id="edit-task-form">
<input type="hidden" id="edit-task-id">
<div class="form-group">
<label for="edit-title">Название задачи:</label>
<input type="text" id="edit-title" name="title" required>
</div>
<div class="form-group">
<label for="edit-description">Описание:</label>
<textarea id="edit-description" name="description" rows="4"></textarea>
</div>
<div class="form-group">
<label for="edit-start-date">Дата и время начала:</label>
<input type="datetime-local" id="edit-start-date" name="startDate">
</div>
<div class="form-group">
<label for="edit-due-date">Дата и время выполнения:</label>
<input type="datetime-local" id="edit-due-date" name="dueDate">
</div>
<div class="form-group">
<label>Исполнители:</label>
<div id="edit-users-checklist" class="checkbox-group"></div>
</div>
<button type="submit">Сохранить изменения</button>
</form>
</div>
</div>
<!-- Модальное окно для копирования задачи -->
<div id="copy-task-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeCopyModal()">&times;</span>
<h3>Создать копию задачи</h3>
<form id="copy-task-form">
<input type="hidden" id="copy-task-id">
<div class="form-group">
<label for="copy-start-date">Дата и время начала для копии:</label>
<input type="datetime-local" id="copy-start-date" name="startDate">
</div>
<div class="form-group">
<label for="copy-due-date">Дата и время выполнения для копии:</label>
<input type="datetime-local" id="copy-due-date" name="dueDate">
</div>
<div class="form-group">
<label>Назначить исполнителей для копии:</label>
<div id="copy-users-checklist" class="checkbox-group"></div>
</div>
<button type="submit">Создать копию</button>
</form>
</div>
</div>
<!-- Модальное окно для редактирования сроков исполнителя -->
<div id="edit-assignment-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditAssignmentModal()">&times;</span>
<h3>Редактировать сроки исполнителя</h3>
<form id="edit-assignment-form">
<input type="hidden" id="edit-assignment-task-id">
<input type="hidden" id="edit-assignment-user-id">
<div class="form-group">
<label for="edit-assignment-start-date">Дата и время начала:</label>
<input type="datetime-local" id="edit-assignment-start-date" name="startDate">
</div>
<div class="form-group">
<label for="edit-assignment-due-date">Дата и время выполнения:</label>
<input type="datetime-local" id="edit-assignment-due-date" name="dueDate">
</div>
<button type="submit">Сохранить сроки</button>
</form>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

816
public/script.js Normal file
View File

@@ -0,0 +1,816 @@
let currentUser = null;
let users = [];
let tasks = [];
// Инициализация при загрузке
document.addEventListener('DOMContentLoaded', function() {
checkAuth();
setupEventListeners();
});
async function checkAuth() {
try {
const response = await fetch('/api/user');
if (response.ok) {
const data = await response.json();
currentUser = data.user;
showMainInterface();
} else {
showLoginInterface();
}
} catch (error) {
showLoginInterface();
}
}
function showLoginInterface() {
document.getElementById('login-modal').style.display = 'block';
document.querySelector('.container').style.display = 'none';
}
function showMainInterface() {
document.getElementById('login-modal').style.display = 'none';
document.querySelector('.container').style.display = 'block';
// Формируем информацию о пользователе
let userInfo = `Вы вошли как: ${currentUser.name}`;
if (currentUser.auth_type === 'ldap') {
userInfo += ` (LDAP)`;
}
if (currentUser.groups && currentUser.groups.length > 0) {
userInfo += ` | Группы: ${currentUser.groups.join(', ')}`;
}
document.getElementById('current-user').textContent = userInfo;
// Показываем чекбокс удаленных задач только для администраторов
if (currentUser.role === 'admin') {
document.getElementById('tasks-controls').style.display = 'block';
} else {
document.getElementById('tasks-controls').style.display = 'none';
}
loadUsers();
loadTasks();
loadActivityLogs();
showSection('tasks');
}
function setupEventListeners() {
document.getElementById('login-form').addEventListener('submit', login);
document.getElementById('create-task-form').addEventListener('submit', createTask);
document.getElementById('edit-task-form').addEventListener('submit', updateTask);
document.getElementById('copy-task-form').addEventListener('submit', copyTask);
document.getElementById('edit-assignment-form').addEventListener('submit', updateAssignment);
document.getElementById('files').addEventListener('change', updateFileList);
}
async function login(event) {
event.preventDefault();
const login = document.getElementById('login').value;
const password = document.getElementById('password').value;
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ login, password })
});
if (response.ok) {
const data = await response.json();
currentUser = data.user;
showMainInterface();
} else {
const error = await response.json();
alert(error.error || 'Ошибка входа');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка подключения к серверу');
}
}
async function logout() {
try {
await fetch('/api/logout', { method: 'POST' });
currentUser = null;
showLoginInterface();
} catch (error) {
console.error('Ошибка выхода:', error);
}
}
function showSection(sectionName) {
document.querySelectorAll('.section').forEach(section => {
section.classList.remove('active');
});
document.getElementById(sectionName + '-section').classList.add('active');
if (sectionName === 'tasks') {
loadTasks();
} else if (sectionName === 'logs') {
loadActivityLogs();
}
}
async function loadUsers() {
try {
const response = await fetch('/api/users');
users = await response.json();
renderUsersChecklist();
renderEditUsersChecklist();
renderCopyUsersChecklist();
} catch (error) {
console.error('Ошибка загрузки пользователей:', error);
}
}
async function loadTasks() {
try {
const response = await fetch('/api/tasks');
tasks = await response.json();
renderTasks();
// Загружаем файлы для каждой задачи
tasks.forEach(task => {
loadTaskFiles(task.id);
});
} catch (error) {
console.error('Ошибка загрузки задач:', error);
}
}
async function loadActivityLogs() {
try {
const response = await fetch('/api/activity-logs');
const logs = await response.json();
renderLogs(logs);
} catch (error) {
console.error('Ошибка загрузки логов:', error);
}
}
function renderUsersChecklist() {
const container = document.getElementById('users-checklist');
container.innerHTML = users
.filter(user => user.id !== currentUser.id)
.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" name="assignedUsers" value="${user.id}">
${user.name} (${user.email})
${user.auth_type === 'ldap' ? '<small style="color: #666;"> - LDAP</small>' : ''}
</label>
</div>
`).join('');
}
function renderEditUsersChecklist() {
const container = document.getElementById('edit-users-checklist');
container.innerHTML = users
.filter(user => user.id !== currentUser.id)
.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" name="assignedUsers" value="${user.id}">
${user.name} (${user.email})
${user.auth_type === 'ldap' ? '<small style="color: #666;"> - LDAP</small>' : ''}
</label>
</div>
`).join('');
}
function renderCopyUsersChecklist() {
const container = document.getElementById('copy-users-checklist');
container.innerHTML = users
.filter(user => user.id !== currentUser.id)
.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" name="assignedUsers" value="${user.id}">
${user.name} (${user.email})
${user.auth_type === 'ldap' ? '<small style="color: #666;"> - LDAP</small>' : ''}
</label>
</div>
`).join('');
}
function renderTasks() {
const container = document.getElementById('tasks-list');
const showDeleted = document.getElementById('show-deleted')?.checked || false;
let filteredTasks = tasks;
if (!showDeleted) {
filteredTasks = tasks.filter(task => task.status === 'active');
}
if (filteredTasks.length === 0) {
container.innerHTML = '<div class="loading">Задачи не найдены</div>';
return;
}
container.innerHTML = filteredTasks.map(task => {
const overallStatus = getTaskOverallStatus(task);
const statusClass = getStatusClass(overallStatus);
const isDeleted = task.status === 'deleted';
const userRole = getUserRoleInTask(task);
const canEdit = canUserEditTask(task);
const isCopy = task.original_task_id !== null;
return `
<div class="task-card ${isDeleted ? 'deleted' : ''}">
<div class="task-actions">
${!isDeleted ? `
<button class="edit-btn" onclick="openEditModal(${task.id})" title="Редактировать">✏️</button>
<button class="copy-btn" onclick="openCopyModal(${task.id})" title="Создать копию">📋</button>
<button class="delete-btn" onclick="deleteTask(${task.id})" title="Удалить">🗑️</button>
` : ''}
${isDeleted && currentUser.role === 'admin' ? `
<button class="restore-btn" onclick="restoreTask(${task.id})" title="Восстановить">↶</button>
` : ''}
</div>
<div class="task-header">
<div class="task-title">
${task.title}
${isDeleted ? '<span class="deleted-badge">Удалена</span>' : ''}
${isCopy ? '<span class="copy-badge">Копия</span>' : ''}
<span class="role-badge ${getRoleBadgeClass(userRole)}">${userRole}</span>
</div>
<div class="task-status ${statusClass}">${getStatusText(overallStatus)}</div>
</div>
${isCopy && task.original_task_title ? `
<div class="task-original">
<small>Оригинал: "${task.original_task_title}" (создал: ${task.original_creator_name})</small>
</div>
` : ''}
<div class="task-description">${task.description || 'Нет описания'}</div>
${task.start_date || task.due_date ? `
<div class="task-dates">
${task.start_date ? `<div><strong>Начать:</strong> ${formatDateTime(task.start_date)}</div>` : ''}
${task.due_date ? `<div><strong>Выполнить до:</strong> ${formatDateTime(task.due_date)}</div>` : ''}
</div>
` : ''}
<div class="task-assignments">
<strong>Исполнители:</strong>
${task.assignments && task.assignments.length > 0 ?
task.assignments.map(assignment => renderAssignment(assignment, task.id, canEdit)).join('') :
'<div>Не назначены</div>'
}
</div>
<div class="file-list" id="files-${task.id}">
<strong>Файлы:</strong>
<div class="loading">Загрузка...</div>
</div>
<div class="task-meta">
<small>Создана: ${formatDateTime(task.created_at)} | Автор: ${task.creator_name}</small>
${task.deleted_at ? `<br><small>Удалена: ${formatDateTime(task.deleted_at)}</small>` : ''}
</div>
</div>
`;
}).join('');
}
function renderAssignment(assignment, taskId, canEdit) {
const statusClass = getStatusClass(assignment.status);
const isCurrentUser = assignment.user_id === currentUser.id;
const isOverdue = assignment.status === 'overdue';
return `
<div class="assignment ${isOverdue ? 'overdue' : ''}">
<span class="assignment-status ${statusClass}"></span>
<div style="flex: 1;">
<strong>${assignment.user_name}</strong>
${isCurrentUser ? '<small>(Вы)</small>' : ''}
${assignment.start_date || assignment.due_date ? `
<div class="assignment-dates">
${assignment.start_date ? `<small>Начать: ${formatDateTime(assignment.start_date)}</small>` : ''}
${assignment.due_date ? `<small>Выполнить до: ${formatDateTime(assignment.due_date)}</small>` : ''}
</div>
` : ''}
</div>
<div class="action-buttons">
${isCurrentUser && assignment.status === 'assigned' ?
`<button onclick="updateStatus(${taskId}, ${assignment.user_id}, 'in_progress')">Приступить</button>` : ''}
${isCurrentUser && (assignment.status === 'in_progress' || assignment.status === 'overdue') ?
`<button onclick="updateStatus(${taskId}, ${assignment.user_id}, 'completed')">Выполнено</button>` : ''}
${canEdit ?
`<button class="edit-date-btn" onclick="openEditAssignmentModal(${taskId}, ${assignment.user_id})" title="Редактировать сроки">📅</button>` : ''}
</div>
</div>
`;
}
async function createTask(event) {
event.preventDefault();
if (!currentUser) {
alert('Требуется аутентификация');
return;
}
const formData = new FormData();
formData.append('title', document.getElementById('title').value);
formData.append('description', document.getElementById('description').value);
const startDate = document.getElementById('start-date').value;
const dueDate = document.getElementById('due-date').value;
if (startDate) formData.append('startDate', startDate);
if (dueDate) formData.append('dueDate', dueDate);
const assignedUsers = document.querySelectorAll('#users-checklist input[name="assignedUsers"]:checked');
assignedUsers.forEach(checkbox => {
formData.append('assignedUsers', checkbox.value);
});
const files = document.getElementById('files').files;
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
try {
const response = await fetch('/api/tasks', {
method: 'POST',
body: formData
});
if (response.ok) {
alert('Задача успешно создана!');
document.getElementById('create-task-form').reset();
document.getElementById('file-list').innerHTML = '';
loadTasks();
loadActivityLogs();
showSection('tasks');
} else {
const error = await response.json();
alert(error.error || 'Ошибка создания задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка создания задачи');
}
}
async function openEditModal(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}`);
if (!response.ok) {
if (response.status === 404) {
alert('Задача не найдена или у вас нет прав доступа');
}
throw new Error('Ошибка загрузки задачи');
}
const task = await response.json();
// Дополнительная проверка прав на клиенте
if (!canUserEditTask(task)) {
alert('У вас нет прав для редактирования этой задачи');
return;
}
document.getElementById('edit-task-id').value = task.id;
document.getElementById('edit-title').value = task.title;
document.getElementById('edit-description').value = task.description || '';
// Заполняем даты
document.getElementById('edit-start-date').value = task.start_date ? formatDateTimeForInput(task.start_date) : '';
document.getElementById('edit-due-date').value = task.due_date ? formatDateTimeForInput(task.due_date) : '';
// Отмечаем текущих исполнителей
const checkboxes = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]');
checkboxes.forEach(checkbox => {
checkbox.checked = task.assignments?.some(assignment =>
assignment.user_id === parseInt(checkbox.value)
) || false;
});
document.getElementById('edit-task-modal').style.display = 'block';
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка загрузки задачи');
}
}
function closeEditModal() {
document.getElementById('edit-task-modal').style.display = 'none';
}
async function updateTask(event) {
event.preventDefault();
const taskId = document.getElementById('edit-task-id').value;
const title = document.getElementById('edit-title').value;
const description = document.getElementById('edit-description').value;
const startDate = document.getElementById('edit-start-date').value;
const dueDate = document.getElementById('edit-due-date').value;
const assignedUsers = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]:checked');
const assignedUserIds = Array.from(assignedUsers).map(cb => parseInt(cb.value));
try {
const response = await fetch(`/api/tasks/${taskId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title,
description,
assignedUsers: assignedUserIds,
startDate: startDate || null,
dueDate: dueDate || null
})
});
if (response.ok) {
alert('Задача успешно обновлена!');
closeEditModal();
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка обновления задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка обновления задачи');
}
}
function openCopyModal(taskId) {
document.getElementById('copy-task-id').value = taskId;
document.getElementById('copy-task-modal').style.display = 'block';
}
function closeCopyModal() {
document.getElementById('copy-task-modal').style.display = 'none';
}
// В функции copyTask улучшаем обработку ошибок
async function copyTask(event) {
event.preventDefault();
const taskId = document.getElementById('copy-task-id').value;
const startDate = document.getElementById('copy-start-date').value;
const dueDate = document.getElementById('copy-due-date').value;
const checkboxes = document.querySelectorAll('#copy-users-checklist input[name="assignedUsers"]:checked');
const assignedUserIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
if (assignedUserIds.length === 0) {
alert('Выберите хотя бы одного исполнителя для копии задачи');
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/copy`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
assignedUsers: assignedUserIds,
startDate: startDate || null,
dueDate: dueDate || null
})
});
if (response.ok) {
alert('Копия задачи успешно создана!');
closeCopyModal();
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка создания копии задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка создания копии задачи');
}
}
function openEditAssignmentModal(taskId, userId) {
const task = tasks.find(t => t.id === taskId);
if (!task) return;
const assignment = task.assignments.find(a => a.user_id === userId);
if (!assignment) return;
document.getElementById('edit-assignment-task-id').value = taskId;
document.getElementById('edit-assignment-user-id').value = userId;
document.getElementById('edit-assignment-start-date').value = assignment.start_date ? formatDateTimeForInput(assignment.start_date) : '';
document.getElementById('edit-assignment-due-date').value = assignment.due_date ? formatDateTimeForInput(assignment.due_date) : '';
document.getElementById('edit-assignment-modal').style.display = 'block';
}
function closeEditAssignmentModal() {
document.getElementById('edit-assignment-modal').style.display = 'none';
}
async function updateAssignment(event) {
event.preventDefault();
const taskId = document.getElementById('edit-assignment-task-id').value;
const userId = document.getElementById('edit-assignment-user-id').value;
const startDate = document.getElementById('edit-assignment-start-date').value;
const dueDate = document.getElementById('edit-assignment-due-date').value;
try {
const response = await fetch(`/api/tasks/${taskId}/assignment/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
startDate: startDate || null,
dueDate: dueDate || null
})
});
if (response.ok) {
alert('Сроки исполнителя обновлены!');
closeEditAssignmentModal();
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка обновления сроков');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка обновления сроков');
}
}
async function deleteTask(taskId) {
if (!confirm('Вы уверены, что хотите удалить эту задачу?')) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}`, {
method: 'DELETE'
});
if (response.ok) {
alert('Задача удалена!');
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка удаления задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка удаления задачи');
}
}
async function restoreTask(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}/restore`, {
method: 'POST'
});
if (response.ok) {
alert('Задача восстановлена!');
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка восстановления задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка восстановления задачи');
}
}
// В функции updateStatus улучшаем обработку ошибок
async function updateStatus(taskId, userId, status) {
try {
const response = await fetch(`/api/tasks/${taskId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId, status })
});
if (response.ok) {
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка обновления статуса');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка обновления статуса');
}
}
function getTaskOverallStatus(task) {
if (task.status === 'deleted') return 'deleted';
if (!task.assignments || task.assignments.length === 0) return 'unassigned';
const assignments = task.assignments;
let hasAssigned = false;
let hasInProgress = false;
let hasOverdue = false;
let allCompleted = true;
for (let assignment of assignments) {
if (assignment.status === 'assigned') {
hasAssigned = true;
allCompleted = false;
} else if (assignment.status === 'in_progress') {
hasInProgress = true;
allCompleted = false;
} else if (assignment.status === 'overdue') {
hasOverdue = true;
allCompleted = false;
} else if (assignment.status !== 'completed') {
allCompleted = false;
}
}
if (allCompleted) return 'completed';
if (hasOverdue) return 'overdue';
if (hasInProgress) return 'in_progress';
if (hasAssigned) return 'assigned';
return 'unassigned';
}
function getStatusClass(status) {
switch (status) {
case 'deleted': return 'status-gray';
case 'unassigned': return 'status-purple';
case 'assigned': return 'status-red';
case 'in_progress': return 'status-orange';
case 'overdue': return 'status-darkred';
case 'completed': return 'status-green';
default: return 'status-purple';
}
}
function getStatusText(status) {
switch (status) {
case 'deleted': return 'Удалена';
case 'unassigned': return 'Не назначена';
case 'assigned': return 'Назначена';
case 'in_progress': return 'В работе';
case 'overdue': return 'Просрочена';
case 'completed': return 'Выполнена';
default: return 'Неизвестно';
}
}
function getUserRoleInTask(task) {
if (!currentUser) return 'Нет доступа';
if (currentUser.role === 'admin') return 'Администратор';
if (parseInt(task.created_by) === currentUser.id) return 'Заказчик';
// Проверяем является ли пользователь исполнителем
if (task.assignments) {
const isExecutor = task.assignments.some(assignment =>
parseInt(assignment.user_id) === currentUser.id
);
if (isExecutor) return 'Исполнитель';
}
return 'Наблюдатель';
}
function getRoleBadgeClass(role) {
switch (role) {
case 'Администратор': return 'role-admin';
case 'Заказчик': return 'role-creator';
case 'Исполнитель': return 'role-executor';
default: return '';
}
}
function canUserEditTask(task) {
if (!currentUser) return false;
if (currentUser.role === 'admin') return true; // Администратор
if (parseInt(task.created_by) === currentUser.id) return true; // Заказчик
return false;
}
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '';
const date = new Date(dateTimeString);
return date.toLocaleString('ru-RU');
}
function formatDateTimeForInput(dateTimeString) {
if (!dateTimeString) return '';
const date = new Date(dateTimeString);
return date.toISOString().slice(0, 16);
}
function updateFileList() {
const fileInput = document.getElementById('files');
const fileList = document.getElementById('file-list');
const files = fileInput.files;
if (files.length === 0) {
fileList.innerHTML = '';
return;
}
let html = '<ul>';
let totalSize = 0;
for (let i = 0; i < files.length; i++) {
const file = files[i];
totalSize += file.size;
html += `<li>${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)</li>`;
}
html += '</ul>';
html += `<p><strong>Общий размер: ${(totalSize / 1024 / 1024).toFixed(2)} MB / 300 MB</strong></p>`;
fileList.innerHTML = html;
}
function renderLogs(logs) {
const container = document.getElementById('logs-list');
if (logs.length === 0) {
container.innerHTML = '<div class="loading">Логи не найдены</div>';
return;
}
container.innerHTML = logs.map(log => `
<div class="log-entry">
<div class="log-time">${formatDateTime(log.created_at)}</div>
<div><strong>${log.user_name}</strong> - ${getActionText(log.action)}</div>
<div>Задача: "${log.task_title}"</div>
${log.details ? `<div>Детали: ${log.details}</div>` : ''}
</div>
`).join('');
}
function getActionText(action) {
const actions = {
'TASK_CREATED': 'создал задачу',
'TASK_COPIED': 'создал копию задачи',
'TASK_UPDATED': 'обновил задачу',
'TASK_DELETED': 'удалил задачу',
'TASK_RESTORED': 'восстановил задачу',
'TASK_ASSIGNED': 'назначил задачу',
'TASK_ASSIGNMENTS_UPDATED': 'обновил назначения',
'ASSIGNMENT_UPDATED': 'обновил сроки исполнителя',
'STATUS_CHANGED': 'изменил статус задачи',
'FILE_UPLOADED': 'загрузил файл'
};
return actions[action] || action;
}
async function loadTaskFiles(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}/files`);
const files = await response.json();
const container = document.getElementById(`files-${taskId}`);
if (container) {
if (files.length === 0) {
container.innerHTML = '<strong>Файлы:</strong> Нет файлов';
} else {
container.innerHTML = `
<strong>Файлы:</strong>
${files.map(file => `
<div class="file-item">
<a href="/api/files/${file.id}/download" download="${file.original_name}">
${file.original_name}
</a>
(${(file.file_size / 1024 / 1024).toFixed(2)} MB)
<small> - загрузил: ${file.user_name}</small>
</div>
`).join('')}
`;
}
}
} catch (error) {
console.error('Ошибка загрузки файлов:', error);
}
}

893
public/style.css Normal file
View File

@@ -0,0 +1,893 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
display: none;
}
/* Header Styles */
header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 1.5rem;
margin-bottom: 25px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
header h1 {
margin-bottom: 1.2rem;
text-align: center;
color: #2c3e50;
font-size: 2.2rem;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-info {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.2rem;
padding: 12px 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 10px;
color: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.user-info span {
font-weight: 600;
font-size: 1.1rem;
}
.user-info button {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.user-info button:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
/* Navigation */
nav {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
nav button {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border: none;
padding: 12px 24px;
border-radius: 10px;
cursor: pointer;
flex: 1;
min-width: 140px;
font-weight: 600;
font-size: 1rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
}
nav button:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
}
/* Section Styles */
.section {
display: none;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 25px;
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
margin-bottom: 20px;
}
.section.active {
display: block;
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Form Styles */
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2c3e50;
font-size: 1rem;
}
input[type="text"],
input[type="password"],
input[type="datetime-local"],
textarea,
select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 10px;
font-size: 1rem;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.9);
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="datetime-local"]:focus,
textarea:focus,
select:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
transform: translateY(-2px);
}
textarea {
resize: vertical;
min-height: 120px;
font-family: inherit;
}
/* Button Styles */
button {
background: linear-gradient(135deg, #27ae60, #219a52);
color: white;
padding: 12px 30px;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3);
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(39, 174, 96, 0.4);
}
button.delete-btn {
background: linear-gradient(135deg, #e74c3c, #c0392b);
box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3);
}
button.delete-btn:hover {
box-shadow: 0 6px 20px rgba(231, 76, 60, 0.4);
}
button.edit-btn {
background: linear-gradient(135deg, #f39c12, #e67e22);
box-shadow: 0 4px 15px rgba(243, 156, 18, 0.3);
}
button.edit-btn:hover {
box-shadow: 0 6px 20px rgba(243, 156, 18, 0.4);
}
button.copy-btn {
background: linear-gradient(135deg, #3498db, #2980b9);
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
}
button.copy-btn:hover {
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
}
button.restore-btn {
background: linear-gradient(135deg, #9b59b6, #8e44ad);
box-shadow: 0 4px 15px rgba(155, 89, 182, 0.3);
}
button.restore-btn:hover {
box-shadow: 0 6px 20px rgba(155, 89, 182, 0.4);
}
button.edit-date-btn {
background: linear-gradient(135deg, #17a2b8, #138496);
padding: 6px 12px;
font-size: 0.85rem;
box-shadow: 0 2px 8px rgba(23, 162, 184, 0.3);
}
button.edit-date-btn:hover {
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.4);
}
/* Tasks Controls */
#tasks-controls {
margin-bottom: 20px;
padding: 15px 20px;
background: linear-gradient(135deg, #ecf0f1, #bdc3c7);
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
#tasks-controls label {
display: flex;
align-items: center;
gap: 10px;
margin: 0;
font-weight: normal;
color: #2c3e50;
cursor: pointer;
}
#tasks-controls input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
/* Task Cards */
.task-card {
border: none;
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
background: rgba(255, 255, 255, 0.9);
position: relative;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
border-left: 5px solid #3498db;
}
.task-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.task-card.deleted {
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
border-left-color: #e74c3c;
opacity: 0.8;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
gap: 20px;
}
.task-title {
font-size: 1.4rem;
font-weight: 700;
color: #2c3e50;
flex: 1;
line-height: 1.4;
}
.task-status {
padding: 8px 16px;
border-radius: 25px;
font-size: 0.9rem;
font-weight: 600;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Status Colors */
.status-purple {
background: linear-gradient(135deg, #9b59b6, #8e44ad);
color: white;
}
.status-red {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
}
.status-orange {
background: linear-gradient(135deg, #f39c12, #e67e22);
color: white;
}
.status-green {
background: linear-gradient(135deg, #27ae60, #219a52);
color: white;
}
.status-gray {
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
color: white;
}
.status-darkred {
background: linear-gradient(135deg, #8b0000, #660000);
color: white;
}
/* Badges */
.deleted-badge {
background: #e74c3c;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
margin-left: 10px;
font-weight: 600;
}
.copy-badge {
background: #3498db;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
margin-left: 10px;
font-weight: 600;
}
.role-badge {
font-size: 0.8rem;
padding: 4px 10px;
border-radius: 15px;
margin-left: 8px;
font-weight: 600;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.role-admin {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
}
.role-creator {
background: linear-gradient(135deg, #27ae60, #219a52);
color: white;
}
.role-executor {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
}
/* Task Content */
.task-original {
margin: 10px 0;
padding: 10px 15px;
background: linear-gradient(135deg, #e8f4fd, #d4e6f1);
border-radius: 8px;
border-left: 4px solid #3498db;
}
.task-original small {
color: #2c3e50;
font-style: italic;
font-size: 0.9rem;
}
.task-description {
margin: 15px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
line-height: 1.6;
color: #495057;
}
.task-dates {
margin: 15px 0;
padding: 15px;
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
border-radius: 10px;
border-left: 4px solid #f39c12;
}
.task-dates div {
margin: 8px 0;
font-size: 0.95rem;
color: #856404;
}
.task-dates strong {
color: #e67e22;
}
/* Assignments */
.task-assignments {
margin: 20px 0;
}
.assignment {
display: flex;
align-items: center;
margin: 10px 0;
padding: 12px 15px;
border-radius: 10px;
background: rgba(248, 249, 250, 0.8);
transition: all 0.3s ease;
border: 1px solid #e9ecef;
}
.assignment:hover {
background: rgba(233, 236, 239, 0.8);
transform: translateX(5px);
}
.assignment.overdue {
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
border: 1px solid #e74c3c;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(231, 76, 60, 0); }
100% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0); }
}
.assignment-status {
width: 16px;
height: 16px;
border-radius: 50%;
margin-right: 15px;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.status-assigned {
background: linear-gradient(135deg, #e74c3c, #c0392b);
}
.status-in_progress {
background: linear-gradient(135deg, #f39c12, #e67e22);
}
.status-completed {
background: linear-gradient(135deg, #27ae60, #219a52);
}
.status-overdue {
background: linear-gradient(135deg, #8b0000, #660000);
}
.assignment-dates {
margin-top: 8px;
font-size: 0.85rem;
color: #6c757d;
}
.assignment-dates small {
display: block;
margin: 3px 0;
}
.action-buttons {
margin-top: 12px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.action-buttons button {
margin-right: 0;
padding: 8px 16px;
font-size: 0.9rem;
border-radius: 8px;
}
/* Task Actions - ПРАВЫЙ НИЖНИЙ УГОЛ */
.task-actions {
position: absolute;
bottom: 20px;
right: 20px;
display: flex;
gap: 8px;
}
.task-actions button {
padding: 8px 12px;
font-size: 1rem;
border-radius: 8px;
min-width: auto;
}
/* File List */
.file-list {
margin-top: 15px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}
.file-item {
display: flex;
align-items: center;
padding: 10px;
background: white;
margin-bottom: 8px;
border-radius: 8px;
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.file-item:hover {
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.file-item a {
color: #3498db;
text-decoration: none;
margin-right: 15px;
font-weight: 500;
transition: color 0.3s ease;
}
.file-item a:hover {
color: #2980b9;
text-decoration: underline;
}
.file-item small {
color: #6c757d;
margin-left: auto;
font-size: 0.85rem;
}
/* Task Meta */
.task-meta {
margin-top: 15px;
padding-top: 15px;
border-top: 2px solid #e9ecef;
}
.task-meta small {
color: #6c757d;
font-size: 0.9rem;
}
/* Logs */
.log-entry {
padding: 15px;
border-bottom: 1px solid #e9ecef;
font-size: 0.95rem;
transition: all 0.3s ease;
}
.log-entry:hover {
background: #f8f9fa;
border-radius: 8px;
}
.log-time {
color: #6c757d;
font-size: 0.85rem;
margin-bottom: 5px;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease;
}
.modal-content {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
margin: 8% auto;
padding: 30px;
border-radius: 20px;
width: 90%;
max-width: 600px;
max-height: 85vh;
overflow-y: auto;
position: relative;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.3);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.close {
color: #6c757d;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
position: absolute;
right: 20px;
top: 15px;
transition: all 0.3s ease;
}
.close:hover {
color: #e74c3c;
transform: rotate(90deg);
}
/* Checkbox Groups */
.checkbox-group {
max-height: 250px;
overflow-y: auto;
border: 2px solid #e9ecef;
padding: 15px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.8);
}
.checkbox-item {
margin: 8px 0;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.3s ease;
}
.checkbox-item:hover {
background: rgba(52, 152, 219, 0.1);
transform: translateX(5px);
}
.checkbox-item label {
display: flex;
align-items: center;
gap: 12px;
margin: 0;
font-weight: normal;
cursor: pointer;
color: #495057;
}
.checkbox-item input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
/* Login Modal */
#login-modal .modal-content {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.85));
text-align: center;
max-width: 500px;
}
#login-form .form-group {
display: flex;
flex-direction: column;
align-items: center;
}
#login-form input[type="text"],
#login-form input[type="password"] {
width: 100%;
max-width: 300px;
margin: 0 auto;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 10px;
font-size: 1rem;
transition: all 0.3s ease;
}
#login-form input[type="text"]:focus,
#login-form input[type="password"]:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
transform: translateY(-2px);
}
.test-users {
margin-top: 25px;
padding: 20px;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 15px;
border-left: 5px solid #3498db;
}
.test-users h3 {
margin-bottom: 15px;
color: #2c3e50;
font-size: 1.2rem;
}
.test-users ul {
list-style: none;
padding: 0;
}
.test-users li {
padding: 8px 0;
border-bottom: 1px solid #dee2e6;
color: #495057;
}
.test-users li:last-child {
border-bottom: none;
}
.test-users strong {
color: #2c3e50;
}
/* Loading States */
.loading {
text-align: center;
padding: 40px 20px;
color: #6c757d;
font-size: 1.1rem;
}
.error {
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
color: #721c24;
padding: 15px 20px;
border-radius: 10px;
margin: 15px 0;
border: 1px solid #f5c6cb;
border-left: 5px solid #e74c3c;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.task-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.task-actions {
position: static;
margin-top: 15px;
justify-content: center;
width: 100%;
}
nav {
flex-direction: column;
}
.modal-content {
width: 95%;
margin: 5% auto;
padding: 20px;
}
header h1 {
font-size: 1.8rem;
}
.user-info {
flex-direction: column;
gap: 10px;
text-align: center;
}
.assignment {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.action-buttons {
width: 100%;
justify-content: flex-start;
}
#login-form input[type="text"],
#login-form input[type="password"] {
max-width: 100%;
}
}
@media (max-width: 480px) {
.section {
padding: 15px;
}
.task-card {
padding: 15px;
}
.task-title {
font-size: 1.2rem;
}
.task-status {
font-size: 0.8rem;
padding: 6px 12px;
}
.task-actions {
gap: 5px;
}
.task-actions button {
padding: 8px 12px;
font-size: 0.8rem;
}
nav button {
padding: 10px 15px;
font-size: 0.9rem;
}
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #3498db, #2980b9);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #2980b9, #1f618d);
}
/* Focus States */
button:focus,
input:focus,
textarea:focus,
select:focus {
outline: 2px solid #3498db;
outline-offset: 2px;
}
/* Print Styles */
@media print {
.task-actions,
nav,
.user-info button {
display: none !important;
}
.task-card {
break-inside: avoid;
box-shadow: none;
border: 1px solid #ddd;
}
}