// ── Constants ───────────────────────────────────────────────────── const TASK_URL = 'https://cleverparking.intradesk.ru/task/list/'; const COMMENT_TYPES = new Set([50, 55]); const EVENT_TYPE_NAMES = { 10:'Смена статуса',20:'Смена приоритета',25:'Смена рейтинга',30:'Смена исполнителя', 40:'Смена группы',45:'Изм. доп. данных',50:'Комментарий',55:'Приватный комментарий', 60:'Добавлен наблюдатель',61:'Добавлен координатор',65:'Добавлена группа наблюдателей', 70:'Удалён наблюдатель',80:'Добавлен файл',85:'Добавлен прив. файл',90:'Удалён файл', 100:'Добавлен тег',110:'Удалён тег',120:'Изм. плановая дата',130:'Изм. дата реакции', 140:'Изменён сервис',150:'Изменён тип',160:'Изменён заявитель',165:'Изм. группа заявителя', 170:'Изм. доп. поле',200:'Изменено название',205:'Изменено описание',210:'Изм. бизнес-процесс', 280:'Архивировано',300:'Выполнен макрос',400:'Эскалация' }; let STATE_CHIP_KEYS = []; const SEEN_KEY = 'intradesk_seen'; // ── App State ───────────────────────────────────────────────────── let authToken = localStorage.getItem('intradesk_token'); let currentUser = null; let allData = []; let marksCache = {}; let notesCache = {}; let commentsCache = {}; let mentionsData = []; let mentionUnread = 0; let knownUsers = []; let lastKnownUpdatedAt = null; let lastKnownCacheVersion = 0; let reloadInFlight = false; let statusPollTimer = null, clientCountdownTimer = null, clientCountdownSec = 0; let serverRefreshInterval = 300; let commentCounts = {}; let currentFilter = 'all'; let selectedTags = new Set(); let openNoteIdx = null; let currentModalIdx = null, currentTab = 'last'; let mentionedFilterOn = false; let acState = { active:false, start:0, query:'' }; const stateChips = { unmarked: true }; let unreadMode = 'highlight'; let exportFieldGroups = new Set(['note','status','comments']); let exportStatusMode = 'A'; const forceComment = true; // всегда включено — для задач без комментария подгружаем историю // taskId -> { updatedat, lastComment } — не перезапрашиваем если updatedat не изменился const forcedCommentCache = new Map(); function rebuildStateChipsFromConfig() { const prev = { ...stateChips }; STATE_CHIP_KEYS = [...MARKS_CONFIG.map((m) => m.key), 'unmarked']; Object.keys(stateChips).forEach((k) => { delete stateChips[k]; }); STATE_CHIP_KEYS.forEach((k) => { stateChips[k] = Object.prototype.hasOwnProperty.call(prev, k) ? !!prev[k] : true; }); } function onMarksConfigChanged() { rebuildStateChipsFromConfig(); if (typeof initMarkChips === 'function') initMarkChips(); if (typeof refreshAdvancedFilters === 'function') refreshAdvancedFilters(); if (typeof syncChipDropdownItems === 'function') syncChipDropdownItems(); if (typeof renderTable === 'function') renderTable(); } rebuildStateChipsFromConfig(); // ── Seen ────────────────────────────────────────────────────────── function loadSeen() { try { return JSON.parse(localStorage.getItem(SEEN_KEY)||'{}'); } catch { return {}; } } function saveSeen(s) { try { localStorage.setItem(SEEN_KEY, JSON.stringify(s)); } catch {} } function markSeen(id,upd) { const s=loadSeen(); s[id]=upd; saveSeen(s); } function markAllRead(){ const s=loadSeen(); const targets=selectedRows.size>0?[...selectedRows].map(idx=>allData[idx]).filter(Boolean):allData; targets.forEach(({task})=>{s[task.id]=task.updatedat;}); saveSeen(s); renderTable(); } function isUnread(task) { const s=loadSeen(); return !s[task.id]||s[task.id]!==task.updatedat; } function setUnreadMode(mode) { unreadMode = mode === 'reset' ? 'reset' : 'highlight'; } // ── Marks helpers ───────────────────────────────────────────────── function isMark(tid, key) { return !!(marksCache[tid]?.[key]?.active); } // Именованные алиасы (для совместимости с существующим кодом) function isResolved(tid) { return isMark(tid, 'resolved'); } function isNeedsReply(tid) { return isMark(tid, 'needs_reply'); } function isWaitClient(tid) { return isMark(tid, 'wait_client'); } function isVyezdnye(tid) { return isMark(tid, 'vyezdnye'); } function isOborudovanie(tid) { return isMark(tid, 'oborudovanie'); } function isHidden(tid) { return isMark(tid, 'hidden'); } function getMarkBy(tid, type) { const m=marksCache[tid]?.[type]; return (m&&m.active)?m.by:null; } function taskNumFromId(taskId) { const item=allData.find(d=>String(d.task.id)===String(taskId)); return item?(item.task.tasknumber||item.task.id):taskId; } // ── Utility ─────────────────────────────────────────────────────── function esc(s){if(!s)return'';return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');} function fmtDate(d){return`${d.toLocaleDateString('ru-RU',{day:'2-digit',month:'2-digit',year:'2-digit'})} ${d.toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'})}`;} function timeAgo(d){const m=Math.floor((Date.now()-d)/60000);if(m<1)return'только что';if(m<60)return`${m} мин назад`;const h=Math.floor(m/60);if(h<24)return`${h} ч назад`;return`${Math.floor(h/24)} д назад`;} function datestamp(){const d=new Date();return`${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}`;} function stripHtml(s){const t=document.createElement('div');t.innerHTML=s;return(t.textContent||t.innerText||'').replace(/\s+/g,' ').trim();} function sanitizeHtml(raw){ if(!raw)return''; if(!/<[a-z]/i.test(raw))return esc(raw); const t=document.createElement('div');t.innerHTML=raw; t.querySelectorAll('script,style,intradesk-initiator-notification-signature').forEach(e=>e.remove()); t.querySelectorAll('intradesk-quote').forEach(e=>{const q=document.createElement('blockquote');q.className='intradesk-quote-block';q.innerHTML=e.innerHTML;e.replaceWith(q);}); t.querySelectorAll('*').forEach(e=>{['style','class','color','face','size','bgcolor','align','contenteditable'].forEach(a=>e.removeAttribute(a));if(e.tagName==='FONT'){const s=document.createElement('span');s.innerHTML=e.innerHTML;e.replaceWith(s);}}); return t.innerHTML; } function renderMentions(text) { return esc(text).replace(/@([\w\u0400-\u04ff]+)/g,'@$1'); } function showLoading() { document.getElementById('table-wrap').innerHTML=`