feat: página de comprobantes, búsqueda por email en conversaciones, quitar menús del bot
- Nueva página /chat/comprobantes: tabla paginada de solicitudes_recarga con búsqueda por email/nombre, filtros por estado y canal, botón "Ver chat" - ShowConversaciones: búsqueda también por email del usuario autenticado - ShowConversaciones: soporta ?conv=ID para abrir conversación desde comprobantes - Nav: reemplaza "Menús del Bot" (no funcional) por "Comprobantes" - SolicitudRecarga: relación user() agregada - ChatController: método comprobantes() agregado Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1572fb72cd
commit
60ddc4b9df
Regular → Executable
+5
@@ -14,6 +14,11 @@ class ChatController extends Controller
|
||||
return view('chat.conversaciones');
|
||||
}
|
||||
|
||||
public function comprobantes()
|
||||
{
|
||||
return view('chat.comprobantes');
|
||||
}
|
||||
|
||||
public function menus()
|
||||
{
|
||||
return view('chat.menus');
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Chat;
|
||||
|
||||
use App\Models\ChatConversation;
|
||||
use App\Models\SolicitudRecarga;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class ShowComprobantes extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public string $busqueda = '';
|
||||
public string $filtroEstado = '';
|
||||
public string $filtroCanal = '';
|
||||
|
||||
protected $queryString = [
|
||||
'busqueda' => ['except' => ''],
|
||||
'filtroEstado' => ['except' => ''],
|
||||
'filtroCanal' => ['except' => ''],
|
||||
];
|
||||
|
||||
public function updatingBusqueda(): void { $this->resetPage(); }
|
||||
public function updatingFiltroEstado(): void { $this->resetPage(); }
|
||||
public function updatingFiltroCanal(): void { $this->resetPage(); }
|
||||
|
||||
public function verChat(int $userId): void
|
||||
{
|
||||
$conv = ChatConversation::whereHas('contact', fn($q) => $q->where('user_id', $userId))
|
||||
->orderByDesc('ultimo_mensaje_at')
|
||||
->first();
|
||||
|
||||
if ($conv) {
|
||||
$this->redirect(route('chat.conversaciones') . '?conv=' . $conv->id);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$solicitudes = SolicitudRecarga::with('user')
|
||||
->when($this->filtroEstado, fn($q) => $q->where('estado', $this->filtroEstado))
|
||||
->when($this->filtroCanal, fn($q) => $q->where('canal', $this->filtroCanal))
|
||||
->when($this->busqueda, fn($q) => $q->whereHas('user', fn($s) =>
|
||||
$s->where('email', 'like', "%{$this->busqueda}%")
|
||||
->orWhere('name', 'like', "%{$this->busqueda}%")
|
||||
))
|
||||
->orderByDesc('id')
|
||||
->paginate(25);
|
||||
|
||||
return view('livewire.chat.show-comprobantes', compact('solicitudes'));
|
||||
}
|
||||
}
|
||||
Regular → Executable
+11
-1
@@ -15,6 +15,15 @@ class ShowConversaciones extends Component
|
||||
public string $replyText = '';
|
||||
public array $mensajes = [];
|
||||
|
||||
protected $queryString = ['convSelecId' => ['except' => null, 'as' => 'conv']];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if ($this->convSelecId) {
|
||||
$this->cargarMensajes();
|
||||
}
|
||||
}
|
||||
|
||||
public function selectConv(int $convId): void
|
||||
{
|
||||
$this->convSelecId = $convId;
|
||||
@@ -124,7 +133,8 @@ class ShowConversaciones extends Component
|
||||
$q->whereHas('contact', function ($s) {
|
||||
$s->where('nombre', 'like', "%{$this->busqueda}%")
|
||||
->orWhere('telefono', 'like', "%{$this->busqueda}%")
|
||||
->orWhere('canal_id', 'like', "%{$this->busqueda}%");
|
||||
->orWhere('canal_id', 'like', "%{$this->busqueda}%")
|
||||
->orWhereHas('user', fn($u) => $u->where('email', 'like', "%{$this->busqueda}%"));
|
||||
});
|
||||
})
|
||||
->orderByDesc('ultimo_mensaje_at')
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SolicitudRecarga extends Model
|
||||
@@ -55,4 +56,9 @@ class SolicitudRecarga extends Model
|
||||
{
|
||||
$this->update(['estado' => 'confirmada']);
|
||||
}
|
||||
|
||||
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Comprobantes de Recarga</h2>
|
||||
</x-slot>
|
||||
<livewire:chat.show-comprobantes />
|
||||
</x-app-layout>
|
||||
Regular → Executable
+3
-3
@@ -276,9 +276,9 @@
|
||||
<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 href="{{ route('chat.comprobantes') }}" class="flex items-center gap-2 py-2 px-3 rounded text-white text-left {{ request()->routeIs('chat.comprobantes') ? '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 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" /></svg>
|
||||
<span>Comprobantes</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>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<div class="p-6">
|
||||
|
||||
{{-- Filtros --}}
|
||||
<div class="flex flex-wrap gap-3 mb-5">
|
||||
<input wire:model.debounce.300ms="busqueda" type="text"
|
||||
placeholder="Buscar por email o nombre..."
|
||||
class="flex-1 min-w-[220px] bg-white border border-gray-200 rounded-lg px-4 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500">
|
||||
|
||||
<select wire:model="filtroEstado"
|
||||
class="bg-white border border-gray-200 rounded-lg px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500">
|
||||
<option value="">Todos los estados</option>
|
||||
<option value="pendiente">🟡 Pendiente</option>
|
||||
<option value="confirmada">✅ Confirmada</option>
|
||||
<option value="expirada">⛔ Expirada</option>
|
||||
</select>
|
||||
|
||||
<select wire:model="filtroCanal"
|
||||
class="bg-white border border-gray-200 rounded-lg px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500">
|
||||
<option value="">Todos los canales</option>
|
||||
<option value="web">🌐 Web</option>
|
||||
<option value="telegram">✈️ Telegram</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Tabla --}}
|
||||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b border-gray-100">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 text-gray-500 font-medium">#</th>
|
||||
<th class="text-left px-4 py-3 text-gray-500 font-medium">Fecha</th>
|
||||
<th class="text-left px-4 py-3 text-gray-500 font-medium">Usuario</th>
|
||||
<th class="text-left px-4 py-3 text-gray-500 font-medium">Canal</th>
|
||||
<th class="text-left px-4 py-3 text-gray-500 font-medium">Titular</th>
|
||||
<th class="text-right px-4 py-3 text-gray-500 font-medium">Monto</th>
|
||||
<th class="text-center px-4 py-3 text-gray-500 font-medium">Estado</th>
|
||||
<th class="text-center px-4 py-3 text-gray-500 font-medium">Chat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
@forelse($solicitudes as $s)
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="px-4 py-3 text-gray-400 text-xs">{{ $s->id }}</td>
|
||||
<td class="px-4 py-3 text-gray-600 whitespace-nowrap">
|
||||
{{ $s->created_at ? $s->created_at->format('d/m/y H:i') : '—' }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
@if($s->user)
|
||||
<p class="font-medium text-gray-800">{{ $s->user->name }}</p>
|
||||
<p class="text-gray-400 text-xs">{{ $s->user->email }}</p>
|
||||
@else
|
||||
<span class="text-gray-400 text-xs">Usuario eliminado</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
@if($s->canal === 'telegram')
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-700">✈️ Telegram</span>
|
||||
@else
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-700">🌐 Web</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-600 text-xs">{{ $s->nombre_remitente ?? '—' }}</td>
|
||||
<td class="px-4 py-3 text-right font-semibold text-gray-800">
|
||||
${{ number_format($s->monto) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
@if($s->estado === 'confirmada')
|
||||
<span class="inline-block px-2.5 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-700">✅ Confirmada</span>
|
||||
@elseif($s->estado === 'pendiente')
|
||||
<span class="inline-block px-2.5 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-700">🟡 Pendiente</span>
|
||||
@else
|
||||
<span class="inline-block px-2.5 py-1 rounded-full text-xs font-semibold bg-red-100 text-red-700">⛔ Expirada</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
@if($s->user_id)
|
||||
<button wire:click="verChat({{ $s->user_id }})"
|
||||
class="text-xs px-3 py-1.5 bg-[#00a884] hover:bg-[#06cf9c] text-white rounded-lg transition font-medium">
|
||||
Ver chat
|
||||
</button>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-10 text-center text-gray-400">No hay registros</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if($solicitudes->hasPages())
|
||||
<div class="px-4 py-3 border-t border-gray-100">
|
||||
{{ $solicitudes->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
Regular → Executable
+1
-1
@@ -97,7 +97,7 @@ Route::middleware(["auth", "solo_usuario_administrador"])->group(function () {
|
||||
// Módulo Chat (web + Telegram)
|
||||
Route::prefix('chat')->name('chat.')->group(function () {
|
||||
Route::get('/conversaciones', [ChatController::class, 'conversaciones'])->name('conversaciones');
|
||||
Route::get('/menus', [ChatController::class, 'menus'])->name('menus');
|
||||
Route::get('/comprobantes', [ChatController::class, 'comprobantes'])->name('comprobantes');
|
||||
Route::get('/configuracion', [ChatController::class, 'configuracion'])->name('configuracion');
|
||||
Route::get('/ia-logs', [ChatController::class, 'iaLogs'])->name('ia-logs');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user