Часы по задачам за месяц и остаток до нормы, через REST от сессии браузера. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
4.1 KiB
JavaScript
77 lines
4.1 KiB
JavaScript
(async () => {
|
|
const old = document.getElementById('mytime');
|
|
if (old) { old.remove(); return; }
|
|
if (!window.BX || !BX.bitrix_sessid) { alert('Открой портал Битрикс24 и войди в него'); return; }
|
|
|
|
const norm = (window.myTimeConfig || {}).norm || 0;
|
|
const call = async (method, params) => {
|
|
const r = await fetch('/rest/' + method + '.json?sessid=' + BX.bitrix_sessid(), {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params),
|
|
});
|
|
const d = await r.json();
|
|
if (d.error) throw d.error + ' ' + (d.error_description || '');
|
|
return d.result;
|
|
};
|
|
const pad = n => String(n).padStart(2, '0');
|
|
const hm = s => Math.floor(s / 3600) + ':' + pad(Math.floor(s % 3600 / 60));
|
|
const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
|
|
const box = document.createElement('div');
|
|
box.id = 'mytime';
|
|
box.style.cssText = 'position:fixed;top:70px;right:20px;z-index:99999;background:#fff;padding:16px 20px;' +
|
|
'box-shadow:0 4px 24px rgba(0,0,0,.25);border-radius:8px;max-height:80vh;overflow:auto;font:14px/1.5 sans-serif;color:#333';
|
|
const now = new Date();
|
|
box.innerHTML = '<b>Моё время</b> <input type="month" id="mytime-m" value="' + now.getFullYear() + '-' + pad(now.getMonth() + 1) + '">' +
|
|
' <button id="mytime-x" style="float:right">✕</button><div id="mytime-out"></div>';
|
|
document.body.append(box);
|
|
const out = document.getElementById('mytime-out');
|
|
document.getElementById('mytime-x').onclick = () => box.remove();
|
|
|
|
async function run() {
|
|
const ym = document.getElementById('mytime-m').value;
|
|
const [y, m] = ym.split('-').map(Number);
|
|
const next = m === 12 ? (y + 1) + '-01' : y + '-' + pad(m + 1);
|
|
out.textContent = 'Считаю…';
|
|
try {
|
|
const me = (await call('user.current', {})).ID;
|
|
const perTask = {};
|
|
for (let page = 1; ; page++) {
|
|
const items = await call('task.elapseditem.getlist', {
|
|
ORDER: { ID: 'asc' },
|
|
FILTER: { USER_ID: me, '>=CREATED_DATE': ym + '-01T00:00:00', '<CREATED_DATE': next + '-01T00:00:00' },
|
|
SELECT: ['ID', 'TASK_ID', 'SECONDS', 'CREATED_DATE'],
|
|
PARAMS: { NAV_PARAMS: { nPageSize: 50, iNumPage: page } },
|
|
});
|
|
// повторная проверка месяца на случай, если портал понял фильтр по дате иначе
|
|
for (const it of items) if (it.CREATED_DATE.startsWith(ym)) perTask[it.TASK_ID] = (perTask[it.TASK_ID] || 0) + Number(it.SECONDS);
|
|
out.textContent = 'Считаю… страница ' + page;
|
|
if (items.length < 50) break;
|
|
}
|
|
|
|
const ids = Object.keys(perTask);
|
|
const titles = {};
|
|
for (let i = 0; i < ids.length; i += 50) {
|
|
const r = await call('tasks.task.list', { filter: { ID: ids.slice(i, i + 50) }, select: ['ID', 'TITLE'] });
|
|
for (const t of r.tasks) titles[t.id] = t.title;
|
|
}
|
|
|
|
const total = ids.reduce((a, id) => a + perTask[id], 0);
|
|
const left = norm * 3600 - total;
|
|
const normRow = !norm ? '' : '<tr><td></td><td>' + (left > 0
|
|
? 'До нормы ' + norm + ' ч осталось <b>' + hm(left) + '</b>'
|
|
: 'Норма ' + norm + ' ч выполнена' + (left < 0 ? ', сверх нормы ' + hm(-left) : '')) + '</td></tr>';
|
|
out.innerHTML = '<table style="border-collapse:collapse;margin-top:10px">' +
|
|
ids.sort((a, b) => perTask[b] - perTask[a]).map(id =>
|
|
'<tr><td style="text-align:right;padding:2px 12px 2px 0;white-space:nowrap">' + hm(perTask[id]) + '</td>' +
|
|
'<td><a href="/company/personal/user/' + me + '/tasks/task/view/' + id + '/">#' + id + '</a> ' + esc(titles[id] || '') + '</td></tr>'
|
|
).join('') +
|
|
'<tr style="font-weight:bold;border-top:2px solid #999"><td style="text-align:right;padding:4px 12px 0 0">' + hm(total) +
|
|
'</td><td>Итого за ' + ym + '</td></tr>' + normRow + '</table>';
|
|
} catch (e) {
|
|
out.textContent = 'Ошибка: ' + e;
|
|
}
|
|
}
|
|
document.getElementById('mytime-m').onchange = run;
|
|
run();
|
|
})();
|