160 lines
7.8 KiB
JavaScript
160 lines
7.8 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════
|
||
// 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 renderTable === 'function') renderTable();
|
||
if (typeof forceComment !== 'undefined' && forceComment) {
|
||
if (typeof fetchMissingComments === 'function') fetchMissingComments();
|
||
}
|
||
|
||
// Если открыта боковая панель — обновить её содержимое (данные задачи могли измениться)
|
||
if (currentModalIdx !== null && typeof renderSidePanel === 'function') {
|
||
renderSidePanel(currentModalIdx);
|
||
}
|
||
};
|
||
})();
|