r
This commit is contained in:
@@ -7,7 +7,6 @@
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Форма логина -->
|
||||
<div id="login-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Вход в School CRM</h2>
|
||||
@@ -46,8 +45,7 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Секция списка задач -->
|
||||
<section id="tasks-section" class="section">
|
||||
<section id="tasks-section" class="section">
|
||||
<h2>Все задачи</h2>
|
||||
<div id="tasks-controls">
|
||||
<div class="filters">
|
||||
@@ -68,8 +66,27 @@
|
||||
<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()">
|
||||
Показать удаленные задачи
|
||||
@@ -78,7 +95,6 @@
|
||||
<div id="tasks-list"></div>
|
||||
</section>
|
||||
|
||||
<!-- Секция создания задачи -->
|
||||
<section id="create-task-section" class="section">
|
||||
<h2>Создать новую задачу</h2>
|
||||
<form id="create-task-form" enctype="multipart/form-data">
|
||||
@@ -93,13 +109,8 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start-date">Дата и время начала (необязательно):</label>
|
||||
<input type="datetime-local" id="start-date" name="startDate">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="due-date">Дата и время выполнения (необязательно):</label>
|
||||
<input type="datetime-local" id="due-date" name="dueDate">
|
||||
<label for="due-date">Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -120,7 +131,6 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Секция логов -->
|
||||
<section id="logs-section" class="section">
|
||||
<h2>Лог активности</h2>
|
||||
<div id="logs-list"></div>
|
||||
@@ -128,7 +138,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для редактирования задачи -->
|
||||
<div id="edit-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeEditModal()">×</span>
|
||||
@@ -145,14 +154,9 @@
|
||||
<textarea id="edit-description" name="description" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-start-date">Дата и время начала:</label>
|
||||
<input type="datetime-local" id="edit-start-date" name="startDate">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-due-date">Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="edit-due-date" name="dueDate">
|
||||
<input type="datetime-local" id="edit-due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -174,7 +178,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для копирования задачи -->
|
||||
<div id="copy-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeCopyModal()">×</span>
|
||||
@@ -182,14 +185,9 @@
|
||||
<form id="copy-task-form">
|
||||
<input type="hidden" id="copy-task-id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="copy-start-date">Дата и время начала для копии:</label>
|
||||
<input type="datetime-local" id="copy-start-date" name="startDate">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="copy-due-date">Дата и время выполнения для копии:</label>
|
||||
<input type="datetime-local" id="copy-due-date" name="dueDate">
|
||||
<input type="datetime-local" id="copy-due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -204,7 +202,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для редактирования сроков исполнителя -->
|
||||
<div id="edit-assignment-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeEditAssignmentModal()">×</span>
|
||||
@@ -212,20 +209,15 @@
|
||||
<form id="edit-assignment-form">
|
||||
<input type="hidden" id="edit-assignment-task-id">
|
||||
<input type="hidden" id="edit-assignment-user-id">
|
||||
<div class="form-group">
|
||||
<label for="edit-assignment-start-date">Дата и время начала:</label>
|
||||
<input type="datetime-local" id="edit-assignment-start-date" name="startDate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-assignment-due-date">Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="edit-assignment-due-date" name="dueDate">
|
||||
<input type="datetime-local" id="edit-assignment-due-date" name="dueDate" required>
|
||||
</div>
|
||||
<button type="submit">Сохранить сроки</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для возврата на доработку -->
|
||||
<div id="rework-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeReworkModal()">×</span>
|
||||
|
||||
143
public/script.js
143
public/script.js
@@ -3,7 +3,6 @@ let users = [];
|
||||
let tasks = [];
|
||||
let filteredUsers = [];
|
||||
|
||||
// Инициализация при загрузке
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkAuth();
|
||||
setupEventListeners();
|
||||
@@ -33,7 +32,6 @@ function showMainInterface() {
|
||||
document.getElementById('login-modal').style.display = 'none';
|
||||
document.querySelector('.container').style.display = 'block';
|
||||
|
||||
// Формируем информацию о пользователе
|
||||
let userInfo = `Вы вошли как: ${currentUser.name}`;
|
||||
if (currentUser.auth_type === 'ldap') {
|
||||
userInfo += ` (LDAP)`;
|
||||
@@ -44,10 +42,8 @@ function showMainInterface() {
|
||||
|
||||
document.getElementById('current-user').textContent = userInfo;
|
||||
|
||||
// Показываем фильтры ВСЕМ пользователям, а не только администраторам
|
||||
document.getElementById('tasks-controls').style.display = 'block';
|
||||
|
||||
// Чекбокс удаленных задач показываем только администраторам
|
||||
const showDeletedLabel = document.querySelector('.show-deleted-label');
|
||||
if (showDeletedLabel) {
|
||||
if (currentUser.role === 'admin') {
|
||||
@@ -135,12 +131,30 @@ async function loadUsers() {
|
||||
renderUsersChecklist();
|
||||
renderEditUsersChecklist();
|
||||
renderCopyUsersChecklist();
|
||||
populateFilterDropdowns();
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки пользователей:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Фильтрация пользователей
|
||||
function populateFilterDropdowns() {
|
||||
const creatorFilter = document.getElementById('creator-filter');
|
||||
const assigneeFilter = document.getElementById('assignee-filter');
|
||||
|
||||
creatorFilter.innerHTML = '<option value="">Все заказчики</option>';
|
||||
assigneeFilter.innerHTML = '<option value="">Все исполнители</option>';
|
||||
|
||||
users.forEach(user => {
|
||||
const creatorOption = document.createElement('option');
|
||||
creatorOption.value = user.id;
|
||||
creatorOption.textContent = `${user.name} (${user.login})`;
|
||||
creatorFilter.appendChild(creatorOption.cloneNode(true));
|
||||
|
||||
const assigneeOption = creatorOption.cloneNode(true);
|
||||
assigneeFilter.appendChild(assigneeOption);
|
||||
});
|
||||
}
|
||||
|
||||
function filterUsers() {
|
||||
const search = document.getElementById('user-search').value.toLowerCase();
|
||||
filteredUsers = users.filter(user =>
|
||||
@@ -175,18 +189,23 @@ async function loadTasks() {
|
||||
try {
|
||||
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 || '';
|
||||
const assigneeFilter = document.getElementById('assignee-filter')?.value || '';
|
||||
const deadlineFilter = document.getElementById('deadline-filter')?.value || '';
|
||||
const showDeleted = document.getElementById('show-deleted')?.checked || false;
|
||||
|
||||
let url = '/api/tasks?';
|
||||
if (search) url += `search=${encodeURIComponent(search)}&`;
|
||||
if (statusFilter) url += `status=${encodeURIComponent(statusFilter)}&`;
|
||||
if (creatorFilter) url += `creator=${encodeURIComponent(creatorFilter)}&`;
|
||||
if (assigneeFilter) url += `assignee=${encodeURIComponent(assigneeFilter)}&`;
|
||||
if (deadlineFilter) url += `deadline=${encodeURIComponent(deadlineFilter)}&`;
|
||||
if (showDeleted) url += `showDeleted=true&`;
|
||||
|
||||
const response = await fetch(url);
|
||||
tasks = await response.json();
|
||||
renderTasks();
|
||||
|
||||
// Загружаем файлы для каждой задачи
|
||||
tasks.forEach(task => {
|
||||
loadTaskFiles(task.id);
|
||||
});
|
||||
@@ -273,6 +292,8 @@ function renderTasks() {
|
||||
const canEdit = canUserEditTask(task);
|
||||
const isCopy = task.original_task_id !== null;
|
||||
|
||||
const timeLeftInfo = getTimeLeftInfo(task);
|
||||
|
||||
return `
|
||||
<div class="task-card ${isDeleted ? 'deleted' : ''} ${isClosed ? 'closed' : ''}">
|
||||
<div class="task-actions">
|
||||
@@ -297,6 +318,7 @@ function renderTasks() {
|
||||
${isDeleted ? '<span class="deleted-badge">Удалена</span>' : ''}
|
||||
${isClosed ? '<span class="closed-badge">Закрыта</span>' : ''}
|
||||
${isCopy ? '<span class="copy-badge">Копия</span>' : ''}
|
||||
${timeLeftInfo ? `<span class="deadline-badge ${timeLeftInfo.class}">${timeLeftInfo.text}</span>` : ''}
|
||||
<span class="role-badge ${getRoleBadgeClass(userRole)}">${userRole}</span>
|
||||
</div>
|
||||
<div class="task-status ${statusClass}">${getStatusText(overallStatus)}</div>
|
||||
@@ -318,7 +340,7 @@ function renderTasks() {
|
||||
|
||||
${task.start_date || task.due_date ? `
|
||||
<div class="task-dates">
|
||||
${task.start_date ? `<div><strong>Начать:</strong> ${formatDateTime(task.start_date)}</div>` : ''}
|
||||
<div><strong>Создана:</strong> ${formatDateTime(task.start_date || task.created_at)}</div>
|
||||
${task.due_date ? `<div><strong>Выполнить до:</strong> ${formatDateTime(task.due_date)}</div>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -346,21 +368,49 @@ function renderTasks() {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getTimeLeftInfo(task) {
|
||||
if (!task.due_date || task.closed_at) return null;
|
||||
|
||||
const dueDate = new Date(task.due_date);
|
||||
const now = new Date();
|
||||
const timeLeft = dueDate.getTime() - now.getTime();
|
||||
const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000));
|
||||
|
||||
if (hoursLeft <= 0) return null;
|
||||
|
||||
if (hoursLeft <= 24) {
|
||||
return {
|
||||
text: `Менее 24ч`,
|
||||
class: 'deadline-24h'
|
||||
};
|
||||
} else if (hoursLeft <= 48) {
|
||||
return {
|
||||
text: `Менее 48ч`,
|
||||
class: 'deadline-48h'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderAssignment(assignment, taskId, canEdit) {
|
||||
const statusClass = getStatusClass(assignment.status);
|
||||
const isCurrentUser = assignment.user_id === currentUser.id;
|
||||
const isOverdue = assignment.status === 'overdue';
|
||||
const isRework = assignment.status === 'rework';
|
||||
|
||||
const timeLeftInfo = getAssignmentTimeLeftInfo(assignment);
|
||||
|
||||
return `
|
||||
<div class="assignment ${isOverdue ? 'overdue' : ''} ${isRework ? 'rework' : ''}">
|
||||
<span class="assignment-status ${statusClass}"></span>
|
||||
<div style="flex: 1;">
|
||||
<strong>${assignment.user_name}</strong>
|
||||
${isCurrentUser ? '<small>(Вы)</small>' : ''}
|
||||
${timeLeftInfo ? `<span class="deadline-indicator ${timeLeftInfo.class}">${timeLeftInfo.text}</span>` : ''}
|
||||
${assignment.start_date || assignment.due_date ? `
|
||||
<div class="assignment-dates">
|
||||
${assignment.start_date ? `<small>Начать: ${formatDateTime(assignment.start_date)}</small>` : ''}
|
||||
${assignment.start_date ? `<small>Начало: ${formatDateTime(assignment.start_date)}</small>` : ''}
|
||||
${assignment.due_date ? `<small>Выполнить до: ${formatDateTime(assignment.due_date)}</small>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -382,6 +432,31 @@ function renderAssignment(assignment, taskId, canEdit) {
|
||||
`;
|
||||
}
|
||||
|
||||
function getAssignmentTimeLeftInfo(assignment) {
|
||||
if (!assignment.due_date || assignment.status === 'completed') return null;
|
||||
|
||||
const dueDate = new Date(assignment.due_date);
|
||||
const now = new Date();
|
||||
const timeLeft = dueDate.getTime() - now.getTime();
|
||||
const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000));
|
||||
|
||||
if (hoursLeft <= 0) return null;
|
||||
|
||||
if (hoursLeft <= 24) {
|
||||
return {
|
||||
text: `Осталось ${hoursLeft}ч`,
|
||||
class: 'deadline-24h'
|
||||
};
|
||||
} else if (hoursLeft <= 48) {
|
||||
return {
|
||||
text: `Осталось ${hoursLeft}ч`,
|
||||
class: 'deadline-48h'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function createTask(event) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -394,12 +469,18 @@ async function createTask(event) {
|
||||
formData.append('title', document.getElementById('title').value);
|
||||
formData.append('description', document.getElementById('description').value);
|
||||
|
||||
const startDate = document.getElementById('start-date').value;
|
||||
const dueDate = document.getElementById('due-date').value;
|
||||
if (startDate) formData.append('startDate', startDate);
|
||||
if (dueDate) formData.append('dueDate', dueDate);
|
||||
if (!dueDate) {
|
||||
alert('Дата и время выполнения обязательны');
|
||||
return;
|
||||
}
|
||||
formData.append('dueDate', dueDate);
|
||||
|
||||
const assignedUsers = document.querySelectorAll('#users-checklist input[name="assignedUsers"]:checked');
|
||||
if (assignedUsers.length === 0) {
|
||||
alert('Выберите хотя бы одного исполнителя');
|
||||
return;
|
||||
}
|
||||
assignedUsers.forEach(checkbox => {
|
||||
formData.append('assignedUsers', checkbox.value);
|
||||
});
|
||||
@@ -446,7 +527,6 @@ async function openEditModal(taskId) {
|
||||
|
||||
const task = await response.json();
|
||||
|
||||
// Дополнительная проверка прав на клиенте
|
||||
if (!canUserEditTask(task)) {
|
||||
alert('У вас нет прав для редактирования этой задачи');
|
||||
return;
|
||||
@@ -456,11 +536,8 @@ async function openEditModal(taskId) {
|
||||
document.getElementById('edit-title').value = task.title;
|
||||
document.getElementById('edit-description').value = task.description || '';
|
||||
|
||||
// Заполняем даты
|
||||
document.getElementById('edit-start-date').value = task.start_date ? formatDateTimeForInput(task.start_date) : '';
|
||||
document.getElementById('edit-due-date').value = task.due_date ? formatDateTimeForInput(task.due_date) : '';
|
||||
|
||||
// Отмечаем текущих исполнителей
|
||||
const checkboxes = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = task.assignments?.some(assignment =>
|
||||
@@ -488,9 +565,13 @@ async function updateTask(event) {
|
||||
const taskId = document.getElementById('edit-task-id').value;
|
||||
const title = document.getElementById('edit-title').value;
|
||||
const description = document.getElementById('edit-description').value;
|
||||
const startDate = document.getElementById('edit-start-date').value;
|
||||
const dueDate = document.getElementById('edit-due-date').value;
|
||||
|
||||
if (!dueDate) {
|
||||
alert('Дата и время выполнения обязательны');
|
||||
return;
|
||||
}
|
||||
|
||||
const assignedUsers = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]:checked');
|
||||
const assignedUserIds = Array.from(assignedUsers).map(cb => parseInt(cb.value));
|
||||
|
||||
@@ -498,8 +579,7 @@ async function updateTask(event) {
|
||||
formData.append('title', title);
|
||||
formData.append('description', description);
|
||||
formData.append('assignedUsers', JSON.stringify(assignedUserIds));
|
||||
if (startDate) formData.append('startDate', startDate);
|
||||
if (dueDate) formData.append('dueDate', dueDate);
|
||||
formData.append('dueDate', dueDate);
|
||||
|
||||
const files = document.getElementById('edit-files').files;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
@@ -542,8 +622,13 @@ async function copyTask(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const taskId = document.getElementById('copy-task-id').value;
|
||||
const startDate = document.getElementById('copy-start-date').value;
|
||||
const dueDate = document.getElementById('copy-due-date').value;
|
||||
|
||||
if (!dueDate) {
|
||||
alert('Дата и время выполнения обязательны для копии задачи');
|
||||
return;
|
||||
}
|
||||
|
||||
const checkboxes = document.querySelectorAll('#copy-users-checklist input[name="assignedUsers"]:checked');
|
||||
const assignedUserIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
||||
|
||||
@@ -560,8 +645,7 @@ async function copyTask(event) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
assignedUsers: assignedUserIds,
|
||||
startDate: startDate || null,
|
||||
dueDate: dueDate || null
|
||||
dueDate: dueDate
|
||||
})
|
||||
});
|
||||
|
||||
@@ -589,7 +673,6 @@ function openEditAssignmentModal(taskId, userId) {
|
||||
|
||||
document.getElementById('edit-assignment-task-id').value = taskId;
|
||||
document.getElementById('edit-assignment-user-id').value = userId;
|
||||
document.getElementById('edit-assignment-start-date').value = assignment.start_date ? formatDateTimeForInput(assignment.start_date) : '';
|
||||
document.getElementById('edit-assignment-due-date').value = assignment.due_date ? formatDateTimeForInput(assignment.due_date) : '';
|
||||
|
||||
document.getElementById('edit-assignment-modal').style.display = 'block';
|
||||
@@ -604,9 +687,13 @@ async function updateAssignment(event) {
|
||||
|
||||
const taskId = document.getElementById('edit-assignment-task-id').value;
|
||||
const userId = document.getElementById('edit-assignment-user-id').value;
|
||||
const startDate = document.getElementById('edit-assignment-start-date').value;
|
||||
const dueDate = document.getElementById('edit-assignment-due-date').value;
|
||||
|
||||
if (!dueDate) {
|
||||
alert('Дата и время выполнения обязательны');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/assignment/${userId}`, {
|
||||
method: 'PUT',
|
||||
@@ -614,8 +701,7 @@ async function updateAssignment(event) {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
startDate: startDate || null,
|
||||
dueDate: dueDate || null
|
||||
dueDate: dueDate
|
||||
})
|
||||
});
|
||||
|
||||
@@ -787,7 +873,7 @@ async function updateStatus(taskId, userId, status) {
|
||||
|
||||
function getTaskOverallStatus(task) {
|
||||
if (task.status === 'deleted') return 'deleted';
|
||||
if (task.closed_at) return 'closed'; // Закрытые задачи всегда имеют статус 'closed'
|
||||
if (task.closed_at) return 'closed';
|
||||
if (!task.assignments || task.assignments.length === 0) return 'unassigned';
|
||||
|
||||
const assignments = task.assignments;
|
||||
@@ -857,7 +943,6 @@ function getUserRoleInTask(task) {
|
||||
if (currentUser.role === 'admin') return 'Администратор';
|
||||
if (parseInt(task.created_by) === currentUser.id) return 'Заказчик';
|
||||
|
||||
// Проверяем является ли пользователь исполнителем
|
||||
if (task.assignments) {
|
||||
const isExecutor = task.assignments.some(assignment =>
|
||||
parseInt(assignment.user_id) === currentUser.id
|
||||
@@ -880,8 +965,8 @@ function getRoleBadgeClass(role) {
|
||||
function canUserEditTask(task) {
|
||||
if (!currentUser) return false;
|
||||
|
||||
if (currentUser.role === 'admin') return true; // Администратор
|
||||
if (parseInt(task.created_by) === currentUser.id) return true; // Заказчик
|
||||
if (currentUser.role === 'admin') return true;
|
||||
if (parseInt(task.created_by) === currentUser.id) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user