Files
soft_usite/resources/views/portal/dashboard.html
T
Lizandro GuarnizoandClaude Sonnet 5 8ef49c5169 feat: bucles de crecimiento y activación de uMind
Cuatro cosas que ya estaban a medio construir y no se usaban como canal.

1. El badge del widget ahora es un enlace con UTM. Cada cliente ya te
   estaba dando exposición en su sitio y no se capitalizaba. El host sale
   de la URL del propio script, así funciona igual en cualquier entorno.

2. Reporte del mes por agente en Excel: conversaciones atendidas, con qué
   las abrió el visitante, y el consumo desglosado. Es lo que el cliente
   necesita para justificar el gasto puertas adentro — un panel al que hay
   que entrar no sirve para eso, un archivo que se reenvía sí. Reusa el
   generador de xlsx del cronograma.

3. Aviso automático de agentes sin base de conocimiento (cron diario). Un
   agente sin fuentes responde de memoria e inventa datos, el cliente
   concluye que el producto no sirve y se va. Es el punto donde más gente
   se cae y se detecta solo. Se avisa UNA vez por agente, usando el log de
   auditoría como registro de envío para no necesitar tabla nueva ni
   convertir el recordatorio en spam.

4. Tarjeta de uMind en el dashboard del portal: atajo si el cliente ya lo
   tiene, oferta si no. Es el punto de contacto más barato que hay — ya
   entró, ya confía, y el cobro se suma a la factura que ya recibe. El
   mensaje lidera con notas de voz y fotos, que es lo más difícil de
   copiar de lo que tenemos.

La consulta de conversaciones agrupa en SQL en vez de traerse el historial
entero para agrupar en Go.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 22:40:28 -05:00

260 lines
13 KiB
HTML

