Compare commits

..

No commits in common. "7d297ad2d979394349a6ed0af29ba94c28ff8e0d" and "7f3e277c43b419e417faf83229bb43198804e0fc" have entirely different histories.

6 changed files with 68 additions and 387 deletions

View file

@ -1310,91 +1310,12 @@ html.dark .stats-card { background:var(--surface); }
border-top:1px dashed var(--border); border-top:1px dashed var(--border);
margin-top:-4px; margin-top:-4px;
padding-top:10px; padding-top:10px;
align-items:stretch;
} }
.filter-native-hidden{display:none!important;} .filter-multi{
min-width:160px;
.adv-filter-wrap{ min-height:74px;
position:relative; background-image:none;
min-width:180px; padding-right:10px;
max-width:260px;
flex:1 1 180px;
}
.adv-filter-btn{
width:100%;
display:flex;
align-items:center;
justify-content:space-between;
gap:8px;
padding:7px 10px;
border:1px solid var(--border);
border-radius:var(--radius-sm);
background:var(--surface2);
color:var(--text2);
font-family:inherit;
font-size:12px;
cursor:pointer;
transition:border-color .15s,color .15s,background .15s;
}
.adv-filter-btn:hover{
border-color:var(--accent);
color:var(--accent);
}
.adv-filter-btn span{
min-width:0;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
text-align:left;
}
.adv-filter-wrap.open .adv-filter-btn{
border-color:var(--accent);
color:var(--accent);
background:var(--accent-light);
}
.adv-filter-panel{
display:none;
position:absolute;
top:calc(100% + 4px);
left:0;
right:0;
z-index:320;
max-height:260px;
overflow:auto;
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius);
box-shadow:0 8px 24px rgba(0,0,0,.13);
padding:6px;
}
.adv-filter-panel.open{display:block;}
.adv-filter-item{
display:flex;
align-items:flex-start;
gap:8px;
padding:6px 7px;
border-radius:6px;
font-size:12px;
color:var(--text2);
cursor:pointer;
user-select:none;
}
.adv-filter-item:hover{background:var(--surface2);}
.adv-filter-item input{
margin-top:2px;
accent-color:var(--accent);
cursor:pointer;
}
.adv-filter-item span{
flex:1;
min-width:0;
line-height:1.35;
word-break:break-word;
}
.adv-filter-empty{
padding:8px 10px;
color:var(--text3);
font-size:12px;
} }
/* ── Chip Dropdown ────────────────────────────────────── */ /* ── Chip Dropdown ────────────────────────────────────── */

View file

@ -298,50 +298,16 @@
<input class="filter-search" type="text" id="search-input" placeholder="Search..." oninput="renderTable()"> <input class="filter-search" type="text" id="search-input" placeholder="Search..." oninput="renderTable()">
</div> </div>
<div class="filter-row filter-row-advanced"> <div class="filter-row filter-row-advanced">
<div class="adv-filter-wrap" id="filter-who-wrap"> <select class="filter-select" id="filter-who-select" onchange="renderTable()">
<button class="adv-filter-btn" type="button" onclick="toggleAdvancedFilterDropdown(event,'filter-who-select')"> <option value="all">Who answered: all</option>
<span id="filter-who-label">Кто ответил: все</span> <option value="internal">Internal</option>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg> <option value="external">External</option>
</button> <option value="nocomment">No comment</option>
<div class="adv-filter-panel" id="filter-who-panel"></div> </select>
<select class="filter-select filter-native-hidden" id="filter-who-select" multiple></select> <select class="filter-select filter-multi" id="filter-executor-select" multiple onchange="renderTable()"></select>
</div> <select class="filter-select filter-multi" id="filter-object-select" multiple onchange="renderTable()"></select>
<select class="filter-select filter-multi" id="filter-intra-select" multiple onchange="renderTable()"></select>
<div class="adv-filter-wrap" id="filter-executor-wrap"> <select class="filter-select filter-multi" id="filter-internal-select" multiple onchange="renderTable()"></select>
<button class="adv-filter-btn" type="button" onclick="toggleAdvancedFilterDropdown(event,'filter-executor-select')">
<span id="filter-executor-label">Исполнители: все</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="adv-filter-panel" id="filter-executor-panel"></div>
<select class="filter-select filter-native-hidden" id="filter-executor-select" multiple></select>
</div>
<div class="adv-filter-wrap" id="filter-object-wrap">
<button class="adv-filter-btn" type="button" onclick="toggleAdvancedFilterDropdown(event,'filter-object-select')">
<span id="filter-object-label">Объекты: все</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="adv-filter-panel" id="filter-object-panel"></div>
<select class="filter-select filter-native-hidden" id="filter-object-select" multiple></select>
</div>
<div class="adv-filter-wrap" id="filter-intra-wrap">
<button class="adv-filter-btn" type="button" onclick="toggleAdvancedFilterDropdown(event,'filter-intra-select')">
<span id="filter-intra-label">Статус Intra: все</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="adv-filter-panel" id="filter-intra-panel"></div>
<select class="filter-select filter-native-hidden" id="filter-intra-select" multiple></select>
</div>
<div class="adv-filter-wrap" id="filter-internal-wrap">
<button class="adv-filter-btn" type="button" onclick="toggleAdvancedFilterDropdown(event,'filter-internal-select')">
<span id="filter-internal-label">Внутр. статусы: все</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="adv-filter-panel" id="filter-internal-panel"></div>
<select class="filter-select filter-native-hidden" id="filter-internal-select" multiple></select>
</div>
</div> </div>
</div> </div>

