From 8027e92685c535018cd1e2b405e8dfc020ed60bd Mon Sep 17 00:00:00 2001 From: dv Date: Wed, 15 Apr 2026 11:06:05 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/auth.js | 3 ++- public/js/statistics.js | 7 ++++-- refresh-service.js | 55 +++++++++++++++++++++++++++++++++++++++++ server.js | 11 +++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/public/js/auth.js b/public/js/auth.js index b514e4f..727cc9c 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -64,7 +64,7 @@ let missingCommentsFetchInFlight = false; async function init() { await loadInternalStatusesConfig(false, true); - await Promise.all([loadMarks(), loadNotes(), loadMentions(), loadUserList(), loadCommentCounts(), loadClientDict()]); + await Promise.all([loadMarks(), loadNotes(), loadMentions(), loadUserList(), loadCommentCounts(), loadClientDict(), loadClientUsers()]); await loadTasksFromServer(); if (currentUser.is_admin) { @@ -107,6 +107,7 @@ async function loadNotes() { const d = await apiGet('/api/notes'); if (d) notesC 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(); diff --git a/public/js/statistics.js b/public/js/statistics.js index 2a33ec8..151eb79 100644 --- a/public/js/statistics.js +++ b/public/js/statistics.js @@ -843,10 +843,13 @@ function renderClientUsersCard() { // Базовый список клиентов берём из _allClients (все usergroup из dictionaries), // чтобы показывать клиентов с нулём заявок тоже. const cMap = {}; + const clientUsers = window._clientUsers || {}; - // Сначала заполняем всех известных клиентов с нулями + // Сначала заполняем всех известных клиентов с нулями, включая всех пользователей группы (window._allClients || []).forEach((c) => { - cMap[c.name] = { total: 0, users: {} }; + const users = {}; + (clientUsers[String(c.client_id)] || []).forEach((u) => { users[u] = 0; }); + cMap[c.name] = { total: 0, users }; }); // Затем считаем реальные заявки diff --git a/refresh-service.js b/refresh-service.js index 4a593c9..dc5ea93 100644 --- a/refresh-service.js +++ b/refresh-service.js @@ -103,6 +103,14 @@ function createRefreshService({ db, fetch, getSettings, sseBroadcast, logger = c updated_at TEXT NOT NULL ); + CREATE TABLE IF NOT EXISTS client_users ( + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + group_id INTEGER NOT NULL, + PRIMARY KEY (user_id, group_id) + ); + CREATE INDEX IF NOT EXISTS idx_client_users_group ON client_users(group_id); + CREATE TABLE IF NOT EXISTS tasks_index ( task_id TEXT PRIMARY KEY, task_number TEXT NOT NULL, @@ -153,6 +161,8 @@ function createRefreshService({ db, fetch, getSettings, sseBroadcast, logger = c INSERT INTO client_dict(client_id, name, updated_at) VALUES(?,?,?) ON CONFLICT(client_id) DO UPDATE SET name=excluded.name, updated_at=excluded.updated_at `), + clearClientUsers: db.prepare('DELETE FROM client_users'), + upsertClientUser: db.prepare('INSERT OR REPLACE INTO client_users(user_id, name, group_id) VALUES(?,?,?)'), upsertIntraStatus: db.prepare(` INSERT INTO intra_status_dict(status_id,name,updated_at) VALUES(?,?,?) ON CONFLICT(status_id) DO UPDATE SET name=excluded.name,updated_at=excluded.updated_at @@ -598,9 +608,45 @@ function createRefreshService({ db, fetch, getSettings, sseBroadcast, logger = c return dayKeyNow !== dayKeyLast; } + async function syncClientUsers() { + const s = getSettings(); + if (!s.api_key) return; + const pageSize = 100; + let skip = 0; + const allUsers = []; + while (true) { + const url = `https://apigw.intradesk.ru/settings/odata/v2/Clients?ApiKey=${s.api_key}` + + `&$filter=${encodeURIComponent('(isarchived eq false)')}` + + `&$top=${pageSize}&$skip=${skip}`; + const response = await fetch(url, { timeout: 30000 }); + if (!response.ok) break; + const json = await response.json(); + const users = json.value || []; + allUsers.push(...users); + if (users.length < pageSize) break; + skip += pageSize; + } + if (!allUsers.length) return; + const tx = db.transaction(() => { + stmts.clearClientUsers.run(); + for (const u of allUsers) { + const name = String(u.name || '').trim(); + if (!name || !u.id) continue; + for (const g of (u.groups || [])) { + if (g.userGroupId) { + stmts.upsertClientUser.run(u.id, name, g.userGroupId); + } + } + } + }); + tx(); + logger.log(`[Refresh] syncClientUsers: stored ${allUsers.length} users`); + } + let running = false; let pending = null; let schedulerTimer = null; + let lastClientUsersSync = null; function jobPriority(type) { if (type === 'full') return 3; @@ -768,6 +814,14 @@ function createRefreshService({ db, fetch, getSettings, sseBroadcast, logger = c if (settings.refresh_mode === 'incremental_scan') { requestIncremental('scheduler_incremental_scan', { wait: false }); } + + const now = new Date(); + const needUsersSync = !lastClientUsersSync + || (now - lastClientUsersSync) > 24 * 60 * 60 * 1000; + if (needUsersSync) { + lastClientUsersSync = now; + syncClientUsers().catch((e) => logger.error('[Refresh] syncClientUsers error:', e.message)); + } } function startScheduler() { @@ -799,6 +853,7 @@ function createRefreshService({ db, fetch, getSettings, sseBroadcast, logger = c parseWebhookAction, extractWebhookTaskId, updateIntraAdditionalField, + syncClientUsers, }; } diff --git a/server.js b/server.js index b2fe2b0..ea8e6e1 100644 --- a/server.js +++ b/server.js @@ -529,6 +529,17 @@ app.get('/api/clients', auth, (req, res) => { res.json(rows); }); +app.get('/api/client-users', auth, (req, res) => { + const rows = db.prepare('SELECT group_id, name FROM client_users ORDER BY name ASC').all(); + const result = {}; + for (const row of rows) { + const gid = String(row.group_id); + if (!result[gid]) result[gid] = []; + result[gid].push(row.name); + } + res.json(result); +}); + app.get('/api/tasks', auth, (req, res) => { const cache = db.prepare('SELECT * FROM tasks_cache WHERE id=1').get(); const sync = refreshService.getStatusSnapshot();