up
This commit is contained in:
@@ -65,6 +65,13 @@
|
||||
'bg-blue-100 text-blue-700': d.estado==='renovado'
|
||||
}"
|
||||
x-text="d.estado"></span>
|
||||
<!-- Indicador de pago -->
|
||||
<template x-if="d.pago_confirmado">
|
||||
<span class="ml-1 px-1.5 py-0.5 rounded text-xs bg-green-100 text-green-700" title="Pago confirmado">✓ Pagado</span>
|
||||
</template>
|
||||
<template x-if="d.enlace_pago && !d.pago_confirmado">
|
||||
<span class="ml-1 px-1.5 py-0.5 rounded text-xs bg-yellow-100 text-yellow-700" title="Enlace de pago activo">⏳ Pendiente</span>
|
||||
</template>
|
||||
</td>
|
||||
<td class="py-2 px-3">
|
||||
<div class="flex gap-1.5">
|
||||
@@ -77,6 +84,14 @@
|
||||
<button @click="enviarCorreo(d)" title="Enviar correo" class="text-gray-400 hover:text-purple-500">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"/></svg>
|
||||
</button>
|
||||
<!-- Abrir enlace de pago (solo si tiene enlace pendiente) -->
|
||||
<a x-show="d.enlace_pago && !d.pago_confirmado" :href="d.enlace_pago" target="_blank" rel="noopener" title="Abrir enlace de pago" class="text-gray-400 hover:text-orange-500">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"/></svg>
|
||||
</a>
|
||||
<!-- Verificar pago manualmente (solo si tiene enlace pendiente) -->
|
||||
<button x-show="d.enlace_pago_link_id && !d.pago_confirmado" @click="verificarPago(d)" :disabled="verificandoID===d.ID" title="Verificar pago en Bold" class="text-gray-400 hover:text-teal-500 disabled:opacity-40">
|
||||
<svg class="w-4 h-4" :class="verificandoID===d.ID ? 'animate-spin' : ''" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"/></svg>
|
||||
</button>
|
||||
<button @click="openHistorial(d)" title="Ver historial" class="text-gray-400 hover:text-indigo-500">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg>
|
||||
</button>
|
||||
@@ -326,6 +341,7 @@ document.addEventListener('alpine:init', () => {
|
||||
historialModal: false, historialTitulo: '', historialTimeline: [], historialLoading: false,
|
||||
clientes: [], servicios: [], reglas: [], reglasBienvenida: [],
|
||||
selectedId: null,
|
||||
verificandoID: null,
|
||||
form: { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, estado:'activo', auto_renovar:false, notas:'' },
|
||||
toast: { show: false, msg: '', type: 'ok' },
|
||||
|
||||
@@ -444,6 +460,23 @@ document.addEventListener('alpine:init', () => {
|
||||
} catch(e) { this.showToast(e.response?.data?.error||'Error', 'error'); }
|
||||
},
|
||||
|
||||
async verificarPago(d) {
|
||||
this.verificandoID = d.ID;
|
||||
try {
|
||||
const { data } = await axios.post(`/app/api/contratos/${d.ID}/verificar-pago`);
|
||||
if (data.confirmado) {
|
||||
this.showToast(data.mensaje || '¡Pago confirmado!', 'success');
|
||||
await this.loadData();
|
||||
} else {
|
||||
this.showToast(data.mensaje || 'Pago aún pendiente', 'info');
|
||||
}
|
||||
} catch(e) {
|
||||
this.showToast(e.response?.data?.error || 'Error al verificar', 'error');
|
||||
} finally {
|
||||
this.verificandoID = null;
|
||||
}
|
||||
},
|
||||
|
||||
enviarCorreo(d) {
|
||||
this.notifContratoId = d.ID;
|
||||
this.notifCliente = (d.cliente?.nombre || '') + (d.cliente?.email ? ' <'+d.cliente.email+'>' : '');
|
||||
|
||||
@@ -373,3 +373,57 @@ func calcularFechaVencimiento(desde time.Time, periodicidad string) time.Time {
|
||||
return desde.AddDate(1, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// VerificarPagoBold consulta la API de Bold para un contrato específico y confirma el pago si está pagado.
|
||||
// POST /api/contratos/:id/verificar-pago
|
||||
func VerificarPagoBold(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||
}
|
||||
|
||||
contrato, err := models.GetContratoParaVerificacion(uint(id))
|
||||
if err != nil {
|
||||
return c.Status(404).JSON(fiber.Map{"error": "Contrato no encontrado"})
|
||||
}
|
||||
if contrato.PagoConfirmado {
|
||||
return c.JSON(fiber.Map{"ok": true, "confirmado": true, "mensaje": "El pago ya estaba confirmado"})
|
||||
}
|
||||
if contrato.EnlacePagoLinkID == "" {
|
||||
return c.JSON(fiber.Map{"ok": false, "confirmado": false, "mensaje": "Este contrato no tiene enlace de pago generado"})
|
||||
}
|
||||
|
||||
boldCfg, err := models.GetBoldConfig()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración Bold activa"})
|
||||
}
|
||||
|
||||
paid, paymentID, monto, err := services.CheckBoldLinkPaid(boldCfg, contrato.EnlacePagoLinkID)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Error consultando Bold: " + err.Error()})
|
||||
}
|
||||
if !paid {
|
||||
return c.JSON(fiber.Map{"ok": true, "confirmado": false, "mensaje": "El enlace aún no ha sido pagado"})
|
||||
}
|
||||
|
||||
ref := "contrato-" + strconv.FormatUint(id, 10)
|
||||
if ok, _ := models.MarcarContratoPagado(uint(id)); ok {
|
||||
go services.EnviarCorreoConfirmacionPago(uint(id), "bold")
|
||||
}
|
||||
go models.EnrichBoldCallbackLog(ref, contrato.Cliente.Email, monto)
|
||||
|
||||
notifID := "api-check-" + paymentID
|
||||
if paymentID != "" && !models.IsBoldNotificationDuplicate(notifID) {
|
||||
_ = models.SaveBoldWebhookLog(models.BoldWebhookLog{
|
||||
NotificationID: notifID,
|
||||
Tipo: "API_CHECK",
|
||||
PaymentID: paymentID,
|
||||
Referencia: ref,
|
||||
PayerEmail: contrato.Cliente.Email,
|
||||
Monto: monto,
|
||||
Procesado: true,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"ok": true, "confirmado": true, "monto": monto, "mensaje": "¡Pago confirmado!"})
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ func RenovacionesRoutes(protected fiber.Router) {
|
||||
protected.Post("/api/contratos/:id/renovar", controllers.RenovarContrato)
|
||||
protected.Post("/api/contratos/:id/enviar-correo", controllers.EnviarCorreoContrato)
|
||||
protected.Get("/api/contratos/:id/historial", controllers.GetHistorialContrato)
|
||||
protected.Post("/api/contratos/:id/verificar-pago", controllers.VerificarPagoBold)
|
||||
|
||||
// ─── Plantillas de correo ─────────────────────────────────────────
|
||||
protected.Get("/plantillas-correo", middlewares.MenuMiddleware, controllers.PlantillasView)
|
||||
|
||||
Reference in New Issue
Block a user