233 lines
11 KiB
JavaScript
233 lines
11 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;
|
||
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'; // flex = topbar-icon-btn
|
||
|
||
}
|
||
|
||
async function init() {
|
||
await Promise.all([loadMarks(),loadNotes(),loadMentions(),loadUserList(),loadCommentCounts()]);
|
||
await loadTasksFromServer();
|
||
if(currentUser.is_admin){
|
||
const s=await apiGet('/api/settings');
|
||
if(s) applySettings(s);
|
||
loadUsersTable();
|
||
}
|
||
const status=await apiGet('/api/status');
|
||
if(status){serverRefreshInterval=status.auto_refresh_interval||300;lastKnownUpdatedAt=status.last_updated;}
|
||
startSSE();
|
||
startStatusPolling();
|
||
}
|
||
|
||
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 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);
|
||
// Восстанавливаем форсированный комментарий из кеша, если updatedat не изменился
|
||
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;
|
||
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();
|
||
renderTable();
|
||
if(forceComment) fetchMissingComments();
|
||
}
|
||
|
||
async function fetchMissingComments() {
|
||
// Только задачи без комментария, для которых кеш отсутствует или устарел
|
||
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;
|
||
for(const item of toFetch) {
|
||
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;
|
||
// Кешируем результат (даже null) — повторный запрос только при изменении updatedat
|
||
forcedCommentCache.set(tid,{updatedat:item.task.updatedat,lastComment:lc});
|
||
}
|
||
renderTable();
|
||
}
|
||
|
||
|
||
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() {
|
||
await Promise.all([loadMarks(),loadNotes()]);
|
||
await loadTasksFromServer(true);
|
||
}
|
||
|
||
function startStatusPolling() {
|
||
if(statusPollTimer) clearInterval(statusPollTimer);
|
||
statusPollTimer=setInterval(async()=>{
|
||
const s=await apiGet('/api/status');
|
||
if(s?.last_updated && s.last_updated!==lastKnownUpdatedAt) {
|
||
lastKnownUpdatedAt=s.last_updated;
|
||
await Promise.all([loadMarks(),loadNotes()]);
|
||
await loadTasksFromServer(true);
|
||
}
|
||
},30000);
|
||
}
|
||
|
||
function applySettings(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('apikey-status')) f('apikey-status').textContent=s.api_key_set?'(ключ задан ✓)':'(не задан!)';
|
||
}
|
||
async function saveSettings() {
|
||
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};
|
||
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(body);document.getElementById('api-key').value='';showToast('✓ Настройки сохранены','success');}
|
||
else showToast('❌ '+(r?.error||'Ошибка'),'warn');
|
||
}
|
||
|
||
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');
|
||
});
|