182 lines
10 KiB
JavaScript
182 lines
10 KiB
JavaScript
// ── 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:'Эскалация'
|
||
};
|
||
const STATE_CHIP_KEYS = [...MARKS_CONFIG.map(m => m.key), 'unmarked'];
|
||
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 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 = Object.fromEntries([
|
||
...MARKS_CONFIG.map(m => [m.key, true]),
|
||
['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();
|
||
|
||
// ── 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,'>').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,'<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('');
|
||
}
|