meraproject/services/user-reader/static/summary.html

174 lines
7.1 KiB
HTML
Raw Normal View History

<!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 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> · <a href="/labor">Трудозатраты</a> · <a href="/time-entry">Запись часов</a> · <strong>Сводка</strong> · <a href="/project-report">Проекты</a>
</nav>
<h1>Сводка: сотрудник × проект</h1>
<p class="meta">ФИО, орготдел (АР, ОВ, ВК…), раздел в проекте (ОВ1, ОВ2, ТХ…) + проект и часы за период.</p>
<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>С <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/work-report">/api/work-report</a></p>
<p id="status" class="meta">Выберите период и нажмите «Загрузить».</p>
<div id="wrap"></div>
<script>
const STORE_KEY = 'meraproject_user_reader_api_key';
const COLUMNS = [
{ key: 'id', label: 'id' },
{ key: 'employee', label: 'Сотрудник' },
{ key: 'staffing_title', label: 'Должность' },
{ key: 'department', label: 'department' },
{ key: 'section', label: 'section' },
{ key: 'project_code', label: 'project_code' },
{ key: 'step_name', label: 'Стадия' },
{ key: 'status_name', label: 'Статус' },
{ key: 'archive', label: 'archive', num: true },
{ key: 'project_name', label: 'project_name' },
{ key: 'hours', label: 'hours', num: true },
{ key: 'over', label: 'over', num: true },
{ key: 'over1', label: 'over1', num: true },
{ key: 'over2', label: 'over2', num: true },
{ key: 'total', label: 'total', num: true },
];
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';
}
function renderTable(items) {
const wrap = document.getElementById('wrap');
if (!items || !items.length) {
wrap.innerHTML = '<p>Нет строк за выбранный период.</p>';
return;
}
const th = COLUMNS.map(c => '<th>' + escapeHtml(c.label) + '</th>').join('');
const tr = items.map(row => '<tr>' + COLUMNS.map(c => {
const v = row[c.key];
const cls = c.num ? ' 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 df = document.getElementById('dateFrom').value;
const dt = document.getElementById('dateTo').value;
if (!df || !dt) throw new Error('Нужны даты «С» и «По»');
const params = new URLSearchParams();
params.set('date_from', df);
params.set('date_to', dt);
params.set('limit', '2000');
const emp = document.getElementById('empId').value.trim();
const proj = document.getElementById('projectId').value.trim();
if (emp) params.set('emp_id', emp);
if (proj) params.set('project_id', proj);
return '/api/work-report?' + 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 || [];
status.textContent = 'период: ' + data.date_from + ' … ' + data.date_to
+ ' · всего строк: ' + (data.total != null ? data.total : items.length);
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>