r
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
147
public/style.css
147
public/style.css
@@ -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%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
69
server.js
69
server.js
@@ -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} в браузере`);
|
||||||
|
|||||||
Reference in New Issue
Block a user