feat: WhatsApp promociones — fechas en español, status webhook y analytics inspector
- Corrige formato de fechas a español con Carbon isoFormat('D [de] MMMM')
- Webhook procesa statuses de Meta (delivered/read/failed) y actualiza pivot alumno_promocion
- Analytics: nuevas métricas de entregados/leídos, embudo de entrega, filtro por tipo
- Inspector de payload raw por log con modal y botón copiar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
{{-- Tarjetas de métricas --}}
|
||||
<div class="row mb-4">
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="col-xl-2 col-md-4 mb-4">
|
||||
<div class="card border-left-success shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
@@ -15,15 +15,41 @@
|
||||
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Enviados</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ number_format($enviados) }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-paper-plane fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-paper-plane fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="col-xl-2 col-md-4 mb-4">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Entregados</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ number_format($entregados) }}</div>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-check-double fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-2 col-md-4 mb-4">
|
||||
<div class="card border-left-info shadow h-100 py-2" style="border-left-color:#00b2ff!important">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color:#00b2ff">Leídos</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ number_format($leidos) }}</div>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-eye fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-2 col-md-4 mb-4">
|
||||
<div class="card border-left-info shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
@@ -31,15 +57,13 @@
|
||||
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Recibidos</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ number_format($recibidos) }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-inbox fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-inbox fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="col-xl-2 col-md-4 mb-4">
|
||||
<div class="card border-left-danger shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
@@ -47,15 +71,13 @@
|
||||
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Fallidos</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ number_format($fallidos) }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-exclamation-triangle fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="col-xl-2 col-md-4 mb-4">
|
||||
<div class="card border-left-warning shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
@@ -63,15 +85,48 @@
|
||||
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Opt-Outs</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ number_format($optOuts) }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-ban fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-ban fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($enviados > 0)
|
||||
{{-- Barra de progreso del embudo de entrega --}}
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
<i class="fas fa-funnel-dollar mr-1"></i> Embudo de entrega
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-1 d-flex justify-content-between small font-weight-bold">
|
||||
<span>Enviados</span><span>{{ $enviados }}</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height:20px">
|
||||
<div class="progress-bar bg-success" style="width:100%">100%</div>
|
||||
</div>
|
||||
@if($entregados > 0)
|
||||
<div class="mb-1 d-flex justify-content-between small font-weight-bold">
|
||||
<span>Entregados</span><span>{{ $entregados }} ({{ round($entregados/$enviados*100) }}%)</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height:20px">
|
||||
<div class="progress-bar bg-primary" style="width:{{ round($entregados/$enviados*100) }}%">{{ round($entregados/$enviados*100) }}%</div>
|
||||
</div>
|
||||
@endif
|
||||
@if($leidos > 0)
|
||||
<div class="mb-1 d-flex justify-content-between small font-weight-bold">
|
||||
<span>Leídos</span><span>{{ $leidos }} ({{ round($leidos/$enviados*100) }}%)</span>
|
||||
</div>
|
||||
<div class="progress mb-1" style="height:20px">
|
||||
<div class="progress-bar" style="width:{{ round($leidos/$enviados*100) }}%; background:#00b2ff">{{ round($leidos/$enviados*100) }}%</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Filtros --}}
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex align-items-center justify-content-between">
|
||||
@@ -106,6 +161,15 @@
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group mr-3 mb-2">
|
||||
<label class="mr-2 font-weight-bold">Tipo</label>
|
||||
<select name="tipo" class="form-control form-control-sm">
|
||||
<option value="">Todos</option>
|
||||
@foreach(['enviado','recibido','error','opt_out','status_sent','status_delivered','status_read','status_failed'] as $t)
|
||||
<option value="{{ $t }}" {{ request('tipo') === $t ? 'selected' : '' }}>{{ $t }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group mr-3 mb-2">
|
||||
<label class="mr-2 font-weight-bold">Desde</label>
|
||||
<input type="date" name="fecha_desde" class="form-control form-control-sm"
|
||||
@@ -128,10 +192,11 @@
|
||||
|
||||
{{-- Logs recientes --}}
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<div class="card-header py-3 d-flex align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
<i class="fas fa-list mr-1"></i> Logs recientes
|
||||
</h6>
|
||||
<span class="small text-muted">Haz clic en <i class="fas fa-code"></i> para ver el payload raw del webhook</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
@@ -141,29 +206,34 @@
|
||||
<th style="width:50px">#</th>
|
||||
<th>Prospecto</th>
|
||||
<th style="width:110px">Teléfono</th>
|
||||
<th style="width:90px" class="text-center">Tipo</th>
|
||||
<th style="width:120px" class="text-center">Tipo</th>
|
||||
<th>Mensaje</th>
|
||||
<th style="width:80px">Media</th>
|
||||
<th style="width:140px">Fecha</th>
|
||||
<th style="width:50px" class="text-center">JSON</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($logsRecientes as $log)
|
||||
@php
|
||||
$user = $log->alumno?->alumnos->first();
|
||||
$badge = match($log->tipo) {
|
||||
'enviado' => 'success',
|
||||
'recibido' => 'info',
|
||||
'opt_out' => 'warning',
|
||||
'error' => 'danger',
|
||||
default => 'secondary',
|
||||
$badge = match(true) {
|
||||
$log->tipo === 'enviado' => 'success',
|
||||
$log->tipo === 'recibido' => 'info',
|
||||
$log->tipo === 'opt_out' => 'warning',
|
||||
$log->tipo === 'error' => 'danger',
|
||||
$log->tipo === 'status_delivered' => 'primary',
|
||||
$log->tipo === 'status_read' => 'info',
|
||||
$log->tipo === 'status_sent' => 'secondary',
|
||||
$log->tipo === 'status_failed' => 'danger',
|
||||
default => 'secondary',
|
||||
};
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $log->id }}</td>
|
||||
<td>
|
||||
@if($user)
|
||||
{{ $user->name }} {{ $user->apellidoPaterno }}
|
||||
{{ $user->name }} {{ $user->apellidoPaterno ?? '' }}
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
@@ -183,10 +253,22 @@
|
||||
@endif
|
||||
</td>
|
||||
<td class="small">{{ $log->created_at?->format('d/m/Y H:i') }}</td>
|
||||
<td class="text-center">
|
||||
@if($log->payload)
|
||||
<button type="button" class="btn btn-xs btn-outline-secondary btn-payload"
|
||||
data-id="{{ $log->id }}"
|
||||
data-payload="{{ htmlspecialchars(json_encode($log->payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), ENT_QUOTES) }}"
|
||||
title="Ver payload">
|
||||
<i class="fas fa-code"></i>
|
||||
</button>
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-3">Sin registros.</td>
|
||||
<td colspan="8" class="text-center text-muted py-3">Sin registros.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
@@ -199,4 +281,55 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Modal inspector de payload --}}
|
||||
<div class="modal fade" id="payloadModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-code mr-1"></i> Payload del webhook
|
||||
<small class="text-muted ml-2" id="payloadLogId"></small>
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div class="d-flex justify-content-end p-2 border-bottom bg-light">
|
||||
<button type="button" class="btn btn-xs btn-outline-secondary" id="btnCopyPayload">
|
||||
<i class="fas fa-copy mr-1"></i> Copiar
|
||||
</button>
|
||||
</div>
|
||||
<pre id="payloadContent" class="m-0 p-3" style="background:#1e1e1e;color:#d4d4d4;font-size:12px;max-height:500px;overflow:auto;border-radius:0 0 4px 4px"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.querySelectorAll('.btn-payload').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var payload = this.getAttribute('data-payload');
|
||||
var id = this.getAttribute('data-id');
|
||||
document.getElementById('payloadLogId').textContent = '#' + id;
|
||||
document.getElementById('payloadContent').textContent = payload;
|
||||
$('#payloadModal').modal('show');
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('btnCopyPayload').addEventListener('click', function() {
|
||||
var text = document.getElementById('payloadContent').textContent;
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
var btn = document.getElementById('btnCopyPayload');
|
||||
btn.innerHTML = '<i class="fas fa-check mr-1"></i> Copiado';
|
||||
setTimeout(function() {
|
||||
btn.innerHTML = '<i class="fas fa-copy mr-1"></i> Copiar';
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@endsection
|
||||
|
||||
Reference in New Issue
Block a user