// ── 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' 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(); } function toggleAllChip(){ const t=!STATE_CHIP_KEYS.every(k=>stateChips[k]); STATE_CHIP_KEYS.forEach(k=>{stateChips[k]=t;setChip(document.getElementById('chip-'+k),t);}); setChip(document.getElementById('chip-all'),t); 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){ 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); }); } } 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 internalStatusBadge(tid){ const m=MARKS_CONFIG.find(m=>isMark(tid,m.key)); if(m) return`${m.label}`; return`Без статуса`; } // ── IntraDesk status badge ──────────────────────────────────────── function intraStatusBadge(task) { const name = task._statusname || task.statusname || ''; if (!name) { // Показываем числовой id статуса как fallback const sid = task.statusid || task.status; if (sid) return `#${sid}`; return ''; } const lc = name.toLowerCase(); let cls = 'sb-intra-other'; if (lc.includes('открыт') || lc.includes('new') || lc.includes('открытый')) 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('ожидает') || 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; filteredData=allData.filter(item=>{ const tid=String(item.task.id); const marked=MARKS_CONFIG.some(m=>isMark(tid,m.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&&!mentionsData.some(m=>String(m.task_id)===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(statFilter!=='all'&&getAuthorType(item)!==statFilter) 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 base=allData.filter(i=>taskMatchesDate(i.task)); const g=id=>document.getElementById(id); const sv=(id,v)=>{const e=g(id);if(e)e.textContent=v;}; sv('chip-all-cnt', base.length); MARKS_CONFIG.forEach(m=>{ sv(`chip-${m.key}-cnt`, base.filter(i=>isMark(String(i.task.id),m.key)).length); }); sv('chip-unmarked-cnt', base.filter(i=>{ const tid=String(i.task.id);return !MARKS_CONFIG.some(m=>isMark(tid,m.key)); }).length); sv('stat-total', base.length); sv('stat-external', base.filter(i=>getAuthorType(i)==='external').length); sv('stat-internal', base.filter(i=>getAuthorType(i)==='internal').length); sv('stat-nocomment', base.filter(i=>getAuthorType(i)==='nocomment').length); sv('stat-unread', allData.filter(i=>isUnread(i.task)).length); sv('nav-badge-tasks', filteredData.length); sv('topbar-sub', `· ${filteredData.length} заявок`); sv('total-loaded', filteredData.length); sv('total-count', allData.length); sv('tf-info', `${filteredData.length} из ${allData.length}`); // Tab counts (using allData for totals regardless of chip filter) sv('tcnt-all', allData.length); sv('tcnt-open', allData.filter(i=>isTaskOpen(i)).length); sv('tcnt-done', allData.filter(i=>isTaskDone(i)).length); sv('tcnt-empty', allData.filter(i=>isTaskEmpty(i)).length); 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 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=allData.indexOf(item); const unread=isUnread(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 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=STATUS_MARKS.find(m=>marksCache[tid][m]?.active); STATUS_MARKS.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{ STATUS_MARKS.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');} 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(); // 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'); } }); });