From d25ac311f3660b65b69c83d2270d4ec338cdd4ed Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Sun, 14 Dec 2025 17:48:56 +0500 Subject: [PATCH] r --- public/index.html | 101 +++++++++++++++---------------- public/script.js | 63 ++++++++++++++++---- public/style.css | 147 +++++++++++++++++++++++++--------------------- server.js | 69 +++++++++++++++++++++- 4 files changed, 250 insertions(+), 130 deletions(-) diff --git a/public/index.html b/public/index.html index 238500d..5ee17e7 100644 --- a/public/index.html +++ b/public/index.html @@ -40,62 +40,63 @@
-
-

Все задачи

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
-
+
+

Все задачи

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+

Создать новую задачу

diff --git a/public/script.js b/public/script.js index 599c7d6..3ea96c5 100644 --- a/public/script.js +++ b/public/script.js @@ -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 ` -
+
@@ -315,7 +351,7 @@ function renderTasks() {
-
+
${!isDeleted && !isClosed ? ` ${canEdit ? `` : ''} @@ -346,12 +382,16 @@ function renderTasks() {
` : ''} - ${task.start_date || task.due_date ? ` +
-
Создана: ${formatDateTime(task.start_date || task.created_at)}
- ${task.due_date ? `
Выполнить до: ${formatDateTime(task.due_date)}
` : ''} + Создана: ${formatDateTime(task.start_date || task.created_at)} + ${task.due_date ? ` | Выполнить до: ${formatDateTime(task.due_date)}` : ''} + ${showingTasksWithoutDate ? 'Без срока' : ''}
- ` : ''} +
+ Файлы: скрыто +
+
Исполнители: @@ -361,11 +401,6 @@ function renderTasks() { }
-
- Файлы: -
Загрузка...
-
-
Создана: ${formatDateTime(task.created_at)} | Автор: ${task.creator_name} ${task.deleted_at ? `
Удалена: ${formatDateTime(task.deleted_at)}` : ''} @@ -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 = 'Файлы: Нет файлов'; + container.innerHTML = 'Файлы: скрыто'; } else { container.innerHTML = ` Файлы: diff --git a/public/style.css b/public/style.css index 9dc8dfd..2806093 100644 --- a/public/style.css +++ b/public/style.css @@ -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%; + } } \ No newline at end of file diff --git a/server.js b/server.js index f16b6e3..501f210 100644 --- a/server.js +++ b/server.js @@ -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} в браузере`);