фикс статистики 5
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
dv 2026-04-15 11:06:05 +03:00
parent d571581395
commit 8027e92685
4 changed files with 73 additions and 3 deletions

View file

@ -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();

View file

@ -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 };
});
// Затем считаем реальные заявки

View file

@ -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,
};
}

View file

@ -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();