diff --git a/backend/api/v1/modules/core/auth/routes.py b/backend/api/v1/modules/core/auth/routes.py
index e5ee398..50d6869 100644
--- a/backend/api/v1/modules/core/auth/routes.py
+++ b/backend/api/v1/modules/core/auth/routes.py
@@ -489,8 +489,9 @@ async def get_my_companies(
rows = db.execute(
text(
"""
- SELECT c.id, c.name, c.rfc, c.logo, c.tenant_id
+ SELECT c.id, c.name, c.rfc, c.logo, c.tenant_id, t.name, t.slug
FROM a76.company c
+ LEFT JOIN core.tenants t ON t.id = c.tenant_id
WHERE c.id IN (
SELECT company_id FROM core.user_tenants
WHERE keycloak_user_id = :uid AND is_active AND company_id IS NOT NULL
@@ -509,6 +510,8 @@ async def get_my_companies(
"id": int(r[0]),
"name": r[1] or "Empresa",
"tenant_id": int(r[4]),
+ "tenant_name": r[5],
+ "tenant_slug": r[6],
"rfc": r[2],
"logo": r[3],
"is_active": True,
@@ -523,23 +526,90 @@ class _CreateCompanyDTO(BaseModel):
rfc: Optional[str] = None
+async def _sync_tenants_from_hub(request: Request, db: Session) -> None:
+ """
+ Auto-sync Workspace→CRM: trae los tenants del Workspace (Hub GET /hub/tenants) y
+ los da de alta/actualiza en core.tenants con su MISMO ID del Workspace. Así los
+ tenants creados en el Workspace aparecen solos en el CRM para asignarles compañías.
+ Best-effort: usa el token KC de la sesión (valkey); si no está fresco o el Hub no
+ responde, no bloquea (se devuelven los tenants ya sincronizados).
+ """
+ import httpx
+ from sqlalchemy import text as _text
+ from core.config import settings
+ from core import session_store
+ from api.v1.modules.core.tenants.models import Tenant, TenantType
+
+ sid = request.cookies.get("crm_sid") if request else None
+ kc_token = None
+ if sid:
+ sess = session_store.get_session(sid)
+ kc_token = (sess or {}).get("access_token")
+ if not kc_token:
+ return
+
+ try:
+ async with httpx.AsyncClient(timeout=8.0) as client:
+ r = await client.get(
+ f"{settings.HUB_URL}api/v1/hub/tenants",
+ headers={"Authorization": f"Bearer {kc_token}"},
+ )
+ if r.status_code != 200:
+ logger.info("sync-tenants: Hub devolvió %s — sin sincronizar", r.status_code)
+ return
+ payload = r.json()
+ items = payload.get("tenants", []) if isinstance(payload, dict) else (payload or [])
+ for t in items:
+ tid = t.get("id")
+ if tid is None:
+ continue
+ name = t.get("name") or t.get("display_name") or t.get("slug")
+ slug = t.get("slug") or f"tenant-{tid}"
+ existing = db.query(Tenant).filter(Tenant.id == int(tid)).first()
+ if existing:
+ if name and existing.name != name:
+ existing.name = name
+ else:
+ db.add(Tenant(
+ id=int(tid), name=name or slug, slug=slug,
+ keycloak_realm=slug, type=TenantType.SHARED, is_active=True,
+ ))
+ db.commit()
+ db.execute(_text("SELECT setval('core.tenants_id_seq', (SELECT MAX(id) FROM core.tenants))"))
+ db.commit()
+ except Exception as exc:
+ logger.warning("sync-tenants desde Hub falló (no bloquea): %s", exc)
+ try:
+ db.rollback()
+ except Exception:
+ pass
+
+
@router.get("/assignable-tenants")
async def assignable_tenants(
+ request: Request,
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_core_db),
):
"""
- Tenants disponibles para asignar una compañía nueva. El tenant lo crea el
- Workspace; aquí solo se elige. hub_admin ve todos; un usuario con tenant ve el suyo.
+ Tenants disponibles para asignar una compañía. El tenant lo crea el Workspace;
+ aquí solo se elige. hub_admin ve TODOS (auto-sincronizados del Hub); un usuario
+ con tenant ve el suyo.
"""
from api.v1.modules.core.tenants.models import Tenant
from core.security import resolve_effective_tenant_id_from_user, is_hub_admin
- q = db.query(Tenant).filter(Tenant.is_active == True) # noqa: E712
+ if is_hub_admin(current_user):
+ # Sincroniza automáticamente los tenants del Workspace antes de listar.
+ await _sync_tenants_from_hub(request, db)
+ rows = db.query(Tenant).filter(Tenant.is_active == True).order_by(Tenant.id).all() # noqa: E712
+ return [{"id": t.id, "name": t.name, "slug": t.slug} for t in rows]
+
tid = resolve_effective_tenant_id_from_user(current_user)
- if tid and not is_hub_admin(current_user):
- q = q.filter(Tenant.id == int(tid))
- return [{"id": t.id, "name": t.name, "slug": t.slug} for t in q.order_by(Tenant.id).all()]
+ if tid:
+ t = db.query(Tenant).filter(Tenant.id == int(tid), Tenant.is_active == True).first() # noqa: E712
+ return [{"id": t.id, "name": t.name, "slug": t.slug}] if t else []
+ return []
@router.post("/companies", status_code=201)
diff --git a/frontend/src/lib/components/sidebar/team-switcher.svelte b/frontend/src/lib/components/sidebar/team-switcher.svelte
index 547c9c1..f4cd37f 100644
--- a/frontend/src/lib/components/sidebar/team-switcher.svelte
+++ b/frontend/src/lib/components/sidebar/team-switcher.svelte
@@ -145,9 +145,18 @@
>
Tenant
{#if userTenants.length === 0}
-
- Sin tenant asignado
-
+ {#if companyStore.activeCompany?.tenant_name}
+
+
+
+
+ {companyStore.activeCompany.tenant_name}
+
+ {:else}
+
+ Sin tenant asignado
+
+ {/if}
{:else}
{#each userTenants as tenant (tenant.id)}