intra_monitor/public/js/diff.js
2026-03-19 11:59:22 +03:00

161 lines
7.8 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.

// ═══════════════════════════════════════════════════════════════════
// diff.js — Безопасное diff-обновление списка заявок
//
// Подключить в index.html ПОСЛЕ state.js и keyboard.js:
// <script src="/js/diff.js"></script>
//
// Заменяет прямое присваивание allData = [...] в auth.js.
// Вместо loadTasksFromServer присваивает данные через applyTasksDiff().
// ═══════════════════════════════════════════════════════════════════
// ── Утилита: Map задач по id ───────────────────────────────────────
function buildTaskMap(dataArr) {
const map = new Map();
for (const item of dataArr) {
map.set(String(item.task.id), item);
}
return map;
}
/**
* Применяет новые данные к allData без потери состояния интерфейса.
*
* @param {Array} newRawTasks — массив tasks с сервера (до map/extractLastComment)
* @param {boolean} silent — если true, тост об изменениях не показывается
* @returns {{ added: number, changed: number, removed: number }}
*/
function applyTasksDiff(newRawTasks, silent = false) {
// Собираем новый массив в том же формате, что и allData
const newData = (newRawTasks || []).map(task => {
const lc = extractLastComment(task);
if (!lc && typeof forceComment !== 'undefined' && forceComment) {
const tid = String(task.id);
const cached = forcedCommentCache.get(tid);
if (cached && cached.updatedat === task.updatedat) {
return { task, lastComment: cached.lastComment };
}
}
return { task, lastComment: lc };
});
// ── Diff ───────────────────────────────────────────────────────────
const oldMap = buildTaskMap(allData);
const newMap = buildTaskMap(newData);
let added = 0, changed = 0, removed = 0;
for (const [id, newItem] of newMap) {
if (!oldMap.has(id)) {
added++;
} else {
const old = oldMap.get(id);
if (old.task.updatedat !== newItem.task.updatedat) {
changed++;
}
}
}
for (const id of oldMap.keys()) {
if (!newMap.has(id)) removed++;
}
// ── Сохраняем позицию активной строки ─────────────────────────────
// activeRowIdx указывает на позицию в отфильтрованном/отображаемом списке.
// После обновления allData порядок может измениться → ищем задачу по id.
let preservedTaskId = null;
if (typeof activeRowIdx !== 'undefined' && activeRowIdx !== null && allData[activeRowIdx]) {
preservedTaskId = String(allData[activeRowIdx].task.id);
}
// Аналогично для открытой боковой панели
let preservedModalTaskId = null;
if (typeof currentModalIdx !== 'undefined' && currentModalIdx !== null && allData[currentModalIdx]) {
preservedModalTaskId = String(allData[currentModalIdx].task.id);
}
// ── Применяем новый массив ─────────────────────────────────────────
allData = newData;
// ── Восстанавливаем activeRowIdx ───────────────────────────────────
if (preservedTaskId !== null) {
const newIdx = allData.findIndex(i => String(i.task.id) === preservedTaskId);
if (newIdx !== -1) {
activeRowIdx = newIdx;
} else {
// Задача удалена — сдвигаем курсор к ближайшей
activeRowIdx = Math.min(activeRowIdx, allData.length - 1);
if (activeRowIdx < 0) activeRowIdx = null;
}
}
// ── Восстанавливаем currentModalIdx ───────────────────────────────
if (preservedModalTaskId !== null) {
const newModalIdx = allData.findIndex(i => String(i.task.id) === preservedModalTaskId);
if (newModalIdx !== -1) {
currentModalIdx = newModalIdx;
} else {
// Задача из открытого меню исчезла — закрываем меню
currentModalIdx = null;
const dc = document.getElementById('detail-col');
if (dc) dc.classList.add('hidden');
}
}
// ── Показать уведомление об изменениях ────────────────────────────
if (!silent && (added || changed || removed)) {
const parts = [];
if (added) parts.push(`+${added} новых`);
if (changed) parts.push(`~${changed} изм.`);
if (removed) parts.push(`${removed} удалено`);
if (typeof showToast === 'function') {
showToast(`📋 Обновление: ${parts.join(', ')}`, 'info', 3500);
}
}
return { added, changed, removed };
}
// ═══════════════════════════════════════════════════════════════════
// Патч loadTasksFromServer: вместо прямого allData = [...] — applyTasksDiff
// ═══════════════════════════════════════════════════════════════════
(function patchLoadTasksFromServer() {
const _orig = window.loadTasksFromServer;
if (typeof _orig !== 'function') {
document.addEventListener('DOMContentLoaded', patchLoadTasksFromServer);
return;
}
window.loadTasksFromServer = async function (silent = false) {
if (!silent && !allData.length) {
if (typeof showLoading === 'function') showLoading();
}
const data = await apiGet('/api/tasks');
if (!data) return;
const { added, changed, removed } = applyTasksDiff(data.tasks || [], silent);
// Обновляем мета-данные (те же строки, что были в оригинале)
lastKnownUpdatedAt = data.updated_at;
if (data.cache_version !== undefined && data.cache_version !== null) {
lastKnownCacheVersion = Number(data.cache_version) || lastKnownCacheVersion;
}
const el = id => document.getElementById(id);
if (el('total-loaded')) el('total-loaded').textContent = allData.length;
if (el('total-count')) el('total-count').textContent = allData.length;
if (el('last-update')) el('last-update').textContent =
data.updated_at ? new Date(data.updated_at).toLocaleTimeString('ru-RU') : '—';
document.getElementById('update-banner')?.classList.remove('visible');
if (typeof onTasksLoaded === 'function') onTasksLoaded();
if (typeof refreshAdvancedFilters === 'function') refreshAdvancedFilters();
if (typeof renderTable === 'function') renderTable();
if (typeof forceComment !== 'undefined' && forceComment) {
if (typeof fetchMissingComments === 'function') fetchMissingComments();
}
// Если открыта боковая панель — обновить её содержимое (данные задачи могли измениться)
if (currentModalIdx !== null && typeof renderSidePanel === 'function') {
renderSidePanel(currentModalIdx);
}
};
})();