62 Módulo de Notificaciones en Laravel: Gestión de Clientes Morosos y Alertas por Email WhatsApp 📱📢
Duración: 15 minDescripción
📱 Lección 62: Módulo de Notificaciones y Gestión de Clientes Morosos
En esta sesión de Benji V2, iniciamos el desarrollo del módulo de Notificaciones. El objetivo es automatizar la identificación de clientes que presentan retrasos en sus pagos, permitiendo al administrador tomar acciones rápidas a través de canales digitales como Email y WhatsApp.
⚙️ Arquitectura del Módulo de Alertas
A diferencia de otros módulos, este no requiere almacenamiento de nuevos datos, sino un procesamiento inteligente de la información existente:
- 🕹️ Controlador de Notificaciones: Creamos el NotificacionController para centralizar la lógica de filtrado de cuotas vencidas sin necesidad de un modelo propio [02:31].
- 🛤️ Rutas de Gestión: Implementamos la ruta admin.notificaciones.index para visualizar el listado de clientes en mora, protegida por el middleware de autenticación [03:53].
🔍 Lógica de Filtrado de Mora (Backend)
Desarrollamos una consulta compleja para identificar quiénes deben ser notificados:
- 📅 Comparación de Fechas: El sistema obtiene la fecha actual (hoy) y la compara con la fecha_vencimiento de cada cuota pendiente [07:44].
- 📉 Filtro de Pendientes: Solo se seleccionan los registros con estado "Pendiente" cuya fecha de vencimiento sea menor a la actual [08:11].
- 👥 Relación Cliente-Préstamo: La consulta agrupa los resultados para mostrar al cliente, el número total de cuotas vencidas y el monto acumulado de su deuda actual [09:07].
🖥️ Panel de Notificaciones (Frontend)
Diseñamos una interfaz clara para el seguimiento de la cobranza:
- 📋 Listado de Morosos: Una tabla que muestra el nombre del cliente, documento, celular, cantidad de cuotas vencidas y el monto total en mora [11:04].
- 🚥 Identificación de Riesgo: El sistema muestra la "Primera Fecha de Vencimiento" para que el administrador sepa cuánto tiempo de retraso real tiene el cliente [11:29].
- 📲 Acciones de Contacto: Añadimos botones (Email y WhatsApp) que servirán como disparadores para enviar los recordatorios de pago en la siguiente lección [11:38].
✅ Resultado de la Lección
Al finalizar, el sistema cuenta con un monitor de cobranza en tiempo real. El administrador ya no tiene que buscar préstamo por préstamo quién no ha pagado; el módulo de notificaciones le entrega una lista depurada de clientes con cuotas vencidas, lista para ser procesada mediante alertas digitales.
Contenido
Código fuente de la lección
<x-layouts.app title="Notificaciones de cuotas vencidas">
<div class="relative mb-6 w-full">
<flux:heading size="xl" level="1">Notificaciones</flux:heading>
<p class="text-slate-500 dark:text-neutral-400">Listado de clientes con cuotas vencidas.</p>
<br>
<flux:separator variant="subtle" />
</div>
<div class="flex gap-4 mb-4">
<div class="flex-1">
<form action="{{ url('/admin/notificaciones') }}" method="GET" class="flex gap-2 w-1/2">
<div class="flex-1">
<flux:input name="buscar" type="text" icon="magnifying-glass"
placeholder="Buscar por cliente, documento o celular..." value="{{ $buscar }}"
class="transition-all duration-200" />
</div>
<button type="submit"
class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition flex items-center gap-2">
<i class="fas fa-search"></i>
Buscar
</button>
@if ($buscar)
<a href="{{ url('/admin/notificaciones') }}"
class="px-4 py-2 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-lg transition flex items-center gap-2">
<i class="fas fa-trash"></i> Limpiar
</a>
@endif
</form>
</div>
</div>
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 mt-6">
<table class="min-w-full border-collapse text-sm">
<thead class="bg-gray-50 dark:bg-zinc-900 text-center">
<tr>
<th
class="px-4 py-3 border-x border-b border-gray-200 dark:border-zinc-700 text-xs font-bold text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Cliente
</th>
<th
class="px-4 py-3 border-x border-b border-gray-200 dark:border-zinc-700 text-xs font-bold text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Documento
</th>
<th
class="px-4 py-3 border-x border-b border-gray-200 dark:border-zinc-700 text-xs font-bold text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Celular
</th>
<th
class="px-4 py-3 border-x border-b border-gray-200 dark:border-zinc-700 text-xs font-bold text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Cuotas vencidas
</th>
<th
class="px-4 py-3 border-x border-b border-gray-200 dark:border-zinc-700 text-xs font-bold text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Monto vencido
</th>
<th
class="px-4 py-3 border-x border-b border-gray-200 dark:border-zinc-700 text-xs font-bold text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Primer vencimiento
</th>
<th
class="px-4 py-3 border-x border-b border-gray-200 dark:border-zinc-700 text-xs font-bold text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-zinc-800">
@forelse ($clientes as $cliente)
<tr
class="even:bg-slate-50 odd:bg-white dark:even:bg-zinc-700/20 dark:odd:bg-zinc-800 hover:bg-blue-50 dark:hover:bg-zinc-700/50 transition">
<td
class="px-4 py-3 border border-gray-200 dark:border-zinc-700 text-gray-900 dark:text-gray-100">
{{ $cliente->apellidos }} {{ $cliente->nombres }}
</td>
<td
class="px-4 py-3 border border-gray-200 dark:border-zinc-700 text-center text-gray-900 dark:text-gray-100">
{{ $cliente->tipo_documento }} {{ $cliente->numero_documento }}
</td>
<td
class="px-4 py-3 border border-gray-200 dark:border-zinc-700 text-center text-gray-900 dark:text-gray-100">
{{ $cliente->celular }}
</td>
<td class="px-4 py-3 border border-gray-200 dark:border-zinc-700 text-center">
<span
class="inline-flex items-center rounded-full bg-amber-100 text-amber-700 px-2.5 py-0.5 text-xs font-semibold">
{{ $cliente->cuotas_vencidas_total }}
</span>
</td>
<td
class="px-4 py-3 border border-gray-200 dark:border-zinc-700 text-center text-gray-900 dark:text-gray-100 font-semibold">
{{ $ajuste->divisa ?? '$' }} {{ number_format($cliente->monto_vencido_total ?? 0, 2) }}
</td>
<td
class="px-4 py-3 border border-gray-200 dark:border-zinc-700 text-center text-gray-900 dark:text-gray-100">
{{ $cliente->primer_vencimiento ? \Carbon\Carbon::parse($cliente->primer_vencimiento)->format('d/m/Y') : '-' }}
</td>
<td class="px-4 py-3 border border-gray-200 dark:border-zinc-700 text-center">
<div class="flex items-center justify-center gap-2">
<form action="{{ route('admin.notificaciones.notificarEmail', $cliente) }}"
method="POST">
@csrf
<button type="submit"
class="inline-flex items-center px-3 py-1.5 bg-blue-500 hover:bg-blue-600 text-white text-xs font-semibold rounded transition">
<i class="fas fa-envelope mr-2"></i> Notificar por email
</button>
</form>
<a href="{{ route('admin.notificaciones.notificarWhatsapp', $cliente) }}"
class="inline-flex items-center px-3 py-1.5 bg-emerald-500 hover:bg-emerald-600 text-white text-xs font-semibold rounded transition">
<i class="fab fa-whatsapp mr-2"></i> Notificar por WhatsApp
</a>
</div>
</td>
</tr>
@empty
<tr>
<td class="px-4 py-8 text-center text-gray-500" colspan="7">
No hay clientes con cuotas vencidas.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</x-layouts.app>