feat(studio): muestra el cupo de agentes del plan en pantalla

Se podían crear 2 agentes con un plan de 1 sin que nada lo explicara. El
límite del backend sí funciona, pero solo aplica si el tenant tiene un
plan asignado — y hasta 115f167 los campos Cliente y Plan no aparecían en
el formulario del tenant, así que en la práctica quedaban todos sin plan
y sin límite.

El problema de fondo era que ese estado no se veía por ningún lado: un
tenant sin plan se comportaba igual que un límite roto.

- La lista de agentes devuelve también el plan y cuántos hay usados.
- La pantalla muestra "Básico · 1 de 1", "Pro · ilimitado" o "sin plan ·
  sin límite", y en este último caso explica que hay que asignarle uno.
- El botón + Nuevo agente se deshabilita al llegar al tope, en vez de
  dejar que el límite aparezca recién como un error al guardar.

El backend sigue siendo la autoridad: esto es feedback, no la validación.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-14 22:05:32 -05:00
co-authored by Claude Sonnet 5
parent 5c931c9400
commit 45f773fb62
7 changed files with 73 additions and 32 deletions
+34 -2
View File
@@ -17,6 +17,24 @@ const showForm = ref(false)
const editing = ref(null)
const form = ref(vacio())
const plan = ref(null)
// max_agentes 0 = ilimitado. Sin plan asignado tampoco hay límite: es
// deliberado (los tenants que existían antes de los planes no se rompen),
// pero hay que decirlo, o parece que el límite está fallando.
const cupo = computed(() => {
if (!plan.value) return { sinPlan: true }
const max = plan.value.max_agentes || 0
return {
sinPlan: false,
nombre: plan.value.nombre,
ilimitado: max <= 0,
max,
usados: agentes.value.length,
lleno: max > 0 && agentes.value.length >= max,
}
})
function vacio() {
return { nombre: '', ai_config_id: null, tono: '', mensaje_bienvenida: '', color: '#8eb02f', activo: true }
}
@@ -31,6 +49,7 @@ async function cargar() {
])
tenant.value = (t.items || []).find((x) => String(x.ID) === props.id) || null
agentes.value = a.items || []
plan.value = a.plan || null
aiConfigs.value = ai.items || []
} catch (e) {
error.value = e.message
@@ -96,9 +115,22 @@ onMounted(cargar)
<p v-if="error" class="text-sm text-red-600 dark:text-red-400 mb-4">{{ error }}</p>
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-medium text-tenue">Agentes</h2>
<button class="btn-primary" @click="nuevoAgente">+ Nuevo agente</button>
<div class="flex items-center gap-2">
<h2 class="text-sm font-medium text-tenue">Agentes</h2>
<span v-if="cupo.sinPlan" class="badge-alerta">sin plan · sin límite</span>
<span v-else-if="cupo.ilimitado" class="badge-neutro">{{ cupo.nombre }} · ilimitado</span>
<span v-else :class="cupo.lleno ? 'badge-alerta' : 'badge-neutro'">
{{ cupo.nombre }} · {{ cupo.usados }} de {{ cupo.max }}
</span>
</div>
<button class="btn-primary" :disabled="cupo.lleno" @click="nuevoAgente">+ Nuevo agente</button>
</div>
<p v-if="cupo.sinPlan && !contexto.esPortal" class="text-xs text-tenue -mt-2 mb-4">
Este tenant no tiene plan asignado, así que no se le aplica ningún límite de agentes. Asignale uno desde el lápiz del tenant en la barra izquierda.
</p>
<p v-else-if="cupo.lleno" class="text-xs text-tenue -mt-2 mb-4">
Alcanzaste el máximo de agentes de tu plan.
</p>
<div class="card divide-y divide-borde overflow-hidden">
<UiEmptyState
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>uMind Studio</title>
<script type="module" crossorigin src="/orchestrator/assets/index-D9xKBSe-.js"></script>
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-CkF90HJJ.css">
<script type="module" crossorigin src="/orchestrator/assets/index-DILPQ-CQ.js"></script>
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-BJ8q_ag8.css">
</head>
<body class="bg-gray-50">
<div id="app"></div>
+10 -1
View File
@@ -171,7 +171,16 @@ func GetUmindAgentesHandler(c *fiber.Ctx) error {
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"items": items})
// El cupo viaja con la lista para que la pantalla pueda mostrar "1 de 1"
// en vez de dejar que el límite aparezca recién como un error al guardar.
// Sin plan no hay límite (ver verificarCupoAgentes), y eso también hay que
// poder verlo: si no, un tenant sin plan parece un límite que no funciona.
resp := fiber.Map{"items": items, "usados": len(items)}
if plan := models.GetPlanDeTenant(uint(tenantID)); plan != nil {
resp["plan"] = fiber.Map{"nombre": plan.Nombre, "max_agentes": plan.MaxAgentes}
}
return c.JSON(resp)
}
type umindAgenteReq struct {