Add files via upload

This commit is contained in:
kalugin66
2025-11-24 11:43:41 +05:00
committed by GitHub
parent 2e69cbcf83
commit bf5e35c61f
5 changed files with 743 additions and 62 deletions

View File

@@ -47,16 +47,36 @@
<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="tasks-section" class="section">
<h2>Все задачи</h2>
<div id="tasks-controls">
<div class="filters">
<div class="filter-group">
<label for="search-tasks">Поиск:</label>
<input type="text" id="search-tasks" placeholder="Поиск по названию и описанию..." oninput="loadTasks()">
</div>
<div class="filter-group">
<label for="status-filter">Статус:</label>
<select id="status-filter" onchange="loadTasks()">
<option value="active,in_progress,assigned,overdue,rework">Все активные</option>
<option value="all">Все статусы</option>
<option value="assigned">Назначена</option>
<option value="in_progress">В работе</option>
<option value="rework">На доработке</option>
<option value="overdue">Просрочена</option>
<option value="completed">Выполнена</option>
<option value="closed">Закрыта</option>
</select>
</div>
</div>
<!-- Чекбокс удаленных задач - только для администраторов -->
<label class="show-deleted-label" style="display: none;">
<input type="checkbox" id="show-deleted" onchange="loadTasks()">
Показать удаленные задачи
</label>
</div>
<div id="tasks-list"></div>
</section>
<!-- Секция создания задачи -->
<section id="create-task-section" class="section">
@@ -84,6 +104,9 @@
<div class="form-group">
<label>Исполнители:</label>
<div class="user-search">
<input type="text" id="user-search" placeholder="Поиск исполнителей..." oninput="filterUsers()">
</div>
<div id="users-checklist" class="checkbox-group"></div>
</div>
@@ -110,7 +133,7 @@
<div class="modal-content">
<span class="close" onclick="closeEditModal()">&times;</span>
<h3>Редактировать задачу</h3>
<form id="edit-task-form">
<form id="edit-task-form" enctype="multipart/form-data">
<input type="hidden" id="edit-task-id">
<div class="form-group">
<label for="edit-title">Название задачи:</label>
@@ -134,9 +157,18 @@
<div class="form-group">
<label>Исполнители:</label>
<div class="user-search">
<input type="text" id="edit-user-search" placeholder="Поиск исполнителей..." oninput="filterEditUsers()">
</div>
<div id="edit-users-checklist" class="checkbox-group"></div>
</div>
<div class="form-group">
<label for="edit-files">Добавить файлы:</label>
<input type="file" id="edit-files" name="files" multiple>
<div id="edit-file-list"></div>
</div>
<button type="submit">Сохранить изменения</button>
</form>
</div>
@@ -162,6 +194,9 @@
<div class="form-group">
<label>Назначить исполнителей для копии:</label>
<div class="user-search">
<input type="text" id="copy-user-search" placeholder="Поиск исполнителей..." oninput="filterCopyUsers()">
</div>
<div id="copy-users-checklist" class="checkbox-group"></div>
</div>
<button type="submit">Создать копию</button>
@@ -190,6 +225,22 @@
</div>
</div>
<!-- Модальное окно для возврата на доработку -->
<div id="rework-task-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeReworkModal()">&times;</span>
<h3>Вернуть задачу на доработку</h3>
<form id="rework-task-form">
<input type="hidden" id="rework-task-id">
<div class="form-group">
<label for="rework-comment">Комментарий к доработке:</label>
<textarea id="rework-comment" name="comment" rows="4" placeholder="Укажите, что нужно исправить..." required></textarea>
</div>
<button type="submit">Вернуть на доработку</button>
</form>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -1,6 +1,7 @@
let currentUser = null;
let users = [];
let tasks = [];
let filteredUsers = [];
// Инициализация при загрузке
document.addEventListener('DOMContentLoaded', function() {
@@ -43,11 +44,17 @@ function showMainInterface() {
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';
// Показываем фильтры ВСЕМ пользователям, а не только администраторам
document.getElementById('tasks-controls').style.display = 'block';
// Чекбокс удаленных задач показываем только администраторам
const showDeletedLabel = document.querySelector('.show-deleted-label');
if (showDeletedLabel) {
if (currentUser.role === 'admin') {
showDeletedLabel.style.display = 'flex';
} else {
showDeletedLabel.style.display = 'none';
}
}
loadUsers();
@@ -62,7 +69,9 @@ function setupEventListeners() {
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('rework-task-form').addEventListener('submit', sendForRework);
document.getElementById('files').addEventListener('change', updateFileList);
document.getElementById('edit-files').addEventListener('change', updateEditFileList);
}
async function login(event) {
@@ -122,6 +131,7 @@ async function loadUsers() {
try {
const response = await fetch('/api/users');
users = await response.json();
filteredUsers = [...users];
renderUsersChecklist();
renderEditUsersChecklist();
renderCopyUsersChecklist();
@@ -130,9 +140,49 @@ async function loadUsers() {
}
}
// Фильтрация пользователей
function filterUsers() {
const search = document.getElementById('user-search').value.toLowerCase();
filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
renderUsersChecklist();
}
function filterEditUsers() {
const search = document.getElementById('edit-user-search').value.toLowerCase();
const filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
renderEditUsersChecklist(filtered);
}
function filterCopyUsers() {
const search = document.getElementById('copy-user-search').value.toLowerCase();
const filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
renderCopyUsersChecklist(filtered);
}
async function loadTasks() {
try {
const response = await fetch('/api/tasks');
const search = document.getElementById('search-tasks')?.value || '';
const statusFilter = document.getElementById('status-filter')?.value || 'active,in_progress,assigned,overdue,rework';
const showDeleted = document.getElementById('show-deleted')?.checked || false;
let url = '/api/tasks?';
if (search) url += `search=${encodeURIComponent(search)}&`;
if (statusFilter) url += `status=${encodeURIComponent(statusFilter)}&`;
if (showDeleted) url += `showDeleted=true&`;
const response = await fetch(url);
tasks = await response.json();
renderTasks();
@@ -157,7 +207,7 @@ async function loadActivityLogs() {
function renderUsersChecklist() {
const container = document.getElementById('users-checklist');
container.innerHTML = users
container.innerHTML = filteredUsers
.filter(user => user.id !== currentUser.id)
.map(user => `
<div class="checkbox-item">
@@ -170,9 +220,9 @@ function renderUsersChecklist() {
`).join('');
}
function renderEditUsersChecklist() {
function renderEditUsersChecklist(filtered = users) {
const container = document.getElementById('edit-users-checklist');
container.innerHTML = users
container.innerHTML = filtered
.filter(user => user.id !== currentUser.id)
.map(user => `
<div class="checkbox-item">
@@ -185,9 +235,9 @@ function renderEditUsersChecklist() {
`).join('');
}
function renderCopyUsersChecklist() {
function renderCopyUsersChecklist(filtered = users) {
const container = document.getElementById('copy-users-checklist');
container.innerHTML = users
container.innerHTML = filtered
.filter(user => user.id !== currentUser.id)
.map(user => `
<div class="checkbox-item">
@@ -218,17 +268,23 @@ function renderTasks() {
const overallStatus = getTaskOverallStatus(task);
const statusClass = getStatusClass(overallStatus);
const isDeleted = task.status === 'deleted';
const isClosed = task.closed_at !== null;
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-card ${isDeleted ? 'deleted' : ''} ${isClosed ? 'closed' : ''}">
<div class="task-actions">
${!isDeleted ? `
<button class="edit-btn" onclick="openEditModal(${task.id})" title="Редактировать">✏️</button>
${!isDeleted && !isClosed ? `
${canEdit ? `<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>
${canEdit ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄</button>` : ''}
${canEdit ? `<button class="close-btn" onclick="closeTask(${task.id})" title="Закрыть задачу">🔒</button>` : ''}
${canEdit ? `<button class="delete-btn" onclick="deleteTask(${task.id})" title="Удалить">🗑️</button>` : ''}
` : ''}
${isClosed && canEdit ? `
<button class="reopen-btn" onclick="reopenTask(${task.id})" title="Открыть задачу">🔓</button>
` : ''}
${isDeleted && currentUser.role === 'admin' ? `
<button class="restore-btn" onclick="restoreTask(${task.id})" title="Восстановить">↶</button>
@@ -239,6 +295,7 @@ function renderTasks() {
<div class="task-title">
${task.title}
${isDeleted ? '<span class="deleted-badge">Удалена</span>' : ''}
${isClosed ? '<span class="closed-badge">Закрыта</span>' : ''}
${isCopy ? '<span class="copy-badge">Копия</span>' : ''}
<span class="role-badge ${getRoleBadgeClass(userRole)}">${userRole}</span>
</div>
@@ -253,6 +310,12 @@ function renderTasks() {
<div class="task-description">${task.description || 'Нет описания'}</div>
${task.rework_comment ? `
<div class="rework-comment">
<strong>Комментарий к доработке:</strong> ${task.rework_comment}
</div>
` : ''}
${task.start_date || task.due_date ? `
<div class="task-dates">
${task.start_date ? `<div><strong>Начать:</strong> ${formatDateTime(task.start_date)}</div>` : ''}
@@ -276,6 +339,7 @@ function renderTasks() {
<div class="task-meta">
<small>Создана: ${formatDateTime(task.created_at)} | Автор: ${task.creator_name}</small>
${task.deleted_at ? `<br><small>Удалена: ${formatDateTime(task.deleted_at)}</small>` : ''}
${task.closed_at ? `<br><small>Закрыта: ${formatDateTime(task.closed_at)}</small>` : ''}
</div>
</div>
`;
@@ -286,9 +350,10 @@ function renderAssignment(assignment, taskId, canEdit) {
const statusClass = getStatusClass(assignment.status);
const isCurrentUser = assignment.user_id === currentUser.id;
const isOverdue = assignment.status === 'overdue';
const isRework = assignment.status === 'rework';
return `
<div class="assignment ${isOverdue ? 'overdue' : ''}">
<div class="assignment ${isOverdue ? 'overdue' : ''} ${isRework ? 'rework' : ''}">
<span class="assignment-status ${statusClass}"></span>
<div style="flex: 1;">
<strong>${assignment.user_name}</strong>
@@ -299,11 +364,16 @@ function renderAssignment(assignment, taskId, canEdit) {
${assignment.due_date ? `<small>Выполнить до: ${formatDateTime(assignment.due_date)}</small>` : ''}
</div>
` : ''}
${assignment.rework_comment ? `
<div class="assignment-rework-comment">
<small><strong>Комментарий:</strong> ${assignment.rework_comment}</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') ?
${isCurrentUser && (assignment.status === 'in_progress' || assignment.status === 'overdue' || assignment.status === 'rework') ?
`<button onclick="updateStatus(${taskId}, ${assignment.user_id}, 'completed')">Выполнено</button>` : ''}
${canEdit ?
`<button class="edit-date-btn" onclick="openEditAssignmentModal(${taskId}, ${assignment.user_id})" title="Редактировать сроки">📅</button>` : ''}
@@ -349,6 +419,8 @@ async function createTask(event) {
alert('Задача успешно создана!');
document.getElementById('create-task-form').reset();
document.getElementById('file-list').innerHTML = '';
document.getElementById('user-search').value = '';
filterUsers();
loadTasks();
loadActivityLogs();
showSection('tasks');
@@ -405,6 +477,9 @@ async function openEditModal(taskId) {
function closeEditModal() {
document.getElementById('edit-task-modal').style.display = 'none';
document.getElementById('edit-file-list').innerHTML = '';
document.getElementById('edit-user-search').value = '';
filterEditUsers();
}
async function updateTask(event) {
@@ -419,19 +494,22 @@ async function updateTask(event) {
const assignedUsers = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]:checked');
const assignedUserIds = Array.from(assignedUsers).map(cb => parseInt(cb.value));
const formData = new FormData();
formData.append('title', title);
formData.append('description', description);
formData.append('assignedUsers', JSON.stringify(assignedUserIds));
if (startDate) formData.append('startDate', startDate);
if (dueDate) formData.append('dueDate', dueDate);
const files = document.getElementById('edit-files').files;
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
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
})
body: formData
});
if (response.ok) {
@@ -456,9 +534,10 @@ function openCopyModal(taskId) {
function closeCopyModal() {
document.getElementById('copy-task-modal').style.display = 'none';
document.getElementById('copy-user-search').value = '';
filterCopyUsers();
}
// В функции copyTask улучшаем обработку ошибок
async function copyTask(event) {
event.preventDefault();
@@ -555,6 +634,90 @@ async function updateAssignment(event) {
}
}
function openReworkModal(taskId) {
document.getElementById('rework-task-id').value = taskId;
document.getElementById('rework-task-modal').style.display = 'block';
}
function closeReworkModal() {
document.getElementById('rework-task-modal').style.display = 'none';
document.getElementById('rework-comment').value = '';
}
async function sendForRework(event) {
event.preventDefault();
const taskId = document.getElementById('rework-task-id').value;
const comment = document.getElementById('rework-comment').value;
try {
const response = await fetch(`/api/tasks/${taskId}/rework`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ comment })
});
if (response.ok) {
alert('Задача возвращена на доработку!');
closeReworkModal();
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка возврата задачи на доработку');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка возврата задачи на доработку');
}
}
async function closeTask(taskId) {
if (!confirm('Вы уверены, что хотите закрыть эту задачу? Исполнители больше не будут видеть её.')) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/close`, {
method: 'POST'
});
if (response.ok) {
alert('Задача закрыта!');
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка закрытия задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка закрытия задачи');
}
}
async function reopenTask(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}/reopen`, {
method: 'POST'
});
if (response.ok) {
alert('Задача открыта!');
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка открытия задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка открытия задачи');
}
}
async function deleteTask(taskId) {
if (!confirm('Вы уверены, что хотите удалить эту задачу?')) {
return;
@@ -599,7 +762,6 @@ async function restoreTask(taskId) {
}
}
// В функции updateStatus улучшаем обработку ошибок
async function updateStatus(taskId, userId, status) {
try {
const response = await fetch(`/api/tasks/${taskId}/status`, {
@@ -625,12 +787,14 @@ async function updateStatus(taskId, userId, status) {
function getTaskOverallStatus(task) {
if (task.status === 'deleted') return 'deleted';
if (task.closed_at) return 'closed'; // Закрытые задачи всегда имеют статус 'closed'
if (!task.assignments || task.assignments.length === 0) return 'unassigned';
const assignments = task.assignments;
let hasAssigned = false;
let hasInProgress = false;
let hasOverdue = false;
let hasRework = false;
let allCompleted = true;
for (let assignment of assignments) {
@@ -643,12 +807,16 @@ function getTaskOverallStatus(task) {
} else if (assignment.status === 'overdue') {
hasOverdue = true;
allCompleted = false;
} else if (assignment.status === 'rework') {
hasRework = true;
allCompleted = false;
} else if (assignment.status !== 'completed') {
allCompleted = false;
}
}
if (allCompleted) return 'completed';
if (hasRework) return 'rework';
if (hasOverdue) return 'overdue';
if (hasInProgress) return 'in_progress';
if (hasAssigned) return 'assigned';
@@ -658,9 +826,11 @@ function getTaskOverallStatus(task) {
function getStatusClass(status) {
switch (status) {
case 'deleted': return 'status-gray';
case 'closed': return 'status-gray';
case 'unassigned': return 'status-purple';
case 'assigned': return 'status-red';
case 'in_progress': return 'status-orange';
case 'rework': return 'status-yellow';
case 'overdue': return 'status-darkred';
case 'completed': return 'status-green';
default: return 'status-purple';
@@ -670,9 +840,11 @@ function getStatusClass(status) {
function getStatusText(status) {
switch (status) {
case 'deleted': return 'Удалена';
case 'closed': return 'Закрыта';
case 'unassigned': return 'Не назначена';
case 'assigned': return 'Назначена';
case 'in_progress': return 'В работе';
case 'rework': return 'На доработке';
case 'overdue': return 'Просрочена';
case 'completed': return 'Выполнена';
default: return 'Неизвестно';
@@ -729,6 +901,16 @@ function formatDateTimeForInput(dateTimeString) {
function updateFileList() {
const fileInput = document.getElementById('files');
const fileList = document.getElementById('file-list');
updateFileListForInput(fileInput, fileList);
}
function updateEditFileList() {
const fileInput = document.getElementById('edit-files');
const fileList = document.getElementById('edit-file-list');
updateFileListForInput(fileInput, fileList);
}
function updateFileListForInput(fileInput, fileList) {
const files = fileInput.files;
if (files.length === 0) {
@@ -780,7 +962,11 @@ function getActionText(action) {
'TASK_ASSIGNMENTS_UPDATED': 'обновил назначения',
'ASSIGNMENT_UPDATED': 'обновил сроки исполнителя',
'STATUS_CHANGED': 'изменил статус задачи',
'FILE_UPLOADED': 'загрузил файл'
'FILE_UPLOADED': 'загрузил файл',
'FILE_COPIED': 'скопировал файл',
'TASK_SENT_FOR_REWORK': 'вернул задачу на доработку',
'TASK_CLOSED': 'закрыл задачу',
'TASK_REOPENED': 'открыл задачу'
};
return actions[action] || action;

View File

@@ -890,4 +890,136 @@ select:focus {
box-shadow: none;
border: 1px solid #ddd;
}
}
/* Добавляем в существующие стили */
/* Фильтры */
.filters {
display: flex;
gap: 20px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.filter-group label {
font-weight: 600;
color: #2c3e50;
margin-bottom: 0;
}
.filter-group input,
.filter-group select {
padding: 8px 12px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 0.9rem;
}
.user-search {
margin-bottom: 10px;
}
.user-search input {
width: 100%;
padding: 8px 12px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 0.9rem;
}
/* Новые статусы */
.status-yellow {
background: linear-gradient(135deg, #ffc107, #e0a800);
color: white;
}
/* Закрытые задачи */
.task-card.closed {
background: linear-gradient(135deg, #e9ecef, #dee2e6);
border-left-color: #6c757d;
opacity: 0.8;
}
.closed-badge {
background: #6c757d;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
margin-left: 10px;
font-weight: 600;
}
/* Комментарии к доработке */
.rework-comment {
margin: 10px 0;
padding: 12px 15px;
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
border-radius: 8px;
border-left: 4px solid #ffc107;
color: #856404;
}
.assignment-rework-comment {
margin-top: 8px;
padding: 8px;
background: #fff3cd;
border-radius: 6px;
border-left: 3px solid #ffc107;
}
.assignment.rework {
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
border: 1px solid #ffc107;
}
/* Новые кнопки */
button.rework-btn {
background: linear-gradient(135deg, #ffc107, #e0a800);
box-shadow: 0 4px 15px rgba(255, 193, 7, 0.3);
}
button.rework-btn:hover {
box-shadow: 0 6px 20px rgba(255, 193, 7, 0.4);
}
button.close-btn {
background: linear-gradient(135deg, #6c757d, #5a6268);
box-shadow: 0 4px 15px rgba(108, 117, 125, 0.3);
}
button.close-btn:hover {
box-shadow: 0 6px 20px rgba(108, 117, 125, 0.4);
}
button.reopen-btn {
background: linear-gradient(135deg, #20c997, #1ea085);
box-shadow: 0 4px 15px rgba(32, 201, 151, 0.3);
}
button.reopen-btn:hover {
box-shadow: 0 6px 20px rgba(32, 201, 151, 0.4);
}
.show-deleted-label {
display: flex;
align-items: center;
gap: 8px;
font-weight: normal;
color: #2c3e50;
cursor: pointer;
}
.show-deleted-label input {
margin: 0;
}
/* В существующие стили добавляем */
.show-deleted-label[style*="display: none"] {
display: none !important;
}