Files
COMERCIAL-GTI/public/js/api.js
T
Gustavo 4f736562f3 Carry over pre-existing WABA outreach automation WIP
This work already existed uncommitted in the main working tree (not
authored by this branch) and is required as a runtime dependency for the
campaign outreach port (zpro-client, templates, outreach routes/UI). Copied
in as-is, untouched, so this feature branch has a working baseline; the
original uncommitted state in the main checkout is left exactly as it was.
2026-08-01 10:12:25 -03:00

100 lines
3.5 KiB
JavaScript

/* Cliente da API local + utilitários compartilhados. */
// 401 fora da tela de login = sessao expirada/inexistente: manda pro login.
function redirecionaSeSessaoExpirou(path, status) {
if (status === 401 && path !== '/api/auth/login' && !location.pathname.endsWith('/login.html')) {
window.location.href = '/login.html';
}
}
const API = {
async get(path) {
const res = await fetch(path);
const data = await res.json();
if (!res.ok) {
redirecionaSeSessaoExpirou(path, res.status);
throw new Error(data.erro || `HTTP ${res.status}`);
}
return data;
},
async send(method, path, body) {
const res = await fetch(path, {
method,
headers: { 'content-type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body)
});
const data = await res.json();
if (!res.ok) {
redirecionaSeSessaoExpirou(path, res.status);
const error = new Error(data.erro || `HTTP ${res.status}`);
error.data = data;
error.status = res.status;
throw error;
}
return data;
},
post(path, body) { return this.send('POST', path, body); },
put(path, body) { return this.send('PUT', path, body); },
// Acompanha um job via SSE; handlers: {progress, log, done, error}
watchJob(jobId, handlers) {
const source = new EventSource(`/api/jobs/${jobId}/events`);
const close = () => source.close();
source.addEventListener('progress', (e) => handlers.progress?.(JSON.parse(e.data)));
source.addEventListener('log', (e) => handlers.log?.(JSON.parse(e.data)));
source.addEventListener('done', (e) => {
handlers.done?.(JSON.parse(e.data));
close();
});
source.addEventListener('error', (e) => {
if (e.data) {
handlers.error?.(JSON.parse(e.data));
close();
}
// sem e.data = queda de conexão; EventSource tenta reconectar sozinho
});
return close;
}
};
const fmt = new Intl.NumberFormat('pt-BR');
function el(id) { return document.getElementById(id); }
function h(tag, attrs = {}, ...children) {
const node = document.createElement(tag);
for (const [key, value] of Object.entries(attrs)) {
if (key === 'class') node.className = value;
else if (key.startsWith('on')) node.addEventListener(key.slice(2), value);
else if (value !== null && value !== undefined) node.setAttribute(key, value);
}
for (const child of children.flat()) {
if (child === null || child === undefined) continue;
node.append(child.nodeType ? child : document.createTextNode(child));
}
return node;
}
let toastTimer = null;
// Aviso: um <dialog> aberto via showModal() fica acima de qualquer div normal
// (top layer do navegador, z-index nao ajuda — e testamos que nem a Popover
// API resolve, o dialog continua por cima). Por isso o toast NAO deve ser a
// unica forma de mostrar erro de uma acao disparada de dentro de um modal
// aberto: use tambem uma mensagem inline no proprio modal nesses casos.
function toast(message, ms = 3500) {
const box = el('toast');
box.textContent = message;
box.classList.remove('hidden');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => box.classList.add('hidden'), ms);
}
function dataBr(iso) {
if (!iso) return '—';
const d = new Date(iso.includes('T') || iso.includes(' ') ? iso.replace(' ', 'T') + (iso.endsWith('Z') ? '' : 'Z') : iso);
return Number.isNaN(d.getTime()) ? iso : d.toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
}