intra_monitor/public/js/state.js
dv 7d297ad2d9
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix vibe 3
2026-03-19 12:16:29 +03:00

204 lines
11 KiB
JavaScript
Raw Permalink 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.

// ── 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 seenStorageKey() {
const username = String(currentUser?.username || '').trim().toLowerCase();
return username ? `${SEEN_KEY}:${username}` : SEEN_KEY;
}
function loadSeen() { try { return JSON.parse(localStorage.getItem(seenStorageKey())||'{}'); } catch { return {}; } }
function saveSeen(s) { try { localStorage.setItem(seenStorageKey(), 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
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,'<span class="mention-tag">@$1</span>');
}
function showLoading() {
document.getElementById('table-wrap').innerHTML=`<div class="state-box"><div class="spinner"></div><div class="msg">Загружаю заявки...</div></div>`;
}
function extractLastComment(task) {
for (const entry of task.lifetime?.data||[]) {
const ev=(entry.events?.data||[]).find(e=>COMMENT_TYPES.has(e.type));
if(ev) return { username:entry.username, userid:entry.userid, usertype:entry.usertype, eventat:entry.eventat, text:ev.stringvalue||'', isPrivate:ev.type===55, eventType:ev.type };
}
return null;
}
function getAuthorType(item) {
if(!item.lastComment) return 'nocomment';
const {usertype}=item.lastComment;
if(usertype===10) return 'internal';
if(usertype===20) return 'external';
return 'unknown';
}
function promptMarkAllRead() {
const targets=selectedRows.size>0?[...selectedRows].map(idx=>allData[idx]).filter(Boolean):allData;
const n=targets.filter(i=>isUnread(i.task)).length;
if(!n) return;
document.getElementById('confirm-count').textContent=n;
document.getElementById('confirm-modal').classList.add('open');
}
// Количество наших комментариев по задаче
function getCommentCount(taskId) {
const tid=String(taskId);
if (commentCounts && Object.prototype.hasOwnProperty.call(commentCounts, tid)) {
return commentCounts[tid]||0;
}
return (commentsCache[tid]||[]).length;
}
// Автодействия после загрузки задач (режим сброса статусов при обновлении)
function onTasksLoaded() {
if (typeof onDataLoaded === 'function') onDataLoaded();
if (unreadMode !== 'reset') return;
const seen = loadSeen();
const toClear = [];
allData.forEach(({ task }) => {
const id = String(task.id);
const unread = !seen[id] || seen[id] !== task.updatedat;
if (!unread) return;
const m = marksCache[id] || {};
MARKS_CONFIG.filter(m => m.clearOnUpdate).map(m => m.key).forEach(type => {
if (m[type]?.active) {
m[type] = { ...(m[type] || {}), active: false };
toClear.push({ taskId: id, type });
}
});
marksCache[id] = m;
});
if (toClear.length && typeof apiPost === 'function') {
toClear.forEach(({ taskId, type }) => {
apiPost('/api/marks', { task_id: String(taskId), mark_type: type, value: false });
});
}
}
// ── Column visibility ─────────────────────────────────────────────
const ALL_COLUMNS = [
{ key:'num', label:'#Номер', default:true },
{ key:'title', label:'Тема', default:true },
{ key:'author', label:'Кто ответил', default:true },
{ key:'date', label:'Дата', default:true },
{ key:'preview',label:'Превью', default:true },
{ key:'status', label:'Статус', default:true },
];
let visibleColumns = new Set(ALL_COLUMNS.filter(c=>c.default).map(c=>c.key));
try {
const saved = JSON.parse(localStorage.getItem('intradesk_cols')||'null');
if(saved && Array.isArray(saved)) visibleColumns = new Set(saved);
} catch {}
function saveColumnPrefs() { try { localStorage.setItem('intradesk_cols', JSON.stringify([...visibleColumns])); } catch {} }
// ── Short name helper ─────────────────────────────────────────────
function shortName(fullName) {
if(!fullName) return '—';
const parts = fullName.trim().split(/\s+/);
if(parts.length === 1) return parts[0];
// Фамилия + инициалы
return parts[0] + ' ' + parts.slice(1).map(p => p[0] ? p[0].toUpperCase()+'.' : '').join('');
}