This commit is contained in:
2025-12-14 17:48:56 +05:00
parent b6e302bf57
commit d25ac311f3
4 changed files with 250 additions and 130 deletions

View File

@@ -40,62 +40,63 @@
<nav> <nav>
<button onclick="showSection('tasks')">Задачи</button> <button onclick="showSection('tasks')">Задачи</button>
<button onclick="showSection('create-task')">Создать задачу</button> <button onclick="showSection('create-task')">Создать задачу</button>
<button onclick="showSection('tasks')">Мои задачи</button>
<button onclick="showTasksWithoutDate()" id="tasks-no-date-btn">Задачи без срока</button>
<button onclick="showSection('logs')">Лог активности</button> <button onclick="showSection('logs')">Лог активности</button>
<button onclick="showSection('/tasks-not-date')" style="background: linear-gradient(135deg, #e74c3c, #c0392b);">Задачи без срока</button>
<button onclick="window.location.href = '/admin'" style="background: linear-gradient(135deg, #e74c3c, #c0392b);">Админ-панель</button> <button onclick="window.location.href = '/admin'" style="background: linear-gradient(135deg, #e74c3c, #c0392b);">Админ-панель</button>
</nav> </nav>
</header> </header>
<main> <main>
<section id="tasks-section" class="section"> <section id="tasks-section" class="section">
<h2>Все задачи</h2> <h2>Все задачи</h2>
<div id="tasks-controls"> <div id="tasks-controls">
<div class="filters"> <div class="filters">
<div class="filter-group"> <div class="filter-group">
<label for="search-tasks">Поиск:</label> <label for="search-tasks">Поиск:</label>
<input type="text" id="search-tasks" placeholder="Поиск по названию и описанию..." oninput="loadTasks()"> <input type="text" id="search-tasks" placeholder="Поиск по названию и описанию..." oninput="loadTasks()">
</div> </div>
<div class="filter-group"> <div class="filter-group">
<label for="status-filter">Статус:</label> <label for="status-filter">Статус:</label>
<select id="status-filter" onchange="loadTasks()"> <select id="status-filter" onchange="loadTasks()">
<option value="active,in_progress,assigned,overdue,rework">Все активные</option> <option value="active,in_progress,assigned,overdue,rework">Все активные</option>
<option value="all">Все статусы</option> <option value="all">Все статусы</option>
<option value="assigned">Назначена</option> <option value="assigned">Назначена</option>
<option value="in_progress">В работе</option> <option value="in_progress">В работе</option>
<option value="rework">На доработке</option> <option value="rework">На доработке</option>
<option value="overdue">Просрочена</option> <option value="overdue">Просрочена</option>
<option value="completed">Выполнена</option> <option value="completed">Выполнена</option>
<option value="closed">Закрыта</option> <option value="closed">Закрыта</option>
</select> </select>
</div> </div>
<div class="filter-group"> <div class="filter-group">
<label for="creator-filter">Заказчик:</label> <label for="creator-filter">Заказчик:</label>
<select id="creator-filter" onchange="loadTasks()"> <select id="creator-filter" onchange="loadTasks()">
<option value="">Все заказчики</option> <option value="">Все заказчики</option>
</select> </select>
</div> </div>
<div class="filter-group"> <div class="filter-group">
<label for="assignee-filter">Исполнитель:</label> <label for="assignee-filter">Исполнитель:</label>
<select id="assignee-filter" onchange="loadTasks()"> <select id="assignee-filter" onchange="loadTasks()">
<option value="">Все исполнители</option> <option value="">Все исполнители</option>
</select> </select>
</div> </div>
<div class="filter-group"> <div class="filter-group">
<label for="deadline-filter">Срок выполнения:</label> <label for="deadline-filter">Срок выполнения:</label>
<select id="deadline-filter" onchange="loadTasks()"> <select id="deadline-filter" onchange="loadTasks()">
<option value="">Все сроки</option> <option value="">Все сроки</option>
<option value="48h">Менее 48 часов</option> <option value="48h">Менее 48 часов</option>
<option value="24h">Менее 24 часов</option> <option value="24h">Менее 24 часов</option>
</select> </select>
</div> </div>
</div> </div>
<label class="show-deleted-label" style="display: none;"> <label class="show-deleted-label" style="display: none;">
<input type="checkbox" id="show-deleted" onchange="loadTasks()"> <input type="checkbox" id="show-deleted" onchange="loadTasks()">
Показать удаленные задачи Показать удаленные задачи
</label> </label>
</div> </div>
<div id="tasks-list"></div> <div id="tasks-list"></div>
</section> </section>
<section id="create-task-section" class="section"> <section id="create-task-section" class="section">
<h2>Создать новую задачу</h2> <h2>Создать новую задачу</h2>

