Files
scud_ai/modules/web_api/static/js/tasks.js
T

210 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ===============================================================================
* FILE: modules/web_api/static/js/tasks.js
* ROLE: Управление персональными задачами оператора (CRUD, фильтры, рендер).
* ===============================================================================
*/
window.escapeHtml = window.escapeHtml || function (str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
};
let currentTasksFilter = 'ALL';
let tasksCache = [];
function getTasksContainer() {
return document.getElementById("tasks-list-container") ||
document.getElementById("sidebar-dynamic-content") ||
document.getElementById("tasks-list");
}
async function loadTasks() {
const container = getTasksContainer();
if (!container) return;
container.innerHTML = `
<div class="text-center py-8 text-xs text-slate-400">
<i class="fa-solid fa-spinner fa-spin mr-1"></i> Загрузка задач...
</div>
`;
try {
const res = await fetch("/api/v1/tasks", {
headers: AuthManager.getAuthHeaders()
});
if (!res.ok) {
if (res.status === 401) {
showAuthModal();
return;
}
throw new Error(`Ошибка сервера (${res.status})`);
}
const data = await res.json();
tasksCache = Array.isArray(data) ? data : (data.tasks || []);
renderTasksUI();
} catch (err) {
console.error("[Tasks] Ошибка загрузки:", err);
container.innerHTML = `
<div class="p-4 text-xs text-rose-500 text-center flex flex-col items-center gap-2">
<i class="fa-solid fa-triangle-exclamation text-base"></i>
<span>Не удалось загрузить задачи</span>
<button onclick="loadTasks()" class="px-2.5 py-1 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded text-[11px] font-semibold transition">Повторить</button>
</div>
`;
}
}
function setTaskFilter(filter) {
currentTasksFilter = filter;
renderTasksUI();
}
function renderTasksUI() {
const container = getTasksContainer();
if (!container) return;
let filtered = tasksCache;
if (currentTasksFilter === 'IN_PROGRESS') {
filtered = tasksCache.filter(t => t.status === 'IN_PROGRESS');
} else if (currentTasksFilter === 'BACKLOG') {
filtered = tasksCache.filter(t => t.status === 'BACKLOG' || t.status === 'PLANNED');
} else if (currentTasksFilter === 'COMPLETED') {
filtered = tasksCache.filter(t => t.status === 'COMPLETED' || t.status === 'DONE');
}
const filtersHtml = `
<div class="flex items-center gap-1 p-1 bg-slate-200/70 rounded-lg text-[11px] font-semibold mb-2">
<button onclick="setTaskFilter('ALL')" class="flex-1 py-1 rounded text-center transition ${currentTasksFilter === 'ALL' ? 'bg-white text-indigo-700 shadow-sm' : 'text-slate-600 hover:text-slate-900'}">Все</button>
<button onclick="setTaskFilter('IN_PROGRESS')" class="flex-1 py-1 rounded text-center transition ${currentTasksFilter === 'IN_PROGRESS' ? 'bg-white text-indigo-700 shadow-sm' : 'text-slate-600 hover:text-slate-900'}">В работе</button>
<button onclick="setTaskFilter('BACKLOG')" class="flex-1 py-1 rounded text-center transition ${currentTasksFilter === 'BACKLOG' ? 'bg-white text-indigo-700 shadow-sm' : 'text-slate-600 hover:text-slate-900'}">Планы</button>
<button onclick="setTaskFilter('COMPLETED')" class="flex-1 py-1 rounded text-center transition ${currentTasksFilter === 'COMPLETED' ? 'bg-white text-indigo-700 shadow-sm' : 'text-slate-600 hover:text-slate-900'}">Готово</button>
</div>
`;
const addBtnHtml = `
<div class="flex items-center justify-between px-1 mb-1.5">
<span class="text-xs font-bold text-slate-700">Задачи: ${filtered.length}</span>
<button onclick="openCreateTaskModal()" class="px-2 py-1 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-[11px] font-bold shadow-sm flex items-center gap-1 transition">
<i class="fa-solid fa-plus text-[10px]"></i> Новая
</button>
</div>
`;
if (filtered.length === 0) {
container.innerHTML = `
${filtersHtml}
${addBtnHtml}
<div class="text-center py-8 text-xs text-slate-400 bg-white border border-slate-200 rounded-xl p-4">
Нет задач в выбранной категории
</div>
`;
return;
}
const itemsHtml = filtered.map(t => {
const isDone = t.status === 'COMPLETED' || t.status === 'DONE';
const priorityColors = {
'HIGH': 'bg-rose-50 text-rose-700 border-rose-200',
'MEDIUM': 'bg-amber-50 text-amber-700 border-amber-200',
'LOW': 'bg-slate-50 text-slate-600 border-slate-200'
};
const pClass = priorityColors[t.priority] || priorityColors['MEDIUM'];
return `
<div class="flex flex-col p-2.5 bg-white border border-slate-200 rounded-xl text-xs gap-1.5 shadow-sm hover:border-indigo-300 transition">
<div class="flex items-start justify-between gap-2">
<div class="flex items-center gap-1.5 min-w-0">
<button onclick="toggleTaskStatus(${t.id}, '${t.status}')" class="text-slate-400 hover:text-indigo-600 transition shrink-0">
<i class="fa-${isDone ? 'solid fa-circle-check text-emerald-500' : 'regular fa-circle'} text-sm"></i>
</button>
<span class="font-bold text-slate-800 ${isDone ? 'line-through text-slate-400' : ''} truncate">${escapeHtml(t.title)}</span>
</div>
<span class="px-1.5 py-0.5 rounded text-[9px] font-semibold border ${pClass} shrink-0">${t.priority || 'NORMAL'}</span>
</div>
<div class="flex items-center justify-between text-[10px] text-slate-400 pt-1 border-t border-slate-100">
<span>${t.task_id || ('#' + t.id)} · ${escapeHtml(t.module || 'general')}</span>
<div class="flex items-center gap-1">
<button onclick="deleteTaskItem(${t.id})" class="text-slate-400 hover:text-rose-600 p-0.5 transition" title="Удалить">
<i class="fa-solid fa-trash-can text-[11px]"></i>
</button>
</div>
</div>
</div>
`;
}).join('');
container.innerHTML = `
${filtersHtml}
${addBtnHtml}
<div class="flex flex-col gap-1.5 max-h-[70vh] overflow-y-auto">
${itemsHtml}
</div>
`;
}
async function toggleTaskStatus(id, currentStatus) {
const newStatus = (currentStatus === 'COMPLETED' || currentStatus === 'DONE') ? 'IN_PROGRESS' : 'COMPLETED';
try {
const res = await fetch(`/api/v1/tasks/${id}`, {
method: "PATCH",
headers: AuthManager.getAuthHeaders(),
body: JSON.stringify({ status: newStatus })
});
if (res.ok) {
loadTasks();
}
} catch (e) {
console.error("Ошибка смены статуса задачи:", e);
}
}
async function deleteTaskItem(id) {
if (!confirm("Удалить эту задачу?")) return;
try {
const res = await fetch(`/api/v1/tasks/${id}`, {
method: "DELETE",
headers: AuthManager.getAuthHeaders()
});
if (res.ok) {
loadTasks();
}
} catch (e) {
alert("Ошибка сети при удалении");
}
}
async function openCreateTaskModal() {
const title = prompt("Введите описание новой задачи:");
if (!title || !title.trim()) return;
try {
const res = await fetch("/api/v1/tasks", {
method: "POST",
headers: AuthManager.getAuthHeaders(),
body: JSON.stringify({
title: title.trim(),
priority: "MEDIUM",
status: "IN_PROGRESS"
})
});
if (res.ok) {
loadTasks();
} else {
alert("Не удалось создать задачу");
}
} catch (e) {
alert("Ошибка сети");
}
}
window.loadTasks = loadTasks;
window.setTaskFilter = setTaskFilter;