/
mainframe
/
Intellectuality_task_manager
Обзор
Документация
Войти
/
mainframe
/
Intellectuality_task_manager
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
frontend/app.js
637 строк
18 KB
Ivan
initial commit
14 апр 2026, 17:18
14 апр 2026, 17:18
d3c4609
Код
Авторство
О чём код?
const API_BASE = "/api/v1"; const priorityLabels = { low: "Низкий", medium: "Средний", high: "Высокий", }; const statusLabels = { pending: "Ожидает", in_progress: "В работе", done: "Готово", }; const dom = { statusBar: document.getElementById("status-bar"), taskForm: document.getElementById("task-form"), taskId: document.getElementById("task-id"), title: document.getElementById("title"), description: document.getElementById("description"), priority: document.getElementById("priority"), status: document.getElementById("status"), dueDate: document.getElementById("due-date"), category: document.getElementById("category"), saveTaskBtn: document.getElementById("save-task-btn"), clearFormBtn: document.getElementById("clear-form-btn"), suggestCategoryBtn: document.getElementById("suggest-category-btn"), suggestPriorityBtn: document.getElementById("suggest-priority-btn"), decomposeBtn: document.getElementById("decompose-btn"), categorySuggestion: document.getElementById("category-suggestion"), prioritySuggestion: document.getElementById("priority-suggestion"), subtasksSuggestion: document.getElementById("subtasks-suggestion"), createSubtasksBtn: document.getElementById("create-subtasks-btn"), filtersForm: document.getElementById("filters-form"), search: document.getElementById("search"), filterStatus: document.getElementById("filter-status"), filterPriority: document.getElementById("filter-priority"), filterDueBefore: document.getElementById("filter-due-before"), resetFiltersBtn: document.getElementById("reset-filters-btn"), loadSummaryBtn: document.getElementById("load-summary-btn"), workloadSummary: document.getElementById("workload-summary"), taskList: document.getElementById("task-list"), tasksCount: document.getElementById("tasks-count"), }; let allTasks = []; let currentSubtaskSuggestions = []; async function api(path, options = {}) { const config = { headers: { "Content-Type": "application/json", ...(options.headers || {}), }, ...options, }; const response = await fetch(path, config); if (response.status === 204) { return null; } const data = await response.json(); if (!response.ok) { const message = data?.error?.message || "Ошибка запроса"; throw new Error(message); } return data; } function showStatus(message, isError = false) { dom.statusBar.textContent = message; dom.statusBar.classList.remove("hidden", "success", "error"); dom.statusBar.classList.add(isError ? "error" : "success"); } function hideStatus() { dom.statusBar.classList.add("hidden"); } function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function formatDate(value) { if (!value) return "—"; const [year, month, day] = value.split("-"); return `${day}.${month}.${year}`; } function formatDateTime(value) { if (!value) return "—"; return new Date(value).toLocaleString("ru-RU"); } function setButtonLoading(button, isLoading, loadingText, defaultText) { button.disabled = isLoading; button.textContent = isLoading ? loadingText : defaultText; } function resetSuggestionBlocks() { currentSubtaskSuggestions = []; dom.categorySuggestion.classList.add("hidden"); dom.prioritySuggestion.classList.add("hidden"); dom.subtasksSuggestion.classList.add("hidden"); dom.createSubtasksBtn.classList.add("hidden"); dom.categorySuggestion.innerHTML = ""; dom.prioritySuggestion.innerHTML = ""; dom.subtasksSuggestion.innerHTML = ""; } function resetTaskForm() { dom.taskForm.reset(); dom.taskId.value = ""; dom.priority.value = "medium"; dom.status.value = "pending"; dom.saveTaskBtn.textContent = "Сохранить задачу"; resetSuggestionBlocks(); } function getTaskPayloadFromForm() { return { title: dom.title.value.trim(), description: dom.description.value.trim() || null, priority: dom.priority.value, status: dom.status.value, due_date: dom.dueDate.value || null, category: dom.category.value.trim() || null, parent_task_id: null, }; } function fillForm(task) { dom.taskId.value = task.id; dom.title.value = task.title || ""; dom.description.value = task.description || ""; dom.priority.value = task.priority || "medium"; dom.status.value = task.status || "pending"; dom.dueDate.value = task.due_date || ""; dom.category.value = task.category || ""; dom.saveTaskBtn.textContent = "Обновить задачу"; window.scrollTo({ top: 0, behavior: "smooth" }); } function renderCategorySuggestion(data) { dom.categorySuggestion.classList.remove("hidden"); dom.categorySuggestion.innerHTML = ` <strong>Предложенная категория:</strong> ${escapeHtml(data.category)} <div class="small-note"> ${escapeHtml(data.reason || "Причина не указана")} </div> <div class="small-note"> Confidence: ${data.confidence ?? "—"} </div> <div class="task-actions" style="margin-top: 12px;"> <button type="button" id="apply-category-btn">Применить</button> <button type="button" id="dismiss-category-btn" class="ghost-btn">Отклонить</button> </div> `; document .getElementById("apply-category-btn") .addEventListener("click", () => { dom.category.value = data.category; showStatus("Категория применена."); }); document .getElementById("dismiss-category-btn") .addEventListener("click", () => { dom.categorySuggestion.classList.add("hidden"); dom.categorySuggestion.innerHTML = ""; }); } function renderPrioritySuggestion(data) { dom.prioritySuggestion.classList.remove("hidden"); dom.prioritySuggestion.innerHTML = ` <strong>Предложенный приоритет:</strong> ${escapeHtml(priorityLabels[data.priority] || data.priority)} <div class="small-note"> ${escapeHtml(data.reason || "Причина не указана")} </div> <div class="task-actions" style="margin-top: 12px;"> <button type="button" id="apply-priority-btn">Применить</button> <button type="button" id="dismiss-priority-btn" class="ghost-btn">Отклонить</button> </div> `; document .getElementById("apply-priority-btn") .addEventListener("click", () => { dom.priority.value = data.priority; showStatus("Приоритет применён."); }); document .getElementById("dismiss-priority-btn") .addEventListener("click", () => { dom.prioritySuggestion.classList.add("hidden"); dom.prioritySuggestion.innerHTML = ""; }); } function renderSubtasksSuggestion(subtasks) { currentSubtaskSuggestions = subtasks; const listHtml = subtasks .map( (item, index) => ` <li> <strong>${index + 1}. ${escapeHtml(item.title)}</strong> <div class="small-note">${escapeHtml(item.description || "")}</div> </li> ` ) .join(""); dom.subtasksSuggestion.classList.remove("hidden"); dom.subtasksSuggestion.innerHTML = ` <strong>Предложенные подзадачи:</strong> <ol>${listHtml}</ol> <div class="small-note"> Чтобы создать подзадачи в БД, сначала задача должна быть сохранена. </div> `; dom.createSubtasksBtn.classList.remove("hidden"); } function renderTasks(tasks) { dom.tasksCount.textContent = String(tasks.length); if (!tasks.length) { dom.taskList.innerHTML = `<div class="summary-box">Задачи не найдены.</div>`; return; } const html = tasks .map((task) => { const description = escapeHtml(task.description || ""); const category = task.category ? `<span class="pill category">Категория: ${escapeHtml(task.category)}</span>` : ""; const subtaskBadge = task.parent_task_id ? `<span class="pill subtask">Подзадача</span>` : ""; return ` <div class="task-card ${task.parent_task_id ? "subtask" : ""}"> <div class="task-card-header"> <div> <h3>${escapeHtml(task.title)}</h3> <div class="badges-row"> <span class="pill priority-${task.priority}"> Приоритет: ${escapeHtml(priorityLabels[task.priority] || task.priority)} </span> <span class="pill status-${task.status}"> Статус: ${escapeHtml(statusLabels[task.status] || task.status)} </span> ${category} ${subtaskBadge} </div> </div> </div> <div class="task-description">${description || "Описание отсутствует."}</div> <div class="task-meta"> <div><strong>Срок:</strong> ${formatDate(task.due_date)}</div> <div><strong>Создано:</strong> ${formatDateTime(task.created_at)}</div> <div><strong>ID:</strong> ${escapeHtml(task.id)}</div> </div> <div class="task-actions"> <button type="button" data-action="edit" data-id="${task.id}"> Редактировать </button> <button type="button" data-action="delete" data-id="${task.id}" class="delete-btn" > Удалить </button> </div> </div> `; }) .join(""); dom.taskList.innerHTML = html; } async function loadTasks() { hideStatus(); try { const params = new URLSearchParams(); if (dom.search.value.trim()) { params.set("search", dom.search.value.trim()); } if (dom.filterStatus.value) { params.set("status", dom.filterStatus.value); } if (dom.filterPriority.value) { params.set("priority", dom.filterPriority.value); } if (dom.filterDueBefore.value) { params.set("due_before", dom.filterDueBefore.value); } const queryString = params.toString(); const url = `${API_BASE}/tasks${queryString ? `?${queryString}` : ""}`; const response = await api(url); allTasks = response.items; renderTasks(allTasks); } catch (error) { showStatus(error.message, true); } } async function saveTask(event) { event.preventDefault(); hideStatus(); try { const payload = getTaskPayloadFromForm(); const taskId = dom.taskId.value; const result = taskId ? await api(`${API_BASE}/tasks/${taskId}`, { method: "PUT", body: JSON.stringify(payload), }) : await api(`${API_BASE}/tasks`, { method: "POST", body: JSON.stringify(payload), }); fillForm(result); await loadTasks(); showStatus(taskId ? "Задача обновлена." : "Задача создана."); } catch (error) { showStatus(error.message, true); } } async function requestCategorySuggestion() { hideStatus(); if (!dom.title.value.trim()) { showStatus("Сначала заполните название задачи.", true); return; } setButtonLoading( dom.suggestCategoryBtn, true, "Генерация...", "LLM: предложить категорию" ); try { const result = await api(`${API_BASE}/ai/suggest-category`, { method: "POST", body: JSON.stringify({ title: dom.title.value.trim(), description: dom.description.value.trim() || null, }), }); renderCategorySuggestion(result); showStatus("LLM предложил категорию."); } catch (error) { showStatus(error.message, true); } finally { setButtonLoading( dom.suggestCategoryBtn, false, "Генерация...", "LLM: предложить категорию" ); } } async function requestPrioritySuggestion() { hideStatus(); if (!dom.title.value.trim()) { showStatus("Сначала заполните название задачи.", true); return; } setButtonLoading( dom.suggestPriorityBtn, true, "Генерация...", "LLM: предложить приоритет" ); try { const result = await api(`${API_BASE}/ai/suggest-priority`, { method: "POST", body: JSON.stringify({ title: dom.title.value.trim(), description: dom.description.value.trim() || null, due_date: dom.dueDate.value || null, }), }); renderPrioritySuggestion(result); showStatus("LLM предложил приоритет."); } catch (error) { showStatus(error.message, true); } finally { setButtonLoading( dom.suggestPriorityBtn, false, "Генерация...", "LLM: предложить приоритет" ); } } async function requestTaskDecomposition() { hideStatus(); if (!dom.title.value.trim()) { showStatus("Сначала заполните название задачи.", true); return; } setButtonLoading( dom.decomposeBtn, true, "Генерация...", "LLM: разбить на подзадачи" ); try { const result = await api(`${API_BASE}/ai/decompose`, { method: "POST", body: JSON.stringify({ title: dom.title.value.trim(), description: dom.description.value.trim() || null, due_date: dom.dueDate.value || null, }), }); renderSubtasksSuggestion(result.subtasks || []); showStatus("LLM предложил подзадачи."); } catch (error) { showStatus(error.message, true); } finally { setButtonLoading( dom.decomposeBtn, false, "Генерация...", "LLM: разбить на подзадачи" ); } } async function createSubtasksFromSuggestions() { hideStatus(); if (!dom.taskId.value) { showStatus( "Сначала сохраните родительскую задачу, затем создавайте подзадачи.", true ); return; } if (!currentSubtaskSuggestions.length) { showStatus("Нет предложенных подзадач для создания.", true); return; } setButtonLoading( dom.createSubtasksBtn, true, "Создание...", "Создать предложенные подзадачи" ); try { const result = await api( `${API_BASE}/tasks/${dom.taskId.value}/subtasks/from-suggestions`, { method: "POST", body: JSON.stringify({ subtasks: currentSubtaskSuggestions, }), } ); await loadTasks(); showStatus(`Создано подзадач: ${result.length}.`); } catch (error) { showStatus(error.message, true); } finally { setButtonLoading( dom.createSubtasksBtn, false, "Создание...", "Создать предложенные подзадачи" ); } } async function loadWorkloadSummary() { hideStatus(); setButtonLoading( dom.loadSummaryBtn, true, "Генерация сводки...", "Сформировать LLM-сводку нагрузки" ); try { const result = await api(`${API_BASE}/ai/workload-summary`); const stats = result.stats || {}; dom.workloadSummary.innerHTML = ` <div><strong>Сводка:</strong></div> <div style="margin-top: 8px;">${escapeHtml(result.summary || "")}</div> <div class="divider"></div> <div><strong>Статистика:</strong></div> <ul> <li>Всего задач: ${stats.total ?? 0}</li> <li>Ожидают: ${stats.pending ?? 0}</li> <li>В работе: ${stats.in_progress ?? 0}</li> <li>Готово: ${stats.done ?? 0}</li> <li>Просрочено: ${stats.overdue ?? 0}</li> <li>Ближайшие дедлайны: ${stats.due_soon ?? 0}</li> <li>Высокий приоритет: ${stats.high_priority ?? 0}</li> </ul> `; showStatus("LLM-сводка успешно сформирована."); } catch (error) { showStatus(error.message, true); } finally { setButtonLoading( dom.loadSummaryBtn, false, "Генерация сводки...", "Сформировать LLM-сводку нагрузки" ); } } function resetFilters() { dom.filtersForm.reset(); loadTasks(); } dom.taskForm.addEventListener("submit", saveTask); dom.clearFormBtn.addEventListener("click", resetTaskForm); dom.suggestCategoryBtn.addEventListener("click", requestCategorySuggestion); dom.suggestPriorityBtn.addEventListener("click", requestPrioritySuggestion); dom.decomposeBtn.addEventListener("click", requestTaskDecomposition); dom.createSubtasksBtn.addEventListener("click", createSubtasksFromSuggestions); dom.filtersForm.addEventListener("submit", (event) => { event.preventDefault(); loadTasks(); }); dom.resetFiltersBtn.addEventListener("click", resetFilters); dom.loadSummaryBtn.addEventListener("click", loadWorkloadSummary); dom.taskList.addEventListener("click", async (event) => { const target = event.target.closest("button"); if (!target) return; const action = target.dataset.action; const taskId = target.dataset.id; if (!action || !taskId) return; if (action === "edit") { const task = allTasks.find((item) => item.id === taskId); if (task) { fillForm(task); showStatus("Задача загружена в форму."); } return; } if (action === "delete") { const confirmed = window.confirm("Удалить задачу?"); if (!confirmed) return; try { await api(`${API_BASE}/tasks/${taskId}`, { method: "DELETE", }); if (dom.taskId.value === taskId) { resetTaskForm(); } await loadTasks(); showStatus("Задача удалена."); } catch (error) { showStatus(error.message, true); } } }); document.addEventListener("DOMContentLoaded", () => { loadTasks(); });