View File

@@ -3,6 +3,8 @@ let users = [];
let tasks = []; let tasks = [];
let filteredUsers = []; let filteredUsers = [];
let expandedTasks = new Set(); let expandedTasks = new Set();
let showingTasksWithoutDate = false;
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
checkAuth(); checkAuth();
setupEventListeners(); setupEventListeners();
@@ -57,6 +59,10 @@ function showMainInterface() {
loadTasks(); loadTasks();
loadActivityLogs(); loadActivityLogs();
showSection('tasks'); showSection('tasks');
showingTasksWithoutDate = false;
const btn = document.getElementById('tasks-no-date-btn');
if (btn) btn.classList.remove('active');
} }
function setupEventListeners() { function setupEventListeners() {
@@ -187,6 +193,10 @@ function filterCopyUsers() {
async function loadTasks() { async function loadTasks() {
try { try {
showingTasksWithoutDate = false;
const btn = document.getElementById('tasks-no-date-btn');
if (btn) btn.classList.remove('active');
const search = document.getElementById('search-tasks')?.value || ''; const search = document.getElementById('search-tasks')?.value || '';
const statusFilter = document.getElementById('status-filter')?.value || 'active,in_progress,assigned,overdue,rework'; const statusFilter = document.getElementById('status-filter')?.value || 'active,in_progress,assigned,overdue,rework';
const creatorFilter = document.getElementById('creator-filter')?.value || ''; const creatorFilter = document.getElementById('creator-filter')?.value || '';
@@ -214,6 +224,32 @@ async function loadTasks() {
} }
} }
function showTasksWithoutDate() {
showingTasksWithoutDate = true;
const btn = document.getElementById('tasks-no-date-btn');
if (btn) btn.classList.add('active');
loadTasksWithoutDate();
}
async function loadTasksWithoutDate() {
try {
const response = await fetch('/api/tasks');
if (!response.ok) throw new Error('Ошибка загрузки задач');
const allTasks = await response.json();
tasks = allTasks.filter(task => {
const hasTaskDueDate = !task.due_date;
const hasAssignmentDueDates = task.assignments &&
task.assignments.every(assignment => !assignment.due_date);
return hasTaskDueDate && hasAssignmentDueDates;
});
renderTasks();
} catch (error) {
console.error('Ошибка загрузки задач без срока:', error);
}
}
async function loadActivityLogs() { async function loadActivityLogs() {
try { try {
const response = await fetch('/api/activity-logs'); const response = await fetch('/api/activity-logs');
@@ -296,7 +332,7 @@ function renderTasks() {
const timeLeftInfo = getTimeLeftInfo(task); const timeLeftInfo = getTimeLeftInfo(task);
return ` return `
<div class="task-card ${isDeleted ? 'deleted' : ''} ${isClosed ? 'closed' : ''}"> <div class="task-card ${isDeleted ? 'deleted' : ''} ${isClosed ? 'closed' : ''}" data-task-id="${task.id}">
<div class="task-header"> <div class="task-header">
<div class="task-title" onclick="toggleTask(${task.id})" style="cursor: pointer; display: flex; justify-content: space-between; align-items: center;"> <div class="task-title" onclick="toggleTask(${task.id})" style="cursor: pointer; display: flex; justify-content: space-between; align-items: center;">
<div style="flex: 1;"> <div style="flex: 1;">
@@ -315,7 +351,7 @@ function renderTasks() {
</div> </div>
</div> </div>
<div class="task-content" style="display: ${isExpanded ? 'block' : 'none'};"> <div class="task-content ${isExpanded ? 'expanded' : ''}">
<div class="task-actions"> <div class="task-actions">
${!isDeleted && !isClosed ? ` ${!isDeleted && !isClosed ? `
${canEdit ? `<button class="edit-btn" onclick="openEditModal(${task.id})" title="Редактировать">✏️</button>` : ''} ${canEdit ? `<button class="edit-btn" onclick="openEditModal(${task.id})" title="Редактировать">✏️</button>` : ''}
@@ -346,12 +382,16 @@ function renderTasks() {
</div> </div>
` : ''} ` : ''}
${task.start_date || task.due_date ? ` <div class="task-dates-files">
<div class="task-dates"> <div class="task-dates">
<div><strong>Создана:</strong> ${formatDateTime(task.start_date || task.created_at)}</div> <strong>Создана:</strong> ${formatDateTime(task.start_date || task.created_at)}
${task.due_date ? `<div><strong>Выполнить до:</strong> ${formatDateTime(task.due_date)}</div>` : ''} ${task.due_date ? ` | <strong>Выполнить до:</strong> ${formatDateTime(task.due_date)}` : ''}
${showingTasksWithoutDate ? '<span class="no-date-badge">Без срока</span>' : ''}
</div> </div>
` : ''} <div class="file-list" id="files-${task.id}">
<strong>Файлы:</strong> <span class="files-placeholder">скрыто</span>
</div>
</div>
<div class="task-assignments"> <div class="task-assignments">
<strong>Исполнители:</strong> <strong>Исполнители:</strong>
@@ -361,11 +401,6 @@ function renderTasks() {
} }
</div> </div>
<div class="file-list" id="files-${task.id}">
<strong>Файлы:</strong>
<div class="loading">Загрузка...</div>
</div>
<div class="task-meta"> <div class="task-meta">
<small>Создана: ${formatDateTime(task.created_at)} | Автор: ${task.creator_name}</small> <small>Создана: ${formatDateTime(task.created_at)} | Автор: ${task.creator_name}</small>
${task.deleted_at ? `<br><small>Удалена: ${formatDateTime(task.deleted_at)}</small>` : ''} ${task.deleted_at ? `<br><small>Удалена: ${formatDateTime(task.deleted_at)}</small>` : ''}
@@ -376,6 +411,7 @@ function renderTasks() {
`; `;
}).join(''); }).join('');
} }
function toggleTask(taskId) { function toggleTask(taskId) {
if (expandedTasks.has(taskId)) { if (expandedTasks.has(taskId)) {
expandedTasks.delete(taskId); expandedTasks.delete(taskId);
@@ -384,6 +420,7 @@ function toggleTask(taskId) {
} }
renderTasks(); renderTasks();
} }
function getTimeLeftInfo(task) { function getTimeLeftInfo(task) {
if (!task.due_date || task.closed_at) return null; if (!task.due_date || task.closed_at) return null;
@@ -838,7 +875,7 @@ async function deleteTask(taskId) {
const error = await response.json(); const error = await response.json();
alert(error.error || 'Ошибка удаления задачи'); alert(error.error || 'Ошибка удаления задачи');
} }
} catch (error) { } catch (error) {
console.error('Ошибка:', error); console.error('Ошибка:', error);
alert('Ошибка удаления задачи'); alert('Ошибка удаления задачи');
} }
@@ -1081,7 +1118,7 @@ async function loadTaskFiles(taskId) {
const container = document.getElementById(`files-${taskId}`); const container = document.getElementById(`files-${taskId}`);
if (container) { if (container) {
if (files.length === 0) { if (files.length === 0) {
container.innerHTML = '<strong>Файлы:</strong> Нет файлов'; container.innerHTML = '<strong>Файлы:</strong> <span class="files-placeholder">скрыто</span>';
} else { } else {
container.innerHTML = ` container.innerHTML = `
<strong>Файлы:</strong> <strong>Файлы:</strong>

View File

@@ -13,13 +13,12 @@ body {
} }
.container { .container {
max-width: 99%; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
display: none; display: none;
} }
/* Header Styles */
header { header {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
@@ -72,7 +71,6 @@ header h1 {
transform: translateY(-2px); transform: translateY(-2px);
} }
/* Navigation */
nav { nav {
display: flex; display: flex;
gap: 12px; gap: 12px;
@@ -99,7 +97,6 @@ nav button:hover {
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4); box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
} }
/* Section Styles */
.section { .section {
display: none; display: none;
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
@@ -121,7 +118,6 @@ nav button:hover {
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
/* Form Styles */
.form-group { .form-group {
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -165,7 +161,6 @@ textarea {
font-family: inherit; font-family: inherit;
} }
/* Button Styles */
button { button {
background: linear-gradient(135deg, #27ae60, #219a52); background: linear-gradient(135deg, #27ae60, #219a52);
color: white; color: white;
@@ -231,7 +226,6 @@ button.edit-date-btn:hover {
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.4); box-shadow: 0 4px 12px rgba(23, 162, 184, 0.4);
} }
/* Tasks Controls */
#tasks-controls { #tasks-controls {
margin-bottom: 20px; margin-bottom: 20px;
padding: 15px 20px; padding: 15px 20px;
@@ -256,11 +250,10 @@ button.edit-date-btn:hover {
cursor: pointer; cursor: pointer;
} }
/* Task Cards */
.task-card { .task-card {
border: none; border: none;
border-radius: 15px; border-radius: 15px;
padding: 20px; padding: 0;
margin-bottom: 20px; margin-bottom: 20px;
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
position: relative; position: relative;
@@ -284,16 +277,26 @@ button.edit-date-btn:hover {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
margin-bottom: 15px; margin-bottom: 0;
gap: 20px; gap: 20px;
} }
.task-title { .task-title {
font-size: 1.4rem; font-size: 1.2rem;
font-weight: 700; font-weight: 700;
color: #2c3e50; color: #2c3e50;
flex: 1; flex: 1;
line-height: 1.4; line-height: 1.4;
cursor: pointer;
padding: 15px;
border-radius: 10px;
background: linear-gradient(135deg, rgba(52, 152, 219, 0.1), rgba(52, 152, 219, 0.05));
transition: all 0.3s ease;
}
.task-title:hover {
background: linear-gradient(135deg, rgba(52, 152, 219, 0.15), rgba(52, 152, 219, 0.1));
transform: translateY(-2px);
} }
.task-status { .task-status {
@@ -305,7 +308,6 @@ button.edit-date-btn:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
/* Status Colors */
.status-purple { .status-purple {
background: linear-gradient(135deg, #9b59b6, #8e44ad); background: linear-gradient(135deg, #9b59b6, #8e44ad);
color: white; color: white;
@@ -331,7 +333,6 @@ button.edit-date-btn:hover {
color: white; color: white;
} }
/* Badges */
.deleted-badge { .deleted-badge {
background: #e74c3c; background: #e74c3c;
color: white; color: white;
@@ -374,7 +375,6 @@ button.edit-date-btn:hover {
color: white; color: white;
} }
/* Task Content */
.task-original { .task-original {
margin: 10px 0; margin: 10px 0;
padding: 10px 15px; padding: 10px 15px;
@@ -398,25 +398,42 @@ button.edit-date-btn:hover {
color: #495057; color: #495057;
} }
.task-dates { .task-dates-files {
margin: 15px 0; margin: 15px 0;
padding: 15px; padding: 15px;
background: linear-gradient(135deg, #fff3cd, #ffeaa7); background: linear-gradient(135deg, #fff3cd, #ffeaa7);
border-radius: 10px; border-radius: 10px;
border-left: 4px solid #f39c12; border-left: 4px solid #f39c12;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
} }
.task-dates div { .task-dates-files .task-dates {
margin: 8px 0; flex: 1;
min-width: 250px;
font-size: 0.95rem; font-size: 0.95rem;
color: #856404; color: #856404;
} }
.task-dates strong { .task-dates-files .file-list {
flex: 1;
min-width: 250px;
font-size: 0.95rem;
color: #856404;
}
.task-dates-files .files-placeholder {
color: #6c757d;
font-style: italic;
}
.task-dates-files strong {
color: #e67e22; color: #e67e22;
} }
/* Assignments */
.task-assignments { .task-assignments {
margin: 20px 0; margin: 20px 0;
} }
@@ -496,7 +513,6 @@ button.edit-date-btn:hover {
border-radius: 8px; border-radius: 8px;
} }
/* Task Actions - ПРАВЫЙ НИЖНИЙ УГОЛ */
.task-actions { .task-actions {
position: absolute; position: absolute;
bottom: 20px; bottom: 20px;
@@ -512,7 +528,6 @@ button.edit-date-btn:hover {
min-width: auto; min-width: auto;
} }
/* File List */
.file-list { .file-list {
margin-top: 15px; margin-top: 15px;
padding: 15px; padding: 15px;
@@ -555,7 +570,6 @@ button.edit-date-btn:hover {
font-size: 0.85rem; font-size: 0.85rem;
} }
/* Task Meta */
.task-meta { .task-meta {
margin-top: 15px; margin-top: 15px;
padding-top: 15px; padding-top: 15px;
@@ -567,7 +581,6 @@ button.edit-date-btn:hover {
font-size: 0.9rem; font-size: 0.9rem;
} }
/* Logs */
.log-entry { .log-entry {
padding: 15px; padding: 15px;
border-bottom: 1px solid #e9ecef; border-bottom: 1px solid #e9ecef;
@@ -586,7 +599,6 @@ button.edit-date-btn:hover {
margin-bottom: 5px; margin-bottom: 5px;
} }
/* Modal Styles */
.modal { .modal {
display: none; display: none;
position: fixed; position: fixed;
@@ -638,7 +650,6 @@ button.edit-date-btn:hover {
transform: rotate(90deg); transform: rotate(90deg);
} }
/* Checkbox Groups */
.checkbox-group { .checkbox-group {
max-height: 250px; max-height: 250px;
overflow-y: auto; overflow-y: auto;
@@ -676,7 +687,6 @@ button.edit-date-btn:hover {
cursor: pointer; cursor: pointer;
} }
/* Login Modal */
#login-modal .modal-content { #login-modal .modal-content {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.85)); background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.85));
text-align: center; text-align: center;
@@ -742,7 +752,6 @@ button.edit-date-btn:hover {
color: #2c3e50; color: #2c3e50;
} }
/* Loading States */
.loading { .loading {
text-align: center; text-align: center;
padding: 40px 20px; padding: 40px 20px;
@@ -760,7 +769,6 @@ button.edit-date-btn:hover {
border-left: 5px solid #e74c3c; border-left: 5px solid #e74c3c;
} }
/* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.container { .container {
padding: 10px; padding: 10px;
@@ -849,7 +857,6 @@ button.edit-date-btn:hover {
} }
} }
/* Scrollbar Styling */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
} }
@@ -868,7 +875,6 @@ button.edit-date-btn:hover {
background: linear-gradient(135deg, #2980b9, #1f618d); background: linear-gradient(135deg, #2980b9, #1f618d);
} }
/* Focus States */
button:focus, button:focus,
input:focus, input:focus,
textarea:focus, textarea:focus,
@@ -877,7 +883,6 @@ select:focus {
outline-offset: 2px; outline-offset: 2px;
} }
/* Print Styles */
@media print { @media print {
.task-actions, .task-actions,
nav, nav,
@@ -891,9 +896,7 @@ select:focus {
border: 1px solid #ddd; border: 1px solid #ddd;
} }
} }
/* Добавляем в существующие стили */
/* Фильтры */
.filters { .filters {
display: flex; display: flex;
gap: 20px; gap: 20px;
@@ -933,13 +936,11 @@ select:focus {
font-size: 0.9rem; font-size: 0.9rem;
} }
/* Новые статусы */
.status-yellow { .status-yellow {
background: linear-gradient(135deg, #ffc107, #e0a800); background: linear-gradient(135deg, #ffc107, #e0a800);
color: white; color: white;
} }
/* Закрытые задачи */
.task-card.closed { .task-card.closed {
background: linear-gradient(135deg, #e9ecef, #dee2e6); background: linear-gradient(135deg, #e9ecef, #dee2e6);
border-left-color: #6c757d; border-left-color: #6c757d;
@@ -956,7 +957,6 @@ select:focus {
font-weight: 600; font-weight: 600;
} }
/* Комментарии к доработке */
.rework-comment { .rework-comment {
margin: 10px 0; margin: 10px 0;
padding: 12px 15px; padding: 12px 15px;
@@ -979,7 +979,6 @@ select:focus {
border: 1px solid #ffc107; border: 1px solid #ffc107;
} }
/* Новые кнопки */
button.rework-btn { button.rework-btn {
background: linear-gradient(135deg, #ffc107, #e0a800); background: linear-gradient(135deg, #ffc107, #e0a800);
box-shadow: 0 4px 15px rgba(255, 193, 7, 0.3); box-shadow: 0 4px 15px rgba(255, 193, 7, 0.3);
@@ -1019,10 +1018,11 @@ button.reopen-btn:hover {
.show-deleted-label input { .show-deleted-label input {
margin: 0; margin: 0;
} }
/* В существующие стили добавляем */
.show-deleted-label[style*="display: none"] { .show-deleted-label[style*="display: none"] {
display: none !important; display: none !important;
} }
.deadline-badge { .deadline-badge {
padding: 4px 12px; padding: 4px 12px;
border-radius: 20px; border-radius: 20px;
@@ -1072,7 +1072,7 @@ button.reopen-btn:hover {
width: 100%; width: 100%;
} }
} }
/* Админ-стили */
.admin-container { .admin-container {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
@@ -1259,11 +1259,11 @@ button.reopen-btn:hover {
border-left-color: #f39c12; border-left-color: #f39c12;
} }
/* Анимация появления */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); } from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
.error { .error {
background: linear-gradient(135deg, #f8d7da, #f5c6cb); background: linear-gradient(135deg, #f8d7da, #f5c6cb);
color: #721c24; color: #721c24;
@@ -1274,6 +1274,7 @@ button.reopen-btn:hover {
border-left: 5px solid #e74c3c; border-left: 5px solid #e74c3c;
text-align: center; text-align: center;
} }
.task-number { .task-number {
background: #3498db; background: #3498db;
color: white; color: white;
@@ -1284,32 +1285,6 @@ button.reopen-btn:hover {
font-weight: bold; font-weight: bold;
} }
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0;
gap: 20px;
}
.task-header .task-title {
font-size: 1.2rem;
font-weight: 700;
color: #2c3e50;
flex: 1;
line-height: 1.4;
cursor: pointer;
padding: 15px;
border-radius: 10px;
background: linear-gradient(135deg, rgba(52, 152, 219, 0.1), rgba(52, 152, 219, 0.05));
transition: all 0.3s ease;
}
.task-header .task-title:hover {
background: linear-gradient(135deg, rgba(52, 152, 219, 0.15), rgba(52, 152, 219, 0.1));
transform: translateY(-2px);
}
.task-header .task-title .expand-icon { .task-header .task-title .expand-icon {
font-size: 0.8rem; font-size: 0.8rem;
color: #6c757d; color: #6c757d;
@@ -1322,9 +1297,49 @@ button.reopen-btn:hover {
border-top: 1px solid #e9ecef; border-top: 1px solid #e9ecef;
margin-top: 10px; margin-top: 10px;
animation: fadeIn 0.3s ease; animation: fadeIn 0.3s ease;
overflow: hidden;
transition: max-height 0.3s ease;
max-height: 0;
}
.task-content.expanded {
max-height: 2000px;
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); } from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
}
.tasks-no-date-btn {
background: linear-gradient(135deg, #17a2b8, #138496);
box-shadow: 0 4px 15px rgba(23, 162, 184, 0.3);
}
.tasks-no-date-btn.active {
background: linear-gradient(135deg, #138496, #117a8b);
box-shadow: 0 6px 20px rgba(23, 162, 184, 0.4);
}
.no-date-badge {
background: linear-gradient(135deg, #6c757d, #5a6268);
color: white;
padding: 3px 8px;
border-radius: 12px;
font-size: 0.75rem;
margin-left: 10px;
font-weight: 600;
}
@media (max-width: 768px) {
.task-dates-files {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.task-dates-files .task-dates,
.task-dates-files .file-list {
min-width: 100%;
}
} }

View File

@@ -8,7 +8,7 @@ require('dotenv').config();
const { db, logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database'); const { db, logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database');
const authService = require('./auth'); const authService = require('./auth');
const adminRouter = require('./admin-server'); const adminRouter = require('./admin-server');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@@ -649,6 +649,71 @@ app.get('/api/tasks', requireAuth, (req, res) => {
}); });
}); });
app.get('/api/tasks/no-date', requireAuth, (req, res) => {
const userId = req.session.user.id;
const query = `
SELECT DISTINCT
t.*,
u.name as creator_name,
u.login as creator_login,
ot.title as original_task_title,
ou.name as original_creator_name,
GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids,
GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names
FROM tasks t
LEFT JOIN users u ON t.created_by = u.id
LEFT JOIN tasks ot ON t.original_task_id = ot.id
LEFT JOIN users ou ON ot.created_by = ou.id
LEFT JOIN task_assignments ta ON t.id = ta.task_id
LEFT JOIN users u2 ON ta.user_id = u2.id
WHERE t.status = 'active'
AND t.closed_at IS NULL
AND (t.due_date IS NULL OR t.due_date = '')
AND (ta.due_date IS NULL OR ta.due_date = '')
`;
const params = [];
if (req.session.user.role !== 'admin') {
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
params.push(userId, userId);
}
query += " GROUP BY t.id ORDER BY t.created_at DESC";
db.all(query, params, (err, tasks) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const taskPromises = tasks.map(task => {
return new Promise((resolve) => {
db.all(`
SELECT ta.*, u.name as user_name, u.login as user_login
FROM task_assignments ta
LEFT JOIN users u ON ta.user_id = u.id
WHERE ta.task_id = ?
`, [task.id], (err, assignments) => {
if (err) {
task.assignments = [];
resolve(task);
return;
}
task.assignments = assignments || [];
resolve(task);
});
});
});
Promise.all(taskPromises).then(completedTasks => {
res.json(completedTasks);
});
});
});
app.post('/api/tasks', requireAuth, upload.array('files', 15), (req, res) => { app.post('/api/tasks', requireAuth, upload.array('files', 15), (req, res) => {
const { title, description, assignedUsers, originalTaskId, dueDate } = req.body; const { title, description, assignedUsers, originalTaskId, dueDate } = req.body;
const createdBy = req.session.user.id; const createdBy = req.session.user.id;
@@ -1299,12 +1364,14 @@ app.get('/api/activity-logs', requireAuth, (req, res) => {
res.json(logs); res.json(logs);
}); });
}); });
app.get('/admin', (req, res) => { app.get('/admin', (req, res) => {
if (!req.session.user || req.session.user.role !== 'admin') { if (!req.session.user || req.session.user.role !== 'admin') {
return res.status(403).send('Доступ запрещен'); return res.status(403).send('Доступ запрещен');
} }
res.sendFile(path.join(__dirname, 'public/admin.html')); res.sendFile(path.join(__dirname, 'public/admin.html'));
}); });
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`CRM сервер запущен на порту ${PORT}`); console.log(`CRM сервер запущен на порту ${PORT}`);
console.log(`Откройте http://localhost:${PORT} в браузере`); console.log(`Откройте http://localhost:${PORT} в браузере`);