chatbot nuevo

This commit is contained in:
Lizandro Guarnizo
2026-04-30 21:32:32 -05:00
parent 88a42ec629
commit f61e8005fc
24 changed files with 1724 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Chat en vivo | {{ config('app.name') }}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
@livewireStyles
</head>
<body class="bg-[#111b21] min-h-screen flex items-center justify-center" style="font-family:'Inter',sans-serif;">
<livewire:chat.public-chat />
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
@livewireScripts
</body>
</html>
@@ -0,0 +1,6 @@
<x-app-layout>
<div class="p-4">
<livewire:chat.show-configuracion-chat />
</div>
@include('layouts.footer')
</x-app-layout>
@@ -0,0 +1,3 @@
<x-app-layout>
<livewire:chat.show-conversaciones />
</x-app-layout>
+6
View File
@@ -0,0 +1,6 @@
<x-app-layout>
<div class="p-4">
<livewire:chat.show-menus-chat />
</div>
@include('layouts.footer')
</x-app-layout>
@@ -291,6 +291,38 @@
</div>
@endif
{{-- Módulo Chat (Web + Telegram) --}}
@if (in_array(auth()->user()->rol->nombre, ['super', 'administrador']))
<div x-data="{ openChat: false }" class="space-y-1">
<button @click="openChat = !openChat"
class="flex items-center justify-between w-full py-2 px-3 rounded text-white hover:bg-white/10 focus:outline-none">
<div class="flex items-center gap-2">
<svg class="w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z" />
</svg>
<span>Chat / Bot</span>
</div>
<svg :class="openChat ? 'rotate-180' : ''" class="w-4 h-4 transform transition-transform" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div x-show="openChat" x-transition class="space-y-1">
<a href="{{ route('chat.conversaciones') }}" class="flex items-center gap-2 py-2 px-3 rounded text-white text-left {{ request()->routeIs('chat.conversaciones') ? 'bg-white/20' : 'hover:bg-white/10' }}">
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" /></svg>
<span>Conversaciones</span>
</a>
<a href="{{ route('chat.menus') }}" class="flex items-center gap-2 py-2 px-3 rounded text-white text-left {{ request()->routeIs('chat.menus') ? 'bg-white/20' : 'hover:bg-white/10' }}">
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12h14M5 6h14M5 18h14" /></svg>
<span>Menús del Bot</span>
</a>
<a href="{{ route('chat.configuracion') }}" class="flex items-center gap-2 py-2 px-3 rounded text-white text-left {{ request()->routeIs('chat.configuracion') ? 'bg-white/20' : 'hover:bg-white/10' }}">
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 0 1 0-.255c.007-.378-.138-.75-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
<span>Configuración</span>
</a>
</div>
</div>
@endif
<livewire:show-saving-ip />
<!-- Logout -->
@@ -0,0 +1,120 @@
<div class="w-full max-w-md mx-auto flex flex-col" style="height:100dvh">
{{-- ══ PASO 1: Formulario de inicio ══ --}}
@if ($paso === 'telefono')
<div class="flex flex-col items-center justify-center h-full px-6">
<div class="w-full bg-[#202c33] rounded-2xl shadow-2xl p-8">
<div class="flex flex-col items-center mb-8">
<div class="w-20 h-20 bg-[#00a884] rounded-full flex items-center justify-center mb-4 shadow-lg">
<svg class="w-10 h-10 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z" />
</svg>
</div>
<h1 class="text-white text-2xl font-semibold">Chat en vivo</h1>
<p class="text-[#8696a0] text-sm mt-1 text-center">Ingresa tu número para comenzar</p>
</div>
<form wire:submit.prevent="iniciar" class="space-y-4">
<div>
<label class="block text-[#8696a0] text-xs font-medium mb-1 uppercase tracking-wider">Teléfono *</label>
<input wire:model.defer="telefono" type="tel" placeholder="Ej: 3001234567"
class="w-full bg-[#2a3942] text-white rounded-xl px-4 py-3 text-sm outline-none border border-transparent focus:border-[#00a884] transition placeholder-[#8696a0]">
@error('telefono')
<span class="text-red-400 text-xs mt-1 block">{{ $message }}</span>
@enderror
</div>
<div>
<label class="block text-[#8696a0] text-xs font-medium mb-1 uppercase tracking-wider">Nombre (opcional)</label>
<input wire:model.defer="nombre" type="text" placeholder="Tu nombre"
class="w-full bg-[#2a3942] text-white rounded-xl px-4 py-3 text-sm outline-none border border-transparent focus:border-[#00a884] transition placeholder-[#8696a0]">
</div>
<button type="submit"
class="w-full bg-[#00a884] hover:bg-[#06cf9c] text-white font-semibold py-3 rounded-xl transition text-sm shadow-lg">
<span wire:loading.remove wire:target="iniciar">Comenzar chat &rarr;</span>
<span wire:loading wire:target="iniciar">Conectando...</span>
</button>
</form>
</div>
</div>
{{-- ══ PASO 2: Interfaz de chat ══ --}}
@else
<div class="flex flex-col h-full bg-[#0b141a]"
x-data
x-on:scroll-chat.window="$nextTick(() => { let el = document.getElementById('pub-messages'); if(el) el.scrollTop = el.scrollHeight; })">
{{-- Header --}}
<div class="bg-[#202c33] px-4 py-3 flex items-center gap-3 shadow-md flex-shrink-0">
<div class="w-10 h-10 bg-[#00a884] rounded-full flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z" />
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="text-white font-semibold text-sm truncate">Soporte en línea</p>
<p class="text-[#8696a0] text-xs">
@if ($estadoConv === 'agente') Atendido por un agente
@elseif ($estadoConv === 'cerrada') Conversación cerrada
@else Bot activo
@endif
</p>
</div>
</div>
{{-- Mensajes --}}
<div class="flex-1 overflow-y-auto px-4 py-4 space-y-2" id="pub-messages"
wire:poll.3000ms="cargarMensajes">
@forelse($mensajes as $msg)
@if ($msg['tipo'] === 'usuario')
<div class="flex justify-end">
<div class="max-w-[78%] bg-[#005c4b] text-white rounded-xl rounded-tr-sm px-4 py-2 shadow text-sm">
<p class="whitespace-pre-line break-words">{{ $msg['contenido'] }}</p>
<span class="text-[#8696a0] text-[10px] float-right mt-1 ml-2">{{ $msg['created_at'] }}</span>
</div>
</div>
@else
<div class="flex justify-start">
<div class="max-w-[78%] bg-[#202c33] text-white rounded-xl rounded-tl-sm px-4 py-2 shadow text-sm">
@if ($msg['tipo'] === 'agente')
<span class="text-amber-400 text-[10px] font-semibold block mb-0.5">Agente</span>
@endif
<p class="whitespace-pre-line break-words">{{ $msg['contenido'] }}</p>
<span class="text-[#8696a0] text-[10px] float-right mt-1 ml-2">{{ $msg['created_at'] }}</span>
</div>
</div>
@endif
@empty
<div class="text-center text-[#8696a0] text-sm py-10">Iniciando chat...</div>
@endforelse
</div>
{{-- Input --}}
@if ($estadoConv !== 'cerrada')
<div class="bg-[#202c33] px-3 py-3 flex items-end gap-2 flex-shrink-0">
<textarea
wire:model.defer="input"
wire:keydown.enter.prevent="enviar"
rows="1"
placeholder="Escribe un mensaje..."
class="flex-1 bg-[#2a3942] text-white text-sm rounded-xl px-4 py-3 resize-none outline-none placeholder-[#8696a0] max-h-28"
style="overflow-y:auto;"
></textarea>
<button wire:click="enviar"
class="w-11 h-11 bg-[#00a884] hover:bg-[#06cf9c] rounded-full flex items-center justify-center transition flex-shrink-0">
<svg class="w-5 h-5 text-white" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
@else
<div class="bg-[#202c33] px-4 py-4 text-center text-[#8696a0] text-sm flex-shrink-0">
Esta conversación ha sido cerrada.
</div>
@endif
</div>
@endif
</div>
@@ -0,0 +1,82 @@
<div class="max-w-2xl mx-auto space-y-6">
<div>
<h2 class="text-xl font-bold text-gray-800">Configuración del Chat</h2>
<p class="text-gray-500 text-sm">Parámetros generales del chatbot y del canal Telegram.</p>
</div>
{{-- Formulario --}}
<div class="bg-white rounded-2xl shadow border border-gray-100 p-6 space-y-5">
{{-- Token Telegram --}}
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Token del bot de Telegram</label>
<p class="text-xs text-gray-400 mb-2">Obtenlo hablando con @BotFather en Telegram. Deja vacío si no usas Telegram.</p>
<input wire:model.defer="telegram_token" type="text"
placeholder="1234567890:ABCDefGhijKlmNoPqrStUVwxYZ"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none font-mono">
</div>
{{-- Webhook URL (solo lectura) --}}
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">URL del Webhook Telegram</label>
<p class="text-xs text-gray-400 mb-2">Esta es la URL que Telegram usará para enviarte los mensajes entrantes.</p>
<div class="flex gap-2">
<input type="text" readonly value="{{ url('/chat/webhook/telegram') }}"
class="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm bg-gray-50 text-gray-600 font-mono">
<button wire:click="registrarWebhook"
class="flex-shrink-0 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold rounded-lg transition">
<span wire:loading.remove wire:target="registrarWebhook">Registrar Webhook</span>
<span wire:loading wire:target="registrarWebhook">Registrando...</span>
</button>
</div>
@if($webhookResult)
<p class="mt-2 text-sm {{ str_contains($webhookResult, '✅') ? 'text-emerald-600' : 'text-red-600' }}">
{{ $webhookResult }}
</p>
@endif
</div>
<hr class="border-gray-100">
{{-- Mensaje de bienvenida --}}
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Mensaje de bienvenida <span class="text-red-500">*</span></label>
<p class="text-xs text-gray-400 mb-2">Primer mensaje cuando el bot no tiene menú configurado o cuando el usuario escribe "hola".</p>
<textarea wire:model.defer="mensaje_bienvenida" rows="3"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none resize-none"></textarea>
@error('mensaje_bienvenida') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
{{-- Mensaje de transferencia --}}
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Mensaje al transferir a agente <span class="text-red-500">*</span></label>
<p class="text-xs text-gray-400 mb-2">Se envía al usuario cuando elige la opción de hablar con un agente.</p>
<textarea wire:model.defer="mensaje_transferencia" rows="3"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none resize-none"></textarea>
@error('mensaje_transferencia') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
{{-- Guardar --}}
<div class="flex justify-end pt-2">
<button wire:click="guardar"
class="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold px-6 py-2.5 rounded-lg transition shadow">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<span wire:loading.remove wire:target="guardar">Guardar configuración</span>
<span wire:loading wire:target="guardar">Guardando...</span>
</button>
</div>
</div>
{{-- Info de canales --}}
<div class="bg-blue-50 border border-blue-100 rounded-2xl p-5">
<h3 class="font-semibold text-blue-800 text-sm mb-2"> Canales disponibles</h3>
<ul class="text-blue-700 text-sm space-y-1">
<li><strong>Web:</strong> Los visitantes acceden en <code class="bg-blue-100 px-1 rounded">{{ url('/chat') }}</code></li>
<li><strong>Telegram:</strong> Configurar el token y registrar el webhook para recibir mensajes de Telegram.</li>
</ul>
</div>
</div>
@@ -0,0 +1,201 @@
<div class="flex overflow-hidden bg-[#111b21]" style="height: calc(100vh - 4.5rem)">
{{-- ══ Panel izquierdo: lista de conversaciones ══ --}}
<div class="w-80 flex flex-col border-r border-[#2a3942] flex-shrink-0 bg-[#111b21]">
{{-- Header --}}
<div class="bg-[#202c33] px-4 py-3 flex items-center gap-2 flex-shrink-0">
<svg class="w-5 h-5 text-[#00a884]" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z" />
</svg>
<span class="text-white font-semibold text-sm">Conversaciones</span>
</div>
{{-- Búsqueda + filtros --}}
<div class="bg-[#111b21] px-3 py-2 space-y-2 flex-shrink-0">
<input wire:model.debounce.300ms="busqueda" type="text" placeholder="Buscar contacto..."
class="w-full bg-[#2a3942] text-white text-sm rounded-full px-4 py-2 outline-none placeholder-[#8696a0]">
<div class="flex flex-wrap gap-1">
<button wire:click="$set('filtroEstado','')"
class="text-xs px-2 py-1 rounded-full transition {{ $filtroEstado === '' ? 'bg-[#00a884] text-white' : 'bg-[#2a3942] text-[#8696a0] hover:text-white' }}">
Todas
</button>
<button wire:click="$set('filtroEstado','bot')"
class="text-xs px-2 py-1 rounded-full transition {{ $filtroEstado === 'bot' ? 'bg-[#00a884] text-white' : 'bg-[#2a3942] text-[#8696a0] hover:text-white' }}">
Bot
</button>
<button wire:click="$set('filtroEstado','agente')"
class="text-xs px-2 py-1 rounded-full transition {{ $filtroEstado === 'agente' ? 'bg-amber-600 text-white' : 'bg-[#2a3942] text-[#8696a0] hover:text-white' }}">
Agente
</button>
<button wire:click="$set('filtroEstado','cerrada')"
class="text-xs px-2 py-1 rounded-full transition {{ $filtroEstado === 'cerrada' ? 'bg-gray-600 text-white' : 'bg-[#2a3942] text-[#8696a0] hover:text-white' }}">
Cerradas
</button>
</div>
</div>
{{-- Lista conversaciones --}}
<div class="flex-1 overflow-y-auto" wire:poll.5000ms>
@forelse($conversaciones as $conv)
<button wire:click="selectConv({{ $conv->id }})"
class="w-full flex items-start gap-3 px-4 py-3 border-b border-[#2a3942] text-left transition
{{ $convSelecId === $conv->id ? 'bg-[#2a3942]' : 'hover:bg-[#182229]' }}">
{{-- Avatar canal --}}
<div class="w-11 h-11 rounded-full flex items-center justify-center flex-shrink-0
{{ $conv->canal === 'telegram' ? 'bg-blue-600' : 'bg-[#00a884]' }}">
<span class="text-white text-sm font-semibold">
{{ strtoupper(substr($conv->contact?->nombre ?? '?', 0, 1)) }}
</span>
</div>
{{-- Info --}}
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-1">
<span class="text-white text-sm font-medium truncate">{{ $conv->contact?->nombre ?? 'Sin nombre' }}</span>
@if($conv->mensajes_no_leidos > 0)
<span class="bg-[#00a884] text-white text-[10px] rounded-full px-1.5 py-0.5 flex-shrink-0 font-semibold">
{{ $conv->mensajes_no_leidos }}
</span>
@endif
</div>
<div class="flex items-center gap-1.5 mt-0.5">
{{-- badge canal --}}
<span class="text-[9px] px-1.5 py-0.5 rounded-full flex-shrink-0 font-semibold
{{ $conv->canal === 'telegram' ? 'bg-blue-900 text-blue-300' : 'bg-emerald-900 text-emerald-300' }}">
{{ $conv->canal === 'telegram' ? 'TG' : 'WEB' }}
</span>
{{-- badge estado --}}
<span class="text-[9px] px-1.5 py-0.5 rounded-full flex-shrink-0
{{ $conv->estado === 'bot' ? 'bg-gray-700 text-gray-300' : ($conv->estado === 'agente' ? 'bg-amber-700 text-amber-200' : 'bg-gray-800 text-gray-500') }}">
{{ $conv->estado }}
</span>
<span class="text-[#8696a0] text-[10px] truncate flex-1">
{{ $conv->contact?->telefono ?? $conv->contact?->canal_id }}
</span>
</div>
@if($conv->ultimo_mensaje_at)
<p class="text-[#8696a0] text-[10px] mt-0.5">{{ $conv->ultimo_mensaje_at->diffForHumans() }}</p>
@endif
</div>
</button>
@empty
<div class="p-6 text-center text-[#8696a0] text-sm">Sin conversaciones</div>
@endforelse
</div>
</div>
{{-- ══ Panel derecho: mensajes ══ --}}
@if($convSelec)
<div class="flex-1 flex flex-col min-w-0 bg-[#0b141a]"
x-data
x-on:scroll-admin.window="$nextTick(() => { let el = document.getElementById('admin-msgs'); if(el) el.scrollTop = el.scrollHeight; })">
{{-- Header conv seleccionada --}}
<div class="bg-[#202c33] px-4 py-3 flex items-center gap-3 flex-shrink-0 border-b border-[#2a3942]">
<div class="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0
{{ $convSelec->canal === 'telegram' ? 'bg-blue-600' : 'bg-[#00a884]' }}">
<span class="text-white text-sm font-semibold">
{{ strtoupper(substr($convSelec->contact?->nombre ?? '?', 0, 1)) }}
</span>
</div>
<div class="flex-1 min-w-0">
<p class="text-white text-sm font-semibold truncate">{{ $convSelec->contact?->nombre ?? 'Sin nombre' }}</p>
<p class="text-[#8696a0] text-xs">
{{ $convSelec->canal === 'telegram' ? 'Telegram' : 'Web' }} ·
{{ $convSelec->contact?->telefono ?? $convSelec->contact?->canal_id }}
</p>
</div>
{{-- Acciones --}}
<div class="flex items-center gap-2">
@if($convSelec->estado === 'bot')
<button wire:click="tomarControl"
class="text-xs px-3 py-1.5 bg-amber-700 hover:bg-amber-600 text-white rounded-lg transition">
Tomar control
</button>
@elseif($convSelec->estado === 'agente')
<button wire:click="devolverAlBot"
class="text-xs px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition">
Al bot
</button>
@endif
@if($convSelec->estado !== 'cerrada')
<button wire:click="cerrarConv" onclick="return confirm('¿Cerrar esta conversación?')"
class="text-xs px-3 py-1.5 bg-red-800 hover:bg-red-700 text-white rounded-lg transition">
Cerrar
</button>
@endif
</div>
</div>
{{-- Mensajes --}}
<div class="flex-1 overflow-y-auto px-4 py-4 space-y-2" id="admin-msgs">
@forelse($mensajes as $msg)
@if ($msg['tipo'] === 'usuario')
<div class="flex justify-start">
<div class="max-w-[70%] bg-[#202c33] text-white rounded-xl rounded-tl-sm px-4 py-2 shadow text-sm">
<p class="whitespace-pre-line break-words">{{ $msg['contenido'] }}</p>
<span class="text-[#8696a0] text-[10px] float-right mt-1 ml-2">{{ $msg['created_at'] }}</span>
</div>
</div>
@elseif ($msg['tipo'] === 'bot')
<div class="flex justify-end">
<div class="max-w-[70%] bg-[#1a3a2a] text-white rounded-xl rounded-tr-sm px-4 py-2 shadow text-sm border border-[#00a884]/20">
<span class="text-[#00a884] text-[10px] font-semibold block mb-0.5">Bot</span>
<p class="whitespace-pre-line break-words">{{ $msg['contenido'] }}</p>
<span class="text-[#8696a0] text-[10px] float-right mt-1 ml-2">{{ $msg['created_at'] }}</span>
</div>
</div>
@else
<div class="flex justify-end">
<div class="max-w-[70%] bg-[#005c4b] text-white rounded-xl rounded-tr-sm px-4 py-2 shadow text-sm">
<span class="text-amber-300 text-[10px] font-semibold block mb-0.5">Agente</span>
<p class="whitespace-pre-line break-words">{{ $msg['contenido'] }}</p>
<span class="text-[#8696a0] text-[10px] float-right mt-1 ml-2">{{ $msg['created_at'] }}</span>
</div>
</div>
@endif
@empty
<div class="text-center text-[#8696a0] text-sm py-10">Sin mensajes</div>
@endforelse
</div>
{{-- Input respuesta (solo en estado agente) --}}
@if($convSelec->estado === 'agente')
<div class="bg-[#202c33] px-3 py-3 flex items-end gap-2 flex-shrink-0">
<textarea wire:model.defer="replyText" rows="2"
placeholder="Escribe tu respuesta como agente..."
class="flex-1 bg-[#2a3942] text-white text-sm rounded-xl px-4 py-3 resize-none outline-none placeholder-[#8696a0] max-h-28"></textarea>
<button wire:click="sendReply"
class="w-11 h-11 bg-[#00a884] hover:bg-[#06cf9c] rounded-full flex items-center justify-center transition flex-shrink-0">
<svg class="w-5 h-5 text-white" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
@elseif($convSelec->estado === 'bot')
<div class="bg-[#202c33] px-4 py-3 text-center text-[#8696a0] text-xs flex-shrink-0 border-t border-[#2a3942]">
El bot está respondiendo. Usa <strong class="text-white">Tomar control</strong> para responder manualmente.
</div>
@else
<div class="bg-[#202c33] px-4 py-3 text-center text-[#8696a0] text-xs flex-shrink-0 border-t border-[#2a3942]">
Conversación cerrada.
</div>
@endif
</div>
@else
<div class="flex-1 flex items-center justify-center bg-[#0b141a]">
<div class="text-center">
<div class="w-20 h-20 bg-[#202c33] rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-10 h-10 text-[#8696a0]" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z" />
</svg>
</div>
<p class="text-[#8696a0] text-sm">Selecciona una conversación para ver los mensajes</p>
</div>
</div>
@endif
</div>
@@ -0,0 +1,231 @@
<div class="max-w-5xl mx-auto space-y-6">
{{-- Cabecera --}}
<div class="flex items-center justify-between">
<div>
<h2 class="text-xl font-bold text-gray-800">Menús del Bot</h2>
<p class="text-gray-500 text-sm">Configura el árbol de menús que usa el chatbot.</p>
</div>
<button wire:click="$set('showMenuForm', true)"
class="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold px-4 py-2 rounded-lg transition">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Nuevo Menú
</button>
</div>
{{-- Modal Menú --}}
@if($showMenuForm)
<div class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg">
<div class="flex items-center justify-between p-6 border-b">
<h3 class="font-bold text-gray-800">{{ $editMenuId ? 'Editar Menú' : 'Nuevo Menú' }}</h3>
<button wire:click="closeMenuForm" class="text-gray-400 hover:text-gray-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nombre del menú <span class="text-red-500">*</span></label>
<input wire:model.defer="menuNombre" type="text" placeholder="Ej: Menú principal"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 focus:border-transparent outline-none">
@error('menuNombre') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Mensaje que muestra <span class="text-red-500">*</span></label>
<textarea wire:model.defer="menuMensaje" rows="4" placeholder="Hola 👋 ¿En qué podemos ayudarte? Escribe el número de tu opción:"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none resize-none"></textarea>
@error('menuMensaje') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div class="flex items-center gap-6">
<label class="flex items-center gap-2 cursor-pointer">
<input wire:model="menuEsRaiz" type="checkbox" class="rounded text-emerald-600">
<span class="text-sm text-gray-700">Es el menú raíz (punto de inicio)</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input wire:model="menuActivo" type="checkbox" class="rounded text-emerald-600">
<span class="text-sm text-gray-700">Activo</span>
</label>
</div>
</div>
<div class="flex justify-end gap-3 px-6 pb-6">
<button wire:click="closeMenuForm"
class="px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition">Cancelar</button>
<button wire:click="saveMenu"
class="px-4 py-2 text-sm bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-semibold transition">Guardar</button>
</div>
</div>
</div>
@endif
{{-- Modal Opción --}}
@if($showOpcionForm)
<div class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg">
<div class="flex items-center justify-between p-6 border-b">
<h3 class="font-bold text-gray-800">{{ $editOpcionId ? 'Editar Opción' : 'Nueva Opción' }}</h3>
<button wire:click="closeOpcionForm" class="text-gray-400 hover:text-gray-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="p-6 space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Clave (lo que escribe) <span class="text-red-500">*</span></label>
<input wire:model.defer="opcionClave" type="text" placeholder="Ej: 1, hola, info"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none">
@error('opcionClave') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Orden</label>
<input wire:model.defer="opcionOrden" type="number" min="0"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Etiqueta (texto visible) <span class="text-red-500">*</span></label>
<input wire:model.defer="opcionEtiqueta" type="text" placeholder="Ej: Consulta de saldo"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none">
@error('opcionEtiqueta') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Acción <span class="text-red-500">*</span></label>
<select wire:model="opcionAccion"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none">
<option value="respuesta">Respuesta de texto</option>
<option value="ir_menu">Ir a otro menú</option>
<option value="agente">Transferir a agente</option>
</select>
</div>
@if($opcionAccion === 'ir_menu')
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Menú destino <span class="text-red-500">*</span></label>
<select wire:model.defer="opcionMenuDestId"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none">
<option value="">-- Selecciona --</option>
@foreach($todosMenus as $m)
<option value="{{ $m->id }}">{{ $m->nombre }}</option>
@endforeach
</select>
@error('opcionMenuDestId') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
@endif
@if($opcionAccion === 'respuesta')
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Texto de respuesta <span class="text-red-500">*</span></label>
<textarea wire:model.defer="opcionRespuesta" rows="4"
placeholder="Escribe la respuesta que enviará el bot..."
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none resize-none"></textarea>
@error('opcionRespuesta') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
@endif
@if($opcionAccion === 'agente')
<p class="text-sm text-amber-600 bg-amber-50 rounded-lg px-3 py-2">
⚠️ Al elegir esta opción, la conversación se transferirá a un agente humano.
</p>
@endif
</div>
<div class="flex justify-end gap-3 px-6 pb-6">
<button wire:click="closeOpcionForm"
class="px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition">Cancelar</button>
<button wire:click="saveOpcion"
class="px-4 py-2 text-sm bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-semibold transition">Guardar</button>
</div>
</div>
</div>
@endif
{{-- Lista de menús --}}
@forelse($menus as $menu)
<div class="bg-white rounded-2xl shadow border border-gray-100 overflow-hidden">
{{-- Header del menú --}}
<div class="flex items-start justify-between px-5 py-4 border-b border-gray-100 bg-gray-50">
<div class="flex items-start gap-3">
<div class="flex flex-col gap-1 mt-0.5">
@if($menu->es_raiz)
<span class="text-[10px] bg-emerald-100 text-emerald-700 font-bold px-2 py-0.5 rounded-full uppercase">RAÍZ</span>
@endif
@if(!$menu->activo)
<span class="text-[10px] bg-red-100 text-red-600 font-bold px-2 py-0.5 rounded-full uppercase">INACTIVO</span>
@endif
</div>
<div>
<h3 class="font-bold text-gray-800">{{ $menu->nombre }}</h3>
<p class="text-gray-500 text-sm mt-1 whitespace-pre-line">{{ $menu->mensaje }}</p>
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0 ml-4">
<button wire:click="openOpcionForm({{ $menu->id }})"
class="text-xs px-3 py-1.5 bg-emerald-50 hover:bg-emerald-100 text-emerald-700 rounded-lg border border-emerald-200 transition font-medium">
+ Opción
</button>
<button wire:click="editMenu({{ $menu->id }})"
class="text-xs px-3 py-1.5 bg-blue-50 hover:bg-blue-100 text-blue-700 rounded-lg border border-blue-200 transition font-medium">
Editar
</button>
<button wire:click="deleteMenu({{ $menu->id }})" onclick="return confirm('¿Eliminar este menú y todas sus opciones?')"
class="text-xs px-3 py-1.5 bg-red-50 hover:bg-red-100 text-red-600 rounded-lg border border-red-200 transition font-medium">
Eliminar
</button>
</div>
</div>
{{-- Opciones del menú --}}
@if($menu->options->count() > 0)
<div class="divide-y divide-gray-100">
@foreach($menu->options as $op)
<div class="flex items-center justify-between px-5 py-3 hover:bg-gray-50 transition">
<div class="flex items-center gap-3">
<span class="w-8 h-8 bg-gray-100 rounded-lg flex items-center justify-center text-sm font-bold text-gray-700">
{{ $op->clave }}
</span>
<div>
<p class="text-sm font-medium text-gray-700">{{ $op->etiqueta }}</p>
<div class="flex items-center gap-2 mt-0.5">
@if($op->accion === 'ir_menu')
<span class="text-[10px] bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-semibold"> Menú</span>
@if($op->menuDestino)
<span class="text-[10px] text-gray-500">{{ $op->menuDestino->nombre }}</span>
@endif
@elseif($op->accion === 'respuesta')
<span class="text-[10px] bg-green-100 text-green-700 px-2 py-0.5 rounded-full font-semibold">Respuesta</span>
<span class="text-[10px] text-gray-500 truncate max-w-xs">{{ $op->respuesta_texto }}</span>
@else
<span class="text-[10px] bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full font-semibold"> Agente</span>
@endif
</div>
</div>
</div>
<div class="flex items-center gap-2">
<button wire:click="editOpcion({{ $op->id }})"
class="text-xs text-blue-600 hover:text-blue-800 font-medium transition">Editar</button>
<button wire:click="deleteOpcion({{ $op->id }})" onclick="return confirm('¿Eliminar esta opción?')"
class="text-xs text-red-500 hover:text-red-700 font-medium transition">Eliminar</button>
</div>
</div>
@endforeach
</div>
@else
<div class="px-5 py-4 text-sm text-gray-400">
Sin opciones. <button wire:click="openOpcionForm({{ $menu->id }})" class="text-emerald-600 hover:underline">Agregar primera opción </button>
</div>
@endif
</div>
@empty
<div class="bg-white rounded-2xl shadow border border-gray-100 p-12 text-center">
<svg class="w-12 h-12 text-gray-300 mx-auto mb-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12h14M5 6h14M5 18h14" />
</svg>
<p class="text-gray-500 mb-4">No hay menús configurados todavía.</p>
<button wire:click="$set('showMenuForm', true)"
class="bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold px-4 py-2 rounded-lg transition">
Crear primer menú
</button>
</div>
@endforelse
</div>