feat: module toggles in POS config and Instance Manager
- Add GET/PUT /pos/api/config/modules endpoints in POS config_bp.py - Update sidebar.js to filter nav items based on enabled modules - Add Modules section to POS config.html with toggles for WhatsApp, Marketplace, MercadoLibre - Add module load/save logic to POS config.js - Preload modules in app-init.js for sidebar caching - Add tenant module management to Instance Manager - get_tenant_modules / update_tenant_modules in tenant_service.py - GET/PUT /api/tenants/<id>/modules endpoints in tenants_bp.py - Add modules modal to manager index.html - Add module editing UI and logic to manager.js - Add toggle-switch CSS to manager.css
This commit is contained in:
@@ -58,3 +58,24 @@ def delete_tenant(tenant_id):
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@tenants_bp.route("/<int:tenant_id>/modules", methods=["GET"])
|
||||
@require_manager_auth
|
||||
def get_tenant_modules(tenant_id):
|
||||
try:
|
||||
result = tenant_service.get_tenant_modules(tenant_id)
|
||||
return jsonify({"data": result})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@tenants_bp.route("/<int:tenant_id>/modules", methods=["PUT"])
|
||||
@require_manager_auth
|
||||
def update_tenant_modules(tenant_id):
|
||||
data = request.get_json() or {}
|
||||
try:
|
||||
result = tenant_service.update_tenant_modules(tenant_id, data)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -311,6 +311,55 @@ def get_tenant_login_url(subdomain):
|
||||
return f"https://{subdomain}.{domain}/pos/login"
|
||||
|
||||
|
||||
def get_tenant_modules(tenant_id):
|
||||
"""Get enabled modules for a tenant from tenant_config."""
|
||||
tenant = get_tenant(tenant_id)
|
||||
if not tenant:
|
||||
raise ValueError("Tenant not found")
|
||||
db_name = tenant["db_name"]
|
||||
dsn = TENANT_DB_URL_TEMPLATE.format(db_name=db_name)
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
modules = {}
|
||||
for key in ["module_whatsapp", "module_marketplace", "module_meli"]:
|
||||
cur.execute("SELECT value FROM tenant_config WHERE key = %s", (key,))
|
||||
row = cur.fetchone()
|
||||
modules[key.replace("module_", "")] = (row[0] or "").lower() == "true" if row else True
|
||||
return modules
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
def update_tenant_modules(tenant_id, modules):
|
||||
"""Update enabled modules for a tenant in tenant_config."""
|
||||
tenant = get_tenant(tenant_id)
|
||||
if not tenant:
|
||||
raise ValueError("Tenant not found")
|
||||
db_name = tenant["db_name"]
|
||||
dsn = TENANT_DB_URL_TEMPLATE.format(db_name=db_name)
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
key_map = {
|
||||
"whatsapp": "module_whatsapp",
|
||||
"marketplace": "module_marketplace",
|
||||
"meli": "module_meli",
|
||||
}
|
||||
for field, key in key_map.items():
|
||||
value = "true" if modules.get(field) else "false"
|
||||
cur.execute("""
|
||||
INSERT INTO tenant_config (key, value) VALUES (%s, %s)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
||||
""", (key, value))
|
||||
conn.commit()
|
||||
return {"success": True, "tenant_id": tenant_id, "modules": modules}
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_dashboard_stats():
|
||||
"""Global stats for the manager dashboard."""
|
||||
conn = get_master_conn()
|
||||
|
||||
@@ -661,3 +661,42 @@ body {
|
||||
.sidebar-brand span, .nav-item span, .user-info span { display: none; }
|
||||
.stats-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Toggle switch for modules modal */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--border);
|
||||
border-radius: 24px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.toggle-slider::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background: var(--success);
|
||||
}
|
||||
.toggle-switch input:checked + .toggle-slider::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ async function loadDemos() {
|
||||
<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>
|
||||
@@ -254,6 +255,7 @@ async function loadTenants(withStats = false) {
|
||||
<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>
|
||||
@@ -475,5 +477,59 @@ 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,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@@ -316,6 +316,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modules Modal -->
|
||||
<div id="modules-modal" class="modal" style="display:none;">
|
||||
<div class="modal-overlay" onclick="closeModulesModal()"></div>
|
||||
<div class="modal-content" style="max-width:480px;">
|
||||
<div class="modal-header">
|
||||
<h3 id="modules-modal-title">Módulos del Tenant</h3>
|
||||
<button class="btn-icon" onclick="closeModulesModal()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group" style="display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid var(--border);">
|
||||
<div>
|
||||
<div style="font-weight:600;color:var(--text);">WhatsApp</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);">Mostrar menú de WhatsApp Bridge</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="mod-whatsapp">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" style="display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid var(--border);">
|
||||
<div>
|
||||
<div style="font-weight:600;color:var(--text);">Marketplace</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);">Mostrar menú de Marketplace interno</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="mod-marketplace">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" style="display:flex;align-items:center;justify-content:space-between;padding:12px 0;">
|
||||
<div>
|
||||
<div style="font-weight:600;color:var(--text);">MercadoLibre</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);">Mostrar menú de MercadoLibre</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="mod-meli">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModulesModal()">Cancelar</button>
|
||||
<button class="btn btn-primary" id="modules-save-btn" onclick="saveModules()">Guardar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div id="toast-container"></div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user