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:
@@ -92,6 +92,14 @@
|
||||
<span class="badge" id="pendingUsersBadge" style="display:none; background:var(--warning); color:#000; font-size:0.7rem; padding:2px 6px; border-radius:10px; margin-left:auto;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h3>Tenants</h3>
|
||||
<div class="sidebar-item" data-section="tenants">
|
||||
<span class="icon">🏢</span>
|
||||
<span>Módulos</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
@@ -660,6 +668,35 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tenants / Modules Section -->
|
||||
<section id="section-tenants" class="admin-section">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Configuración de Módulos por Tenant</h1>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Tenants Activos</h2>
|
||||
</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nombre</th>
|
||||
<th>WhatsApp</th>
|
||||
<th>Marketplace</th>
|
||||
<th>MercadoLibre</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tenantsTable">
|
||||
<tr><td colspan="6" class="loading"><div class="spinner"></div></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -121,6 +121,9 @@ function showSection(sectionId) {
|
||||
case 'users':
|
||||
loadUsers();
|
||||
break;
|
||||
case 'tenants':
|
||||
loadTenants();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2074,3 +2077,99 @@ async function toggleUserActive(userId, currentActive) {
|
||||
showAlert(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tenants / Modules ─────────────────────────────────────────────────────
|
||||
|
||||
async function loadTenants() {
|
||||
var token = localStorage.getItem('access_token');
|
||||
var tbody = document.getElementById('tenantsTable');
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="loading"><div class="spinner"></div></td></tr>';
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/admin/tenants', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (!res.ok) throw new Error('Error al cargar tenants (' + res.status + ')');
|
||||
var data = await res.json();
|
||||
var tenants = data.tenants || [];
|
||||
|
||||
if (tenants.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; color:var(--text-secondary); padding:2rem;">No hay tenants activos</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Load modules for each tenant
|
||||
var modulesMap = {};
|
||||
await Promise.all(tenants.map(async function(t) {
|
||||
try {
|
||||
var mres = await fetch('/api/admin/tenants/' + t.id + '/modules', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (mres.ok) {
|
||||
modulesMap[t.id] = await mres.json();
|
||||
} else {
|
||||
modulesMap[t.id] = {};
|
||||
}
|
||||
} catch (e) {
|
||||
modulesMap[t.id] = {};
|
||||
}
|
||||
}));
|
||||
|
||||
renderTenantsTable(tenants, modulesMap);
|
||||
} catch (e) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; color:#ef4444; padding:2rem;">' + e.message + '</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTenantsTable(tenants, modulesMap) {
|
||||
var tbody = document.getElementById('tenantsTable');
|
||||
tbody.innerHTML = tenants.map(function(t) {
|
||||
var mods = modulesMap[t.id] || {};
|
||||
function toggleBtn(tenantId, key, enabled) {
|
||||
var label = enabled ? 'Activado' : 'Desactivado';
|
||||
var cls = enabled ? 'btn-primary' : 'btn-secondary';
|
||||
return '<button class="btn ' + cls + '" style="font-size:0.75rem; padding:3px 10px;" ' +
|
||||
'onclick="toggleTenantModule(' + tenantId + ', \'' + key + '\', ' + enabled + ')">' + label + '</button>';
|
||||
}
|
||||
return '<tr>' +
|
||||
'<td>' + t.id + '</td>' +
|
||||
'<td>' + (t.name || '-') + '</td>' +
|
||||
'<td>' + toggleBtn(t.id, 'whatsapp_enabled', !!mods.whatsapp_enabled) + '</td>' +
|
||||
'<td>' + toggleBtn(t.id, 'marketplace_enabled', !!mods.marketplace_enabled) + '</td>' +
|
||||
'<td>' + toggleBtn(t.id, 'meli_enabled', !!mods.meli_enabled) + '</td>' +
|
||||
'<td><button class="btn btn-primary" style="font-size:0.75rem; padding:3px 10px;" onclick="loadTenants()">🔄 Recargar</button></td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function toggleTenantModule(tenantId, key, currentValue) {
|
||||
var token = localStorage.getItem('access_token');
|
||||
var moduleNames = {
|
||||
'whatsapp_enabled': 'WhatsApp',
|
||||
'marketplace_enabled': 'Marketplace',
|
||||
'meli_enabled': 'MercadoLibre'
|
||||
};
|
||||
var action = currentValue ? 'desactivar' : 'activar';
|
||||
if (!confirm('¿Seguro que deseas ' + action + ' ' + moduleNames[key] + ' para el tenant #' + tenantId + '?')) return;
|
||||
|
||||
try {
|
||||
var payload = {};
|
||||
payload[key] = !currentValue;
|
||||
var res = await fetch('/api/admin/tenants/' + tenantId + '/modules', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res.ok) {
|
||||
var err = await res.json();
|
||||
throw new Error(err.error || 'Error al actualizar módulo');
|
||||
}
|
||||
showAlert(moduleNames[key] + ' ' + (currentValue ? 'desactivado' : 'activado') + ' para tenant #' + tenantId);
|
||||
loadTenants();
|
||||
} catch (e) {
|
||||
showAlert(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ sys.path.insert(0, os.path.join(_base, '..', 'pos')) # pos/ for auth, services
|
||||
sys.path.insert(0, os.path.join(_base, '..')) # root config.py (has DB_URL)
|
||||
from config import DB_URL
|
||||
from auth import hash_password, check_password, create_access_token, create_refresh_token, decode_token, require_auth
|
||||
from tenant_db import get_tenant_conn
|
||||
from services.translations import translate_part_name, translate_category
|
||||
|
||||
sys.path.insert(0, os.path.join(_base, '..', 'pos'))
|
||||
@@ -4628,6 +4629,76 @@ def part_aftermarket(part_id):
|
||||
session.close()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tenant Module Config Endpoints
|
||||
# ============================================================================
|
||||
|
||||
MODULE_CONFIG_KEYS = [
|
||||
'whatsapp_enabled',
|
||||
'marketplace_enabled',
|
||||
'meli_enabled',
|
||||
]
|
||||
|
||||
|
||||
@app.route('/api/admin/tenants')
|
||||
def api_admin_tenants():
|
||||
session = Session()
|
||||
try:
|
||||
rows = session.execute(text(
|
||||
"SELECT id, name, db_name, is_active, is_seller FROM tenants WHERE is_active = true ORDER BY id"
|
||||
)).mappings().all()
|
||||
return jsonify({'tenants': [dict(r) for r in rows]})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@app.route('/api/admin/tenants/<int:tenant_id>/modules')
|
||||
def api_admin_tenant_modules(tenant_id):
|
||||
try:
|
||||
conn = get_tenant_conn(tenant_id)
|
||||
cur = conn.cursor()
|
||||
result = {}
|
||||
for key in MODULE_CONFIG_KEYS:
|
||||
cur.execute("SELECT value FROM tenant_config WHERE key = %s", (key,))
|
||||
row = cur.fetchone()
|
||||
result[key] = (row[0] or '').lower() == 'true' if row else False
|
||||
cur.close()
|
||||
conn.close()
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/admin/tenants/<int:tenant_id>/modules', methods=['PUT'])
|
||||
def api_admin_tenant_modules_update(tenant_id):
|
||||
data = request.get_json() or {}
|
||||
if not data:
|
||||
return jsonify({'error': 'No data provided'}), 400
|
||||
|
||||
try:
|
||||
conn = get_tenant_conn(tenant_id)
|
||||
cur = conn.cursor()
|
||||
for key, value in data.items():
|
||||
if key not in MODULE_CONFIG_KEYS:
|
||||
continue
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO tenant_config (key, value, updated_at)
|
||||
VALUES (%s, %s, NOW())
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()
|
||||
""",
|
||||
(key, 'true' if value else 'false'),
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
return jsonify({'ok': True})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Static files from dashboard root (CSS/JS/HTML)
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user