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

192 lines
7.8 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 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>