- Add catalog module to POS config endpoints, sidebar filter, config UI - Add catalog toggle to Instance Manager tenant modules modal
537 lines
24 KiB
JavaScript
537 lines
24 KiB
JavaScript
/**
|
|
* Nexus Instance Manager — Frontend SPA
|
|
*/
|
|
|
|
const API_BASE = "";
|
|
let currentToken = localStorage.getItem("manager_token") || "";
|
|
|
|
// ─── Router ────────────────────────────────────────────────────────────────
|
|
const routes = {
|
|
"#dashboard": "dashboard",
|
|
"#demos": "demos",
|
|
"#tenants": "tenants",
|
|
"#health": "health",
|
|
"#migrations": "migrations"
|
|
};
|
|
|
|
function navigate() {
|
|
const hash = window.location.hash || "#dashboard";
|
|
const page = routes[hash] || "dashboard";
|
|
|
|
document.querySelectorAll(".page").forEach(p => p.style.display = "none");
|
|
document.getElementById(`page-${page}`).style.display = "block";
|
|
|
|
document.querySelectorAll(".nav-item").forEach(n => n.classList.remove("active"));
|
|
const nav = document.querySelector(`.nav-item[data-page="${page}"]`);
|
|
if (nav) nav.classList.add("active");
|
|
|
|
const titles = {
|
|
dashboard: "Dashboard",
|
|
demos: "Crear Demos",
|
|
tenants: "Tenants",
|
|
health: "Salud del Sistema",
|
|
migrations: "Migraciones"
|
|
};
|
|
document.getElementById("page-title").textContent = titles[page] || "Dashboard";
|
|
|
|
// Load page data
|
|
if (page === "dashboard") loadDashboard();
|
|
if (page === "demos") loadDemos();
|
|
if (page === "tenants") loadTenants();
|
|
if (page === "health") loadHealth();
|
|
if (page === "migrations") loadMigrations();
|
|
}
|
|
|
|
window.addEventListener("hashchange", navigate);
|
|
|
|
// ─── Auth ──────────────────────────────────────────────────────────────────
|
|
async function api(url, opts = {}) {
|
|
const options = {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${currentToken}`
|
|
},
|
|
...opts
|
|
};
|
|
if (opts.body && typeof opts.body !== "string") {
|
|
options.body = JSON.stringify(opts.body);
|
|
}
|
|
const res = await fetch(`${API_BASE}${url}`, options);
|
|
if (res.status === 401) {
|
|
logout();
|
|
return null;
|
|
}
|
|
const data = await res.json().catch(() => ({}));
|
|
return { status: res.status, data };
|
|
}
|
|
|
|
function showLogin() {
|
|
document.getElementById("login-screen").style.display = "flex";
|
|
document.getElementById("app").style.display = "none";
|
|
}
|
|
|
|
function showApp() {
|
|
document.getElementById("login-screen").style.display = "none";
|
|
document.getElementById("app").style.display = "flex";
|
|
navigate();
|
|
}
|
|
|
|
async function initAuth() {
|
|
if (!currentToken) {
|
|
showLogin();
|
|
return;
|
|
}
|
|
const res = await api("/api/auth/me");
|
|
if (res && res.status === 200) {
|
|
document.getElementById("user-email").textContent = res.data.user.email;
|
|
showApp();
|
|
} else {
|
|
showLogin();
|
|
}
|
|
}
|
|
|
|
document.getElementById("login-form").addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
const email = document.getElementById("login-email").value;
|
|
const password = document.getElementById("login-password").value;
|
|
const errEl = document.getElementById("login-error");
|
|
errEl.style.display = "none";
|
|
|
|
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, password })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (res.ok) {
|
|
currentToken = data.access_token;
|
|
localStorage.setItem("manager_token", currentToken);
|
|
document.getElementById("user-email").textContent = data.user.email;
|
|
showApp();
|
|
} else {
|
|
errEl.textContent = data.error || "Error de autenticación";
|
|
errEl.style.display = "block";
|
|
}
|
|
});
|
|
|
|
function logout() {
|
|
currentToken = "";
|
|
localStorage.removeItem("manager_token");
|
|
showLogin();
|
|
}
|
|
|
|
// ─── Dashboard ─────────────────────────────────────────────────────────────
|
|
async function loadDashboard() {
|
|
const statsRes = await api("/api/admin/stats");
|
|
if (statsRes && statsRes.status === 200) {
|
|
const s = statsRes.data;
|
|
document.getElementById("stat-total").textContent = s.tenants.total;
|
|
document.getElementById("stat-active").textContent = s.tenants.active;
|
|
document.getElementById("stat-demos").textContent = s.tenants.demos;
|
|
document.getElementById("stat-expiring").textContent = s.tenants.expiring_soon;
|
|
|
|
const healthEl = document.getElementById("system-health-summary");
|
|
healthEl.innerHTML = `
|
|
<div class="health-item">
|
|
<span class="health-label">Disco usado</span>
|
|
<span class="health-value">${s.system.disk_percent}%</span>
|
|
</div>
|
|
<div class="health-bar-bg"><div class="health-bar-fill bg-blue" style="width:${s.system.disk_percent}%; background:${getBarColor(s.system.disk_percent)}"></div></div>
|
|
<div class="health-item" style="margin-top:12px">
|
|
<span class="health-label">Memoria usada</span>
|
|
<span class="health-value">${s.system.memory_percent}%</span>
|
|
</div>
|
|
<div class="health-bar-bg"><div class="health-bar-fill bg-blue" style="width:${s.system.memory_percent}%; background:${getBarColor(s.system.memory_percent)}"></div></div>
|
|
<div class="health-item" style="margin-top:12px">
|
|
<span class="health-label">Disco libre</span>
|
|
<span class="health-value">${s.system.disk_free_gb} GB</span>
|
|
</div>
|
|
<div class="health-item">
|
|
<span class="health-label">RAM disponible</span>
|
|
<span class="health-value">${s.system.memory_available_gb} GB</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const tenantsRes = await api("/api/demos");
|
|
if (tenantsRes && tenantsRes.status === 200) {
|
|
const tbody = document.getElementById("recent-demos-table");
|
|
const demos = tenantsRes.data.data.slice(0, 5);
|
|
tbody.innerHTML = demos.map(d => `
|
|
<tr>
|
|
<td><strong>${escapeHtml(d.name)}</strong></td>
|
|
<td><code>${escapeHtml(d.subdomain)}</code></td>
|
|
<td>${d.demo_days_left !== null ? d.demo_days_left + " días" : "N/A"}</td>
|
|
<td>${d.is_active ? tag("Activo", "success") : tag("Inactivo", "danger")}</td>
|
|
</tr>
|
|
`).join("") || `<tr><td colspan="4" class="text-muted text-center">No hay demos activas</td></tr>`;
|
|
}
|
|
}
|
|
|
|
function getBarColor(pct) {
|
|
if (pct < 60) return "var(--success)";
|
|
if (pct < 85) return "var(--warning)";
|
|
return "var(--danger)";
|
|
}
|
|
|
|
// ─── Demos ─────────────────────────────────────────────────────────────────
|
|
async function loadDemos() {
|
|
const res = await api("/api/demos");
|
|
if (!res || res.status !== 200) return;
|
|
|
|
const tbody = document.getElementById("demos-table");
|
|
const demos = res.data.data;
|
|
tbody.innerHTML = demos.map(d => `
|
|
<tr>
|
|
<td><strong>${escapeHtml(d.name)}</strong></td>
|
|
<td><a href="https://${escapeHtml(d.subdomain)}.nexusautoparts.com.mx/pos/login" target="_blank" style="color:var(--accent)">${escapeHtml(d.subdomain)}</a></td>
|
|
<td>${d.demo_days_left !== null ? d.demo_days_left + " días" : "N/A"}</td>
|
|
<td>
|
|
<button class="btn-icon" onclick="openModulesModal(${d.id}, '${escapeHtml(d.name)}')" title="Módulos"><i class="fas fa-cubes" style="color:var(--accent)"></i></button>
|
|
<button class="btn-icon" onclick="resetTenant(${d.id})" title="Resetear"><i class="fas fa-undo"></i></button>
|
|
<button class="btn-icon" onclick="toggleTenant(${d.id}, ${!d.is_active})" title="${d.is_active ? "Desactivar" : "Activar"}"><i class="fas fa-${d.is_active ? "pause" : "play"}"></i></button>
|
|
<button class="btn-icon" onclick="confirmDelete(${d.id}, '${escapeHtml(d.name)}')" title="Eliminar"><i class="fas fa-trash" style="color:var(--danger)"></i></button>
|
|
</td>
|
|
</tr>
|
|
`).join("") || `<tr><td colspan="4" class="text-muted text-center">No hay demos</td></tr>`;
|
|
}
|
|
|
|
document.getElementById("demo-form").addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
const btn = e.target.querySelector("button[type=submit]");
|
|
const originalText = btn.innerHTML;
|
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Creando...`;
|
|
btn.disabled = true;
|
|
|
|
const payload = {
|
|
name: document.getElementById("demo-name").value,
|
|
email: document.getElementById("demo-email").value,
|
|
days: parseInt(document.getElementById("demo-days").value),
|
|
pin: document.getElementById("demo-pin").value,
|
|
subdomain: document.getElementById("demo-subdomain").value || undefined
|
|
};
|
|
|
|
const res = await api("/api/demos", { method: "POST", body: payload });
|
|
const resultBox = document.getElementById("demo-result");
|
|
|
|
if (res && res.status === 201) {
|
|
const d = res.data.data;
|
|
resultBox.innerHTML = `
|
|
<h4><i class="fas fa-check-circle"></i> Demo creada exitosamente</h4>
|
|
<div class="copy-row"><strong>URL:</strong> <code>${d.access_url}</code> <button class="btn-icon" onclick="copyText('${d.access_url}')"><i class="fas fa-copy"></i></button></div>
|
|
<div class="copy-row"><strong>Subdominio:</strong> <code>${d.subdomain}</code></div>
|
|
<div class="copy-row"><strong>PIN Owner:</strong> <code>${d.owner_pin}</code></div>
|
|
<div class="copy-row"><strong>Expira:</strong> ${new Date(d.expires_at).toLocaleDateString()}</div>
|
|
`;
|
|
resultBox.style.display = "block";
|
|
toast("Demo creada correctamente", "success");
|
|
document.getElementById("demo-form").reset();
|
|
loadDemos();
|
|
} else {
|
|
toast(res?.data?.error || "Error al crear demo", "error");
|
|
}
|
|
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
});
|
|
|
|
// ─── Tenants ───────────────────────────────────────────────────────────────
|
|
async function loadTenants(withStats = false) {
|
|
const res = await api(`/api/tenants?stats=${withStats}`);
|
|
if (!res || res.status !== 200) return;
|
|
|
|
const tbody = document.getElementById("tenants-table");
|
|
const tenants = res.data.data;
|
|
document.getElementById("tenant-count").textContent = tenants.length;
|
|
|
|
tbody.innerHTML = tenants.map(t => `
|
|
<tr>
|
|
<td>${t.id}</td>
|
|
<td><strong>${escapeHtml(t.name)}</strong></td>
|
|
<td><code>${escapeHtml(t.subdomain)}</code></td>
|
|
<td>${tag(t.plan || "basic", t.plan === "demo" ? "info" : "default")}</td>
|
|
<td>${t.schema_version || "v0.0"}</td>
|
|
<td>${t.is_active ? tag("Activo", "success") : tag("Inactivo", "danger")}</td>
|
|
<td>${formatDate(t.created_at)}</td>
|
|
<td>
|
|
<button class="btn-icon" onclick="openModulesModal(${t.id}, '${escapeHtml(t.name)}')" title="Módulos"><i class="fas fa-cubes" style="color:var(--accent)"></i></button>
|
|
<button class="btn-icon" onclick="resetTenant(${t.id})" title="Resetear datos"><i class="fas fa-undo"></i></button>
|
|
<button class="btn-icon" onclick="toggleTenant(${t.id}, ${!t.is_active})" title="${t.is_active ? "Desactivar" : "Activar"}"><i class="fas fa-${t.is_active ? "pause" : "play"}"></i></button>
|
|
<button class="btn-icon" onclick="confirmDelete(${t.id}, '${escapeHtml(t.name)}')" title="Eliminar"><i class="fas fa-trash" style="color:var(--danger)"></i></button>
|
|
</td>
|
|
</tr>
|
|
`).join("") || `<tr><td colspan="8" class="text-muted text-center">No hay tenants</td></tr>`;
|
|
}
|
|
|
|
document.getElementById("tenant-search")?.addEventListener("input", (e) => {
|
|
const term = e.target.value.toLowerCase();
|
|
document.querySelectorAll("#tenants-table tr").forEach(row => {
|
|
row.style.display = row.textContent.toLowerCase().includes(term) ? "" : "none";
|
|
});
|
|
});
|
|
|
|
// ─── Health ────────────────────────────────────────────────────────────────
|
|
async function loadHealth() {
|
|
const res = await api("/api/health");
|
|
if (!res || res.status !== 200) return;
|
|
|
|
const h = res.data;
|
|
|
|
// PostgreSQL
|
|
const pg = h.postgresql;
|
|
document.getElementById("health-postgresql").innerHTML = pg.status === "ok" ? `
|
|
<div class="health-item"><span class="health-label">Estado</span><span class="health-value" style="color:var(--success)">Online</span></div>
|
|
<div class="health-item"><span class="health-label">Versión</span><span class="health-value">${pg.version}</span></div>
|
|
<div class="health-item"><span class="health-label">Master DB</span><span class="health-value">${pg.master_size_mb} MB</span></div>
|
|
` : renderError(pg.error);
|
|
|
|
// Redis
|
|
const rd = h.redis;
|
|
document.getElementById("health-redis").innerHTML = rd.status === "ok" ? `
|
|
<div class="health-item"><span class="health-label">Estado</span><span class="health-value" style="color:var(--success)">Online</span></div>
|
|
<div class="health-item"><span class="health-label">Versión</span><span class="health-value">${rd.version}</span></div>
|
|
<div class="health-item"><span class="health-label">Memoria</span><span class="health-value">${rd.used_memory_human}</span></div>
|
|
<div class="health-item"><span class="health-label">Clientes</span><span class="health-value">${rd.connected_clients}</span></div>
|
|
` : renderError(rd.error);
|
|
|
|
// Disk
|
|
const dk = h.disk;
|
|
document.getElementById("health-disk").innerHTML = dk.status === "ok" ? `
|
|
<div class="health-item"><span class="health-label">Total</span><span class="health-value">${dk.total_gb} GB</span></div>
|
|
<div class="health-item"><span class="health-label">Usado</span><span class="health-value">${dk.used_gb} GB (${dk.percent_used}%)</span></div>
|
|
<div class="health-bar-bg"><div class="health-bar-fill" style="width:${dk.percent_used}%; background:${getBarColor(dk.percent_used)}"></div></div>
|
|
<div class="health-item" style="margin-top:12px"><span class="health-label">Libre</span><span class="health-value">${dk.free_gb} GB</span></div>
|
|
` : renderError(dk.error);
|
|
|
|
// Memory
|
|
const mem = h.memory;
|
|
document.getElementById("health-memory").innerHTML = mem.status === "ok" ? `
|
|
<div class="health-item"><span class="health-label">Total</span><span class="health-value">${mem.total_gb} GB</span></div>
|
|
<div class="health-item"><span class="health-label">Usada</span><span class="health-value">${mem.used_gb} GB (${mem.percent_used}%)</span></div>
|
|
<div class="health-bar-bg"><div class="health-bar-fill" style="width:${mem.percent_used}%; background:${getBarColor(mem.percent_used)}"></div></div>
|
|
<div class="health-item" style="margin-top:12px"><span class="health-label">Disponible</span><span class="health-value">${mem.available_gb} GB</span></div>
|
|
` : renderError(mem.error);
|
|
|
|
// Services
|
|
const svcs = h.services || {};
|
|
document.getElementById("health-services").innerHTML = Object.entries(svcs).map(([name, s]) => `
|
|
<div class="health-item">
|
|
<span class="health-label"><i class="fas fa-${s.active ? "check-circle" : "times-circle"}" style="color:${s.active ? "var(--success)" : "var(--danger)"}; margin-right:6px"></i>${name}</span>
|
|
<span class="health-value" style="color:${s.active ? "var(--success)" : "var(--danger)"}">${s.state}</span>
|
|
</div>
|
|
`).join("");
|
|
|
|
// HTTP
|
|
const httpChecks = ["pos", "dashboard", "quart"];
|
|
document.getElementById("health-http").innerHTML = `
|
|
<div class="grid-3">
|
|
${httpChecks.map(key => {
|
|
const svc = h[key];
|
|
const ok = svc && svc.status === "ok";
|
|
return `
|
|
<div class="health-item">
|
|
<span class="health-label">${key.toUpperCase()}</span>
|
|
<span class="health-value" style="color:${ok ? "var(--success)" : "var(--danger)"}">
|
|
${ok ? `HTTP ${svc.http_status}` : (svc.error || "Offline")}
|
|
</span>
|
|
</div>
|
|
`;
|
|
}).join("")}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderError(msg) {
|
|
return `<div class="text-muted" style="padding:20px; text-align:center; color:var(--danger)"><i class="fas fa-exclamation-triangle"></i> ${escapeHtml(msg)}</div>`;
|
|
}
|
|
|
|
// ─── Migrations ────────────────────────────────────────────────────────────
|
|
async function loadMigrations() {
|
|
const res = await api("/api/admin/migrations");
|
|
if (!res || res.status !== 200) return;
|
|
|
|
const tbody = document.getElementById("migrations-table");
|
|
const tenants = res.data.tenants || [];
|
|
tbody.innerHTML = tenants.map(t => {
|
|
const needsUpdate = t.version !== (res.data.migrations.slice(-1)[0]?.version || t.version);
|
|
return `
|
|
<tr>
|
|
<td>${escapeHtml(t.name)}</td>
|
|
<td><code>${t.db_name}</code></td>
|
|
<td>${t.version}</td>
|
|
<td>${needsUpdate ? tag("Pendiente", "warning") : tag("OK", "success")}</td>
|
|
</tr>
|
|
`;
|
|
}).join("") || `<tr><td colspan="4" class="text-muted text-center">No hay tenants</td></tr>`;
|
|
}
|
|
|
|
async function runAllMigrations() {
|
|
if (!confirm("¿Ejecutar todas las migraciones pendientes en TODOS los tenants?")) return;
|
|
|
|
const logBox = document.getElementById("migration-log");
|
|
logBox.style.display = "block";
|
|
logBox.textContent = "Ejecutando migraciones...";
|
|
|
|
const res = await api("/api/admin/migrations/run-all", { method: "POST" });
|
|
if (res && res.status === 200) {
|
|
logBox.textContent = res.data.log || "Completado";
|
|
toast("Migraciones ejecutadas", "success");
|
|
loadMigrations();
|
|
} else {
|
|
logBox.textContent = "Error: " + (res?.data?.error || "Unknown");
|
|
toast("Error en migraciones", "error");
|
|
}
|
|
}
|
|
|
|
// ─── Actions ───────────────────────────────────────────────────────────────
|
|
async function toggleTenant(id, active) {
|
|
const res = await api(`/api/tenants/${id}/toggle`, {
|
|
method: "POST",
|
|
body: { active }
|
|
});
|
|
if (res && res.status === 200) {
|
|
toast(active ? "Tenant activado" : "Tenant desactivado", "success");
|
|
loadTenants();
|
|
loadDemos();
|
|
} else {
|
|
toast(res?.data?.error || "Error", "error");
|
|
}
|
|
}
|
|
|
|
async function resetTenant(id) {
|
|
if (!confirm("¿Resetear TODOS los datos de negocio de este tenant? Se conservan empleados y configuración.")) return;
|
|
|
|
const res = await api(`/api/tenants/${id}/reset`, { method: "POST" });
|
|
if (res && res.status === 200) {
|
|
toast("Tenant reseteado", "success");
|
|
} else {
|
|
toast(res?.data?.error || "Error al resetear", "error");
|
|
}
|
|
}
|
|
|
|
function confirmDelete(id, name) {
|
|
openModal(
|
|
"Eliminar Tenant",
|
|
`¿Eliminar permanentemente <strong>${escapeHtml(name)}</strong>? Esta acción no se puede deshacer. Se borrará la base de datos completa.`,
|
|
async () => {
|
|
const res = await api(`/api/tenants/${id}`, { method: "DELETE" });
|
|
if (res && res.status === 200) {
|
|
toast("Tenant eliminado", "success");
|
|
loadTenants();
|
|
loadDemos();
|
|
} else {
|
|
toast(res?.data?.error || "Error al eliminar", "error");
|
|
}
|
|
closeModal();
|
|
}
|
|
);
|
|
}
|
|
|
|
// ─── Modal ─────────────────────────────────────────────────────────────────
|
|
function openModal(title, body, onConfirm) {
|
|
document.getElementById("modal-title").textContent = title;
|
|
document.getElementById("modal-body").innerHTML = body;
|
|
const btn = document.getElementById("modal-confirm-btn");
|
|
btn.onclick = onConfirm;
|
|
document.getElementById("modal").style.display = "flex";
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById("modal").style.display = "none";
|
|
}
|
|
|
|
// ─── Toast ─────────────────────────────────────────────────────────────────
|
|
function toast(message, type = "info") {
|
|
const container = document.getElementById("toast-container");
|
|
const el = document.createElement("div");
|
|
el.className = `toast ${type}`;
|
|
el.innerHTML = `<i class="fas fa-${type === "success" ? "check-circle" : type === "error" ? "exclamation-circle" : "info-circle"}"></i> ${escapeHtml(message)}`;
|
|
container.appendChild(el);
|
|
setTimeout(() => {
|
|
el.style.opacity = "0";
|
|
el.style.transform = "translateX(100%)";
|
|
setTimeout(() => el.remove(), 300);
|
|
}, 4000);
|
|
}
|
|
|
|
// ─── Utilities ─────────────────────────────────────────────────────────────
|
|
function escapeHtml(text) {
|
|
if (!text) return "";
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function tag(text, type) {
|
|
return `<span class="tag tag-${type}">${escapeHtml(text)}</span>`;
|
|
}
|
|
|
|
function formatDate(iso) {
|
|
if (!iso) return "-";
|
|
const d = new Date(iso);
|
|
return d.toLocaleDateString("es-MX");
|
|
}
|
|
|
|
function copyText(text) {
|
|
navigator.clipboard.writeText(text).then(() => toast("Copiado al portapapeles", "success"));
|
|
}
|
|
|
|
// ─── Modules ───────────────────────────────────────────────────────────────
|
|
let currentModulesTenantId = null;
|
|
|
|
async function openModulesModal(tenantId, name) {
|
|
currentModulesTenantId = tenantId;
|
|
document.getElementById("modules-modal-title").textContent = `Módulos — ${escapeHtml(name)}`;
|
|
document.getElementById("modules-modal").style.display = "flex";
|
|
|
|
// Load current state
|
|
const res = await api(`/api/tenants/${tenantId}/modules`);
|
|
if (res && res.status === 200) {
|
|
const m = res.data.data;
|
|
document.getElementById("mod-whatsapp").checked = m.whatsapp !== false;
|
|
document.getElementById("mod-marketplace").checked = m.marketplace !== false;
|
|
document.getElementById("mod-meli").checked = m.meli !== false;
|
|
} else {
|
|
toast("Error al cargar módulos", "error");
|
|
}
|
|
}
|
|
|
|
function closeModulesModal() {
|
|
document.getElementById("modules-modal").style.display = "none";
|
|
currentModulesTenantId = null;
|
|
}
|
|
|
|
async function saveModules() {
|
|
if (!currentModulesTenantId) return;
|
|
const btn = document.getElementById("modules-save-btn");
|
|
const originalText = btn.innerHTML;
|
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Guardando...`;
|
|
btn.disabled = true;
|
|
|
|
const payload = {
|
|
whatsapp: document.getElementById("mod-whatsapp").checked,
|
|
marketplace: document.getElementById("mod-marketplace").checked,
|
|
meli: document.getElementById("mod-meli").checked,
|
|
catalog: document.getElementById("mod-catalog").checked,
|
|
};
|
|
|
|
const res = await api(`/api/tenants/${currentModulesTenantId}/modules`, {
|
|
method: "PUT",
|
|
body: payload
|
|
});
|
|
|
|
if (res && res.status === 200) {
|
|
toast("Módulos actualizados", "success");
|
|
closeModulesModal();
|
|
} else {
|
|
toast(res?.data?.error || "Error al guardar", "error");
|
|
}
|
|
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
}
|
|
|
|
// ─── Init ──────────────────────────────────────────────────────────────────
|
|
document.addEventListener("DOMContentLoaded", initAuth);
|