"use strict"; // Armazenamento e chaves const STORAGE = { UNITS: "telseg_units", CREDS: "telseg_passwords", THEME: "telseg_theme", NOTES: "telseg_notes", AUTH: "telseg_auth", }; const $ = (sel, el = document) => el.querySelector(sel); const $$ = (sel, el = document) => Array.from(el.querySelectorAll(sel)); // Detecção de API const state = { useApi: false, units: [], creds: [], notes: [], auth: null }; async function probeApi() { try { const r = await fetch('/api/health', { cache: 'no-store' }); state.useApi = r.ok; return state.useApi; } catch { state.useApi = false; return false; } } function updateStorageInfo() { const el = document.getElementById('storageInfo'); if (!el) return; el.textContent = state.useApi ? 'Servidor • Dados persistem no banco (Postgres) do container.' : 'Local • Seus dados ficam somente no seu navegador.'; } // ---- Auth (API) ---- function getToken() { try { return JSON.parse(localStorage.getItem(STORAGE.AUTH) || 'null')?.token || null; } catch { return null; } } function setToken(token) { localStorage.setItem(STORAGE.AUTH, JSON.stringify({ token })); } async function apiFetch(path, opts = {}) { const o = { ...opts, headers: { ...(opts.headers||{}), 'Content-Type': 'application/json' } }; const token = getToken(); if (token) o.headers['Authorization'] = `Bearer ${token}`; const r = await fetch(path, o); if (r.status === 401) { showLogin(); throw new Error('unauthorized'); } return r; } function canMaster() { return !!state.auth?.user && state.auth.user.role === 'master'; } function can(flag) { if (canMaster()) return true; return !!state.auth?.user && !!state.auth.user.permissions?.[flag]; } function applyPermissionsUI() { if (!state.useApi || !state.auth?.user) return; // tabs const tabs = [ { id: 'unidades', read: 'units_read' }, { id: 'senhas', read: 'creds_read' }, { id: 'notas', read: 'notes_read' }, ]; tabs.forEach(t => { const btn = document.querySelector(`.tab[data-tab="${t.id}"]`); const view = document.getElementById(`view-${t.id}`); const visible = can(t.read); if (btn) btn.style.display = visible ? '' : 'none'; if (view) view.style.display = visible ? '' : 'none'; }); // users tab only master const userTab = document.querySelector('#tab-users'); const userView = document.getElementById('view-usuarios'); if (userTab) userTab.style.display = canMaster() ? '' : 'none'; if (userView) userView.style.display = canMaster() ? '' : 'none'; // action buttons document.getElementById('addUnidadeBtn').style.display = can('units_write') ? '' : 'none'; document.getElementById('addCredBtn').style.display = can('creds_write') ? '' : 'none'; document.getElementById('addNoteBtn').style.display = can('notes_write') ? '' : 'none'; } function showLogin() { document.getElementById('authGate')?.classList.remove('hidden'); } function hideLogin() { document.getElementById('authGate')?.classList.add('hidden'); } // Tema (claro/escuro) function loadTheme() { const t = localStorage.getItem(STORAGE.THEME) || "dark"; document.documentElement.setAttribute("data-theme", t); } function toggleTheme() { const cur = document.documentElement.getAttribute("data-theme") || "dark"; const next = cur === "dark" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", next); localStorage.setItem(STORAGE.THEME, next); } // Utilidades function uid() { // ID simples e estável suficiente para uso local return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; } function saveJSON(key, value) { localStorage.setItem(key, JSON.stringify(value)); } function loadJSON(key, fallback) { try { const v = JSON.parse(localStorage.getItem(key) || ""); return v ?? fallback; } catch { return fallback; } } function todayStr() { return new Date().toISOString().slice(0, 10); } function copyText(text) { navigator.clipboard?.writeText(text).catch(() => { const ta = document.createElement("textarea"); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); }); } // Navegação por abas function initTabs() { $$(".tab").forEach(btn => { btn.addEventListener("click", () => { $$(".tab").forEach(b => b.classList.remove("active")); btn.classList.add("active"); const tab = btn.dataset.tab; $$(".view").forEach(v => v.classList.remove("active")); $(`#view-${tab}`).classList.add("active"); if (tab === 'usuarios') renderUsers(); }); }); } // ------- Unidades ------- async function refreshUnits() { if (state.useApi) { const r = await apiFetch('/api/units'); state.units = r.ok ? await r.json() : []; } else { state.units = loadJSON(STORAGE.UNITS, []); } return state.units; } function loadUnits() { return state.units || []; } function saveUnits(list) { saveJSON(STORAGE.UNITS, list); state.units = list; } function parseOCs(str) { return (str || "") .split(/[\n,]/g) .map(s => s.trim()) .filter(Boolean); } async function renderUnits(filter = "") { if (state.useApi) await refreshUnits(); const listEl = $("#unidadesList"); const items = loadUnits(); const f = filter.trim().toLowerCase(); const filtered = !f ? items : items.filter(u => { return ( u.nome.toLowerCase().includes(f) || (u.ip || "").toLowerCase().includes(f) || (u.ocs || []).some(x => x.toLowerCase().includes(f)) ); }); // Ordena por nome: números primeiro, depois letras (ordem alfabética/natural) filtered.sort((a, b) => { const an = (a.nome || '').trim(); const bn = (b.nome || '').trim(); const aNum = /^[0-9]/.test(an); const bNum = /^[0-9]/.test(bn); if (aNum !== bNum) return aNum ? -1 : 1; // números primeiro return an.localeCompare(bn, 'pt-BR', { numeric: true, sensitivity: 'base' }); }); const canWrite = !state.useApi || can('units_write'); listEl.innerHTML = filtered.map(u => { const ocs = (u.ocs || []).map(x => `OC ${escapeHTML(x)}`).join(" "); return `

