567 lines
22 KiB
JavaScript
567 lines
22 KiB
JavaScript
async function doLogin() {
|
||
const username = document.getElementById('login-username').value.trim();
|
||
const password = document.getElementById('login-password').value;
|
||
const errEl = document.getElementById('login-error');
|
||
errEl.textContent = '';
|
||
if (!username || !password) {
|
||
errEl.textContent = 'Заполните все поля';
|
||
return;
|
||
}
|
||
const r = await fetch('/api/auth/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username, password }),
|
||
});
|
||
const data = await r.json();
|
||
if (!r.ok) {
|
||
errEl.textContent = data.error || 'Ошибка входа';
|
||
return;
|
||
}
|
||
authToken = data.token;
|
||
currentUser = { username: data.username, is_admin: data.is_admin };
|
||
localStorage.setItem('intradesk_token', authToken);
|
||
showApp();
|
||
init();
|
||
}
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter' && !document.getElementById('login-overlay').classList.contains('hidden')) doLogin();
|
||
});
|
||
|
||
function doLogout() {
|
||
localStorage.removeItem('intradesk_token');
|
||
authToken = null;
|
||
currentUser = null;
|
||
reloadInFlight = false;
|
||
if (statusPollTimer) clearInterval(statusPollTimer);
|
||
if (clientCountdownTimer) clearInterval(clientCountdownTimer);
|
||
stopSSE();
|
||
document.getElementById('app-shell').style.display = 'none';
|
||
document.getElementById('login-overlay').classList.remove('hidden');
|
||
document.getElementById('login-password').value = '';
|
||
}
|
||
|
||
function showApp() {
|
||
document.getElementById('login-overlay').classList.add('hidden');
|
||
document.getElementById('app-shell').style.display = 'flex';
|
||
const u = currentUser.username;
|
||
const initials = avatarInitials(u);
|
||
const color = avatarColor(u);
|
||
const av = document.getElementById('sb-avatar');
|
||
if (av) {
|
||
av.textContent = initials;
|
||
av.style.background = color;
|
||
}
|
||
if (document.getElementById('sb-username')) document.getElementById('sb-username').textContent = u;
|
||
if (document.getElementById('sb-email')) document.getElementById('sb-email').textContent = `${u}@cleverparks.io`;
|
||
const ns = document.getElementById('nav-settings');
|
||
if (ns) ns.style.display = currentUser.is_admin ? 'flex' : 'none';
|
||
}
|
||
|
||
let lastLoadedSettings = {};
|
||
let internalStatusesAdminCache = [];
|
||
let missingCommentsFetchInFlight = false;
|
||
|
||
async function init() {
|
||
await loadInternalStatusesConfig(false, true);
|
||
await Promise.all([loadMarks(), loadNotes(), loadMentions(), loadUserList(), loadCommentCounts(), loadClientDict()]);
|
||
await loadTasksFromServer();
|
||
|
||
if (currentUser.is_admin) {
|
||
const s = await apiGet('/api/settings');
|
||
if (s) {
|
||
applySettings(s);
|
||
await loadIntraStatusDictionary();
|
||
}
|
||
await loadInternalStatusesAdmin();
|
||
loadUsersTable();
|
||
}
|
||
|
||
const status = await apiGet('/api/status');
|
||
if (status) {
|
||
serverRefreshInterval = status.auto_refresh_interval || 300;
|
||
lastKnownUpdatedAt = status.last_updated;
|
||
if (status.cache_version !== undefined && status.cache_version !== null) {
|
||
lastKnownCacheVersion = Number(status.cache_version) || 0;
|
||
}
|
||
}
|
||
startSSE();
|
||
startStatusPolling();
|
||
}
|
||
|
||
async function loadInternalStatusesConfig(includeAll = false, applyToRuntime = true) {
|
||
const suffix = includeAll ? '?include_all=1' : '';
|
||
const d = await apiGet(`/api/internal-statuses${suffix}`);
|
||
const statuses = d?.statuses || [];
|
||
if (applyToRuntime && statuses.length && typeof setMarksConfig === 'function') {
|
||
const active = statuses.filter((s) => s.is_active !== false);
|
||
setMarksConfig(active);
|
||
if (typeof syncChipDropdownItems === 'function') syncChipDropdownItems();
|
||
if (typeof initMarkChips === 'function') initMarkChips();
|
||
}
|
||
return statuses;
|
||
}
|
||
|
||
async function loadMarks() { const d = await apiGet('/api/marks'); if (d) marksCache = d; }
|
||
async function loadNotes() { const d = await apiGet('/api/notes'); if (d) notesCache = d; }
|
||
async function loadUserList() { const d = await apiGet('/api/users/list'); if (d) knownUsers = d.map((u) => u.toLowerCase()); }
|
||
async function loadCommentCounts() { const d = await apiGet('/api/comments/counts'); if (d && typeof d === 'object') commentCounts = d; }
|
||
async function loadClientDict() { const d = await apiGet('/api/clients'); if (Array.isArray(d)) window._allClients = d; }
|
||
|
||
async function loadTasksFromServer(silent = false) {
|
||
if (!silent && !allData.length) showLoading();
|
||
const data = await apiGet('/api/tasks');
|
||
if (!data) return;
|
||
|
||
allData = (data.tasks || []).map((task) => {
|
||
const lc = extractLastComment(task);
|
||
if (!lc && forceComment) {
|
||
const tid = String(task.id);
|
||
const cached = forcedCommentCache.get(tid);
|
||
if (cached && cached.updatedat === task.updatedat) return { task, lastComment: cached.lastComment };
|
||
}
|
||
return { task, lastComment: lc };
|
||
});
|
||
|
||
lastKnownUpdatedAt = data.updated_at;
|
||
if (data.cache_version !== undefined && data.cache_version !== null) {
|
||
lastKnownCacheVersion = Number(data.cache_version) || lastKnownCacheVersion;
|
||
}
|
||
const el = (id) => document.getElementById(id);
|
||
if (el('total-loaded')) el('total-loaded').textContent = allData.length;
|
||
if (el('total-count')) el('total-count').textContent = allData.length;
|
||
if (el('last-update')) el('last-update').textContent = data.updated_at ? new Date(data.updated_at).toLocaleTimeString('ru-RU') : '—';
|
||
document.getElementById('update-banner')?.classList.remove('visible');
|
||
onTasksLoaded();
|
||
if (typeof refreshAdvancedFilters === 'function') refreshAdvancedFilters();
|
||
renderTable();
|
||
if (forceComment) fetchMissingComments();
|
||
}
|
||
|
||
async function fetchMissingComments() {
|
||
if (missingCommentsFetchInFlight) return;
|
||
const toFetch = allData.filter((i) => {
|
||
if (i.lastComment) return false;
|
||
const tid = String(i.task.id);
|
||
const cached = forcedCommentCache.get(tid);
|
||
return !cached || cached.updatedat !== i.task.updatedat;
|
||
});
|
||
if (!toFetch.length) return;
|
||
|
||
missingCommentsFetchInFlight = true;
|
||
try {
|
||
const CONCURRENCY = 8;
|
||
let cursor = 0;
|
||
let loaded = 0;
|
||
|
||
async function worker() {
|
||
while (true) {
|
||
const idx = cursor;
|
||
cursor += 1;
|
||
if (idx >= toFetch.length) return;
|
||
|
||
const item = toFetch[idx];
|
||
const tid = String(item.task.id);
|
||
const upd = encodeURIComponent(item.task.updatedat || '');
|
||
let lc = null;
|
||
|
||
try {
|
||
const res = await apiGet(`/api/tasks/lastcomment/${tid}?upd=${upd}`);
|
||
if (res?.entries?.length) {
|
||
const entry = res.entries[0];
|
||
const ev = (entry.events?.data || []).find((e) => COMMENT_TYPES.has(e.type));
|
||
if (ev) {
|
||
lc = {
|
||
username: entry.username,
|
||
userid: entry.userid,
|
||
usertype: entry.usertype,
|
||
eventat: entry.eventat,
|
||
text: ev.stringvalue || '',
|
||
isPrivate: ev.type === 55,
|
||
eventType: ev.type,
|
||
};
|
||
}
|
||
}
|
||
} catch {}
|
||
|
||
item.lastComment = lc;
|
||
forcedCommentCache.set(tid, { updatedat: item.task.updatedat, lastComment: lc });
|
||
loaded += 1;
|
||
if (loaded % 25 === 0 && typeof renderTable === 'function') renderTable(true);
|
||
}
|
||
}
|
||
|
||
const workers = Array.from({ length: Math.min(CONCURRENCY, toFetch.length) }, () => worker());
|
||
await Promise.all(workers);
|
||
} finally {
|
||
missingCommentsFetchInFlight = false;
|
||
if (typeof renderTable === 'function') renderTable(true);
|
||
}
|
||
}
|
||
|
||
async function loadTasks() {
|
||
const btn = document.getElementById('load-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Загрузка...';
|
||
try {
|
||
const r = await apiPost('/api/tasks/refresh', {});
|
||
if (r?.error) {
|
||
showToast(`❌ ${r.error}`, 'warn');
|
||
return;
|
||
}
|
||
await Promise.all([loadMarks(), loadNotes()]);
|
||
await loadTasksFromServer(true);
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '↻ Обновить';
|
||
}
|
||
}
|
||
|
||
async function loadAllTasks() {
|
||
const btn = document.getElementById('refresh-all-btn');
|
||
const status = document.getElementById('refresh-all-status');
|
||
if (btn) {
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Загружаю все...';
|
||
}
|
||
if (status) status.textContent = 'Может занять ~30 секунд...';
|
||
try {
|
||
const r = await apiPost('/api/tasks/refresh/full', {});
|
||
if (r?.error) {
|
||
showToast(`❌ ${r.error}`, 'warn');
|
||
if (status) status.textContent = `Ошибка: ${r.error}`;
|
||
return;
|
||
}
|
||
await Promise.all([loadMarks(), loadNotes()]);
|
||
await loadTasksFromServer(true);
|
||
if (status) status.textContent = `✓ Загружено ${r.count} заявок`;
|
||
showToast(`✓ Загружено ${r.count} заявок`, 'success');
|
||
} finally {
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
btn.textContent = '🔄 Обновить ВСЕ заявки';
|
||
}
|
||
}
|
||
}
|
||
|
||
async function reloadData() {
|
||
if (reloadInFlight) return;
|
||
reloadInFlight = true;
|
||
try {
|
||
await Promise.all([loadMarks(), loadNotes()]);
|
||
await loadTasksFromServer(true);
|
||
} finally {
|
||
reloadInFlight = false;
|
||
}
|
||
}
|
||
|
||
function startStatusPolling() {
|
||
if (statusPollTimer) clearInterval(statusPollTimer);
|
||
statusPollTimer = setInterval(async () => {
|
||
const s = await apiGet('/api/status');
|
||
if (!s) return;
|
||
const serverVersion = Number(s.cache_version || 0);
|
||
if (serverVersion && serverVersion !== lastKnownCacheVersion) {
|
||
lastKnownCacheVersion = serverVersion;
|
||
if (s.last_updated) lastKnownUpdatedAt = s.last_updated;
|
||
await reloadData();
|
||
return;
|
||
}
|
||
if (s?.last_updated && s.last_updated !== lastKnownUpdatedAt) {
|
||
lastKnownUpdatedAt = s.last_updated;
|
||
await reloadData();
|
||
}
|
||
}, 30000);
|
||
}
|
||
|
||
function applySettings(s) {
|
||
lastLoadedSettings = { ...(s || {}) };
|
||
const f = (id) => document.getElementById(id);
|
||
if (f('status-ids')) f('status-ids').value = s.status_ids || '';
|
||
if (f('event-types')) f('event-types').value = s.event_types || '';
|
||
if (f('server-refresh-interval')) f('server-refresh-interval').value = s.auto_refresh_interval || '300';
|
||
if (f('setting-auto-reset-on-update')) f('setting-auto-reset-on-update').checked = String(s.auto_reset_on_update || '0') === '1';
|
||
if (f('setting-sync-reply-wait')) f('setting-sync-reply-wait').checked = String(s.sync_reply_wait || '0') === '1';
|
||
if (f('setting-sync-done-intra')) f('setting-sync-done-intra').checked = String(s.sync_done_with_intra || '0') === '1';
|
||
if (f('setting-sync-addfield-status')) f('setting-sync-addfield-status').checked = String(s.sync_addfield_status || '0') === '1';
|
||
if (f('setting-intra-done-status-ids')) f('setting-intra-done-status-ids').value = s.intra_done_status_ids || '';
|
||
if (f('apikey-status')) f('apikey-status').textContent = s.api_key_set ? '(ключ задан ✓)' : '(не задан!)';
|
||
syncDoneSelectFromInput();
|
||
}
|
||
|
||
function getMultiValues(selectId) {
|
||
const el = document.getElementById(selectId);
|
||
if (!el) return [];
|
||
return [...el.selectedOptions].map((o) => String(o.value).trim()).filter(Boolean);
|
||
}
|
||
|
||
function setMultiValues(selectId, values) {
|
||
const selected = new Set((values || []).map((v) => String(v)));
|
||
const el = document.getElementById(selectId);
|
||
if (!el) return;
|
||
[...el.options].forEach((opt) => { opt.selected = selected.has(String(opt.value)); });
|
||
}
|
||
|
||
async function loadIntraStatusDictionary() {
|
||
const d = await apiGet('/api/intra/status-dictionary');
|
||
const items = d?.items || [];
|
||
const sel = document.getElementById('setting-intra-done-select');
|
||
if (!sel) return items;
|
||
|
||
const selectedRaw = String(lastLoadedSettings.intra_done_status_ids || '')
|
||
.split(',')
|
||
.map((v) => String(v).trim())
|
||
.filter(Boolean);
|
||
const prevSelected = selectedRaw.length ? selectedRaw : getMultiValues('setting-intra-done-select');
|
||
|
||
sel.innerHTML = items.map((it) => `<option value="${esc(it.status_id)}">#${esc(it.status_id)} - ${esc(it.name)}</option>`).join('');
|
||
setMultiValues('setting-intra-done-select', prevSelected);
|
||
const idsInput = document.getElementById('setting-intra-done-status-ids');
|
||
if (idsInput) idsInput.value = prevSelected.join(',');
|
||
return items;
|
||
}
|
||
|
||
function syncDoneIdsFromSelect() {
|
||
const ids = getMultiValues('setting-intra-done-select');
|
||
const idsInput = document.getElementById('setting-intra-done-status-ids');
|
||
if (idsInput) idsInput.value = ids.join(',');
|
||
}
|
||
|
||
function syncDoneSelectFromInput() {
|
||
const idsInput = document.getElementById('setting-intra-done-status-ids');
|
||
if (!idsInput) return;
|
||
const values = String(idsInput.value || '').split(',').map((v) => String(v).trim()).filter(Boolean);
|
||
setMultiValues('setting-intra-done-select', values);
|
||
}
|
||
|
||
async function saveSettings() {
|
||
syncDoneIdsFromSelect();
|
||
const body = {
|
||
status_ids: document.getElementById('status-ids').value.trim(),
|
||
event_types: document.getElementById('event-types').value.trim(),
|
||
auto_refresh_interval: document.getElementById('server-refresh-interval').value,
|
||
auto_reset_on_update: document.getElementById('setting-auto-reset-on-update')?.checked ? '1' : '0',
|
||
sync_reply_wait: document.getElementById('setting-sync-reply-wait')?.checked ? '1' : '0',
|
||
sync_done_with_intra: document.getElementById('setting-sync-done-intra')?.checked ? '1' : '0',
|
||
sync_addfield_status: document.getElementById('setting-sync-addfield-status')?.checked ? '1' : '0',
|
||
intra_done_status_ids: document.getElementById('setting-intra-done-status-ids')?.value.trim() || '',
|
||
};
|
||
const apiKey = document.getElementById('api-key').value.trim();
|
||
if (apiKey) body.api_key = apiKey;
|
||
const r = await apiPost('/api/settings', body);
|
||
if (r?.ok) {
|
||
applySettings({ ...lastLoadedSettings, ...body, api_key_set: true });
|
||
document.getElementById('api-key').value = '';
|
||
showToast('✓ Настройки сохранены', 'success');
|
||
} else {
|
||
showToast(`❌ ${r?.error || 'Ошибка'}`, 'warn');
|
||
}
|
||
}
|
||
|
||
function openSettingsTab(tabName) {
|
||
document.querySelectorAll('.settings-tab-btn').forEach((btn) => {
|
||
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
||
});
|
||
document.querySelectorAll('.settings-tab-pane').forEach((pane) => {
|
||
pane.style.display = pane.dataset.tab === tabName ? 'block' : 'none';
|
||
});
|
||
}
|
||
|
||
async function loadInternalStatusesAdmin() {
|
||
const statuses = await loadInternalStatusesConfig(true, false);
|
||
internalStatusesAdminCache = statuses;
|
||
renderInternalStatusesAdmin();
|
||
}
|
||
|
||
function statusAdminRow(status) {
|
||
const delBtn = status.is_system
|
||
? '<span style="color:var(--text3);font-size:11px;">system</span>'
|
||
: `<button class="btn btn-danger" style="font-size:11px;padding:4px 8px;" onclick="deleteInternalStatus('${esc(status.key)}')">Удалить</button>`;
|
||
return `
|
||
<tr>
|
||
<td style="font-family:monospace;font-size:11px;">${esc(status.key)}</td>
|
||
<td><input class="inline-pass" type="text" id="st-label-${esc(status.key)}" value="${esc(status.label)}"></td>
|
||
<td><input type="color" id="st-dot-${esc(status.key)}" value="${toHex(status.colorDot || '#1677ff')}"></td>
|
||
<td><input type="color" id="st-bg-${esc(status.key)}" value="${toHex(status.colorBg || '#e6f4ff')}"></td>
|
||
<td><input type="number" id="st-order-${esc(status.key)}" value="${Number(status.sort_order || 0)}" style="width:72px;"></td>
|
||
<td style="text-align:center;"><input type="checkbox" id="st-active-${esc(status.key)}" ${status.is_active ? 'checked' : ''}></td>
|
||
<td><button class="btn btn-default" style="font-size:11px;padding:4px 8px;" onclick="saveInternalStatus('${esc(status.key)}')">Сохранить</button></td>
|
||
<td>${delBtn}</td>
|
||
</tr>`;
|
||
}
|
||
|
||
function toHex(color) {
|
||
if (!color) return '#1677ff';
|
||
const c = String(color).trim();
|
||
const cssVarMap = {
|
||
'var(--red)': '#ff4d4f',
|
||
'var(--red-light)': '#fff1f0',
|
||
'var(--red-border)': '#ffa39e',
|
||
'var(--orange)': '#fa8c16',
|
||
'var(--orange-light)': '#fff7e6',
|
||
'var(--orange-border)': '#ffd591',
|
||
'var(--green)': '#52c41a',
|
||
'var(--green-light)': '#f6ffed',
|
||
'var(--green-border)': '#b7eb8f',
|
||
'var(--purple)': '#722ed1',
|
||
'var(--purple-light)': '#f9f0ff',
|
||
};
|
||
if (cssVarMap[c]) return cssVarMap[c];
|
||
if (/^#[0-9a-fA-F]{6}$/.test(c)) return c;
|
||
if (/^#[0-9a-fA-F]{3}$/.test(c)) return `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`;
|
||
return '#1677ff';
|
||
}
|
||
|
||
function renderInternalStatusesAdmin() {
|
||
const tbody = document.getElementById('internal-statuses-tbody');
|
||
if (!tbody) return;
|
||
tbody.innerHTML = internalStatusesAdminCache.map(statusAdminRow).join('');
|
||
}
|
||
|
||
async function createInternalStatus() {
|
||
const label = document.getElementById('new-status-label')?.value.trim();
|
||
const key = document.getElementById('new-status-key')?.value.trim();
|
||
const dot = document.getElementById('new-status-dot')?.value || '#1677ff';
|
||
const bg = document.getElementById('new-status-bg')?.value || '#e6f4ff';
|
||
const order = document.getElementById('new-status-order')?.value || '';
|
||
if (!label) {
|
||
showToast('Введите название статуса', 'warn');
|
||
return;
|
||
}
|
||
const r = await apiPost('/api/internal-statuses', {
|
||
key,
|
||
label,
|
||
color_dot: dot,
|
||
color_bg: bg,
|
||
color_border: dot,
|
||
color_text: dot,
|
||
sort_order: order ? Number(order) : undefined,
|
||
is_active: true,
|
||
});
|
||
if (!r?.ok) {
|
||
showToast(`❌ ${r?.error || 'Ошибка'}`, 'warn');
|
||
return;
|
||
}
|
||
document.getElementById('new-status-label').value = '';
|
||
document.getElementById('new-status-key').value = '';
|
||
await loadInternalStatusesConfig(false, true);
|
||
await loadInternalStatusesAdmin();
|
||
await loadMarks();
|
||
if (typeof refreshAdvancedFilters === 'function') refreshAdvancedFilters();
|
||
renderTable();
|
||
showToast('✓ Статус добавлен', 'success');
|
||
}
|
||
|
||
async function saveInternalStatus(key) {
|
||
const payload = {
|
||
label: document.getElementById(`st-label-${key}`)?.value.trim(),
|
||
color_dot: document.getElementById(`st-dot-${key}`)?.value || '#1677ff',
|
||
color_bg: document.getElementById(`st-bg-${key}`)?.value || '#e6f4ff',
|
||
color_border: document.getElementById(`st-dot-${key}`)?.value || '#1677ff',
|
||
color_text: document.getElementById(`st-dot-${key}`)?.value || '#1677ff',
|
||
sort_order: Number(document.getElementById(`st-order-${key}`)?.value || 0),
|
||
is_active: !!document.getElementById(`st-active-${key}`)?.checked,
|
||
};
|
||
const r = await apiPost(`/api/internal-statuses/${encodeURIComponent(key)}`, payload, 'PUT');
|
||
if (!r?.ok) {
|
||
showToast(`❌ ${r?.error || 'Ошибка'}`, 'warn');
|
||
return;
|
||
}
|
||
await loadInternalStatusesConfig(false, true);
|
||
await loadInternalStatusesAdmin();
|
||
await loadMarks();
|
||
if (typeof refreshAdvancedFilters === 'function') refreshAdvancedFilters();
|
||
renderTable();
|
||
showToast('✓ Статус сохранен', 'success');
|
||
}
|
||
|
||
async function deleteInternalStatus(key) {
|
||
if (!confirm(`Удалить статус "${key}"?`)) return;
|
||
const r = await apiDelete(`/api/internal-statuses/${encodeURIComponent(key)}`);
|
||
if (!r?.ok) {
|
||
showToast(`❌ ${r?.error || 'Ошибка'}`, 'warn');
|
||
return;
|
||
}
|
||
await loadInternalStatusesConfig(false, true);
|
||
await loadInternalStatusesAdmin();
|
||
await loadMarks();
|
||
if (typeof refreshAdvancedFilters === 'function') refreshAdvancedFilters();
|
||
renderTable();
|
||
showToast('✓ Статус удален', 'success');
|
||
}
|
||
|
||
async function createUser() {
|
||
const username = document.getElementById('new-user-login').value.trim();
|
||
const password = document.getElementById('new-user-pass').value;
|
||
const msgEl = document.getElementById('user-msg');
|
||
const r = await apiPost('/api/auth/register', { username, password });
|
||
if (r?.ok) {
|
||
msgEl.style.color = 'var(--green)';
|
||
msgEl.textContent = `✓ Пользователь "${username}" создан`;
|
||
document.getElementById('new-user-login').value = '';
|
||
document.getElementById('new-user-pass').value = '';
|
||
loadUsersTable();
|
||
loadUserList();
|
||
} else {
|
||
msgEl.style.color = 'var(--red)';
|
||
msgEl.textContent = r?.error || 'Ошибка';
|
||
}
|
||
}
|
||
|
||
async function loadUsersTable() {
|
||
const users = await apiGet('/api/users');
|
||
if (!users) return;
|
||
const tbody = document.getElementById('users-tbody');
|
||
if (!tbody) return;
|
||
tbody.innerHTML = users.map((u) => `
|
||
<tr>
|
||
<td style="font-weight:600;">${esc(u.username)}</td>
|
||
<td>${u.created_at ? new Date(u.created_at).toLocaleDateString('ru-RU') : '—'}</td>
|
||
<td><input class="inline-pass" type="password" id="pass-${u.id}" placeholder="новый пароль">
|
||
<button class="btn btn-default" style="font-size:11px;padding:4px 10px;margin-left:6px;" onclick="changePassword(${u.id})">Сменить</button></td>
|
||
<td>${u.username !== 'admin' ? `<button class="btn btn-danger" style="font-size:11px;padding:4px 10px;" onclick="deleteUser(${u.id},'${esc(u.username)}')">Удалить</button>` : ''}</td>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
async function changePassword(id) {
|
||
const inp = document.getElementById(`pass-${id}`);
|
||
if (!inp || inp.value.length < 6) {
|
||
showToast('Пароль минимум 6 символов', 'warn');
|
||
return;
|
||
}
|
||
const r = await apiPost(`/api/users/${id}/password`, { password: inp.value }, 'PUT');
|
||
if (r?.ok) {
|
||
inp.value = '';
|
||
showToast('✓ Пароль изменен', 'success');
|
||
} else {
|
||
showToast(r?.error || 'Ошибка', 'warn');
|
||
}
|
||
}
|
||
|
||
async function deleteUser(id, username) {
|
||
if (!confirm(`Удалить пользователя "${username}"?`)) return;
|
||
const r = await apiDelete(`/api/users/${id}`);
|
||
if (r?.ok) {
|
||
loadUsersTable();
|
||
loadUserList();
|
||
} else {
|
||
showToast(r?.error || 'Ошибка', 'warn');
|
||
}
|
||
}
|
||
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
if (authToken) {
|
||
try {
|
||
const p = JSON.parse(atob(authToken.split('.')[1]));
|
||
if (p.exp * 1000 > Date.now()) {
|
||
currentUser = { username: p.username, is_admin: p.username === 'admin' };
|
||
showApp();
|
||
init();
|
||
return;
|
||
}
|
||
} catch {}
|
||
localStorage.removeItem('intradesk_token');
|
||
authToken = null;
|
||
}
|
||
document.getElementById('login-overlay').classList.remove('hidden');
|
||
});
|