// ── Column visibility ─────────────────────────────────────────────
const ALL_COLS = [
{ key:'num', label:'# Заявка', def:true },
{ key:'title', label:'Тема', def:true },
{ key:'intrastatus', label:'Статус в интре', def:true },
{ key:'servstatus', 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: 'Объекты',
hasSearch: true,
},
'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=`${emptyLabel} `;
return;
}
const html=items.map(({value,label})=>`${esc(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 getIntraStatusFilterColor(label){
const lc=String(label||'').toLowerCase();
if(lc.includes('открыт')||lc.includes('нов')||lc.includes('назнач')) return 'var(--blue)';
if(lc.includes('работ')||lc.includes('ожид')||lc.includes('progress')) return 'var(--orange)';
if(lc.includes('выполн')||lc.includes('закрыт')||lc.includes('реш')) return 'var(--green)';
if(lc.includes('hold')||lc.includes('пауза')||lc.includes('приост')) return '#faad14';
return 'var(--text3)';
}
function getAdvancedFilterOptionColor(selectId,opt){
const value=String(opt?.value||'');
const label=String(opt?.textContent||'');
if(selectId==='filter-who-select'){
if(value==='internal') return 'var(--green)';
if(value==='external') return 'var(--red)';
return 'var(--text3)';
}
if(selectId==='filter-internal-select'){
const m=MARKS_CONFIG.find((x)=>String(x.key)===value);
return m?.colorDot||m?.chipDotColor||m?.ispColor||'var(--text3)';
}
if(selectId==='filter-intra-select') return getIntraStatusFilterColor(label);
return avatarColor(label||value);
}
function setAdvancedFilterSelection(selectId,selected){
const sel=document.getElementById(selectId);
if(!sel) return;
[...sel.options].forEach((opt)=>{
if(opt.disabled) return;
if(String(opt.value).trim()==='') return;
opt.selected=!!selected;
});
updateAdvancedFilterLabel(selectId);
buildAdvancedFilterPanel(selectId);
renderTable();
}
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;
}
if (cfg.hasSearch) {
const searchWrap = document.createElement('div');
searchWrap.className = 'adv-filter-search-wrap';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'adv-filter-search';
searchInput.placeholder = 'Поиск…';
searchInput.setAttribute('autocomplete', 'off');
searchInput.addEventListener('input', () => {
const q = searchInput.value.toLowerCase();
panel.querySelectorAll('.adv-filter-item').forEach(item => {
const text = item.querySelector('.adv-filter-text')?.textContent || '';
item.style.display = text.toLowerCase().includes(q) ? '' : 'none';
});
});
searchInput.addEventListener('click', e => e.stopPropagation());
searchWrap.appendChild(searchInput);
panel.appendChild(searchWrap);
// Focus after render
requestAnimationFrame(() => searchInput.focus());
}
const selectedCount=opts.filter((o)=>o.selected).length;
const actions=document.createElement('div');
actions.className='adv-filter-actions';
const btnAll=document.createElement('button');
btnAll.type='button';
btnAll.className='adv-filter-action-btn';
btnAll.textContent='Выбрать все';
btnAll.disabled=selectedCount===opts.length;
btnAll.addEventListener('click',(e)=>{
e.stopPropagation();
setAdvancedFilterSelection(selectId,true);
});
const btnNone=document.createElement('button');
btnNone.type='button';
btnNone.className='adv-filter-action-btn';
btnNone.textContent='Снять все';
btnNone.disabled=selectedCount===0;
btnNone.addEventListener('click',(e)=>{
e.stopPropagation();
setAdvancedFilterSelection(selectId,false);
});
actions.appendChild(btnAll);
actions.appendChild(btnNone);
panel.appendChild(actions);
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 dot=document.createElement('span');
dot.className='adv-filter-dot';
dot.style.background=getAdvancedFilterOptionColor(selectId,opt);
const text=document.createElement('span');
text.className='adv-filter-text';
text.textContent=opt.textContent;
item.appendChild(cb);
item.appendChild(dot);
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='Set status Clear status ';
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)} `;
}
// ── IntraDesk additional field — badge helper ─────────────────────
function _addfieldBadgeHtml(val) {
if (!val) return '— ';
const match = (typeof MARKS_CONFIG !== 'undefined' ? MARKS_CONFIG : [])
.find((m) => m.label && m.label.toLowerCase() === val.toLowerCase());
if (match) {
return `` +
` ${esc(val)} `;
}
return ` ${esc(val)} `;
}
// ── IntraDesk additional field — interactive picker ───────────────
function intraAddfieldPickerHtml(task) {
const tid = String(task.id);
const val = task._addfield_status || null;
const cfg = typeof MARKS_CONFIG !== 'undefined' ? MARKS_CONFIG : [];
const items = cfg.map((m) => `
${esc(m.label)}
`).join('');
const clearItem = `
— очистить
`;
return `
${_addfieldBadgeHtml(val)}
`;
}
async function setIntraAddfield(taskId, value, e) {
if (e) e.stopPropagation();
const r = await apiPost(`/api/tasks/${taskId}/intra-addfield`, { value: value || null });
if (!r?.ok) { showToast(`❌ ${r?.error || 'Ошибка'}`, 'warn'); return; }
// Обновляем кеш локально до следующего SSE
const item = allData.find((d) => String(d.task.id) === String(taskId));
if (item) { item.task._addfield_status = value || null; renderTable(); }
}
// 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)}
`;
}
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('servstatus') ?'Служ. статус в интре ':''}
${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)?``:'';
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('servstatus') ?`${intraAddfieldPickerHtml(task)} `:''}
${sc('status') ?`${statusPickerHtml(tid)} `:''}
${sc('client') ?`
${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=``;
// 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+=`${p} `;
} 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=`
${MARKS_CONFIG.map(m=>{
const active=isMark(tid,m.key);
return`${m.btnIcon} ${m.btnLabel} `;
}).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`
${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``;
}).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`${body}
`;
}).join('');
return`
${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();
}
});
});