${escapeHTML(u.nome)}

Criado em ${formatDate(u.dataCriacao)}${u.ip ? ` • IP ${escapeHTML(u.ip)}` : ""}
${ocs ? `
${ocs}
` : ""} ${u.link ? `
Link: ${escapeHTML(u.link)}
` : ""}
${u.link ? `` : ""} ${u.ip ? `` : ""} ${canWrite ? `` : ''} ${canWrite ? `` : ''}
`; }).join(""); // Complementa UI com login/senha filtered.forEach(u => { const card = listEl.querySelector(`.card[data-id="${u.id}"]`); if (!card) return; const meta = card.querySelector('.meta'); if (u.login && meta && !meta.textContent.includes('Login')) { meta.innerHTML = `${meta.innerHTML} • Login ${escapeHTML(u.login)}`; } const actions = card.querySelector('.actions'); if (actions) { const copyIpBtn = card.querySelector('[data-act="copyip"]'); if (copyIpBtn && u.ip) { copyIpBtn.addEventListener('click', (ev) => { ev.stopPropagation(); copyText(u.ip); }); } if (u.login && !actions.querySelector('[data-act="copylogin"]')) { const b = document.createElement('button'); b.className = 'btn small'; b.setAttribute('data-act', 'copylogin'); b.textContent = 'Copiar Login'; b.addEventListener('click', (ev) => { ev.stopPropagation(); copyText(u.login); }); actions.insertBefore(b, actions.querySelector('[data-act="edit"]') || null); } if (u.senha && !actions.querySelector('[data-act="copypass"]')) { const b2 = document.createElement('button'); b2.className = 'btn small'; b2.setAttribute('data-act', 'copypass'); b2.textContent = 'Copiar Senha'; b2.addEventListener('click', (ev) => { ev.stopPropagation(); copyText(u.senha); }); actions.insertBefore(b2, actions.querySelector('[data-act="edit"]') || null); } } }); // Delegação de eventos nos cartões listEl.onclick = async (ev) => { const btn = ev.target.closest("[data-act]"); if (!btn) return; const card = ev.target.closest(".card"); const id = card?.dataset.id; if (!id) return; const items2 = loadUnits(); const idx = items2.findIndex(x => x.id === id); const u = items2[idx]; if (!u) return; const act = btn.dataset.act; if (act === "open" && u.link) { window.open(u.link, "_blank", "noopener"); } else if (act === "copyip" && u.ip) { copyText(u.ip); } else if (act === "copylogin" && u.login) { copyText(u.login); } else if (act === "copypass" && u.senha) { copyText(u.senha); } else if (act === "edit") { openUnidadeModal(u); } else if (act === "del") { if (confirm(`Excluir unidade "${u.nome}"?`)) { if (state.useApi) { await apiFetch(`/api/units/${encodeURIComponent(u.id)}`, { method: 'DELETE' }); await refreshUnits(); } else { items2.splice(idx, 1); saveUnits(items2); } renderUnits($("#searchUnidades").value); } } }; } function openUnidadeModal(u) { $("#unidadeId").value = u?.id || ""; $("#unidadeNome").value = u?.nome || ""; $("#unidadeData").value = u?.dataCriacao || todayStr(); $("#unidadeOCs").value = (u?.ocs || []).join(", "); $("#unidadeLink").value = u?.link || ""; $("#unidadeIP").value = u?.ip || ""; $("#unidadeLogin").value = u?.login || ""; $("#unidadeSenha").value = ""; // nunca preencher senha existente showModal("unidadeModal"); } // ------- Modal Helpers ------- function showModal(id) { const el = document.getElementById(id); el?.classList.remove("hidden"); } function hideModal(id) { const el = document.getElementById(id); el?.classList.add("hidden"); } // Escapar HTML básico function escapeHTML(s) { return (s ?? "").replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">","\"":"""}[c])); } function escapeAttr(s) { return escapeHTML(s).replace(/'/g, "'"); } function formatDate(s) { try { const [y,m,d] = (s||"").split("-"); return d && m && y ? `${d}/${m}/${y}` : s; } catch { return s; } } async function refreshCreds() { if (state.useApi) { const r = await apiFetch('/api/creds'); state.creds = r.ok ? await r.json() : []; } else { state.creds = loadJSON(STORAGE.CREDS, []); } return state.creds; } function loadCreds() { return state.creds || []; } function saveCreds(list) { saveJSON(STORAGE.CREDS, list); state.creds = list; } function getCredPassword(cred) { return cred?.passwordEnc ?? ""; } // ------- Notas ------- async function refreshNotes() { if (state.useApi) { const r = await apiFetch('/api/notes'); state.notes = r.ok ? await r.json() : []; } else { state.notes = loadJSON(STORAGE.NOTES, []); } return state.notes; } function loadNotes() { return state.notes || []; } function saveNotes(list) { saveJSON(STORAGE.NOTES, list); state.notes = list; } function openNoteModal(n) { $("#noteId").value = n?.id || ""; $("#noteTitle").value = n?.title || ""; $("#noteBody").value = n?.body || ""; $("#noteType").value = n?.data?.type || 'texto'; editingSteps = (n?.data?.steps || []).map(s => ({ id: s.id || uid(), text: s.text || '' })); toggleStepsBlock(); renderSteps(); showModal("noteModal"); } async function renderNotes(filter = "") { if (state.useApi) await refreshNotes(); const listText = document.getElementById('notesTextList'); const listSteps = document.getElementById('notesStepsList'); const items = loadNotes(); const f = filter.trim().toLowerCase(); const filtered = !f ? items : items.filter(n => ( (n.title || "").toLowerCase().includes(f) || (n.body || "").toLowerCase().includes(f) )); filtered.sort((a,b) => (b.updatedAt||0)-(a.updatedAt||0)); const renderCard = (n) => { const preview = escapeHTML((n.body || '').split('\n').slice(0,3).join(' ')); const when = n.updatedAt ? new Date(n.updatedAt).toLocaleString() : ''; return `

