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>
<button onclick="showSection('tasks')">Задачи</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('/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>
</nav>
</header>
<main>
<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 class="filter-group">
<label for="creator-filter">Заказчик:</label>
<select id="creator-filter" onchange="loadTasks()">
<option value="">Все заказчики</option>
</select>
</div>
<div class="filter-group">
<label for="assignee-filter">Исполнитель:</label>
<select id="assignee-filter" onchange="loadTasks()">
<option value="">Все исполнители</option>
</select>
</div>
<div class="filter-group">
<label for="deadline-filter">Срок выполнения:</label>
<select id="deadline-filter" onchange="loadTasks()">
<option value="">Все сроки</option>
<option value="48h">Менее 48 часов</option>
<option value="24h">Менее 24 часов</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="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 class="filter-group">
<label for="creator-filter">Заказчик:</label>
<select id="creator-filter" onchange="loadTasks()">
<option value="">Все заказчики</option>
</select>
</div>
<div class="filter-group">
<label for="assignee-filter">Исполнитель:</label>
<select id="assignee-filter" onchange="loadTasks()">
<option value="">Все исполнители</option>
</select>
</div>
<div class="filter-group">
<label for="deadline-filter">Срок выполнения:</label>
<select id="deadline-filter" onchange="loadTasks()">
<option value="">Все сроки</option>
<option value="48h">Менее 48 часов</option>
<option value="24h">Менее 24 часов</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">
<h2>Создать новую задачу</h2>

View File

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

View File

@@ -13,13 +13,12 @@ body {
}
.container {
max-width: 99%;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
display: none;
}
/* Header Styles */
header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
@@ -72,7 +71,6 @@ header h1 {
transform: translateY(-2px);
}
/* Navigation */
nav {
display: flex;
gap: 12px;
@@ -99,7 +97,6 @@ nav button:hover {
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
}
/* Section Styles */
.section {
display: none;
background: rgba(255, 255, 255, 0.95);
@@ -121,7 +118,6 @@ nav button:hover {
to { opacity: 1; transform: translateY(0); }
}
/* Form Styles */
.form-group {
margin-bottom: 20px;
}
@@ -165,7 +161,6 @@ textarea {
font-family: inherit;
}
/* Button Styles */
button {
background: linear-gradient(135deg, #27ae60, #219a52);
color: white;
@@ -231,7 +226,6 @@ 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;
@@ -256,11 +250,10 @@ button.edit-date-btn:hover {
cursor: pointer;
}
/* Task Cards */
.task-card {
border: none;
border-radius: 15px;
padding: 20px;
padding: 0;
margin-bottom: 20px;
background: rgba(255, 255, 255, 0.9);
position: relative;
@@ -284,16 +277,26 @@ button.edit-date-btn:hover {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
margin-bottom: 0;
gap: 20px;
}
.task-title {
font-size: 1.4rem;
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-title:hover {
background: linear-gradient(135deg, rgba(52, 152, 219, 0.15), rgba(52, 152, 219, 0.1));
transform: translateY(-2px);
}
.task-status {
@@ -305,7 +308,6 @@ button.edit-date-btn:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Status Colors */
.status-purple {
background: linear-gradient(135deg, #9b59b6, #8e44ad);
color: white;
@@ -331,7 +333,6 @@ button.edit-date-btn:hover {
color: white;
}
/* Badges */
.deleted-badge {
background: #e74c3c;
color: white;
@@ -374,7 +375,6 @@ button.edit-date-btn:hover {
color: white;
}
/* Task Content */
.task-original {
margin: 10px 0;
padding: 10px 15px;
@@ -398,25 +398,42 @@ button.edit-date-btn:hover {
color: #495057;
}
.task-dates {
.task-dates-files {
margin: 15px 0;
padding: 15px;
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
border-radius: 10px;
border-left: 4px solid #f39c12;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.task-dates div {
margin: 8px 0;
.task-dates-files .task-dates {
flex: 1;
min-width: 250px;
font-size: 0.95rem;
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;
}
/* Assignments */
.task-assignments {
margin: 20px 0;
}
@@ -496,7 +513,6 @@ button.edit-date-btn:hover {
border-radius: 8px;
}
/* Task Actions - ПРАВЫЙ НИЖНИЙ УГОЛ */
.task-actions {
position: absolute;
bottom: 20px;
@@ -512,7 +528,6 @@ button.edit-date-btn:hover {
min-width: auto;
}
/* File List */
.file-list {
margin-top: 15px;
padding: 15px;
@@ -555,7 +570,6 @@ button.edit-date-btn:hover {
font-size: 0.85rem;
}
/* Task Meta */
.task-meta {
margin-top: 15px;
padding-top: 15px;
@@ -567,7 +581,6 @@ button.edit-date-btn:hover {
font-size: 0.9rem;
}
/* Logs */
.log-entry {
padding: 15px;
border-bottom: 1px solid #e9ecef;
@@ -586,7 +599,6 @@ button.edit-date-btn:hover {
margin-bottom: 5px;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
@@ -638,7 +650,6 @@ button.edit-date-btn:hover {
transform: rotate(90deg);
}
/* Checkbox Groups */
.checkbox-group {
max-height: 250px;
overflow-y: auto;
@@ -676,7 +687,6 @@ button.edit-date-btn:hover {
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;
@@ -742,7 +752,6 @@ button.edit-date-btn:hover {
color: #2c3e50;
}
/* Loading States */
.loading {
text-align: center;
padding: 40px 20px;
@@ -760,7 +769,6 @@ button.edit-date-btn:hover {
border-left: 5px solid #e74c3c;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 10px;
@@ -849,7 +857,6 @@ button.edit-date-btn:hover {
}
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
}
@@ -868,7 +875,6 @@ button.edit-date-btn:hover {
background: linear-gradient(135deg, #2980b9, #1f618d);
}
/* Focus States */
button:focus,
input:focus,
textarea:focus,
@@ -877,7 +883,6 @@ select:focus {
outline-offset: 2px;
}
/* Print Styles */
@media print {
.task-actions,
nav,
@@ -891,9 +896,7 @@ select:focus {
border: 1px solid #ddd;
}
}
/* Добавляем в существующие стили */
/* Фильтры */
.filters {
display: flex;
gap: 20px;
@@ -933,13 +936,11 @@ select:focus {
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;
@@ -956,7 +957,6 @@ select:focus {
font-weight: 600;
}
/* Комментарии к доработке */
.rework-comment {
margin: 10px 0;
padding: 12px 15px;
@@ -979,7 +979,6 @@ select:focus {
border: 1px solid #ffc107;
}
/* Новые кнопки */
button.rework-btn {
background: linear-gradient(135deg, #ffc107, #e0a800);
box-shadow: 0 4px 15px rgba(255, 193, 7, 0.3);
@@ -1019,10 +1018,11 @@ button.reopen-btn:hover {
.show-deleted-label input {
margin: 0;
}
/* В существующие стили добавляем */
.show-deleted-label[style*="display: none"] {
display: none !important;
}
.deadline-badge {
padding: 4px 12px;
border-radius: 20px;
@@ -1072,7 +1072,7 @@ button.reopen-btn:hover {
width: 100%;
}
}
/* Админ-стили */
.admin-container {
max-width: 1400px;
margin: 0 auto;
@@ -1259,11 +1259,11 @@ button.reopen-btn:hover {
border-left-color: #f39c12;
}
/* Анимация появления */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.error {
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
color: #721c24;
@@ -1274,6 +1274,7 @@ button.reopen-btn:hover {
border-left: 5px solid #e74c3c;
text-align: center;
}
.task-number {
background: #3498db;
color: white;
@@ -1284,32 +1285,6 @@ button.reopen-btn:hover {
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 {
font-size: 0.8rem;
color: #6c757d;
@@ -1322,9 +1297,49 @@ button.reopen-btn:hover {
border-top: 1px solid #e9ecef;
margin-top: 10px;
animation: fadeIn 0.3s ease;
overflow: hidden;
transition: max-height 0.3s ease;
max-height: 0;
}
.task-content.expanded {
max-height: 2000px;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
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 authService = require('./auth');
const adminRouter = require('./admin-server');
const adminRouter = require('./admin-server');
const app = express();
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) => {
const { title, description, assignedUsers, originalTaskId, dueDate } = req.body;
const createdBy = req.session.user.id;
@@ -1299,12 +1364,14 @@ app.get('/api/activity-logs', requireAuth, (req, res) => {
res.json(logs);
});
});
app.get('/admin', (req, res) => {
if (!req.session.user || req.session.user.role !== 'admin') {
return res.status(403).send('Доступ запрещен');
}
res.sendFile(path.join(__dirname, 'public/admin.html'));
});
app.listen(PORT, () => {
console.log(`CRM сервер запущен на порту ${PORT}`);
console.log(`Откройте http://localhost:${PORT} в браузере`);