View file

@ -60,7 +60,6 @@ function showApp() {
let lastLoadedSettings = {}; let lastLoadedSettings = {};
let internalStatusesAdminCache = []; let internalStatusesAdminCache = [];
let missingCommentsFetchInFlight = false;
async function init() { async function init() {
await loadInternalStatusesConfig(false, true); await loadInternalStatusesConfig(false, true);
@ -138,7 +137,6 @@ async function loadTasksFromServer(silent = false) {
} }
async function fetchMissingComments() { async function fetchMissingComments() {
if (missingCommentsFetchInFlight) return;
const toFetch = allData.filter((i) => { const toFetch = allData.filter((i) => {
if (i.lastComment) return false; if (i.lastComment) return false;
const tid = String(i.task.id); const tid = String(i.task.id);
@ -147,55 +145,32 @@ async function fetchMissingComments() {
}); });
if (!toFetch.length) return; if (!toFetch.length) return;
missingCommentsFetchInFlight = true; for (const item of toFetch) {
try { const tid = String(item.task.id);
const CONCURRENCY = 8; const upd = encodeURIComponent(item.task.updatedat || '');
let cursor = 0; let lc = null;
let loaded = 0; try {
const res = await apiGet(`/api/tasks/lastcomment/${tid}?upd=${upd}`);
async function worker() { if (res?.entries?.length) {
while (true) { const entry = res.entries[0];
const idx = cursor; const ev = (entry.events?.data || []).find((e) => COMMENT_TYPES.has(e.type));
cursor += 1; if (ev) {
if (idx >= toFetch.length) return; lc = {
username: entry.username,
const item = toFetch[idx]; userid: entry.userid,
const tid = String(item.task.id); usertype: entry.usertype,
const upd = encodeURIComponent(item.task.updatedat || ''); eventat: entry.eventat,
let lc = null; text: ev.stringvalue || '',
isPrivate: ev.type === 55,
try { eventType: ev.type,
const res = await apiGet(`/api/tasks/lastcomment/${tid}?upd=${upd}`); };
if (res?.entries?.length) { }
const entry = res.entries[0];
const ev = (entry.events?.data || []).find((e) => COMMENT_TYPES.has(e.type));
if (ev) {
lc = {
username: entry.username,
userid: entry.userid,
usertype: entry.usertype,
eventat: entry.eventat,
text: ev.stringvalue || '',
isPrivate: ev.type === 55,
eventType: ev.type,
};
}
}
} catch {}
item.lastComment = lc;
forcedCommentCache.set(tid, { updatedat: item.task.updatedat, lastComment: lc });
loaded += 1;
if (loaded % 25 === 0 && typeof renderTable === 'function') renderTable(true);
} }
} } catch {}
item.lastComment = lc;
const workers = Array.from({ length: Math.min(CONCURRENCY, toFetch.length) }, () => worker()); forcedCommentCache.set(tid, { updatedat: item.task.updatedat, lastComment: lc });
await Promise.all(workers);
} finally {
missingCommentsFetchInFlight = false;
if (typeof renderTable === 'function') renderTable(true);
} }
renderTable();
} }
async function loadTasks() { async function loadTasks() {

View file

@ -147,7 +147,6 @@ function applyTasksDiff(newRawTasks, silent = false) {
document.getElementById('update-banner')?.classList.remove('visible'); document.getElementById('update-banner')?.classList.remove('visible');
if (typeof onTasksLoaded === 'function') onTasksLoaded(); if (typeof onTasksLoaded === 'function') onTasksLoaded();
if (typeof refreshAdvancedFilters === 'function') refreshAdvancedFilters();
if (typeof renderTable === 'function') renderTable(); if (typeof renderTable === 'function') renderTable();
if (typeof forceComment !== 'undefined' && forceComment) { if (typeof forceComment !== 'undefined' && forceComment) {
if (typeof fetchMissingComments === 'function') fetchMissingComments(); if (typeof fetchMissingComments === 'function') fetchMissingComments();

View file

@ -64,12 +64,8 @@ function onMarksConfigChanged() {
rebuildStateChipsFromConfig(); rebuildStateChipsFromConfig();
// ── Seen ────────────────────────────────────────────────────────── // ── Seen ──────────────────────────────────────────────────────────
function seenStorageKey() { function loadSeen() { try { return JSON.parse(localStorage.getItem(SEEN_KEY)||'{}'); } catch { return {}; } }
const username = String(currentUser?.username || '').trim().toLowerCase(); function saveSeen(s) { try { localStorage.setItem(SEEN_KEY, JSON.stringify(s)); } catch {} }
return username ? `${SEEN_KEY}:${username}` : SEEN_KEY;
}
function loadSeen() { try { return JSON.parse(localStorage.getItem(seenStorageKey())||'{}'); } catch { return {}; } }
function saveSeen(s) { try { localStorage.setItem(seenStorageKey(), JSON.stringify(s||{})); } catch {} }
function markSeen(id,upd) { const s=loadSeen(); s[id]=upd; saveSeen(s); } function markSeen(id,upd) { const s=loadSeen(); s[id]=upd; saveSeen(s); }
function markAllRead(){ function markAllRead(){
const s=loadSeen(); const s=loadSeen();

View file

@ -16,44 +16,6 @@ let statFilter = 'all';
let selectedRows = new Set(); let selectedRows = new Set();
let mainTab = 'all'; // 'all' | 'open' | 'done' | 'empty' 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){ function getSelectedSet(selectId){
const el=document.getElementById(selectId); const el=document.getElementById(selectId);
if(!el) return new Set(); if(!el) return new Set();
@ -72,102 +34,10 @@ function fillMultiSelect(selectId, items, selectedSet, emptyLabel){
[...el.options].forEach((opt)=>{ opt.selected=selectedSet.has(String(opt.value)); }); [...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(){ function refreshAdvancedFilters(){
const selectedWho=getSelectedSet('filter-who-select'); const who=document.getElementById('filter-who-select');
if(who && !who.value) who.value='all';
const selectedExecutors=getSelectedSet('filter-executor-select'); const selectedExecutors=getSelectedSet('filter-executor-select');
const selectedObjects=getSelectedSet('filter-object-select'); const selectedObjects=getSelectedSet('filter-object-select');
const selectedIntra=getSelectedSet('filter-intra-select'); const selectedIntra=getSelectedSet('filter-intra-select');
@ -192,19 +62,12 @@ function refreshAdvancedFilters(){
const execItems=[...execMap.entries()].map(([value,label])=>({value,label})).sort((a,b)=>a.label.localeCompare(b.label,'ru')); 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 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 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})); 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-executor-select', execItems, selectedExecutors, 'No executors');
fillMultiSelect('filter-object-select', objectItems, selectedObjects, 'No objects'); fillMultiSelect('filter-object-select', objectItems, selectedObjects, 'No objects');
fillMultiSelect('filter-intra-select', intraItems, selectedIntra, 'No Intra statuses'); fillMultiSelect('filter-intra-select', intraItems, selectedIntra, 'No Intra statuses');
fillMultiSelect('filter-internal-select', internalItems, selectedInternal, 'No statuses'); fillMultiSelect('filter-internal-select', internalItems, selectedInternal, 'No statuses');
syncAdvancedFiltersUi();
} }
function setMainTab(tab, el) { function setMainTab(tab, el) {
@ -499,14 +362,11 @@ function renderTable(keepPage){
if(!keepPage) currentPage = 1; if(!keepPage) currentPage = 1;
const search =document.getElementById('search-input')?.value?.toLowerCase()||''; const search =document.getElementById('search-input')?.value?.toLowerCase()||'';
const remoteOnly =document.getElementById('opt-remote-only')?.checked; const remoteOnly =document.getElementById('opt-remote-only')?.checked;
const selectedWho=getSelectedSet('filter-who-select'); const whoSelect =document.getElementById('filter-who-select')?.value||'all';
const selectedExecutors=getSelectedSet('filter-executor-select'); const selectedExecutors=getSelectedSet('filter-executor-select');
const selectedObjects=getSelectedSet('filter-object-select'); const selectedObjects=getSelectedSet('filter-object-select');
const selectedIntra=getSelectedSet('filter-intra-select'); const selectedIntra=getSelectedSet('filter-intra-select');
const selectedInternal=getSelectedSet('filter-internal-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=>{ filteredData=allData.filter(item=>{
const tid=String(item.task.id); const tid=String(item.task.id);
@ -514,7 +374,7 @@ function renderTable(keepPage){
const activeInternal=MARKS_CONFIG.find((m)=>isMark(tid,m.key))?.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;} for(const m of MARKS_CONFIG){if(isMark(tid,m.key)&&!stateChips[m.key])return false;}
if(!marked&&!stateChips.unmarked) return false; if(!marked&&!stateChips.unmarked) return false;
if(mentionedFilterOn&&mentionTaskIds&&!mentionTaskIds.has(tid)) 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(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(remoteOnly&&item.task.service!==84683) return false;
if(!taskMatchesDate(item.task)) return false; if(!taskMatchesDate(item.task)) return false;
@ -530,12 +390,8 @@ function renderTable(keepPage){
if(selectedInternal.size){ if(selectedInternal.size){
if(!activeInternal || !selectedInternal.has(activeInternal)) return false; if(!activeInternal || !selectedInternal.has(activeInternal)) return false;
} }
const authorType=getAuthorType(item); const whoFilter=whoSelect!=='all'?whoSelect:statFilter;
if(statFilter!=='all'){ if(whoFilter!=='all'&&getAuthorType(item)!==whoFilter) return false;
if(authorType!==statFilter) return false;
}else if(selectedWho.size&& !selectedWho.has(authorType)){
return false;
}
// Main tab filter // Main tab filter
if(mainTab==='open' && !isTaskOpen(item)) return false; if(mainTab==='open' && !isTaskOpen(item)) return false;
if(mainTab==='done' && !isTaskDone(item)) return false; if(mainTab==='done' && !isTaskDone(item)) return false;
@ -558,57 +414,31 @@ function renderTable(keepPage){
} }
// Counts // Counts
const markCounts=Object.fromEntries(MARKS_CONFIG.map((m)=>[m.key,0])); const base=allData.filter(i=>taskMatchesDate(i.task));
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 g=id=>document.getElementById(id);
const sv=(id,v)=>{const e=g(id);if(e)e.textContent=v;}; const sv=(id,v)=>{const e=g(id);if(e)e.textContent=v;};
sv('chip-all-cnt', baseLen); sv('chip-all-cnt', base.length);
MARKS_CONFIG.forEach((m)=>{ sv(`chip-${m.key}-cnt`, markCounts[m.key]||0); }); MARKS_CONFIG.forEach(m=>{
sv('chip-unmarked-cnt', baseUnmarked); sv(`chip-${m.key}-cnt`, base.filter(i=>isMark(String(i.task.id),m.key)).length);
sv('stat-total', baseLen); });
sv('stat-external', baseExternal); sv('chip-unmarked-cnt', base.filter(i=>{
sv('stat-internal', baseInternal); const tid=String(i.task.id);return !MARKS_CONFIG.some(m=>isMark(tid,m.key));
sv('stat-nocomment', baseNoComment); }).length);
sv('stat-unread', unreadTotal); 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('nav-badge-tasks', filteredData.length);
sv('topbar-sub', `\u00b7 ${filteredData.length} \u0437\u0430\u044f\u0432\u043e\u043a`); sv('topbar-sub', `· ${filteredData.length} заявок`);
sv('total-loaded', filteredData.length); sv('total-loaded', filteredData.length);
sv('total-count', allData.length); sv('total-count', allData.length);
sv('tf-info', `${filteredData.length} \u0438\u0437 ${allData.length}`); sv('tf-info', `${filteredData.length} из ${allData.length}`);
// Tab counts (using allData for totals regardless of chip filter) // Tab counts (using allData for totals regardless of chip filter)
sv('tcnt-all', allData.length); sv('tcnt-all', allData.length);
sv('tcnt-open', tabOpen); sv('tcnt-open', allData.filter(i=>isTaskOpen(i)).length);
sv('tcnt-done', tabDone); sv('tcnt-done', allData.filter(i=>isTaskDone(i)).length);
sv('tcnt-empty', tabEmpty); sv('tcnt-empty', allData.filter(i=>isTaskEmpty(i)).length);
if(!filteredData.length){ if(!filteredData.length){
g('table-wrap').innerHTML=`<div class="state-box"><span class="icon">🔍</span><div class="msg">Заявок не найдено</div><div class="sub">Измените фильтры</div></div>`; g('table-wrap').innerHTML=`<div class="state-box"><span class="icon">🔍</span><div class="msg">Заявок не найдено</div><div class="sub">Измените фильтры</div></div>`;
@ -621,8 +451,6 @@ function renderTable(keepPage){
const totalPages = PAGE_SIZE > 0 ? Math.ceil(filteredData.length / PAGE_SIZE) : 1; const totalPages = PAGE_SIZE > 0 ? Math.ceil(filteredData.length / PAGE_SIZE) : 1;
if(currentPage > totalPages) currentPage = totalPages; if(currentPage > totalPages) currentPage = totalPages;
const pageData = PAGE_SIZE > 0 ? filteredData.slice((currentPage-1)*PAGE_SIZE, currentPage*PAGE_SIZE) : filteredData; 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 sc=k=>visibleCols.has(k);
const sortArrow = col => sortCol===col ? (sortDir==='asc' ? ' ↑' : ' ↓') : ' ↕'; const sortArrow = col => sortCol===col ? (sortDir==='asc' ? ' ↑' : ' ↓') : ' ↕';
@ -643,8 +471,8 @@ function renderTable(keepPage){
const {task,lastComment}=item; const {task,lastComment}=item;
const tid=String(task.id); const tid=String(task.id);
const num=task.tasknumber||task.id; const num=task.tasknumber||task.id;
const idx=idxMap.get(item); const idx=allData.indexOf(item);
const unread=isUnreadFast(task); const unread=isUnread(task);
const note=getNote(tid); const note=getNote(tid);
const cc=rowColorClass(tid); const cc=rowColorClass(tid);
const rowCls=((unread&&!cc?'row-unread ':'')+cc+(currentModalIdx===idx?' active-row':'')).trim(); const rowCls=((unread&&!cc?'row-unread ':'')+cc+(currentModalIdx===idx?' active-row':'')).trim();
@ -1123,14 +951,10 @@ function refreshDetailIfOpen(tid){
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initMarkChips(); initMarkChips();
syncChipDropdownItems(); syncChipDropdownItems();
syncAdvancedFiltersUi();
// Close chip dropdown on outside click // Close chip dropdown on outside click
document.addEventListener('click', e => { document.addEventListener('click', e => {
if (!e.target.closest('#chip-dropdown-wrap')) { if (!e.target.closest('#chip-dropdown-wrap')) {
document.getElementById('chip-dropdown-panel')?.classList.remove('chip-dd-open'); document.getElementById('chip-dropdown-panel')?.classList.remove('chip-dd-open');
} }
if (!e.target.closest('.adv-filter-wrap')) {
closeAdvancedFilterDropdowns();
}
}); });
}); });