feat(umind): vincula tenants a clientes y agrega planes con precios
Base para que el cliente administre uMind desde su portal y para el cobro por consumo. Todavía no cambia nada del comportamiento actual. - UmindTenant gana ClienteID (punteros, sin not null: los tenants creados antes del portal quedan sin asignar y el ALTER TABLE no falla sobre datos existentes) y PlanID. - GetUmindTenantsByClientes es fail-closed: sin clientes, no ve nada. - UmindPlan define el máximo de agentes y los precios por 1k tokens, por imagen OCR, por transcripción y la mensualidad, más un tope de consumo que solo avisa. CRUD para staff en /app/umind-planes. - El modelo va en AMBAS listas de AutoMigrate (main.go y migrations), no repetir el error de agregarlo solo en una. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
84506a98e2
commit
c69b904b03
@@ -7,6 +7,8 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const tenants = ref([])
|
||||
const clientes = ref([])
|
||||
const planes = ref([])
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const showForm = ref(false)
|
||||
@@ -18,7 +20,7 @@ const form = ref(vacio())
|
||||
const tenantActivoId = computed(() => route.params.tenantId || route.params.id)
|
||||
|
||||
function vacio() {
|
||||
return { nombre: '', dominios_permitidos: '', activo: true }
|
||||
return { nombre: '', dominios_permitidos: '', activo: true, cliente_id: null, plan_id: null }
|
||||
}
|
||||
|
||||
async function cargar() {
|
||||
@@ -34,6 +36,22 @@ async function cargar() {
|
||||
}
|
||||
}
|
||||
|
||||
// Clientes y planes solo los necesita el staff para asignarlos; si el endpoint
|
||||
// no está disponible (portal del cliente) el formulario sigue funcionando.
|
||||
async function cargarAsignables() {
|
||||
try {
|
||||
const [c, p] = await Promise.all([
|
||||
api.get('/app/api/clientes/select'),
|
||||
api.get('/app/umind-planes/list'),
|
||||
])
|
||||
clientes.value = c.registros || c.items || []
|
||||
planes.value = p.items || []
|
||||
} catch {
|
||||
clientes.value = []
|
||||
planes.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function nuevoTenant() {
|
||||
editing.value = null
|
||||
form.value = vacio()
|
||||
@@ -46,6 +64,8 @@ function editarTenant(t) {
|
||||
nombre: t.nombre,
|
||||
dominios_permitidos: t.dominios_permitidos,
|
||||
activo: t.activo,
|
||||
cliente_id: t.cliente_id ?? null,
|
||||
plan_id: t.plan_id ?? null,
|
||||
}
|
||||
showForm.value = true
|
||||
}
|
||||
@@ -82,7 +102,10 @@ async function eliminar(t) {
|
||||
}
|
||||
|
||||
defineExpose({ recargar: cargar })
|
||||
onMounted(cargar)
|
||||
onMounted(() => {
|
||||
cargar()
|
||||
cargarAsignables()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -175,6 +198,32 @@ onMounted(cargar)
|
||||
class="w-full border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100 rounded-lg px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="clientes.length" class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="text-xs text-gray-500 dark:text-gray-400">Cliente</label>
|
||||
<select
|
||||
v-model="form.cliente_id"
|
||||
class="w-full border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100 rounded-lg px-3 py-2 text-sm"
|
||||
>
|
||||
<option :value="null">— sin asignar —</option>
|
||||
<option v-for="c in clientes" :key="c.ID" :value="c.ID">{{ c.nombre }}</option>
|
||||
</select>
|
||||
<p class="text-[11px] text-gray-400 mt-1">Define quién ve este tenant desde el portal.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-500 dark:text-gray-400">Plan</label>
|
||||
<select
|
||||
v-model="form.plan_id"
|
||||
class="w-full border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100 rounded-lg px-3 py-2 text-sm"
|
||||
>
|
||||
<option :value="null">— sin plan —</option>
|
||||
<option v-for="p in planes" :key="p.ID" :value="p.ID">
|
||||
{{ p.nombre }} ({{ p.max_agentes === 0 ? '∞' : p.max_agentes }} agentes)
|
||||
</option>
|
||||
</select>
|
||||
<p class="text-[11px] text-gray-400 mt-1">Límite de agentes y precios de consumo.</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<input v-model="form.activo" type="checkbox" />
|
||||
Activo
|
||||
|
||||
Reference in New Issue
Block a user