<!-- Portal Dashboard -->
<div x-data="portalDashboard()" x-init="init()">
<!-- Saludo -->
<div class="mb-8">
<h1 class="text-2xl font-bold text-slate-800">
Hola, {{ if .portalUser }}{{ .portalUser.Nombre }}{{ end }} 👋
</h1>
<p class="text-slate-500 text-sm mt-1">
{{ if .isPartner }}
Estás viendo todos los proyectos de tus clientes.
{{ else }}
Aquí puedes ver el avance de tus proyectos.
{{ end }}
</p>
</div>
<!-- Un fallo al cargar tiene que verse como un fallo, no como "no tenés
proyectos": son dos situaciones muy distintas para quien mira. -->
{{ if .error }}
<div class="bg-red-50 border border-red-200 text-red-700 rounded-xl p-4 text-sm mb-6">
{{ .error }}
</div>
{{ end }}
<!-- uMind: atajo si ya lo tiene, oferta si no. El dueño ya entró y ya
confía; es el punto de contacto más barato que existe. -->
{{ if .tieneUmind }}
<a href="/portal/studio" class="flex items-center gap-3 bg-white border border-slate-200 hover:border-[#8eb02f] rounded-2xl p-4 mb-6 transition-colors group">
<span class="w-10 h-10 rounded-xl flex items-center justify-center text-white text-sm font-bold flex-shrink-0" style="background:#8eb02f">uM</span>
<div class="min-w-0 flex-1">
<p class="font-semibold text-slate-800 text-sm">uMind Studio</p>
<p class="text-slate-500 text-xs mt-0.5">Configurá tus asistentes, revisá conversaciones y consumo.</p>
</div>
<span class="text-slate-300 group-hover:text-[#8eb02f] transition-colors">&rarr;</span>
</a>
{{ else }}
<div class="rounded-2xl p-5 mb-6 border border-slate-200 bg-gradient-to-br from-[#8eb02f]/8 to-transparent">
<div class="flex items-start gap-3">
<span class="w-10 h-10 rounded-xl flex items-center justify-center text-white text-sm font-bold flex-shrink-0" style="background:#8eb02f">uM</span>
<div class="min-w-0">
<p class="font-semibold text-slate-800 text-sm">Tu WhatsApp puede responder solo</p>
<p class="text-slate-600 text-sm mt-1">
Un asistente que contesta con la información de tu negocio — precios, horarios, servicios.
Entiende las <strong>notas de voz</strong> y lee las <strong>fotos</strong> que te mandan tus clientes.
</p>
<a href="mailto:{{ if .portalUser }}{{ end }}soporte@u-site.app?subject=Quiero%20activar%20uMind"
class="inline-block mt-3 text-sm font-semibold text-white rounded-lg px-4 py-2" style="background:#8eb02f">
Quiero activarlo
</a>
</div>
</div>
</div>
{{ end }}
<!-- Proyectos cards -->
{{ if .isPartner }}
<!-- Agrupado por cliente -->
{{ range .grupos }}
<div class="mb-8">
<h2 class="text-sm font-bold text-slate-500 uppercase tracking-wide mb-3">
{{ if .Cliente.Empresa }}{{ .Cliente.Empresa }}{{ else }}{{ .Cliente.Nombre }}{{ end }}
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{{ range .Proyectos }}
<a href="/portal/proyecto/{{ .Slug }}" class="bg-white rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow p-5 block group">
<div class="flex items-center gap-3 mb-3">
<span class="w-3 h-3 rounded-full" style="background:{{ .Color }}"></span>
<span class="font-semibold text-slate-800 group-hover:text-[#8eb02f] transition-colors text-sm">{{ .Nombre }}</span>
</div>
<p class="text-xs text-slate-500 mb-3 line-clamp-2">{{ .Descripcion }}</p>
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-medium text-slate-500">Progreso</span>
<span class="text-xs font-bold text-slate-700">{{ .Progreso }}%</span>
</div>
<div class="w-full bg-slate-100 rounded-full h-2">
<div class="h-2 rounded-full" style="background:{{ .Color }};width:{{ .Progreso }}%"></div>
</div>
<div class="mt-3 flex justify-between items-center">
<span class="text-xs px-2 py-0.5 rounded-full font-semibold" style="background:#f1f5f9;color:#475569">{{ .Estado }}</span>
<span class="text-xs text-slate-400">Ver detalle →</span>
</div>
</a>
{{ end }}
</div>
</div>
{{ end }}
{{ else }}
<!-- Vista simple para cliente -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{{ range .proyectos }}
<a href="/portal/proyecto/{{ .Slug }}" class="bg-white rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow p-5 block group">
<div class="flex items-center gap-3 mb-3">
<span class="w-3 h-3 rounded-full" style="background:{{ .Color }}"></span>
<span class="font-semibold text-slate-800 group-hover:text-[#8eb02f] transition-colors text-sm">{{ .Nombre }}</span>
</div>
<p class="text-xs text-slate-500 mb-3 line-clamp-2">{{ .Descripcion }}</p>
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-medium text-slate-500">Progreso</span>
<span class="text-xs font-bold text-slate-700">{{ .Progreso }}%</span>
</div>
<div class="w-full bg-slate-100 rounded-full h-2">
<div class="h-2 rounded-full" style="background:{{ .Color }};width:{{ .Progreso }}%"></div>
</div>
<div class="mt-3 flex justify-between items-center">
<span class="text-xs px-2 py-0.5 rounded-full font-semibold" style="background:#f1f5f9;color:#475569">{{ .Estado }}</span>
<span class="text-xs text-slate-400">Ver detalle →</span>
</div>
</a>
{{ end }}
</div>
{{ if not .proyectos }}
<div class="text-center py-20 text-slate-400">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"/></svg>
<p>Aún no tienes proyectos asignados.</p>
</div>
{{ end }}
{{ end }}
</div>
{{ if .isPartner }}
<!-- ─── Sección Partner: Recursos y Comunicados ─────────────────────────────── -->
<div x-data="partnerHub()" x-init="init()" class="mt-10">
<div class="flex items-center gap-3 mb-5">
<span class="w-8 h-8 rounded-lg bg-[#8eb02f] text-white flex items-center justify-center text-sm font-bold">P</span>
<div>
<h2 class="text-base font-bold text-slate-800">Zona Partner</h2>
<p class="text-xs text-slate-400">Recursos y comunicados exclusivos para ti</p>
</div>
</div>
<!-- Tabs -->
<div class="flex border-b border-slate-200 mb-5 gap-0">
<button @click="tab='comunicados'" class="px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors"
:class="tab==='comunicados' ? 'border-[#8eb02f] text-[#8eb02f]' : 'border-transparent text-slate-500 hover:text-slate-700'">
📢 Comunicados
<span x-show="comunicados.length > 0" class="ml-1 bg-slate-100 text-slate-500 text-xs px-1.5 py-0.5 rounded-full" x-text="comunicados.length"></span>
</button>
<button @click="tab='recursos'" class="px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors"
:class="tab==='recursos' ? 'border-[#8eb02f] text-[#8eb02f]' : 'border-transparent text-slate-500 hover:text-slate-700'">
📁 Recursos
<span x-show="recursos.length > 0" class="ml-1 bg-slate-100 text-slate-500 text-xs px-1.5 py-0.5 rounded-full" x-text="recursos.length"></span>
</button>
</div>
<!-- Comunicados -->
<div x-show="tab==='comunicados'" x-cloak>
<div class="space-y-4">
<template x-for="com in comunicados" :key="com.ID">
<div class="bg-white border border-slate-200 rounded-xl p-5">
<div class="flex items-start justify-between gap-3">
<div class="flex-1">
<h3 class="font-semibold text-slate-800 text-sm mb-1" x-text="com.titulo"></h3>
<p class="text-sm text-slate-600 whitespace-pre-line leading-relaxed" x-text="com.contenido"></p>
<p class="text-xs text-slate-400 mt-2" x-text="formatDate(com.CreatedAt)"></p>
</div>
<template x-if="com.archivo">
<a :href="`/portal/partner-comunicados/${com.ID}/download`" target="_blank"
class="flex-shrink-0 inline-flex items-center gap-1.5 text-xs text-[#8eb02f] hover:text-[#6d8c24] border border-[#8eb02f] px-2.5 py-1.5 rounded-lg hover:bg-[#f6ffe0] transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
Adjunto
</a>
</template>
</div>
</div>
</template>
<p x-show="comunicados.length === 0 && !loading" class="text-slate-400 text-sm text-center py-10">Sin comunicados por ahora.</p>
</div>
</div>
<!-- Recursos -->
<div x-show="tab==='recursos'" x-cloak>
<!-- Filtro por categoría -->
<div class="flex flex-wrap gap-2 mb-4">
<template x-for="cat in ['todos','general','tecnico','comercial','marketing']" :key="cat">
<button @click="filtro=cat" class="text-xs px-3 py-1 rounded-full border font-medium transition-colors capitalize"
:class="filtro===cat ? 'bg-[#8eb02f] text-white border-[#8eb02f]' : 'border-slate-200 text-slate-500 hover:border-slate-400'" x-text="cat">
</button>
</template>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<template x-for="r in recursosFiltrados" :key="r.ID">
<div class="bg-white border border-slate-200 rounded-xl p-4 flex flex-col gap-2">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="text-xs px-2 py-0.5 rounded-full font-medium capitalize" :class="catClass(r.categoria)" x-text="r.categoria"></span>
</div>
<h4 class="font-semibold text-slate-800 text-sm" x-text="r.nombre"></h4>
<p class="text-xs text-slate-500 mt-0.5 line-clamp-2" x-text="r.descripcion"></p>
</div>
</div>
<template x-if="r.archivo">
<a :href="`/portal/partner-recursos/${r.ID}/download`" target="_blank"
class="mt-auto inline-flex items-center gap-2 text-xs font-medium text-white bg-[#8eb02f] hover:bg-[#6d8c24] px-3 py-1.5 rounded-lg transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
Descargar
</a>
</template>
<template x-if="r.enlace">
<a :href="r.enlace" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center gap-2 text-xs font-medium text-[#8eb02f] border border-[#8eb02f] hover:bg-[#f0f6e4] px-3 py-1.5 rounded-lg transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
Ver enlace
</a>
</template>
<template x-if="!r.archivo && !r.enlace">
<span class="text-xs text-slate-400 italic">Sin archivo adjunto</span>
</template>
</div>
</template>
<p x-show="recursosFiltrados.length === 0 && !loading" class="sm:col-span-2 lg:col-span-3 text-slate-400 text-sm text-center py-10">Sin recursos disponibles.</p>
</div>
</div>
</div>
{{ end }}
<script>
function partnerHub() {
return {
tab: 'comunicados',
filtro: 'todos',
recursos: [],
comunicados: [],
loading: false,
async init() {
this.loading = true;
try {
const r = await axios.get('/portal/partner-recursos');
this.recursos = r.data.recursos || [];
this.comunicados = r.data.comunicados || [];
} catch(e) {} finally { this.loading = false; }
},
get recursosFiltrados() {
if (this.filtro === 'todos') return this.recursos;
return this.recursos.filter(r => r.categoria === this.filtro);
},
catClass(cat) {
return {
general: 'bg-slate-100 text-slate-600',
tecnico: 'bg-blue-100 text-blue-700',
comercial: 'bg-yellow-100 text-yellow-700',
marketing: 'bg-pink-100 text-pink-700',
}[cat] || 'bg-slate-100 text-slate-600';
},
formatDate(d) {
if (!d) return '';
return new Date(d).toLocaleDateString('es-CO', { day:'2-digit', month:'short', year:'numeric' });
},
}
}
</script>