async function doLogin() { const username = document.getElementById('login-username').value.trim(); const password = document.getElementById('login-password').value; const errEl = document.getElementById('login-error'); errEl.textContent = ''; if (!username || !password) { errEl.textContent = 'Заполните все поля'; return; } const r = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); const data = await r.json(); if (!r.ok) { errEl.textContent = data.error || 'Ошибка входа'; return; } authToken = data.token; currentUser = { username: data.username, is_admin: data.is_admin }; localStorage.setItem('intradesk_token', authToken); showApp(); init(); } document.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !document.getElementById('login-overlay').classList.contains('hidden')) doLogin(); }); function doLogout() { localStorage.removeItem('intradesk_token'); authToken = null; currentUser = null; reloadInFlight = false; if (statusPollTimer) clearInterval(statusPollTimer); if (clientCountdownTimer) clearInterval(clientCountdownTimer); stopSSE(); document.getElementById('app-shell').style.display = 'none'; document.getElementById('login-overlay').classList.remove('hidden'); document.getElementById('login-password').value = ''; } function showApp() { document.getElementById('login-overlay').classList.add('hidden'); document.getElementById('app-shell').style.display = 'flex'; const u = currentUser.username; const initials = avatarInitials(u); const color = avatarColor(u); const av = document.getElementById('sb-avatar'); if (av) { av.textContent = initials; av.style.background = color; } if (document.getElementById('sb-username')) document.getElementById('sb-username').textContent = u; if (document.getElementById('sb-email')) document.getElementById('sb-email').textContent = `${u}@cleverparks.io`; const ns = document.getElementById('nav-settings'); if (ns) ns.style.display = currentUser.is_admin ? 'flex' : 'none'; } let lastLoadedSettings = {}; let internalStatusesAdminCache = []; let missingCommentsFetchInFlight = false; async function init() { await loadInternalStatusesConfig(false, true); await Promise.all([loadMarks(), loadNotes(), loadMentions(), loadUserList(), loadCommentCounts(), loadClientDict(), loadClientUsers()]); await loadTasksFromServer(); if (currentUser.is_admin) { const s = await apiGet('/api/settings'); if (s) { applySettings(s); await loadIntraStatusDictionary(); } await loadInternalStatusesAdmin(); loadUsersTable(); } const status = await apiGet('/api/status'); if (status) { serverRefreshInterval = status.auto_refresh_interval || 300; lastKnownUpdatedAt = status.last_updated; if (status.cache_version !== undefined && status.cache_version !== null) { lastKnownCacheVersion = Number(status.cache_version) || 0; } } startSSE(); startStatusPolling(); } async function loadInternalStatusesConfig(includeAll = false, applyToRuntime = true) { const suffix = includeAll ? '?include_all=1' : ''; const d = await apiGet(`/api/internal-statuses${suffix}`); const statuses = d?.statuses || []; if (applyToRuntime && statuses.length && typeof setMarksConfig === 'function') { const active = statuses.filter((s) => s.is_active !== false); setMarksConfig(active); if (typeof syncChipDropdownItems === 'function') syncChipDropdownItems(); if (typeof initMarkChips === 'function') initMarkChips(); } return statuses; } async function loadMarks() { const d = await apiGet('/api/marks'); if (d) marksCache = d; } async function loadNotes() { const d = await apiGet('/api/notes'); if (d) notesCache = d; } async function loadUserList() { const d = await apiGet('/api/users/list'); if (d) knownUsers = d.map((u) => u.toLowerCase()); } async function loadCommentCounts() { const d = await apiGet('/api/comments/counts'); if (d && typeof d === 'object') commentCounts = d; } async function loadClientDict() { const d = await apiGet('/api/clients'); if (Array.isArray(d)) window._allClients = d; } async function loadClientUsers() { const d = await apiGet('/api/client-users'); if (d && typeof d === 'object') window._clientUsers = d; } async function loadTasksFromServer(silent = false) { if (!silent && !allData.length) showLoading(); const data = await apiGet('/api/tasks'); if (!data) return; allData = (data.tasks || []).map((task) => { const lc = extractLastComment(task); if (!lc && forceComment) { const tid = String(task.id); const cached = forcedCommentCache.get(tid); if (cached && cached.updatedat === task.updatedat) return { task, lastComment: cached.lastComment }; } return { task, lastComment: lc }; }); lastKnownUpdatedAt = data.updated_at; if (data.cache_version !== undefined && data.cache_version !== null) { lastKnownCacheVersion = Number(data.cache_version) || lastKnownCacheVersion; } const el = (id) => document.getElementById(id); if (el('total-loaded')) el('total-loaded').textContent = allData.length; if (el('total-count')) el('total-count').textContent = allData.length; if (el('last-update')) el('last-update').textContent = data.updated_at ? new Date(data.updated_at).toLocaleTimeString('ru-RU') : '—'; document.getElementById('update-banner')?.classList.remove('visible'); onTasksLoaded(); if (typeof refreshAdvancedFilters === 'function') refreshAdvancedFilters(); renderTable(); if (forceComment) fetchMissingComments(); } async function fetchMissingComments() { if (missingCommentsFetchInFlight) return; const toFetch = allData.filter((i) => { if (i.lastComment) return false; const tid = String(i.task.id); const cached = forcedCommentCache.get(tid); return !cached || cached.updatedat !== i.task.updatedat; }); if (!toFetch.length) return; missingCommentsFetchInFlight = true; try { const CONCURRENCY = 8; let cursor = 0; let loaded = 0; async function worker() { while (true) { const idx = cursor; cursor += 1; if (idx >= toFetch.length) return; const item = toFetch[idx]; const tid = String(item.task.id); const upd = encodeURIComponent(item.task.updatedat || ''); let lc = null; try { 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); } } const workers = Array.from({ length: Math.min(CONCURRENCY, toFetch.length) }, () => worker()); await Promise.all(workers); } finally { missingCommentsFetchInFlight = false; if (typeof renderTable === 'function') renderTable(true); } } async function loadTasks() { const btn = document.getElementById('load-btn'); btn.disabled = true; btn.textContent = '⏳ Загрузка...'; try { const r = await apiPost('/api/tasks/refresh', {}); if (r?.error) { showToast(`❌ ${r.error}`, 'warn'); return; } await Promise.all([loadMarks(), loadNotes()]); await loadTasksFromServer(true); } finally { btn.disabled = false; btn.textContent = '↻ Обновить'; } } async function loadAllTasks() { const btn = document.getElementById('refresh-all-btn'); const status = document.getElementById('refresh-all-status'); if (btn) { btn.disabled = true; btn.textContent = '⏳ Загружаю все...'; } if (status) status.textContent = 'Может занять ~30 секунд...'; try { const r = await apiPost('/api/tasks/refresh/full', {}); if (r?.error) { showToast(`❌ ${r.error}`, 'warn'); if (status) status.textContent = `Ошибка: ${r.error}`; return; } await Promise.all([loadMarks(), loadNotes()]); await loadTasksFromServer(true); if (status) status.textContent = `✓ Загружено ${r.count} заявок`; showToast(`✓ Загружено ${r.count} заявок`, 'success'); } finally { if (btn) { btn.disabled = false; btn.textContent = '🔄 Обновить ВСЕ заявки'; } } } async function reloadData() { if (reloadInFlight) return; reloadInFlight = true; try { await Promise.all([loadMarks(), loadNotes()]); await loadTasksFromServer(true); } finally { reloadInFlight = false; } } function startStatusPolling() { if (statusPollTimer) clearInterval(statusPollTimer); statusPollTimer = setInterval(async () => { const s = await apiGet('/api/status'); if (!s) return; const serverVersion = Number(s.cache_version || 0); if (serverVersion && serverVersion !== lastKnownCacheVersion) { lastKnownCacheVersion = serverVersion; if (s.last_updated) lastKnownUpdatedAt = s.last_updated; await reloadData(); return; } if (s?.last_updated && s.last_updated !== lastKnownUpdatedAt) { lastKnownUpdatedAt = s.last_updated; await reloadData(); } }, 30000); } function applySettings(s) { lastLoadedSettings = { ...(s || {}) }; const f = (id) => document.getElementById(id); if (f('status-ids')) f('status-ids').value = s.status_ids || ''; if (f('event-types')) f('event-types').value = s.event_types || ''; if (f('server-refresh-interval')) f('server-refresh-interval').value = s.auto_refresh_interval || '300'; if (f('setting-auto-reset-on-update')) f('setting-auto-reset-on-update').checked = String(s.auto_reset_on_update || '0') === '1'; if (f('setting-sync-reply-wait')) f('setting-sync-reply-wait').checked = String(s.sync_reply_wait || '0') === '1'; if (f('setting-sync-done-intra')) f('setting-sync-done-intra').checked = String(s.sync_done_with_intra || '0') === '1'; if (f('setting-sync-addfield-status')) f('setting-sync-addfield-status').checked = String(s.sync_addfield_status || '0') === '1'; if (f('setting-intra-done-status-ids')) f('setting-intra-done-status-ids').value = s.intra_done_status_ids || ''; if (f('apikey-status')) f('apikey-status').textContent = s.api_key_set ? '(ключ задан ✓)' : '(не задан!)'; syncDoneSelectFromInput(); } function getMultiValues(selectId) { const el = document.getElementById(selectId); if (!el) return []; return [...el.selectedOptions].map((o) => String(o.value).trim()).filter(Boolean); } function setMultiValues(selectId, values) { const selected = new Set((values || []).map((v) => String(v))); const el = document.getElementById(selectId); if (!el) return; [...el.options].forEach((opt) => { opt.selected = selected.has(String(opt.value)); }); } async function loadIntraStatusDictionary() { const d = await apiGet('/api/intra/status-dictionary'); const items = d?.items || []; const sel = document.getElementById('setting-intra-done-select'); if (!sel) return items; const selectedRaw = String(lastLoadedSettings.intra_done_status_ids || '') .split(',') .map((v) => String(v).trim()) .filter(Boolean); const prevSelected = selectedRaw.length ? selectedRaw : getMultiValues('setting-intra-done-select'); sel.innerHTML = items.map((it) => ``).join(''); setMultiValues('setting-intra-done-select', prevSelected); const idsInput = document.getElementById('setting-intra-done-status-ids'); if (idsInput) idsInput.value = prevSelected.join(','); return items; } function syncDoneIdsFromSelect() { const ids = getMultiValues('setting-intra-done-select'); const idsInput = document.getElementById('setting-intra-done-status-ids'); if (idsInput) idsInput.value = ids.join(','); } function syncDoneSelectFromInput() { const idsInput = document.getElementById('setting-intra-done-status-ids'); if (!idsInput) return; const values = String(idsInput.value || '').split(',').map((v) => String(v).trim()).filter(Boolean); setMultiValues('setting-intra-done-select', values); } async function saveSettings() { syncDoneIdsFromSelect(); const body = { status_ids: document.getElementById('status-ids').value.trim(), event_types: document.getElementById('event-types').value.trim(), auto_refresh_interval: document.getElementById('server-refresh-interval').value, auto_reset_on_update: document.getElementById('setting-auto-reset-on-update')?.checked ? '1' : '0', sync_reply_wait: document.getElementById('setting-sync-reply-wait')?.checked ? '1' : '0', sync_done_with_intra: document.getElementById('setting-sync-done-intra')?.checked ? '1' : '0', sync_addfield_status: document.getElementById('setting-sync-addfield-status')?.checked ? '1' : '0', intra_done_status_ids: document.getElementById('setting-intra-done-status-ids')?.value.trim() || '', }; const apiKey = document.getElementById('api-key').value.trim(); if (apiKey) body.api_key = apiKey; const r = await apiPost('/api/settings', body); if (r?.ok) { applySettings({ ...lastLoadedSettings, ...body, api_key_set: true }); document.getElementById('api-key').value = ''; showToast('✓ Настройки сохранены', 'success'); } else { showToast(`❌ ${r?.error || 'Ошибка'}`, 'warn'); } } function openSettingsTab(tabName) { document.querySelectorAll('.settings-tab-btn').forEach((btn) => { btn.classList.toggle('active', btn.dataset.tab === tabName); }); document.querySelectorAll('.settings-tab-pane').forEach((pane) => { pane.style.display = pane.dataset.tab === tabName ? 'block' : 'none'; }); } async function loadInternalStatusesAdmin() { const statuses = await loadInternalStatusesConfig(true, false); internalStatusesAdminCache = statuses; renderInternalStatusesAdmin(); } function statusAdminRow(status) { const delBtn = status.is_system ? 'system' : ``; return `