${escapeHTML(n.title || 'Sem título')}

${n.data?.type === 'passos' ? 'Passo a passo' : ''} ${when ? `• Atualizado: ${when}` : ''}
${n.data?.type === 'passos' ? `
${(n.data.steps||[]).slice(0,3).map((s,i)=>`${i+1}. ${escapeHTML(s.text||'')}`).join('
')}
` : (preview ? `
${preview}
` : '')}
`; }; const texts = filtered.filter(n => (n.data?.type || 'texto') !== 'passos'); const steps = filtered.filter(n => n.data?.type === 'passos'); if (listText) listText.innerHTML = texts.map(renderCard).join(""); if (listSteps) listSteps.innerHTML = steps.map(renderCard).join(""); const attach = (el) => { if (!el) return; el.onclick = (ev) => { const card = ev.target.closest('.card'); if (!card) return; const id = card.dataset.id; const n = loadNotes().find(x => x.id === id); if (n) openNoteView(n); }; }; attach(listText); attach(listSteps); } // --- Editor de passos (modal Nota) let editingSteps = []; function toggleStepsBlock() { const type = $("#noteType").value; const block = $("#stepsBlock"); if (type === 'passos') block.classList.remove('hidden'); else block.classList.add('hidden'); } function renderSteps() { const el = $("#stepsList"); el.innerHTML = editingSteps.map((s, idx) => `
${escapeHTML(s.text)}
`).join(""); } // Visualização de nota (popup) function openNoteView(n) { const body = document.getElementById('noteViewBody'); const title = document.getElementById('noteViewTitle'); title.textContent = n.title || 'Sem título'; const when = n.updatedAt ? new Date(n.updatedAt).toLocaleString() : ''; const chip = n.data?.type === 'passos' ? 'Passo a passo' : 'Texto'; const sections = []; if (n.data?.type === 'passos' && (n.data.steps || []).length) { const items = (n.data.steps || []).map(s => `
  • ${escapeHTML(s.text || '')}
  • `).join(''); sections.push(`
    Passos
      ${items}
    `); } if ((n.body || '').trim()) { sections.push(`
    Conteúdo
    ${escapeHTML(n.body)}
    `); } if (!sections.length) { sections.push('
    Sem conteúdo
    '); } body.innerHTML = `
    ${chip} ${when ? `• Atualizado: ${when}` : ''}
    ${sections.join('')}
    `; // Ações document.getElementById('noteViewEdit').onclick = () => { hideModal('noteViewModal'); openNoteModal(n); }; document.getElementById('noteViewDelete').onclick = async () => { if (!confirm('Excluir esta nota?')) return; if (state.useApi) { await apiFetch(`/api/notes/${encodeURIComponent(n.id)}`, { method: 'DELETE' }); await refreshNotes(); } else { const list = loadNotes().filter(x => x.id !== n.id); saveNotes(list); } hideModal('noteViewModal'); renderNotes($("#searchNotes").value); }; showModal('noteViewModal'); } async function renderCreds(filter = "") { if (state.useApi) await refreshCreds(); const listEl = $("#credsList"); const items = loadCreds(); const f = filter.trim().toLowerCase(); const filtered = !f ? items : items.filter(c => { return ( c.nome.toLowerCase().includes(f) || (c.usuario || "").toLowerCase().includes(f) || (c.url || "").toLowerCase().includes(f) ); }); filtered.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)); const canWrite = !state.useApi || can('creds_write'); listEl.innerHTML = filtered.map(c => { const preview = escapeHTML((c.notas || '').split('\n').slice(0,2).join(' ')); const when = c.updatedAt ? new Date(c.updatedAt).toLocaleString() : ''; return `

    ${escapeHTML(c.nome)}

    ${c.usuario ? `Usuário ${escapeHTML(c.usuario)} • `: ''}${c.url ? `${escapeHTML(c.url)}` : ''} ${when ? `• Atualizado: ${when}` : ''}
    ${preview ? `
    ${preview}
    ` : ''}
    `; }).join(""); listEl.onclick = async (ev) => { const btn = ev.target.closest("[data-act]"); const card = ev.target.closest('.card'); if (!card) return; const id = card.dataset.id; const items2 = loadCreds(); const idx = items2.findIndex(x => x.id === id); const c = items2[idx]; if (!c) return; if (btn) { const act = btn.dataset.act; if (act === "open" && c.url) { window.open(c.url, "_blank", "noopener"); } else if (act === "copyuser" && c.usuario) { copyText(c.usuario); } else if (act === "copypass") { const pass = getCredPassword(c); if (!pass) { alert("Senha indisponível."); return; } copyText(pass); } else if (act === "reveal") { const pass = getCredPassword(c); if (!pass) { alert("Senha indisponível."); return; } alert(`Senha: ${pass}`); } else if (act === "edit") { openCredModal(c); } else if (act === "del") { if (confirm(`Excluir credencial "${c.nome}"?`)) { if (state.useApi) { await apiFetch(`/api/creds/${encodeURIComponent(c.id)}`, { method: 'DELETE' }); await refreshCreds(); } else { items2.splice(idx,1); saveCreds(items2); } renderCreds($("#searchCreds").value); } } } else { openCredView(c, canWrite); } }; } function openCredModal(c) { $("#credId").value = c?.id || ""; $("#credNome").value = c?.nome || ""; $("#credUsuario").value = c?.usuario || ""; $("#credSenha").value = getCredPassword(c); $("#credUrl").value = c?.url || ""; $("#credNotas").value = c?.notas || ""; showModal("credModal"); } // Visualização de credencial (popup) function openCredView(c, canWrite) { const body = document.getElementById('credViewBody'); const title = document.getElementById('credViewTitle'); title.textContent = c.nome || 'Credencial'; const when = c.updatedAt ? new Date(c.updatedAt).toLocaleString() : ''; const sections = []; const access = []; if (c.usuario) access.push(`
    Usuário: ${escapeHTML(c.usuario)}
    `); if (c.url) access.push(`
    URL: ${escapeHTML(c.url)}
    `); if (access.length) sections.push(`
    Acesso
    ${access.join('')}
    `); if ((c.notas || '').trim()) sections.push(`
    Notas
    ${escapeHTML(c.notas)}
    `); if (!sections.length) sections.push('
    Sem detalhes
    '); body.innerHTML = `
    ${when ? `Atualizado: ${when}` : ''}
    ${sections.join('')}
    `; // Ações const btnOpen = document.getElementById('credViewOpen'); const btnCopyUser = document.getElementById('credViewCopyUser'); const btnCopyPass = document.getElementById('credViewCopyPass'); const btnReveal = document.getElementById('credViewReveal'); const btnEdit = document.getElementById('credViewEdit'); const btnDelete = document.getElementById('credViewDelete'); btnOpen.style.display = c.url ? '' : 'none'; btnCopyUser.style.display = c.usuario ? '' : 'none'; btnEdit.style.display = canWrite ? '' : 'none'; btnDelete.style.display = canWrite ? '' : 'none'; btnOpen.onclick = () => { if (c.url) window.open(c.url, '_blank', 'noopener'); }; btnCopyUser.onclick = () => { if (c.usuario) copyText(c.usuario); }; btnCopyPass.onclick = () => { const pass = getCredPassword(c); if (!pass) { alert('Senha indisponível.'); return; } copyText(pass); }; btnReveal.onclick = () => { const pass = getCredPassword(c); if (!pass) { alert('Senha indisponível.'); return; } alert(`Senha: ${pass}`); }; btnEdit.onclick = () => { hideModal('credViewModal'); openCredModal(c); }; btnDelete.onclick = async () => { if (!confirm(`Excluir credencial "${c.nome}"?`)) return; const items2 = loadCreds(); const idx = items2.findIndex(x => x.id === c.id); if (state.useApi) { await apiFetch(`/api/creds/${encodeURIComponent(c.id)}`, { method: 'DELETE' }); await refreshCreds(); } else if (idx >= 0) { items2.splice(idx,1); saveCreds(items2); } hideModal('credViewModal'); renderCreds($("#searchCreds").value); }; showModal('credViewModal'); } // ------- Exportar / Importar ------- function exportAll() { const payload = { version: 1, exportedAt: new Date().toISOString(), theme: document.documentElement.getAttribute("data-theme"), units: loadUnits(), creds: loadCreds(), notes: loadNotes(), }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const a = document.createElement("a"); const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth()+1).padStart(2,"0"); const day = String(d.getDate()).padStart(2,"0"); a.download = `telseg_dashboard_backup_${y}${m}${day}.json`; a.href = URL.createObjectURL(blob); a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 3000); } function importAll(file) { const reader = new FileReader(); reader.onload = () => { try { const data = JSON.parse(String(reader.result || "{}")); if (!confirm("Importar backup e substituir dados atuais?")) return; if (data.theme) { localStorage.setItem(STORAGE.THEME, data.theme); document.documentElement.setAttribute("data-theme", data.theme); } if (Array.isArray(data.units)) saveUnits(data.units); if (Array.isArray(data.creds)) saveCreds(data.creds); if (Array.isArray(data.notes)) saveNotes(data.notes); alert("Importação concluída."); renderUnits($("#searchUnidades").value); renderNotes($("#searchNotes").value); } catch (e) { alert("Arquivo inválido."); } }; reader.readAsText(file); } // ------- Inicialização ------- document.addEventListener("DOMContentLoaded", async () => { loadTheme(); await probeApi(); // Auth flow when API is enabled if (state.useApi) { // try existing token const token = getToken(); if (token) { try { const r = await apiFetch('/api/auth/me'); const data = await r.json(); state.auth = data; hideLogin(); applyPermissionsUI(); if (canMaster()) await renderUsers(); } catch { showLogin(); } } else { showLogin(); } } if (!state.useApi || state.auth?.user) { await Promise.all([refreshUnits(), refreshCreds(), refreshNotes()]); } updateStorageInfo(); applyPermissionsUI(); $("#themeToggle").addEventListener("click", toggleTheme); initTabs(); // Unidades $("#addUnidadeBtn").addEventListener("click", () => openUnidadeModal()); $("#searchUnidades").addEventListener("input", ev => renderUnits(ev.target.value)); $("#unidadeForm").addEventListener("submit", async (ev) => { ev.preventDefault(); const id = $("#unidadeId").value || uid(); const listBefore = loadUnits(); const idxBefore = listBefore.findIndex(x => x.id === id); const existing = idxBefore >= 0 ? listBefore[idxBefore] : null; const item = { id, nome: $("#unidadeNome").value.trim(), dataCriacao: $("#unidadeData").value || todayStr(), ocs: parseOCs($("#unidadeOCs").value), link: $("#unidadeLink").value.trim(), ip: $("#unidadeIP").value.trim(), login: $("#unidadeLogin").value.trim(), senha: $("#unidadeSenha").value ? $("#unidadeSenha").value : (existing?.senha || ''), }; if (!item.nome) { alert("Informe o nome do cliente."); return; } if (state.useApi) { const exists = idxBefore >= 0; await apiFetch(exists ? `/api/units/${encodeURIComponent(id)}` : '/api/units', { method: exists ? 'PUT' : 'POST', body: JSON.stringify(item) }); await refreshUnits(); } else { const list = listBefore.slice(); if (idxBefore >= 0) list[idxBefore] = item; else list.push(item); saveUnits(list); } $("#unidadeSenha").value = ""; hideModal("unidadeModal"); renderUnits($("#searchUnidades").value); }); // Fechar modais document.body.addEventListener("click", (ev) => { const btn = ev.target.closest("[data-close-modal]"); if (btn) hideModal(btn.getAttribute("data-close-modal")); }); // Senhas $("#searchCreds").addEventListener("input", ev => renderCreds(ev.target.value)); $("#addCredBtn").addEventListener("click", () => openCredModal()); // Notas $("#addNoteBtn").addEventListener("click", () => openNoteModal()); $("#searchNotes").addEventListener("input", ev => renderNotes(ev.target.value)); $("#noteType").addEventListener("change", toggleStepsBlock); $("#addStepBtn").addEventListener("click", () => { const input = $("#newStepText"); const text = input.value.trim(); if (!text) return; editingSteps.push({ id: uid(), text }); input.value = ""; renderSteps(); }); $("#stepsList").addEventListener("click", (ev) => { const del = ev.target.closest('[data-del]'); const row = ev.target.closest('.step-card'); if (del && row) { const idx = Number(row.dataset.idx); editingSteps.splice(idx,1); renderSteps(); } }); $("#stepsList").addEventListener("blur", (ev) => { const row = ev.target.closest('.step-card'); if (!row) return; const idx = Number(row.dataset.idx); const textEl = row.querySelector('.text'); if (textEl) editingSteps[idx].text = textEl.textContent.trim(); }, true); $("#noteForm").addEventListener("submit", async (ev) => { ev.preventDefault(); const id = $("#noteId").value || uid(); const note = { id, title: $("#noteTitle").value.trim(), body: $("#noteBody").value, updatedAt: Date.now(), data: $("#noteType").value === 'passos' ? { type: 'passos', steps: editingSteps.slice() } : { type: 'texto' } }; if (state.useApi) { const exists = loadNotes().some(n => n.id === id); await apiFetch(exists ? `/api/notes/${encodeURIComponent(id)}` : '/api/notes', { method: exists ? 'PUT' : 'POST', body: JSON.stringify(note) }); await refreshNotes(); } else { const list = loadNotes(); const idx = list.findIndex(n => n.id === id); if (idx>=0) list[idx] = note; else list.push(note); saveNotes(list); } hideModal('noteModal'); renderNotes($("#searchNotes").value); }); $("#credForm").addEventListener("submit", async (ev) => { ev.preventDefault(); const id = $("#credId").value || uid(); const list = loadCreds(); const idx = list.findIndex(x => x.id === id); const passwordPlain = $("#credSenha").value.trim(); const item = { id, nome: $("#credNome").value.trim(), usuario: $("#credUsuario").value.trim(), url: $("#credUrl").value.trim(), notas: $("#credNotas").value.trim(), passwordEnc: passwordPlain, updatedAt: Date.now(), }; if (!item.nome) { alert("Informe o nome."); return; } if (!item.passwordEnc) { alert("Informe a senha."); return; } if (state.useApi) { const exists = idx >= 0; await apiFetch(exists ? `/api/creds/${encodeURIComponent(id)}` : '/api/creds', { method: exists ? 'PUT' : 'POST', body: JSON.stringify(item) }); await refreshCreds(); } else { if (idx >= 0) list[idx] = item; else list.push(item); saveCreds(list); } hideModal("credModal"); renderCreds($("#searchCreds").value); }); // Exportar / Importar $("#exportBtn").addEventListener("click", async () => { if (state.useApi) { const r = await apiFetch('/api/export'); const payload = await r.json(); const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const a = document.createElement("a"); a.download = `telseg_dashboard_backup_api_${new Date().toISOString().slice(0,10)}.json`; a.href = URL.createObjectURL(blob); a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 3000); } else { exportAll(); } }); $("#importFile").addEventListener("change", async (ev) => { const f = ev.target.files?.[0]; if (!f) return; if (state.useApi) { const text = await f.text(); const data = JSON.parse(text); if (!confirm("Importar backup e substituir dados atuais no servidor?")) return; await apiFetch('/api/import', { method: 'POST', body: JSON.stringify(data) }); await Promise.all([refreshUnits(), refreshCreds(), refreshNotes()]); renderUnits($("#searchUnidades").value); // re-render renderNotes($("#searchNotes").value); } else { importAll(f); } ev.target.value = ""; // permite importar o mesmo arquivo novamente }); // Login form const loginForm = document.getElementById('loginForm'); if (loginForm) { loginForm.addEventListener('submit', async (ev) => { ev.preventDefault(); const username = document.getElementById('loginUser').value.trim(); const password = document.getElementById('loginPass').value; try { const r = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (!r.ok) throw new Error(); const data = await r.json(); setToken(data.token); state.auth = { user: data.user }; hideLogin(); applyPermissionsUI(); await Promise.all([refreshUnits(), refreshCreds(), refreshNotes()]); if (canMaster()) await renderUsers(); renderUnits(document.getElementById('searchUnidades').value); renderNotes(document.getElementById('searchNotes').value); } catch { alert('Login inválido'); } }); } // Render inicial if (!state.useApi || state.auth?.user) { renderUnits(""); renderNotes(""); renderCreds(""); } }); // ---- Usuários (somente Master) ---- async function renderUsers() { if (!state.useApi || !canMaster()) return; try { const r = await apiFetch('/api/users'); const users = await r.json(); const list = document.getElementById('usersList'); list.innerHTML = users.map(u => { const p = u.permissions || {}; return `

    ${escapeHTML(u.username)} ${u.role === 'master' ? 'Master' : ''}

    `; }).join(''); // Handlers list.onchange = async (ev) => { const card = ev.target.closest('.card'); if (!card) return; const id = card.dataset.id; if (ev.target.matches('[data-perm]')) { // Collect card perms const inputs = Array.from(card.querySelectorAll('[data-perm]')); const body = Object.fromEntries(inputs.map(i => [i.getAttribute('data-perm'), i.checked])); await apiFetch(`/api/users/${encodeURIComponent(id)}/permissions`, { method: 'PUT', body: JSON.stringify(body) }); } else if (ev.target.matches('[data-role]')) { const role = ev.target.value; await apiFetch(`/api/users/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify({ role }) }); await renderUsers(); } }; list.onclick = async (ev) => { const card = ev.target.closest('.card'); if (!card) return; const id = card.dataset.id; const actBtn = ev.target.closest('[data-act]'); if (!actBtn) return; if (actBtn.dataset.act === 'reset') { const pass = prompt('Nova senha:'); if (!pass) return; await apiFetch(`/api/users/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify({ password: pass }) }); alert('Senha atualizada'); } else if (actBtn.dataset.act === 'del') { if (!confirm('Excluir este usuário?')) return; await apiFetch(`/api/users/${encodeURIComponent(id)}`, { method: 'DELETE' }); await renderUsers(); } }; } catch (e) { console.error('renderUsers', e); } } // Create user form document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('userForm'); if (!form) return; form.addEventListener('submit', async (ev) => { ev.preventDefault(); const username = document.getElementById('newUserName').value.trim(); const password = document.getElementById('newUserPass').value; const role = document.getElementById('newUserRole').value; const body = { username, password, role, permissions: { units_read: document.getElementById('p_units_read').checked, units_write: document.getElementById('p_units_write').checked, creds_read: document.getElementById('p_creds_read').checked, creds_write: document.getElementById('p_creds_write').checked, notes_read: document.getElementById('p_notes_read').checked, notes_write: document.getElementById('p_notes_write').checked, } }; try { await apiFetch('/api/users', { method: 'POST', body: JSON.stringify(body) }); form.reset(); await renderUsers(); alert('Usuário criado'); } catch { alert('Falha ao criar usuário'); } }); });