feat(studio): la puesta en marcha guiada y una bandeja de conversaciones que informa
Dos mitades de la misma experiencia: el primer día y todos los días después. El primer día. Un agente recién creado mostraba tres avisos ámbar sueltos —"no atiende", "no sabe nada"— sin ningún orden: el dueño sabía que faltaban cosas pero no cuál iba primero. Ahora hay una guía de tres pasos (contale de tu negocio → hacele una pregunta → ponelo a atender) donde solo el siguiente paso pendiente se resalta, porque una guía donde todo grita a la vez no guía nada. Mientras falte algo, la guía reemplaza al estado — una sola voz por etapa; con los pasos hechos desaparece para siempre y el estado operativo toma su lugar. Todos los días. La bandeja de conversaciones mostraba el último mensaje de cada sesión —casi siempre la respuesta del bot, "¡Hola! ¿En qué te ayudo?" cincuenta veces— y el session_id crudo (tg:8812). Ahora cada conversación muestra lo que el visitante preguntó al abrir, que es el dato que le importa al dueño: qué le preguntan de verdad a su negocio. Con canal deducido del prefijo de la sesión (WhatsApp, Telegram, Tu web, Prueba), cantidad de mensajes, y fecha relativa. Las pruebas propias se separan detrás de un filtro: mezcladas inflan la lista y el dueño no distingue cuáles son clientes reales. El contador del estado cuenta solo las reales, por lo mismo — si arriba dice 4 y la lista muestra 3, el número parece roto. El hilo muestra la hora de cada mensaje, y el backend gana una consulta que agrupa por sesión con el primer mensaje del visitante en vez de mandar filas crudas; la vieja quedó sin usos y se fue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
22158a10a7
commit
be01d95631
@@ -12,6 +12,7 @@ import TabConversaciones from './agente/TabConversaciones.vue'
|
||||
import TabHerramientas from './agente/TabHerramientas.vue'
|
||||
import TabCanales from './agente/TabCanales.vue'
|
||||
import EstadoAgente from './agente/EstadoAgente.vue'
|
||||
import PrimerosPasos from './agente/PrimerosPasos.vue'
|
||||
|
||||
const props = defineProps({
|
||||
tenantId: { type: String, required: true },
|
||||
@@ -254,6 +255,12 @@ const AVANZADO = [
|
||||
['auditoria', 'Problemas'],
|
||||
]
|
||||
|
||||
// La guía se muestra hasta que sabe algo Y atiende en algún lado. Probar es
|
||||
// deseable pero no bloquea: nadie queda preso de la guía por no haber chateado.
|
||||
const configurando = computed(
|
||||
() => !documentos.value.some((d) => d.estado === 'listo') || !canales.value.some((c) => c.activo),
|
||||
)
|
||||
|
||||
const mostrarAvanzado = ref(false)
|
||||
|
||||
// Si se entra directo a una pestaña avanzada (por la URL o desde el estado de
|
||||
@@ -313,10 +320,18 @@ watch(
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- El estado va antes que la navegación: es lo primero que se quiere
|
||||
saber y, además, lleva directo a lo que haya que arreglar. -->
|
||||
<!-- Mientras el agente esté a medio configurar, la guía de puesta en
|
||||
marcha; después, el estado de todos los días. Nunca los dos: dos
|
||||
resúmenes a la vez es no tener ninguno. -->
|
||||
<PrimerosPasos
|
||||
v-if="agente && configurando"
|
||||
:documentos="documentos"
|
||||
:canales="canales"
|
||||
:sesiones="sesiones"
|
||||
@ir="irA"
|
||||
/>
|
||||
<EstadoAgente
|
||||
v-if="agente"
|
||||
v-else-if="agente"
|
||||
:canales="canales"
|
||||
:documentos="documentos"
|
||||
:sesiones="sesiones"
|
||||
|
||||
@@ -36,6 +36,12 @@ const fuentesViejas = computed(() =>
|
||||
|
||||
const problemas = computed(() => props.eventos.filter((e) => e.nivel === 'error').length)
|
||||
|
||||
// Solo conversaciones de clientes: la bandeja separa las pruebas propias, y si
|
||||
// este número las incluyera diría una cosa distinta que la lista de al lado.
|
||||
const reales = computed(() =>
|
||||
props.sesiones.filter((s) => !String(s.session_id).startsWith('staff-preview')),
|
||||
)
|
||||
|
||||
// Cada señal dice qué pasa y, cuando algo anda mal, a dónde ir a arreglarlo.
|
||||
const señales = computed(() => [
|
||||
{
|
||||
@@ -63,9 +69,9 @@ const señales = computed(() => [
|
||||
icono: 'grafico',
|
||||
// Que nadie haya escrito todavía no es un problema: es un dato.
|
||||
bien: true,
|
||||
neutro: props.sesiones.length === 0,
|
||||
texto: props.sesiones.length
|
||||
? `${props.sesiones.length} ${props.sesiones.length === 1 ? 'conversación' : 'conversaciones'}`
|
||||
neutro: reales.value.length === 0,
|
||||
texto: reales.value.length
|
||||
? `${reales.value.length} ${reales.value.length === 1 ? 'conversación' : 'conversaciones'}`
|
||||
: 'Nadie escribió todavía',
|
||||
destino: 'conversaciones',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import UiIcono from '../../components/ui/UiIcono.vue'
|
||||
import UiMascota from '../../components/ui/UiMascota.vue'
|
||||
|
||||
// El camino de puesta en marcha, en orden. Existe porque un agente recién
|
||||
// creado mostraba tres avisos ámbar sueltos ("no atiende", "no sabe nada") y
|
||||
// ningún orden: el dueño sabía que faltaban cosas pero no cuál iba primero.
|
||||
//
|
||||
// Mientras falte algo, esta guía reemplaza al estado de arriba — una sola voz
|
||||
// por etapa. Cuando los tres pasos están hechos, desaparece para siempre y el
|
||||
// estado operativo toma su lugar. La puesta en marcha se hace una vez; el
|
||||
// estado se mira todos los días.
|
||||
const props = defineProps({
|
||||
documentos: { type: Array, default: () => [] },
|
||||
canales: { type: Array, default: () => [] },
|
||||
sesiones: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['ir'])
|
||||
|
||||
const sabe = computed(() => props.documentos.some((d) => d.estado === 'listo'))
|
||||
const probado = computed(() =>
|
||||
props.sesiones.some((s) => String(s.session_id).startsWith('staff-preview')),
|
||||
)
|
||||
const atiende = computed(() => props.canales.some((c) => c.activo))
|
||||
|
||||
const pasos = computed(() => [
|
||||
{
|
||||
n: 1,
|
||||
hecho: sabe.value,
|
||||
titulo: 'Contale de tu negocio',
|
||||
detalle: 'Escribí tus horarios, precios y lo que te preguntan siempre — o subí tu lista de precios, o dale tu sitio web.',
|
||||
accion: 'Cargar información',
|
||||
destino: 'conocimiento',
|
||||
},
|
||||
{
|
||||
n: 2,
|
||||
hecho: probado.value,
|
||||
titulo: 'Hacele una pregunta',
|
||||
detalle: 'Preguntale algo como lo haría un cliente. Si contesta mal, corregí su información y probá de nuevo.',
|
||||
accion: 'Probarlo',
|
||||
destino: 'conocimiento',
|
||||
},
|
||||
{
|
||||
n: 3,
|
||||
hecho: atiende.value,
|
||||
titulo: 'Ponelo a atender',
|
||||
detalle: 'Conectá tu WhatsApp o Telegram, o pegá el widget en tu web. Desde ese momento responde solo.',
|
||||
accion: 'Elegir dónde',
|
||||
destino: 'canales',
|
||||
},
|
||||
])
|
||||
|
||||
const hechos = computed(() => pasos.value.filter((p) => p.hecho).length)
|
||||
|
||||
// El siguiente paso pendiente es el único que se resalta: una guía donde todo
|
||||
// grita a la vez no guía nada.
|
||||
const siguiente = computed(() => pasos.value.find((p) => !p.hecho)?.n ?? null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card p-5 mb-6">
|
||||
<div class="flex items-start gap-4">
|
||||
<UiMascota :estado="hechos === 0 ? 'durmiendo' : 'contenta'" :tam="52" class="text-brand shrink-0 hidden sm:block" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-baseline justify-between gap-3">
|
||||
<h2 class="font-semibold text-texto">Pongamos a andar tu asistente</h2>
|
||||
<span class="text-xs text-tenue tabular-nums shrink-0">{{ hechos }} de {{ pasos.length }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-tenue mt-0.5 mb-4">
|
||||
Tres pasos y empieza a responderle a tus clientes. Diez minutos, sin nada técnico.
|
||||
</p>
|
||||
|
||||
<ol class="space-y-2.5">
|
||||
<li
|
||||
v-for="p in pasos"
|
||||
:key="p.n"
|
||||
class="flex items-start gap-3 rounded-lg p-2.5 -m-0.5 transition-colors"
|
||||
:class="p.n === siguiente ? 'bg-brand/5 border border-brand/30' : ''"
|
||||
>
|
||||
<span
|
||||
class="w-6 h-6 rounded-full flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5"
|
||||
:class="p.hecho ? 'bg-brand text-white' : p.n === siguiente ? 'border-2 border-brand text-brand' : 'border border-borde text-tenue'"
|
||||
>
|
||||
<UiIcono v-if="p.hecho" nombre="ok" :tam="13" />
|
||||
<template v-else>{{ p.n }}</template>
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium" :class="p.hecho ? 'text-tenue line-through' : 'text-texto'">
|
||||
{{ p.titulo }}
|
||||
</p>
|
||||
<p v-if="!p.hecho" class="text-xs text-tenue mt-0.5">{{ p.detalle }}</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="!p.hecho && p.n === siguiente"
|
||||
class="btn-primary !py-1.5 !px-3 text-sm shrink-0"
|
||||
@click="emit('ir', p.destino)"
|
||||
>
|
||||
{{ p.accion }}
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { api } from '../../lib/api.js'
|
||||
import { apiUmind } from '../../lib/contexto.js'
|
||||
import UiEmptyState from '../../components/ui/UiEmptyState.vue'
|
||||
import UiIcono from '../../components/ui/UiIcono.vue'
|
||||
|
||||
const props = defineProps({
|
||||
agenteId: { type: Number, required: true },
|
||||
@@ -13,42 +14,115 @@ const props = defineProps({
|
||||
// las conversaciones completas para mostrar una lista sería traer de más.
|
||||
const historial = ref([])
|
||||
const sesionActiva = ref(null)
|
||||
const cargandoHistorial = ref(false)
|
||||
|
||||
// El canal se deduce del prefijo de la sesión — es como los generan los
|
||||
// canales (wa:, tg:) y el chat de prueba (staff-preview-). Lo demás es el
|
||||
// widget de la web.
|
||||
function canalDe(sessionId) {
|
||||
const id = String(sessionId || '')
|
||||
if (id.startsWith('wa:')) return { nombre: 'WhatsApp', icono: 'mensaje', clase: 'badge-ok' }
|
||||
if (id.startsWith('tg:')) return { nombre: 'Telegram', icono: 'mensaje', clase: 'badge-neutro' }
|
||||
if (id.startsWith('staff-preview')) return { nombre: 'Prueba', icono: 'escribir', clase: 'badge-alerta' }
|
||||
return { nombre: 'Tu web', icono: 'sitio', clase: 'badge-neutro' }
|
||||
}
|
||||
|
||||
// Las pruebas propias se separan de las conversaciones reales: mezcladas,
|
||||
// inflan la lista y el dueño no sabe cuáles son clientes de verdad.
|
||||
const verPruebas = ref(false)
|
||||
const reales = computed(() => props.sesiones.filter((s) => !String(s.session_id).startsWith('staff-preview')))
|
||||
const pruebas = computed(() => props.sesiones.filter((s) => String(s.session_id).startsWith('staff-preview')))
|
||||
const visibles = computed(() => (verPruebas.value ? props.sesiones : reales.value))
|
||||
|
||||
function cuando(iso) {
|
||||
if (!iso) return ''
|
||||
const d = new Date(iso)
|
||||
const hoy = new Date()
|
||||
const dias = Math.floor((hoy.setHours(0, 0, 0, 0) - new Date(d).setHours(0, 0, 0, 0)) / 86400000)
|
||||
const hora = new Date(iso).toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit' })
|
||||
if (dias <= 0) return `hoy ${hora}`
|
||||
if (dias === 1) return `ayer ${hora}`
|
||||
if (dias < 7) return `hace ${dias} días`
|
||||
return new Date(iso).toLocaleDateString('es', { day: 'numeric', month: 'short' })
|
||||
}
|
||||
|
||||
async function verHistorial(sessionId) {
|
||||
sesionActiva.value = sessionId
|
||||
const r = await api.get(apiUmind(`/umind/historial?agente_id=${props.agenteId}&session_id=${sessionId}`))
|
||||
historial.value = r.items || []
|
||||
cargandoHistorial.value = true
|
||||
try {
|
||||
const r = await api.get(apiUmind(`/umind/historial?agente_id=${props.agenteId}&session_id=${sessionId}`))
|
||||
historial.value = r.items || []
|
||||
} finally {
|
||||
cargandoHistorial.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="col-span-1 card divide-y divide-borde max-h-[28rem] overflow-y-auto">
|
||||
<div class="grid lg:grid-cols-[minmax(16rem,22rem)_1fr] gap-4 items-start">
|
||||
<div class="card divide-y divide-borde max-h-[30rem] overflow-y-auto">
|
||||
<UiEmptyState
|
||||
v-if="sesiones.length === 0"
|
||||
v-if="visibles.length === 0 && pruebas.length === 0"
|
||||
titulo="Nadie escribió todavía"
|
||||
detalle="Acá vas a ver todo lo que le preguntan y qué contestó."
|
||||
detalle="Cuando alguien le hable a tu asistente —por WhatsApp, Telegram o tu web— la conversación aparece acá, con lo que preguntó y lo que se le respondió."
|
||||
/>
|
||||
<UiEmptyState
|
||||
v-else-if="visibles.length === 0"
|
||||
estado="durmiendo"
|
||||
titulo="Ningún cliente escribió todavía"
|
||||
:detalle="`Hay ${pruebas.length} ${pruebas.length === 1 ? 'conversación de prueba tuya' : 'conversaciones de prueba tuyas'} — activá el filtro de abajo para verlas.`"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-for="s in sesiones"
|
||||
v-for="s in visibles"
|
||||
:key="s.session_id"
|
||||
class="w-full text-left p-3 hover:bg-elevado text-sm"
|
||||
class="w-full text-left p-3 hover:bg-elevado transition-colors"
|
||||
:class="sesionActiva === s.session_id ? 'bg-gray-50 dark:bg-gray-800' : ''"
|
||||
@click="verHistorial(s.session_id)"
|
||||
>
|
||||
<div class="text-texto truncate">{{ s.content }}</div>
|
||||
<div class="text-xs text-tenue mt-0.5">{{ s.session_id }}</div>
|
||||
<!-- Lo que abrió la conversación es el dato que importa: muestra qué
|
||||
le preguntan de verdad al negocio. -->
|
||||
<div class="text-sm text-texto line-clamp-2">
|
||||
{{ s.primer_mensaje || '(sin mensaje)' }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-1.5 text-xs text-tenue">
|
||||
<span class="px-1.5 py-0.5 rounded" :class="canalDe(s.session_id).clase">
|
||||
{{ canalDe(s.session_id).nombre }}
|
||||
</span>
|
||||
<span class="tabular-nums">{{ s.mensajes }} msj</span>
|
||||
<span class="ml-auto tabular-nums">{{ cuando(s.ultimo) }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-span-2 card p-4 max-h-[28rem] overflow-y-auto space-y-2">
|
||||
<p v-if="!sesionActiva" class="text-sm text-tenue">Elegí una conversación de la izquierda.</p>
|
||||
<div
|
||||
v-for="m in historial"
|
||||
:key="m.ID"
|
||||
class="max-w-[80%] px-3 py-2 rounded-lg text-sm"
|
||||
:class="m.role === 'user' ? 'bg-brand text-white ml-auto' : 'bg-elevado text-texto'"
|
||||
|
||||
<label
|
||||
v-if="pruebas.length"
|
||||
class="flex items-center gap-2 p-3 text-xs text-tenue cursor-pointer select-none"
|
||||
>
|
||||
{{ m.content }}
|
||||
<input v-model="verPruebas" type="checkbox" class="rounded border-borde text-brand focus:ring-brand" />
|
||||
{{ pruebas.length === 1 ? 'Mostrar mi prueba' : `Mostrar mis ${pruebas.length} pruebas` }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 max-h-[30rem] overflow-y-auto">
|
||||
<UiEmptyState
|
||||
v-if="!sesionActiva"
|
||||
estado="normal"
|
||||
titulo="Elegí una conversación"
|
||||
detalle="Vas a ver el intercambio completo: lo que escribió la persona y lo que contestó tu asistente. Si una respuesta no te gusta, corregí su información en «Lo que sabe»."
|
||||
/>
|
||||
<p v-else-if="cargandoHistorial" class="text-sm text-tenue">Cargando…</p>
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="m in historial" :key="m.ID" :class="m.role === 'user' ? 'text-right' : ''">
|
||||
<div
|
||||
class="inline-block max-w-[80%] px-3 py-2 rounded-lg text-sm text-left whitespace-pre-wrap"
|
||||
:class="m.role === 'user' ? 'bg-brand text-white' : 'bg-elevado text-texto'"
|
||||
>
|
||||
{{ m.content }}
|
||||
</div>
|
||||
<div class="text-[10px] text-tenue mt-0.5 tabular-nums" :class="m.role === 'user' ? 'pr-1' : 'pl-1'">
|
||||
{{ cuando(m.CreatedAt) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+30
-17
@@ -326,23 +326,6 @@ func GetUmindHistorial(agenteID uint, sessionID string, n int) ([]UmindMensaje,
|
||||
return items, err
|
||||
}
|
||||
|
||||
// GetUmindSesiones lista las sesiones de conversación recientes de un agente
|
||||
// (para el panel admin), con el último mensaje como resumen.
|
||||
func GetUmindSesiones(agenteID uint, limit int) ([]UmindMensaje, error) {
|
||||
var items []UmindMensaje
|
||||
err := app.Http.Database.DB.Raw(`
|
||||
SELECT * FROM (
|
||||
SELECT DISTINCT ON (session_id) *
|
||||
FROM umind_mensajes
|
||||
WHERE agente_id = ? AND deleted_at IS NULL
|
||||
ORDER BY session_id, created_at DESC
|
||||
) ultimos
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`, agenteID, limit).Scan(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
// SesionResumen es una conversación vista desde el reporte: cuándo empezó,
|
||||
// cuántos mensajes tuvo y con qué la abrió el visitante. El primer mensaje es
|
||||
// lo más útil del conjunto — muestra qué le preguntan de verdad al negocio.
|
||||
@@ -351,6 +334,36 @@ type SesionResumen struct {
|
||||
Inicio time.Time `json:"inicio"`
|
||||
Mensajes int64 `json:"mensajes"`
|
||||
PrimerMensaje string `json:"primer_mensaje"`
|
||||
// Ultimo es la última actividad: es lo que ordena la bandeja del panel.
|
||||
// El reporte mensual no lo usa (ordena por inicio) y ahí queda en cero.
|
||||
Ultimo time.Time `json:"ultimo"`
|
||||
}
|
||||
|
||||
// GetUmindSesionesPanel es la bandeja de conversaciones del panel: las más
|
||||
// recientes primero, con lo que el visitante preguntó al abrir — no con la
|
||||
// última respuesta del bot, que era lo que se mostraba antes y no decía nada
|
||||
// ("¡Hola! ¿En qué te ayudo?" cincuenta veces).
|
||||
func GetUmindSesionesPanel(agenteID uint, limit int) ([]SesionResumen, error) {
|
||||
var items []SesionResumen
|
||||
err := app.Http.Database.DB.Raw(`
|
||||
SELECT
|
||||
m.session_id,
|
||||
MIN(m.created_at) AS inicio,
|
||||
MAX(m.created_at) AS ultimo,
|
||||
COUNT(*) AS mensajes,
|
||||
COALESCE((
|
||||
SELECT p.content FROM umind_mensajes p
|
||||
WHERE p.session_id = m.session_id AND p.agente_id = m.agente_id
|
||||
AND p.role = 'user' AND p.deleted_at IS NULL
|
||||
ORDER BY p.created_at ASC LIMIT 1
|
||||
), '') AS primer_mensaje
|
||||
FROM umind_mensajes m
|
||||
WHERE m.agente_id = ? AND m.deleted_at IS NULL
|
||||
GROUP BY m.session_id, m.agente_id
|
||||
ORDER BY ultimo DESC
|
||||
LIMIT ?
|
||||
`, agenteID, limit).Scan(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
// GetUmindSesionesRango agrupa los mensajes en conversaciones dentro de un
|
||||
|
||||
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
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' rx='22' fill='%238eb02f'/%3E%3Cpath d='M32,42 V58 A14,14 0 0 0 60,58 V42' fill='none' stroke='%23fff' stroke-width='10' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M60,58 V64' fill='none' stroke='%23fff' stroke-width='10' stroke-linecap='round'/%3E%3Ccircle cx='60' cy='28' r='7' fill='%23fff'/%3E%3C/svg%3E" />
|
||||
<title>uMind Studio</title>
|
||||
<script type="module" crossorigin src="/orchestrator/assets/index-CxArDUWK.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-Cd5jgLRW.css">
|
||||
<script type="module" crossorigin src="/orchestrator/assets/index-NXjEScsg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-CZn9dJyg.css">
|
||||
</head>
|
||||
<!-- Sin clase de fondo: el color lo pone body en style.css desde los tokens,
|
||||
que son los que cambian con el tema. Una utilidad acá le ganaba a la
|
||||
|
||||
@@ -371,7 +371,7 @@ func GetUmindSesionesHandler(c *fiber.Ctx) error {
|
||||
if _, err := accesoAgente(c, uint(agenteID)); err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := models.GetUmindSesiones(uint(agenteID), 50)
|
||||
items, err := models.GetUmindSesionesPanel(uint(agenteID), 50)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user