meraproject/services/user-reader/static/labor.html
keboss-m 5c21d25d45 Initial commit: Merakomis portal, Docker stack and user-reader API.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:04:05 +03:00

192 lines
7.8 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Трудозатраты (read-only)</title>
<style>
body { font-family: system-ui, sans-serif; margin: 1rem; background: #111; color: #e6e6e6; }
h1 { font-size: 1.2rem; }
.meta { color: #888; font-size: 0.85rem; margin-bottom: 1rem; }
table { border-collapse: collapse; width: 100%; font-size: 0.85rem; }
th, td { border: 1px solid #333; padding: 0.35rem 0.45rem; text-align: left; }
th { background: #222; position: sticky; top: 0; }
tr:nth-child(even) { background: #1a1a1a; }
.err { color: #f66; }
a { color: #8cf; }
.panel { margin-bottom: 1rem; padding: 0.75rem; background: #1a1a1a; border: 1px solid #333; }
.panel label { margin-right: 0.75rem; font-size: 0.85rem; }
.panel input, .panel select, .panel button {
padding: 0.35rem; margin: 0.2rem 0.35rem 0.2rem 0;
background: #222; border: 1px solid #444; color: #e6e6e6;
}
.panel button { cursor: pointer; }
#wrap { overflow-x: auto; max-height: 70vh; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<nav class="meta" style="margin-bottom:0.75rem;">
<a href="/">Сотрудники</a> · <strong>Трудозатраты</strong> · <a href="/time-entry">Запись часов</a> · <a href="/summary">Сводка</a> · <a href="/project-report">Проекты</a>
</nav>
<h1>Трудозатраты</h1>
<div id="keybox" class="panel" style="display:none;">
<div>API-ключ (<code>USER_READER_API_KEY</code>):</div>
<input type="password" id="apiKeyInput" placeholder="ключ" autocomplete="off" />
<button type="button" id="saveKey">Сохранить</button>
</div>
<div class="panel">
<label>Данные
<select id="dataset">
<option value="labor-summary">Сводка (сотр. + проект + раздел)</option>
<option value="time-entries">Записи времени (по дням)</option>
<option value="project-members">Участники проектов</option>
<option value="projects">Проекты</option>
<option value="sections">Разделы</option>
</select>
</label>
<label>С <input type="date" id="dateFrom" /></label>
<label>По <input type="date" id="dateTo" /></label>
<label>emp_id <input type="number" id="empId" min="1" placeholder="—" style="width:5rem;" /></label>
<label>project_id <input type="number" id="projectId" min="1" placeholder="—" style="width:5rem;" /></label>
<button type="button" id="loadBtn">Загрузить</button>
</div>
<p class="meta">
API: <a href="/api/labor/meta">/api/labor/meta</a>,
<a href="/api/labor-summary">/api/labor-summary</a>,
<a href="/api/time-entries">/api/time-entries</a>,
<a href="/api/projects">/api/projects</a>,
<a href="/api/sections">/api/sections</a>,
<a href="/api/project-members">/api/project-members</a>
</p>
<p id="status" class="meta">Выберите период и нажмите «Загрузить».</p>
<div id="wrap"></div>
<script>
const STORE_KEY = 'meraproject_user_reader_api_key';
function headers() {
const k = sessionStorage.getItem(STORE_KEY);
const h = {};
if (k) h['X-Api-Key'] = k;
return h;
}
function escapeHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function defaultDates() {
const to = new Date();
const from = new Date();
from.setMonth(from.getMonth() - 1);
const fmt = d => d.toISOString().slice(0, 10);
document.getElementById('dateFrom').value = fmt(from);
document.getElementById('dateTo').value = fmt(to);
}
function showKeyBox(show) {
document.getElementById('keybox').style.display = show ? 'block' : 'none';
}
const NUM_COLS = new Set(['hours','over','over1','over2','total','duration','role','section_id','emp_id','project_id','member_id','id','parent','step','director','team','active','is_over','archive','removed']);
function renderTable(items) {
const wrap = document.getElementById('wrap');
if (!items || !items.length) {
wrap.innerHTML = '<p>Нет строк.</p>';
return;
}
const cols = Object.keys(items[0]);
const th = cols.map(c => '<th>' + escapeHtml(c) + '</th>').join('');
const tr = items.map(row => '<tr>' + cols.map(c => {
const v = row[c];
const cls = NUM_COLS.has(c) ? ' class="num"' : '';
return '<td' + cls + '>' + escapeHtml(v == null ? '' : v) + '</td>';
}).join('') + '</tr>').join('');
wrap.innerHTML = '<table><thead><tr>' + th + '</tr></thead><tbody>' + tr + '</tbody></table>';
}
function buildUrl() {
const ds = document.getElementById('dataset').value;
const params = new URLSearchParams();
params.set('limit', '500');
const df = document.getElementById('dateFrom').value;
const dt = document.getElementById('dateTo').value;
const emp = document.getElementById('empId').value.trim();
const proj = document.getElementById('projectId').value.trim();
if (ds === 'labor-summary') {
if (!df || !dt) throw new Error('Для сводки нужны даты «С» и «По»');
params.set('date_from', df);
params.set('date_to', dt);
} else if (ds === 'time-entries') {
if (df) params.set('date_from', df);
if (dt) params.set('date_to', dt);
}
if (emp) params.set('emp_id', emp);
if (proj) params.set('project_id', proj);
const paths = {
'labor-summary': '/api/labor-summary',
'time-entries': '/api/time-entries',
'project-members': '/api/project-members',
'projects': '/api/projects',
'sections': '/api/sections',
};
return paths[ds] + '?' + params.toString();
}
async function load() {
const status = document.getElementById('status');
try {
const h = await fetch('/api/health').then(r => r.json());
if (!h.ok) {
status.textContent = 'БД недоступна: ' + (h.error || '');
status.className = 'meta err';
return;
}
const url = buildUrl();
const r = await fetch(url, { headers: headers() });
if (r.status === 401) {
status.textContent = 'Нужен API-ключ.';
status.className = 'meta err';
showKeyBox(true);
return;
}
if (!r.ok) {
const err = await r.json().catch(() => ({}));
throw new Error(err.detail || (r.status + ' ' + r.statusText));
}
const data = await r.json();
showKeyBox(false);
const items = data.items || [];
const extra = [];
if (data.date_from) extra.push('период: ' + data.date_from + ' … ' + data.date_to);
if (data.total != null) extra.push('всего: ' + data.total);
if (data.count != null && data.count !== data.total) extra.push('в ответе: ' + data.count);
status.textContent = document.getElementById('dataset').selectedOptions[0].text + (extra.length ? ' · ' + extra.join(' · ') : '');
status.className = 'meta';
renderTable(items);
} catch (e) {
status.textContent = 'Ошибка: ' + e.message;
status.className = 'meta err';
}
}
document.getElementById('loadBtn').addEventListener('click', load);
document.getElementById('saveKey').addEventListener('click', function() {
const v = document.getElementById('apiKeyInput').value.trim();
if (v) sessionStorage.setItem(STORE_KEY, v);
load();
});
defaultDates();
if (!sessionStorage.getItem(STORE_KEY)) showKeyBox(true);
</script>
</body>
</html>