154 lines
6.5 KiB
HTML
154 lines
6.5 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="ru">
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8" />
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
|
|
<title>Состав проекта</title>
|
||
|
|
<style>
|
||
|
|
body { font-family: system-ui, sans-serif; margin: 1rem; max-width: 42rem; background: #111; color: #e6e6e6; }
|
||
|
|
h1 { font-size: 1.2rem; }
|
||
|
|
.meta { color: #888; font-size: 0.85rem; margin-bottom: 1rem; }
|
||
|
|
a { color: #8cf; }
|
||
|
|
.panel { margin-bottom: 1rem; padding: 0.75rem; background: #1a1a1a; border: 1px solid #333; }
|
||
|
|
label { display: block; margin: 0.5rem 0 0.25rem; font-size: 0.85rem; color: #aaa; }
|
||
|
|
input, select, button {
|
||
|
|
width: 100%; box-sizing: border-box; padding: 0.45rem;
|
||
|
|
background: #222; border: 1px solid #444; color: #e6e6e6; font-size: 0.95rem;
|
||
|
|
}
|
||
|
|
input[type="checkbox"] { width: auto; margin-right: 0.5rem; }
|
||
|
|
button { cursor: pointer; margin-top: 0.75rem; width: auto; padding: 0.5rem 1rem; }
|
||
|
|
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
|
||
|
|
pre { background: #0d0d0d; border: 1px solid #333; padding: 0.75rem; overflow: auto; font-size: 0.8rem; }
|
||
|
|
.hint { font-size: 0.8rem; color: #777; margin-top: 0.25rem; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<nav class="meta">
|
||
|
|
<a href="/">Сотрудники</a> · <a href="/labor">Трудозатраты</a> · <a href="/time-entry">Запись часов</a> · <strong>Состав проекта</strong>
|
||
|
|
</nav>
|
||
|
|
<h1>Добавить / изменить участника</h1>
|
||
|
|
<p class="meta">Требуются права РП, ГИП или директора отдела. Acting = кто выполняет действие.</p>
|
||
|
|
|
||
|
|
<div id="keybox" class="panel" style="display:none;">
|
||
|
|
<label>API-ключ</label>
|
||
|
|
<input type="password" id="apiKeyInput" placeholder="local-dev-key-change-in-prod" />
|
||
|
|
<button type="button" id="saveKey">Сохранить</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<form id="form" class="panel">
|
||
|
|
<label for="actingId">Acting (X-Acting-Emp-Id)</label>
|
||
|
|
<input type="number" id="actingId" min="1" required />
|
||
|
|
|
||
|
|
<label for="projectId">Проект (id)</label>
|
||
|
|
<input type="number" id="projectId" min="1" required />
|
||
|
|
|
||
|
|
<label for="empId">Сотрудник (emp_id)</label>
|
||
|
|
<input type="number" id="empId" min="1" required />
|
||
|
|
|
||
|
|
<label for="sectionId">Раздел</label>
|
||
|
|
<select id="sectionId" required><option value="">— загрузите проект —</option></select>
|
||
|
|
|
||
|
|
<label for="roleId">Роль</label>
|
||
|
|
<select id="roleId" required><option value="">Загрузка…</option></select>
|
||
|
|
|
||
|
|
<label style="display:flex;align-items:center;margin-top:0.75rem;">
|
||
|
|
<input type="checkbox" id="active" checked /> Активен в команде
|
||
|
|
</label>
|
||
|
|
|
||
|
|
<label for="text">Комментарий</label>
|
||
|
|
<input type="text" id="text" />
|
||
|
|
|
||
|
|
<button type="submit">Сохранить (PUT)</button>
|
||
|
|
<button type="button" id="loadSections">Загрузить разделы проекта</button>
|
||
|
|
<button type="button" id="checkPerm">Проверить права</button>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
<p id="status" class="meta"></p>
|
||
|
|
<pre id="result" hidden></pre>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
const STORE_KEY = 'meraproject_user_reader_api_key';
|
||
|
|
|
||
|
|
function headers(json) {
|
||
|
|
const h = json ? { 'Content-Type': 'application/json' } : {};
|
||
|
|
const k = sessionStorage.getItem(STORE_KEY);
|
||
|
|
if (k) h['X-Api-Key'] = k;
|
||
|
|
return h;
|
||
|
|
}
|
||
|
|
|
||
|
|
function actingHeaders(json) {
|
||
|
|
const h = headers(json);
|
||
|
|
const id = document.getElementById('actingId').value.trim();
|
||
|
|
if (id) h['X-Acting-Emp-Id'] = id;
|
||
|
|
return h;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadRoles() {
|
||
|
|
const r = await fetch('/api/member-roles', { headers: headers() });
|
||
|
|
if (r.status === 401) { document.getElementById('keybox').style.display = 'block'; return; }
|
||
|
|
const items = (await r.json()).items || [];
|
||
|
|
document.getElementById('roleId').innerHTML = items.map(x =>
|
||
|
|
'<option value="' + x.id + '">' + x.title + '</option>'
|
||
|
|
).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadSections() {
|
||
|
|
const pid = document.getElementById('projectId').value;
|
||
|
|
const status = document.getElementById('status');
|
||
|
|
if (!pid) { status.textContent = 'Укажите project_id'; return; }
|
||
|
|
const r = await fetch('/api/project-sections?project_id=' + pid, { headers: headers() });
|
||
|
|
const data = await r.json();
|
||
|
|
const sel = document.getElementById('sectionId');
|
||
|
|
const items = data.items || [];
|
||
|
|
sel.innerHTML = items.length
|
||
|
|
? items.map(s => '<option value="' + s.section_id + '">' + s.section_name + '</option>').join('')
|
||
|
|
: '<option value="">Нет разделов</option>';
|
||
|
|
status.textContent = 'Разделов: ' + items.length;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function checkPerm() {
|
||
|
|
const acting = document.getElementById('actingId').value;
|
||
|
|
const emp = document.getElementById('empId').value;
|
||
|
|
const pid = document.getElementById('projectId').value;
|
||
|
|
const r = await fetch(
|
||
|
|
'/api/labor/permissions?target_emp_id=' + emp + '&project_id=' + pid,
|
||
|
|
{ headers: actingHeaders() }
|
||
|
|
);
|
||
|
|
document.getElementById('result').hidden = false;
|
||
|
|
document.getElementById('result').textContent = JSON.stringify(await r.json(), null, 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
document.getElementById('form').addEventListener('submit', async (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const status = document.getElementById('status');
|
||
|
|
const body = {
|
||
|
|
project_id: +document.getElementById('projectId').value,
|
||
|
|
emp_id: +document.getElementById('empId').value,
|
||
|
|
section_id: +document.getElementById('sectionId').value,
|
||
|
|
role: +document.getElementById('roleId').value,
|
||
|
|
active: document.getElementById('active').checked,
|
||
|
|
text: document.getElementById('text').value || ''
|
||
|
|
};
|
||
|
|
const r = await fetch('/api/project-members', {
|
||
|
|
method: 'PUT',
|
||
|
|
headers: actingHeaders(true),
|
||
|
|
body: JSON.stringify(body)
|
||
|
|
});
|
||
|
|
const data = await r.json();
|
||
|
|
document.getElementById('result').hidden = false;
|
||
|
|
document.getElementById('result').textContent = JSON.stringify(data, null, 2);
|
||
|
|
status.textContent = r.ok ? 'Сохранено' : 'Ошибка ' + r.status;
|
||
|
|
});
|
||
|
|
|
||
|
|
document.getElementById('loadSections').addEventListener('click', loadSections);
|
||
|
|
document.getElementById('checkPerm').addEventListener('click', checkPerm);
|
||
|
|
document.getElementById('saveKey').addEventListener('click', () => {
|
||
|
|
sessionStorage.setItem(STORE_KEY, document.getElementById('apiKeyInput').value);
|
||
|
|
loadRoles();
|
||
|
|
});
|
||
|
|
|
||
|
|
loadRoles();
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|