// ── Column visibility ───────────────────────────────────────────── const ALL_COLS = [ { key:'num', label:'# Заявка', def:true }, { key:'title', label:'Тема', def:true }, { key:'intrastatus', label:'Статус в интре', def:true }, { key:'client', label:'Клиент', def:true }, { key:'date', label:'Время ответа', def:true }, { key:'created', label:'Создана', def:true }, { key:'preview', label:'Превью/Заметка', def:true }, { key:'status', label:'Статус', def:true }, ]; let visibleCols = new Set(ALL_COLS.filter(c=>c.def).map(c=>c.key)); let colMenuOpen = false; let filteredData = []; let statFilter = 'all'; let selectedRows = new Set(); let mainTab = 'all'; // 'all' | 'open' | 'done' | 'empty' const ADV_FILTER_CONFIG = { 'filter-who-select': { panelId: 'filter-who-panel', wrapId: 'filter-who-wrap', labelId: 'filter-who-label', allLabel: 'Кто ответил: все', shortLabel: 'Кто ответил', }, 'filter-executor-select': { panelId: 'filter-executor-panel', wrapId: 'filter-executor-wrap', labelId: 'filter-executor-label', allLabel: 'Исполнители: все', shortLabel: 'Исполнители', }, 'filter-object-select': { panelId: 'filter-object-panel', wrapId: 'filter-object-wrap', labelId: 'filter-object-label', allLabel: 'Объекты: все', shortLabel: 'Объекты', }, 'filter-intra-select': { panelId: 'filter-intra-panel', wrapId: 'filter-intra-wrap', labelId: 'filter-intra-label', allLabel: 'Статус Intra: все', shortLabel: 'Статус Intra', }, 'filter-internal-select': { panelId: 'filter-internal-panel', wrapId: 'filter-internal-wrap', labelId: 'filter-internal-label', allLabel: 'Внутр. статусы: все', shortLabel: 'Внутр. статусы', }, }; function getSelectedSet(selectId){ const el=document.getElementById(selectId); if(!el) return new Set(); return new Set([...el.selectedOptions].map(o=>String(o.value)).filter(Boolean)); } function fillMultiSelect(selectId, items, selectedSet, emptyLabel){ const el=document.getElementById(selectId); if(!el) return; if(!items.length){ el.innerHTML=``; return; } const html=items.map(({value,label})=>``).join(''); el.innerHTML=html; [...el.options].forEach((opt)=>{ opt.selected=selectedSet.has(String(opt.value)); }); } function updateAdvancedFilterLabel(selectId){ const cfg=ADV_FILTER_CONFIG[selectId]; if(!cfg) return; const el=document.getElementById(selectId); const lbl=document.getElementById(cfg.labelId); if(!el||!lbl) return; const selected=[...el.selectedOptions].filter((o)=>!o.disabled&&String(o.value).trim()!==''); if(!selected.length){ lbl.textContent=cfg.allLabel; return; } if(selected.length===1){ lbl.textContent=`${cfg.shortLabel}: ${selected[0].textContent}`; return; } lbl.textContent=`${cfg.shortLabel}: ${selected.length}`; } function buildAdvancedFilterPanel(selectId){ const cfg=ADV_FILTER_CONFIG[selectId]; if(!cfg) return; const sel=document.getElementById(selectId); const panel=document.getElementById(cfg.panelId); if(!sel||!panel) return; panel.innerHTML=''; const opts=[...sel.options].filter((o)=>!o.disabled&&String(o.value).trim()!==''); if(!opts.length){ const empty=document.createElement('div'); empty.className='adv-filter-empty'; empty.textContent='Нет значений'; panel.appendChild(empty); return; } const frag=document.createDocumentFragment(); opts.forEach((opt)=>{ const item=document.createElement('label'); item.className='adv-filter-item'; const cb=document.createElement('input'); cb.type='checkbox'; cb.checked=!!opt.selected; cb.addEventListener('change',()=>{ opt.selected=cb.checked; updateAdvancedFilterLabel(selectId); renderTable(); }); const text=document.createElement('span'); text.textContent=opt.textContent; item.appendChild(cb); item.appendChild(text); frag.appendChild(item); }); panel.appendChild(frag); } function closeAdvancedFilterDropdowns(exceptSelectId=''){ Object.entries(ADV_FILTER_CONFIG).forEach(([selectId,cfg])=>{ if(exceptSelectId&&selectId===exceptSelectId) return; document.getElementById(cfg.panelId)?.classList.remove('open'); document.getElementById(cfg.wrapId)?.classList.remove('open'); }); } function toggleAdvancedFilterDropdown(e,selectId){ e?.stopPropagation(); const cfg=ADV_FILTER_CONFIG[selectId]; if(!cfg) return; const panel=document.getElementById(cfg.panelId); const wrap=document.getElementById(cfg.wrapId); if(!panel||!wrap) return; const willOpen=!panel.classList.contains('open'); closeAdvancedFilterDropdowns(selectId); if(!willOpen){ panel.classList.remove('open'); wrap.classList.remove('open'); return; } buildAdvancedFilterPanel(selectId); panel.classList.add('open'); wrap.classList.add('open'); } function syncAdvancedFiltersUi(){ Object.keys(ADV_FILTER_CONFIG).forEach((selectId)=>{ updateAdvancedFilterLabel(selectId); const cfg=ADV_FILTER_CONFIG[selectId]; const panel=document.getElementById(cfg.panelId); if(panel?.classList.contains('open')) buildAdvancedFilterPanel(selectId); }); } function refreshAdvancedFilters(){ const selectedWho=getSelectedSet('filter-who-select'); const selectedExecutors=getSelectedSet('filter-executor-select'); const selectedObjects=getSelectedSet('filter-object-select'); const selectedIntra=getSelectedSet('filter-intra-select'); const selectedInternal=getSelectedSet('filter-internal-select'); const execMap=new Map(); const objectMap=new Map(); const intraMap=new Map(); allData.forEach(({task})=>{ const execId=String(task.executor||'').trim(); if(execId){ const execName=String(task._executorname||task.executor||execId).trim(); execMap.set(execId, execName); } const objectName=String(task._clientname||task.clientname||task.organization||'').trim(); if(objectName) objectMap.set(objectName, objectName); const intraId=String(task.statusid||task.status||'').trim(); const intraName=String(task._statusname||task.statusname||intraId).trim(); if(intraId) intraMap.set(intraId, intraName||intraId); }); const execItems=[...execMap.entries()].map(([value,label])=>({value,label})).sort((a,b)=>a.label.localeCompare(b.label,'ru')); const objectItems=[...objectMap.entries()].map(([value,label])=>({value,label})).sort((a,b)=>a.label.localeCompare(b.label,'ru')); const intraItems=[...intraMap.entries()].map(([value,label])=>({value,label:`#${value} ${label}`})); const whoItems=[ { value:'internal', label:'Сотрудник' }, { value:'external', label:'Клиент' }, { value:'nocomment', label:'Нет ответа' }, ]; const internalItems=MARKS_CONFIG.map((m)=>({value:m.key,label:m.label})); fillMultiSelect('filter-who-select', whoItems, selectedWho, 'No values'); fillMultiSelect('filter-executor-select', execItems, selectedExecutors, 'No executors'); fillMultiSelect('filter-object-select', objectItems, selectedObjects, 'No objects'); fillMultiSelect('filter-intra-select', intraItems, selectedIntra, 'No Intra statuses'); fillMultiSelect('filter-internal-select', internalItems, selectedInternal, 'No statuses'); syncAdvancedFiltersUi(); } function setMainTab(tab, el) { mainTab = tab; document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('tab-btn-active')); if (el) el.classList.add('tab-btn-active'); renderTable(); } function isTaskOpen(item) { const sn = (item.task._statusname || item.task.statusname || '').toLowerCase(); return sn.includes('открыт') || sn.includes('в работе') || sn.includes('новая') || sn.includes('new') || sn.includes('назнач'); } function isTaskDone(item) { const sn = (item.task._statusname || item.task.statusname || '').toLowerCase(); return sn.includes('выполн') || sn.includes('закрыт') || sn.includes('решен'); } function isTaskEmpty(item) { return !item.lastComment; } // ── Helpers ─────────────────────────────────────────────────────── function getTaskTags(task){const r=task.tags?.data??task.tags??[];return r.map(t=>({id:t.id??t,name:t.name??String(t)}));} function buildTagIndex(){return [];} function renderTagFilter(){} function abbrevName(name){ if(!name||name==='—') return name||'—'; const p=name.trim().split(/\s+/); if(p.length===1) return p[0]; return p[0]+' '+p[1][0]+'.'; } const AV_COLORS=['#5b7fff','#ff4d4f','#52c41a','#fa8c16','#722ed1','#13c2c2','#eb2f96']; function avatarColor(n){let h=0;for(let i=0;i<(n||'').length;i++)h=(h*31+n.charCodeAt(i))%AV_COLORS.length;return AV_COLORS[h];} function avatarInitials(n){ if(!n||n==='—') return'?'; const p=n.trim().split(/\s+/); return p.length>1?(p[0][0]+p[1][0]).toUpperCase():n.slice(0,2).toUpperCase(); } // ── Chips ───────────────────────────────────────────────────────── function setChip(el,on){ if(!el)return; el.classList.toggle('chip-on',on); el.classList.toggle('chip-off',!on); } function syncAllChip(){setChip(document.getElementById('chip-all'),STATE_CHIP_KEYS.every(k=>stateChips[k]));} function toggleStateChip(name){ stateChips[name]=!stateChips[name]; setChip(document.getElementById('chip-'+name),stateChips[name]); syncAllChip(); renderTable(); } // ── Init mark chips + bulk-select from MARKS_CONFIG ─────────────── function initMarkChips(){ // Chip dropdown items const container=document.getElementById('mark-chips-container'); if(container){ container.innerHTML=MARKS_CONFIG.map(m=>`
${m.chipLabel} 0
`).join(''); } // Bulk-status select options const sel=document.getElementById('bulk-status-sel'); if(sel){ sel.innerHTML=''; const clearOpt=sel.querySelector('option[value="clear"]'); MARKS_CONFIG.slice().reverse().forEach(m=>{ const opt=document.createElement('option'); opt.value=m.key; opt.textContent=m.label; sel.insertBefore(opt, clearOpt); }); } if(typeof refreshAdvancedFilters==='function') refreshAdvancedFilters(); } function toggleMentionedFilter(){ mentionedFilterOn=!mentionedFilterOn; setChip(document.getElementById('chip-mentioned'),mentionedFilterOn); renderTable(); } function toggleChipDropdown() { const panel = document.getElementById('chip-dropdown-panel'); if (!panel) return; panel.classList.toggle('chip-dd-open'); } function updateChipDropdownLabel() { const allOn = STATE_CHIP_KEYS.every(k => stateChips[k]); const activeKeys = STATE_CHIP_KEYS.filter(k => stateChips[k]); const lbl = document.getElementById('chip-dropdown-label'); if (!lbl) return; if (allOn) { lbl.textContent = 'Все статусы'; } else if (activeKeys.length === 0) { lbl.textContent = 'Ни один'; } else if (activeKeys.length === 1) { const key = activeKeys[0]; if (key === 'unmarked') { lbl.textContent = 'Без статуса'; return; } const m = MARKS_CONFIG.find(x => x.key === key); lbl.textContent = m ? m.chipLabel : key; } else { lbl.textContent = `Статусов: ${activeKeys.length}`; } } function syncChipDropdownItems() { const allOn = STATE_CHIP_KEYS.every(k => stateChips[k]); const allItem = document.getElementById('chipdd-all'); if (allItem) allItem.classList.toggle('chip-dd-active', allOn); MARKS_CONFIG.forEach(m => { const el = document.getElementById(`chipdd-${m.key}`); if (el) el.classList.toggle('chip-dd-active', !!stateChips[m.key]); }); const uEl = document.getElementById('chipdd-unmarked'); if (uEl) uEl.classList.toggle('chip-dd-active', !!stateChips['unmarked']); updateChipDropdownLabel(); } function setStatFilter(f,el){ statFilter=f; document.querySelectorAll('.stat-card').forEach(c=>c.classList.remove('active')); if(el)el.classList.add('active'); renderTable(); } function setNavView(v,e){} // ── Date filter ─────────────────────────────────────────────────── function taskMatchesDate(task){ const v=document.getElementById('date-filter')?.value||'0'; if(v==='0')return true; const now=new Date(),upd=new Date(task.updatedat); if(v==='today')return upd.toDateString()===now.toDateString(); if(v==='yesterday'){const y=new Date(now);y.setDate(y.getDate()-1);return upd.toDateString()===y.toDateString();} return upd>=new Date(now.setDate(now.getDate()-parseInt(v))); } // ── Status badges ───────────────────────────────────────────────── function statusBadgeStyle(m){ const bg=m.colorBg||'var(--surface2)'; const border=m.colorBorder||'var(--border2)'; const text=m.colorText||'var(--text2)'; const dot=m.colorDot||m.chipDotColor||m.ispColor||text; return { outer:`background:${bg};border-color:${border};color:${text};`, dot:`background:${dot};`, }; } function internalStatusBadge(tid){ const m=MARKS_CONFIG.find((status)=>isMark(tid,status.key)); if(m){ const style=statusBadgeStyle(m); return `${esc(m.label)}`; } return`Без статуса`; } // ── IntraDesk status badge ──────────────────────────────────────── function intraStatusBadge(task) { const name = task._statusname || task.statusname || ''; if (!name) { const sid = task.statusid || task.status; if (sid) return `#${sid}`; return ''; } const lc = String(name).toLowerCase(); let cls = 'sb-intra-other'; if (lc.includes('открыт') || lc.includes('new')) cls = 'sb-intra-open'; else if (lc.includes('работ') || lc.includes('прогресс') || lc.includes('назнач')) cls = 'sb-intra-work'; else if (lc.includes('выполн') || lc.includes('закрыт') || lc.includes('решен')) cls = 'sb-intra-done'; else if (lc.includes('ожидан') || lc.includes('hold')) cls = 'sb-intra-hold'; return `${esc(name)}`; } // Who answered — ring class for avatar function whoAnsweredRing(lc){ if(!lc) return 'ring-none'; return lc.usertype===10 ? 'ring-internal' : 'ring-external'; } // Who answered — badge html function whoAnsweredBadge(lc){ if(!lc) return`Нет ответа`; if(lc.usertype===10) return`Сотрудник`; return`Клиент`; } // Row color — ONLY by internal marks, not by who answered function rowColorClass(tid){ const m=MARKS_CONFIG.find(m=>isMark(tid,m.key)); return m?m.rowClass:''; } // ── Who set a mark ──────────────────────────────────────────────── function markSetByHtml(tid) { const lines=MARKS_CONFIG .filter(cfg=>marksCache[tid]?.[cfg.key]?.active&&marksCache[tid][cfg.key].by) .map(cfg=>{ const mc=marksCache[tid][cfg.key]; return`👤 ${esc(abbrevName(mc.by))} · «${cfg.label}»${mc.at?` · ${timeAgo(new Date(mc.at))}`:''}`; }); return lines.length?`
${lines.join('')}
`:''; } // ── Inline status picker ────────────────────────────────────────── function statusPickerHtml(tid){ const items=MARKS_CONFIG.map(m=>`
${m.label}
`).join(''); return`
${internalStatusBadge(tid)}
${items}
`; } function ispShow(el){ const dd=el.querySelector('.isp-dropdown'); if(!dd) return; const r=el.getBoundingClientRect(); dd.style.top=(r.bottom)+'px'; dd.style.left=r.left+'px'; dd.style.display='block'; } function ispHide(el){ const dd=el.querySelector('.isp-dropdown'); if(dd) dd.style.display='none'; } // ── Column picker ───────────────────────────────────────────────── function toggleColMenu(e){ e.stopPropagation(); colMenuOpen=!colMenuOpen; document.getElementById('col-menu')?.classList.toggle('open',colMenuOpen); } document.addEventListener('click',()=>{ colMenuOpen=false; document.getElementById('col-menu')?.classList.remove('open'); }); function toggleCol(key){ if(visibleCols.has(key))visibleCols.delete(key);else visibleCols.add(key); renderColMenu(); renderTable(); } function renderColMenu(){ const m=document.getElementById('col-menu'); if(!m)return; m.innerHTML=ALL_COLS.map(c=>` `).join(''); } // ── Sort state ──────────────────────────────────────────────────── let sortCol = null; // 'date' | 'created' let sortDir = 'desc'; function toggleSort(col) { if (sortCol === col) sortDir = sortDir === 'desc' ? 'asc' : 'desc'; else { sortCol = col; sortDir = 'desc'; } renderTable(); } // ── Pagination state ────────────────────────────────────────────── let currentPage = 1; function getPageSize() { const v=parseInt(document.getElementById('opt-page-size')?.value||'50'); return v||0; } function setPage(p) { currentPage = p; renderTable(true); } function onPageSizeChange() { currentPage = 1; renderTable(); } // ── Chip logic: click label = solo, click dot = toggle ──────────── function chipClickLabel(name) { STATE_CHIP_KEYS.forEach(k => { stateChips[k] = (k === name); }); syncChipDropdownItems(); renderTable(); } function chipClickDot(name, e) { e && e.stopPropagation(); stateChips[name] = !stateChips[name]; syncChipDropdownItems(); renderTable(); } function toggleAllChip() { const t = !STATE_CHIP_KEYS.every(k => stateChips[k]); STATE_CHIP_KEYS.forEach(k => { stateChips[k] = t; }); syncChipDropdownItems(); renderTable(); } // ── Render table ────────────────────────────────────────────────── function renderTable(keepPage){ if(!keepPage) currentPage = 1; const search =document.getElementById('search-input')?.value?.toLowerCase()||''; const remoteOnly =document.getElementById('opt-remote-only')?.checked; const selectedWho=getSelectedSet('filter-who-select'); const selectedExecutors=getSelectedSet('filter-executor-select'); const selectedObjects=getSelectedSet('filter-object-select'); const selectedIntra=getSelectedSet('filter-intra-select'); const selectedInternal=getSelectedSet('filter-internal-select'); const mentionTaskIds=mentionedFilterOn?new Set(mentionsData.map((m)=>String(m.task_id))):null; const seenMap=loadSeen(); const isUnreadFast=(task)=>!seenMap[task.id]||seenMap[task.id]!==task.updatedat; filteredData=allData.filter(item=>{ const tid=String(item.task.id); const marked=MARKS_CONFIG.some(m=>isMark(tid,m.key)); const activeInternal=MARKS_CONFIG.find((m)=>isMark(tid,m.key))?.key||''; for(const m of MARKS_CONFIG){if(isMark(tid,m.key)&&!stateChips[m.key])return false;} if(!marked&&!stateChips.unmarked) return false; if(mentionedFilterOn&&mentionTaskIds&&!mentionTaskIds.has(tid)) return false; if(search&&!(item.task.name?.toLowerCase().includes(search)||String(item.task.tasknumber).includes(search)||(item.task._clientname||item.task.clientname||'').toLowerCase().includes(search))) return false; if(remoteOnly&&item.task.service!==84683) return false; if(!taskMatchesDate(item.task)) return false; if(selectedExecutors.size&& !selectedExecutors.has(String(item.task.executor||''))) return false; if(selectedObjects.size){ const objectName=String(item.task._clientname||item.task.clientname||item.task.organization||'').trim(); if(!objectName || !selectedObjects.has(objectName)) return false; } if(selectedIntra.size){ const intraId=String(item.task.statusid||item.task.status||'').trim(); if(!intraId || !selectedIntra.has(intraId)) return false; } if(selectedInternal.size){ if(!activeInternal || !selectedInternal.has(activeInternal)) return false; } const authorType=getAuthorType(item); if(statFilter!=='all'){ if(authorType!==statFilter) return false; }else if(selectedWho.size&& !selectedWho.has(authorType)){ return false; } // Main tab filter if(mainTab==='open' && !isTaskOpen(item)) return false; if(mainTab==='done' && !isTaskDone(item)) return false; if(mainTab==='empty' && !isTaskEmpty(item)) return false; return true; }); // Sort if(sortCol) { filteredData = [...filteredData].sort((a,b) => { const da = sortCol==='date' ? (a.lastComment?.eventat||a.task.updatedat||'') : (a.task.createdat||''); const db = sortCol==='date' ? (b.lastComment?.eventat||b.task.updatedat||'') : (b.task.createdat||''); const cmp = da < db ? -1 : da > db ? 1 : 0; return sortDir==='asc' ? cmp : -cmp; }); } // Counts const markCounts=Object.fromEntries(MARKS_CONFIG.map((m)=>[m.key,0])); let baseLen=0; let baseUnmarked=0; let baseExternal=0; let baseInternal=0; let baseNoComment=0; let unreadTotal=0; let tabOpen=0; let tabDone=0; let tabEmpty=0; allData.forEach((item)=>{ if(isUnreadFast(item.task)) unreadTotal++; if(isTaskOpen(item)) tabOpen++; if(isTaskDone(item)) tabDone++; if(isTaskEmpty(item)) tabEmpty++; if(!taskMatchesDate(item.task)) return; baseLen++; const author=getAuthorType(item); if(author==='external') baseExternal++; else if(author==='internal') baseInternal++; else if(author==='nocomment') baseNoComment++; const tid=String(item.task.id); const markedKey=MARKS_CONFIG.find((m)=>isMark(tid,m.key))?.key||''; if(markedKey) markCounts[markedKey]=(markCounts[markedKey]||0)+1; else baseUnmarked++; }); const g=id=>document.getElementById(id); const sv=(id,v)=>{const e=g(id);if(e)e.textContent=v;}; sv('chip-all-cnt', baseLen); MARKS_CONFIG.forEach((m)=>{ sv(`chip-${m.key}-cnt`, markCounts[m.key]||0); }); sv('chip-unmarked-cnt', baseUnmarked); sv('stat-total', baseLen); sv('stat-external', baseExternal); sv('stat-internal', baseInternal); sv('stat-nocomment', baseNoComment); sv('stat-unread', unreadTotal); sv('nav-badge-tasks', filteredData.length); sv('topbar-sub', `\u00b7 ${filteredData.length} \u0437\u0430\u044f\u0432\u043e\u043a`); sv('total-loaded', filteredData.length); sv('total-count', allData.length); sv('tf-info', `${filteredData.length} \u0438\u0437 ${allData.length}`); // Tab counts (using allData for totals regardless of chip filter) sv('tcnt-all', allData.length); sv('tcnt-open', tabOpen); sv('tcnt-done', tabDone); sv('tcnt-empty', tabEmpty); if(!filteredData.length){ g('table-wrap').innerHTML=`
🔍
Заявок не найдено
Измените фильтры
`; const pb=g('pagination-bar'); if(pb){pb.style.display='none';pb.innerHTML='';} return; } // Pagination slice const PAGE_SIZE = getPageSize(); const totalPages = PAGE_SIZE > 0 ? Math.ceil(filteredData.length / PAGE_SIZE) : 1; if(currentPage > totalPages) currentPage = totalPages; const pageData = PAGE_SIZE > 0 ? filteredData.slice((currentPage-1)*PAGE_SIZE, currentPage*PAGE_SIZE) : filteredData; const idxMap=new Map(); allData.forEach((item,idx)=>idxMap.set(item,idx)); const sc=k=>visibleCols.has(k); const sortArrow = col => sortCol===col ? (sortDir==='asc' ? ' ↑' : ' ↓') : ' ↕'; const sortStyle = col => `cursor:pointer;user-select:none;${sortCol===col?'color:var(--accent);':'color:inherit;'}`; const thead=` ${sc('num') ?'#' :''} ${sc('title') ?'Тема' :''} ${sc('intrastatus')?'Статус в интре':''} ${sc('status') ?'Статус' :''} ${sc('client') ?'Кто ответил' :''} ${sc('date') ?`Время ответа${sortArrow('date')}` :''} ${sc('created')?`Создана${sortArrow('created')}` :''} ${sc('preview')?'Превью' :''} `; const rows=pageData.map(item=>{ const {task,lastComment}=item; const tid=String(task.id); const num=task.tasknumber||task.id; const idx=idxMap.get(item); const unread=isUnreadFast(task); const note=getNote(tid); const cc=rowColorClass(tid); const rowCls=((unread&&!cc?'row-unread ':'')+cc+(currentModalIdx===idx?' active-row':'')).trim(); const authorName=lastComment?.username||task.updatedby||task.username||''; const orgName=task._clientname||task.clientname||task.organization||''; const unreadDot=unread?'':''; const cntBadge=getCommentCount(tid)?`
💬 ${getCommentCount(tid)}
`:''; const prevCell=note ?`
📝 ${esc(note.slice(0,42))}${note.length>42?'…':''}
` :(lastComment?`
${esc(stripHtml(lastComment.text||'').slice(0,52))}
`:''); const whoLabel=lastComment ?(lastComment.usertype===10 ?'● Сотрудник' :'● Клиент') :(task.updatedby ?`${esc(abbrevName(task.updatedby))}` :'нет ответа'); return` ${sc('num') ?`#${num}${unreadDot}`:''} ${sc('title') ?`
${esc(task.name)}
${orgName?`
${esc(orgName)}
`:''}`:''} ${sc('intrastatus')?`${intraStatusBadge(task)}`:''} ${sc('status') ?`${statusPickerHtml(tid)}`:''} ${sc('client') ?`
${avatarInitials(authorName)}
${esc(abbrevName(authorName)||'—')}
${whoLabel}
`:''} ${sc('date') ?`${lastComment?`
${fmtDateShort(new Date(lastComment.eventat))}
${fmtTime(new Date(lastComment.eventat))}
${timeAgo(new Date(lastComment.eventat))}
`:task.updatedat?`
${fmtDateShort(new Date(task.updatedat))}
${fmtTime(new Date(task.updatedat))}
${timeAgo(new Date(task.updatedat))}
`:'
'}`:''} ${sc('created')?`${task.createdat?`
${fmtDateShort(new Date(task.createdat))}
${fmtTime(new Date(task.createdat))}
`:'
'}`:''} ${sc('preview')?`${prevCell}${cntBadge}`:''} `; }).join(''); g('table-wrap').innerHTML=`${thead}${rows}
`; // Pagination bar const pb = g('pagination-bar'); if(pb) { if(totalPages <= 1 || PAGE_SIZE === 0){ pb.style.display='none'; pb.innerHTML=''; } else { pb.style.display='flex'; const start=(currentPage-1)*PAGE_SIZE+1, end=Math.min(currentPage*PAGE_SIZE,filteredData.length); let pages=''; for(let p=1;p<=totalPages;p++){ if(p===1||p===totalPages||Math.abs(p-currentPage)<=2){ pages+=``; } else if(Math.abs(p-currentPage)===3){ pages+=``; } } pb.innerHTML=` ${pages} ${start}–${end} из ${filteredData.length}`; } } } function fmtDateShort(d){return d.toLocaleDateString('ru-RU',{day:'2-digit',month:'2-digit',year:'2-digit'});} function fmtTime(d){return d.toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'});} function toggleSelectAll(cb){document.querySelectorAll('.row-check').forEach(c=>{const i=+c.dataset.idx;if(cb.checked)selectedRows.add(i);else selectedRows.delete(i);c.checked=cb.checked;});updateBulkBar();} function toggleRowCheck(idx,cb){if(cb.checked)selectedRows.add(idx);else selectedRows.delete(idx);updateBulkBar();} function updateBulkBar(){ const info=document.getElementById('bulk-sel-info'); const cnt =document.getElementById('bulk-sel-count'); const sel =document.getElementById('bulk-status-sel'); if(!info)return; if(selectedRows.size>0){ info.style.display='flex'; if(cnt)cnt.textContent=`${selectedRows.size} выбрано`; if(sel)sel.value=''; }else{ info.style.display='none'; } } function bulkApplyStatusFromSelect(sel){ const v=sel.value; sel.value=''; if(!v)return; bulkApplyStatus(v==='clear'?null:v); } function bulkApplyStatus(markType){ if(!selectedRows.size)return; const now=new Date().toISOString(); const statusKeys=MARKS_CONFIG.map((m)=>m.key); const tids=[...selectedRows].map(idx=>allData[idx]?String(allData[idx].task.id):null).filter(Boolean); for(const tid of tids){ if(!marksCache[tid])marksCache[tid]={}; if(!markType){ const active=statusKeys.find(m=>marksCache[tid][m]?.active); statusKeys.forEach(m=>{marksCache[tid][m]={...(marksCache[tid][m]||{}),active:false};}); if(active) apiPost('/api/marks',{task_id:tid,mark_type:active,value:false,clear_others:false}); }else{ statusKeys.forEach(m=>{marksCache[tid][m]={...(marksCache[tid][m]||{}),active:false};}); marksCache[tid][markType]={active:true,by:currentUser?.username,at:now}; apiPost('/api/marks',{task_id:tid,mark_type:markType,value:true,clear_others:true}); } } renderTable(); if(currentModalIdx!==null){ const openTid=String(allData[currentModalIdx]?.task?.id); if(tids.includes(openTid))renderSidePanel(currentModalIdx); } } // ── SIDE PANEL open/close ───────────────────────────────────────── function openModal(idx){ if(idx===null||idx===undefined||!allData[idx]) return; currentModalIdx=idx; currentTab='last'; const {task,lastComment}=allData[idx]; // Immediately mark read & update that row in-place if(isUnread(task)){ markSeen(task.id,task.updatedat); const row=document.querySelector(`tbody tr[data-idx="${idx}"]`); if(row){ ['row-unread',...MARKS_CONFIG.map(m=>m.rowClass)].forEach(c=>row.classList.remove(c)); const cc=rowColorClass(String(task.id)); if(cc) row.classList.add(cc); row.querySelector('.unread-dot-inline')?.remove(); } const su=document.getElementById('stat-unread'); if(su) su.textContent=allData.filter(i=>isUnread(i.task)).length; } // Mark mentions read if(mentionsData.some(m=>String(m.task_id)===String(task.id)&&!m.read_at)){ apiPost('/api/mentions/read',{task_id:String(task.id)}).then(r=>{ if(r){ mentionUnread=r.unread||0; mentionsData=mentionsData.map(m=> String(m.task_id)===String(task.id)?{...m,read_at:new Date().toISOString()}:m); renderMentionsBell(); } }); } // Highlight active row document.querySelectorAll('.tasks-table tbody tr').forEach(r=>r.classList.remove('active-row')); document.querySelector(`tbody tr[data-idx="${idx}"]`)?.classList.add('active-row'); // Show side panel const dc=document.getElementById('detail-col'); if(dc) dc.classList.remove('hidden'); renderSidePanel(idx); loadCommentsForTask(String(task.id)); } function closeModal(){ const dc=document.getElementById('detail-col'); if(dc) dc.classList.add('hidden'); document.querySelectorAll('.tasks-table tbody tr').forEach(r=>r.classList.remove('active-row')); currentModalIdx=null; } function closeModalOnOverlay(){} document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal();}); // ── SIDE PANEL render ───────────────────────────────────────────── function renderSidePanel(idx){ const container=document.getElementById('detail-content'); if(!container) return; if(idx===null||idx===undefined||!allData[idx]){ container.innerHTML=''; return; } const {task,lastComment}=allData[idx]; const tid=String(task.id); const num=task.tasknumber||task.id; const note=getNote(tid),noteBy=getNoteBy(tid),noteAt=getNoteAt(tid); const cnt=getCommentCount(tid); const authorName=lastComment?.username||task.username||'—'; const rawDesc=task.description||task.descriptionhtml||task.body||task.text||''; const noteHtml=(openNoteIdx===idx) ?`
` :note ?`
📝 ${renderMentions(note)}
${noteBy?`👤 ${esc(abbrevName(noteBy))}`:''}${noteAt?` · ${fmtDate(new Date(noteAt))}`:''}
` :`
+ Добавить заметку
`; container.innerHTML=`
${internalStatusBadge(tid)} ${whoAnsweredBadge(lastComment)}
#${num} ${esc(task.name)}
${esc(abbrevName(authorName))}${task.clientname||task.organization?` · ${esc(task.clientname||task.organization)}`:''}
↗ Открыть в IntraDesk
${MARKS_CONFIG.map(m=>{ const active=isMark(tid,m.key); return``; }).join('')}
${markSetByHtml(tid)}
${noteHtml}
Последний комментарий
Загрузить заявку
${renderTabContent(idx,currentTab,rawDesc,lastComment)}
`; const ta=document.getElementById('composer-ta'); if(ta){ ta.addEventListener('keydown',e=>{ if(e.ctrlKey&&e.key==='Enter'){e.preventDefault();submitComment();} handleAutocompleteKey(e); }); } if(openNoteIdx===idx){ const nta=document.getElementById('detail-note-ta'); if(nta){nta.focus();nta.setSelectionRange(nta.value.length,nta.value.length);} } } // aliases for SSE/mentions compatibility function renderDetailPanel(idx){renderSidePanel(idx);} function renderModalContent(idx){renderSidePanel(idx);} function openDetail(idx){openModal(idx);} function closeDetail(){closeModal();} // ── Tab content ─────────────────────────────────────────────────── function renderTabContent(idx,tab,rawDesc,lastComment){ const {task}=allData[idx]; const tid=String(task.id); if(tab==='last'){ const org=task.clientname||task.organization||''; const authorName=lastComment?.username||'—'; const created=task.createdat?new Date(task.createdat):''; const descHtml=rawDesc ?``:''; const actHtml=lastComment ?`
Последняя активность
${avatarInitials(authorName)}
${esc(abbrevName(authorName))}${timeAgo(new Date(lastComment.eventat))}
${esc(stripHtml(lastComment.text||'').slice(0,180))}
`:''; const executorName = task._executorname || ''; const executorGroupName = task._executorgroupname || ''; return`
${executorGroupName?`
Группа👥 ${esc(executorGroupName)}
`:''} ${executorName?`
Исполнитель👤 ${esc(executorName)}
`:''}
${descHtml}${actHtml}`; } if(tab==='comments'){ return renderCommentsHtml(tid,commentsCache[tid]||[]); } if(tab==='history'){ setTimeout(()=>loadAndRenderHistory(idx,'sp-body'),10); return`
Загружаю историю...
`; } return''; } function switchTab(tab){ currentTab=tab; if(currentModalIdx===null) return; ['last','comments','history'].forEach(t=>document.getElementById(`dtab-${t}`)?.classList.toggle('active',t===tab)); const {task}=allData[currentModalIdx]; const rawDesc=task.description||task.descriptionhtml||task.body||task.text||''; const sb=document.getElementById('sp-body'); if(sb) sb.innerHTML=renderTabContent(currentModalIdx,tab,rawDesc,allData[currentModalIdx].lastComment); } // ── Comments ────────────────────────────────────────────────────── function renderCommentsHtml(tid,comments){ if(!comments.length) return`
Комментариев нет. Напишите первый!
`; return`
${comments.map(c=>{ const own=c.author===currentUser.username; const canEdit=own||currentUser.is_admin; const te=c.text.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,'\\n'); return`
${avatarInitials(c.author)}
${esc(abbrevName(c.author))} ${fmtDate(new Date(c.created_at))} ${c.updated_at?`(ред.)`:''}
${renderMentions(c.text)}
${canEdit?`
✎ Ред. ✕ Удалить
`:''}
`; }).join('')}
`; } async function loadAndRenderHistory(idx,containerId){ const {task}=allData[idx]; const tid=String(task.id); const res=await apiGet(`/api/tasks/history/${tid}`); if(currentModalIdx!==idx||currentTab!=='history') return; const el=document.getElementById(containerId); if(!el) return; const entries=res?.entries||[]; if(!entries.length){el.innerHTML=`
История пуста
`;return;} const rawDesc=task.description||task.descriptionhtml||task.body||task.text||''; let db=''; if(rawDesc) db=`
Описание
${sanitizeHtml(rawDesc)}
`; const items=entries.map(entry=>{ const staff=entry.usertype===10; const evHtml=(entry.events?.data||[]).map(ev=>{ const tn=EVENT_TYPE_NAMES[ev.type]||`тип ${ev.type}`; const isCmt=COMMENT_TYPES.has(ev.type); let body=''; if(isCmt&&ev.stringvalue) body=`
${sanitizeHtml(ev.stringvalue)}
`; else if(ev.type===10&&ev.entitylanguagevalue) body=`
«${esc(ev.entitylanguagevalue.valueold?.name?.ru||'?')}» → «${esc(ev.entitylanguagevalue.valuenew?.name?.ru||'?')}»
`; else if(ev.entityvalue) body=`
«${esc(ev.entityvalue.valueold?.name||'?')}» → «${esc(ev.entityvalue.valuenew?.name||'?')}»
`; return`
${esc(tn)}
${body}
`; }).join(''); return`
${avatarInitials(entry.username)}
${esc(abbrevName(entry.username))} ${new Date(entry.eventat).toLocaleString('ru-RU')} · ${timeAgo(new Date(entry.eventat))}
${evHtml}
`; }).join(''); el.innerHTML=`${db}
Событий: ${entries.length}
${items}`; } // ── Submit comment ──────────────────────────────────────────────── async function submitComment(){ if(currentModalIdx===null) return; const ta=document.getElementById('composer-ta'); const text=ta?.value?.trim(); if(!text) return; const tid=String(allData[currentModalIdx].task.id); ta.disabled=true; const r=await apiPost('/api/comments',{task_id:tid,text}); if(r?.id){ if(!commentsCache[tid]) commentsCache[tid]=[]; commentsCache[tid].push(r); if(typeof commentCounts==='object') commentCounts[tid]=commentsCache[tid].length; ta.value=''; if(currentTab==='comments'){ const sb=document.getElementById('sp-body'); if(sb) sb.innerHTML=renderCommentsHtml(tid,commentsCache[tid]); } const dt=document.getElementById('dtab-comments'); if(dt) dt.textContent=`💬 Наши (${commentsCache[tid].length})`; renderTable(); } else showToast('❌ Ошибка отправки','warn'); ta.disabled=false; } // alias function submitCommentFromModal(){submitComment();} function insertAtSymbol(){ const ta=document.getElementById('composer-ta'); if(!ta) return; const p=ta.selectionStart; ta.value=ta.value.slice(0,p)+'@'+ta.value.slice(p); ta.selectionStart=ta.selectionEnd=p+1; ta.focus(); onCommentInput(ta); } // ── Note editor ─────────────────────────────────────────────────── function openNoteEditor(idx,e){ e&&e.stopPropagation(); openNoteIdx=openNoteIdx===idx?null:idx; if(currentModalIdx===idx) renderSidePanel(idx); else renderTable(); } function closeNoteEditor(){ openNoteIdx=null; if(currentModalIdx!==null) renderSidePanel(currentModalIdx); else renderTable(); } async function saveNoteAndClose(idx,e){ e&&e.stopPropagation(); const ta=document.getElementById('detail-note-ta'); const tid=String(allData[idx].task.id); const text=ta?ta.value:''; if(text.trim()) notesCache[tid]={text:text.trim(),by:currentUser.username,at:new Date().toISOString()}; else delete notesCache[tid]; openNoteIdx=null; if(currentModalIdx===idx) renderSidePanel(idx); renderTable(); const r=await apiPost('/api/notes',{task_id:tid,text}); if(r&&!r.ok&&r.error) showToast('❌ '+r.error,'warn'); } // ── Settings ────────────────────────────────────────────────────── function openSettingsModal(){ document.getElementById('settings-modal')?.classList.add('open'); if(typeof openSettingsTab==='function') openSettingsTab('api'); if(currentUser?.is_admin){ if(typeof loadIntraStatusDictionary==='function') loadIntraStatusDictionary(); if(typeof loadInternalStatusesAdmin==='function') loadInternalStatusesAdmin(); } } function closeSettingsModal(){document.getElementById('settings-modal')?.classList.remove('open');} // ── SSE refresh ─────────────────────────────────────────────────── function refreshDetailIfOpen(tid){ if(currentModalIdx===null) return; if(String(allData[currentModalIdx].task.id)!==String(tid)) return; renderSidePanel(currentModalIdx); if(currentTab==='comments'){ const sb=document.getElementById('sp-body'); if(sb) sb.innerHTML=renderCommentsHtml(tid,commentsCache[tid]||[]); } } // Инициализация динамических чипов и bulk-select при загрузке DOM document.addEventListener('DOMContentLoaded', () => { initMarkChips(); syncChipDropdownItems(); syncAdvancedFiltersUi(); // Close chip dropdown on outside click document.addEventListener('click', e => { if (!e.target.closest('#chip-dropdown-wrap')) { document.getElementById('chip-dropdown-panel')?.classList.remove('chip-dd-open'); } if (!e.target.closest('.adv-filter-wrap')) { closeAdvancedFilterDropdowns(); } }); });