up
This commit is contained in:
@@ -88,6 +88,22 @@ func GetContratosProximosVencer(diasAntes int) ([]Contrato, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetContratosYaVencidos retorna contratos cuya fecha de vencimiento es hoy o anterior
|
||||||
|
// y su estado sigue siendo 'activo' (aún no han sido marcados como vencidos)
|
||||||
|
func GetContratosYaVencidos() ([]Contrato, error) {
|
||||||
|
var items []Contrato
|
||||||
|
hoy := time.Now().UTC()
|
||||||
|
startOfDay := time.Date(hoy.Year(), hoy.Month(), hoy.Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
if err := app.Http.Database.DB.Preload("Cliente").Preload("Servicios").
|
||||||
|
Where("estado = 'activo' AND fecha_vencimiento < ?", startOfDay).
|
||||||
|
Find(&items).Error; err != nil {
|
||||||
|
log.Printf("Error getting contratos vencidos: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
// syncContratoServicios sincroniza la tabla join contrato_servicios usando SQL directo
|
// syncContratoServicios sincroniza la tabla join contrato_servicios usando SQL directo
|
||||||
// para evitar que GORM intente hacer upsert de los servicios existentes.
|
// para evitar que GORM intente hacer upsert de los servicios existentes.
|
||||||
func syncContratoServicios(contratoID uint, servicioIDs []uint) error {
|
func syncContratoServicios(contratoID uint, servicioIDs []uint) error {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
type NotificacionRegla struct {
|
type NotificacionRegla struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Nombre string `json:"nombre" gorm:"column:nombre"`
|
Nombre string `json:"nombre" gorm:"column:nombre"`
|
||||||
|
TipoEvento string `json:"tipo_evento" gorm:"column:tipo_evento;default:'vencimiento_proximo'"` // vencimiento_proximo | ya_vencido | bienvenida | pago_recibido | manual
|
||||||
DiasAntes int `json:"dias_antes" gorm:"column:dias_antes"`
|
DiasAntes int `json:"dias_antes" gorm:"column:dias_antes"`
|
||||||
PlantillaID uint `json:"plantilla_id" gorm:"column:plantilla_id"`
|
PlantillaID uint `json:"plantilla_id" gorm:"column:plantilla_id"`
|
||||||
Plantilla PlantillaCorreo `json:"plantilla" gorm:"foreignKey:PlantillaID"`
|
Plantilla PlantillaCorreo `json:"plantilla" gorm:"foreignKey:PlantillaID"`
|
||||||
@@ -35,6 +36,16 @@ func GetReglasActivas() ([]NotificacionRegla, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetReglasByTipoEvento(tipoEvento string) ([]NotificacionRegla, error) {
|
||||||
|
var items []NotificacionRegla
|
||||||
|
if err := app.Http.Database.DB.Preload("Plantilla").
|
||||||
|
Where("activo = ? AND tipo_evento = ?", true, tipoEvento).
|
||||||
|
Order("dias_antes DESC").Find(&items).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetReglaByID(id uint) (*NotificacionRegla, error) {
|
func GetReglaByID(id uint) (*NotificacionRegla, error) {
|
||||||
var item NotificacionRegla
|
var item NotificacionRegla
|
||||||
if err := app.Http.Database.DB.Preload("Plantilla").First(&item, id).Error; err != nil {
|
if err := app.Http.Database.DB.Preload("Plantilla").First(&item, id).Error; err != nil {
|
||||||
@@ -50,6 +61,7 @@ func CreateRegla(r NotificacionRegla) error {
|
|||||||
func UpdateRegla(r NotificacionRegla) error {
|
func UpdateRegla(r NotificacionRegla) error {
|
||||||
return app.Http.Database.DB.Model(&r).Updates(map[string]interface{}{
|
return app.Http.Database.DB.Model(&r).Updates(map[string]interface{}{
|
||||||
"nombre": r.Nombre,
|
"nombre": r.Nombre,
|
||||||
|
"tipo_evento": r.TipoEvento,
|
||||||
"dias_antes": r.DiasAntes,
|
"dias_antes": r.DiasAntes,
|
||||||
"plantilla_id": r.PlantillaID,
|
"plantilla_id": r.PlantillaID,
|
||||||
"activo": r.Activo,
|
"activo": r.Activo,
|
||||||
|
|||||||
+100
-61
@@ -13,15 +13,20 @@ var cronScheduler *cron.Cron
|
|||||||
func IniciarCron() {
|
func IniciarCron() {
|
||||||
cronScheduler = cron.New()
|
cronScheduler = cron.New()
|
||||||
|
|
||||||
// Ejecutar todos los días a las 8:00 AM
|
// Vencimientos próximos — todos los días a las 8:00 AM
|
||||||
_, err := cronScheduler.AddFunc("0 8 * * *", ProcesarVencimientos)
|
if _, err := cronScheduler.AddFunc("0 8 * * *", ProcesarVencimientosProximos); err != nil {
|
||||||
if err != nil {
|
log.Printf("[CRON] Error registrando tarea vencimientos_proximo: %v", err)
|
||||||
log.Printf("[CRON] Error registrando tarea: %v", err)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contratos ya vencidos — todos los días a las 9:00 AM
|
||||||
|
if _, err := cronScheduler.AddFunc("0 9 * * *", ProcesarYaVencidos); err != nil {
|
||||||
|
log.Printf("[CRON] Error registrando tarea ya_vencido: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cronScheduler.Start()
|
cronScheduler.Start()
|
||||||
log.Println("[CRON] Scheduler iniciado — verificando vencimientos diariamente a las 8:00 AM")
|
log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM")
|
||||||
}
|
}
|
||||||
|
|
||||||
// DetenerCron para graceful shutdown
|
// DetenerCron para graceful shutdown
|
||||||
@@ -31,13 +36,19 @@ func DetenerCron() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcesarVencimientos es la función principal del cron
|
// ProcesarVencimientos mantiene compatibilidad para llamadas manuales
|
||||||
func ProcesarVencimientos() {
|
func ProcesarVencimientos() {
|
||||||
log.Println("[CRON] Iniciando procesamiento de vencimientos...")
|
ProcesarVencimientosProximos()
|
||||||
|
ProcesarYaVencidos()
|
||||||
|
}
|
||||||
|
|
||||||
reglas, err := models.GetReglasActivas()
|
// ProcesarVencimientosProximos procesa reglas de tipo "vencimiento_proximo"
|
||||||
|
func ProcesarVencimientosProximos() {
|
||||||
|
log.Println("[CRON] Procesando vencimientos próximos...")
|
||||||
|
|
||||||
|
reglas, err := models.GetReglasByTipoEvento("vencimiento_proximo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[CRON] Error obteniendo reglas: %v", err)
|
log.Printf("[CRON] Error obteniendo reglas vencimiento_proximo: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,58 +58,86 @@ func ProcesarVencimientos() {
|
|||||||
log.Printf("[CRON] Error obteniendo contratos para regla %d: %v", regla.ID, err)
|
log.Printf("[CRON] Error obteniendo contratos para regla %d: %v", regla.ID, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(contratos) == 0 {
|
procesarContratos(®la, contratos)
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrar por AplicaA
|
|
||||||
var filtrados []models.Contrato
|
|
||||||
for _, c := range contratos {
|
|
||||||
switch regla.AplicaA {
|
|
||||||
case "renovable":
|
|
||||||
for _, s := range c.Servicios {
|
|
||||||
if s.Tipo == "renovable" {
|
|
||||||
filtrados = append(filtrados, c)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "unico":
|
|
||||||
for _, s := range c.Servicios {
|
|
||||||
if s.Tipo == "unico" {
|
|
||||||
filtrados = append(filtrados, c)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
filtrados = append(filtrados, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(filtrados) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Agrupar contratos por cliente
|
|
||||||
porCliente := make(map[uint][]models.Contrato)
|
|
||||||
for _, c := range filtrados {
|
|
||||||
porCliente[c.ClienteID] = append(porCliente[c.ClienteID], c)
|
|
||||||
}
|
|
||||||
|
|
||||||
for clienteID, grupoContratos := range porCliente {
|
|
||||||
// Evitar duplicados: ya enviado hoy para esta regla + cliente
|
|
||||||
if models.YaEnviadoHoy(clienteID, regla.ID) {
|
|
||||||
log.Printf("[CRON] Ya enviado hoy a cliente %d para regla %d — saltando", clienteID, regla.ID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cliente := &grupoContratos[0].Cliente
|
|
||||||
if err := EnviarNotificacionGrupo(®la, cliente, grupoContratos); err != nil {
|
|
||||||
log.Printf("[CRON] Error enviando a cliente %d: %v", clienteID, err)
|
|
||||||
} else {
|
|
||||||
log.Printf("[CRON] Enviado a cliente %d (%s) — %d contrato(s)",
|
|
||||||
clienteID, cliente.Email, len(grupoContratos))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("[CRON] Procesamiento de vencimientos finalizado")
|
log.Println("[CRON] Vencimientos próximos finalizado")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcesarYaVencidos procesa reglas de tipo "ya_vencido"
|
||||||
|
func ProcesarYaVencidos() {
|
||||||
|
log.Println("[CRON] Procesando contratos ya vencidos...")
|
||||||
|
|
||||||
|
reglas, err := models.GetReglasByTipoEvento("ya_vencido")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[CRON] Error obteniendo reglas ya_vencido: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(reglas) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contratos, err := models.GetContratosYaVencidos()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[CRON] Error obteniendo contratos vencidos: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, regla := range reglas {
|
||||||
|
procesarContratos(®la, contratos)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[CRON] Ya vencidos finalizado")
|
||||||
|
}
|
||||||
|
|
||||||
|
// procesarContratos aplica filtros y envía notificaciones para una regla y lista de contratos
|
||||||
|
func procesarContratos(regla *models.NotificacionRegla, contratos []models.Contrato) {
|
||||||
|
if len(contratos) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrar por AplicaA
|
||||||
|
var filtrados []models.Contrato
|
||||||
|
for _, c := range contratos {
|
||||||
|
switch regla.AplicaA {
|
||||||
|
case "renovable":
|
||||||
|
for _, s := range c.Servicios {
|
||||||
|
if s.Tipo == "renovable" {
|
||||||
|
filtrados = append(filtrados, c)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "unico":
|
||||||
|
for _, s := range c.Servicios {
|
||||||
|
if s.Tipo == "unico" {
|
||||||
|
filtrados = append(filtrados, c)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
filtrados = append(filtrados, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filtrados) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agrupar por cliente
|
||||||
|
porCliente := make(map[uint][]models.Contrato)
|
||||||
|
for _, c := range filtrados {
|
||||||
|
porCliente[c.ClienteID] = append(porCliente[c.ClienteID], c)
|
||||||
|
}
|
||||||
|
|
||||||
|
for clienteID, grupoContratos := range porCliente {
|
||||||
|
if models.YaEnviadoHoy(clienteID, regla.ID) {
|
||||||
|
log.Printf("[CRON] Ya enviado hoy a cliente %d para regla %d — saltando", clienteID, regla.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cliente := &grupoContratos[0].Cliente
|
||||||
|
if err := EnviarNotificacionGrupo(regla, cliente, grupoContratos); err != nil {
|
||||||
|
log.Printf("[CRON] Error enviando a cliente %d: %v", clienteID, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("[CRON] Enviado a cliente %d (%s) — %d contrato(s)", clienteID, cliente.Email, len(grupoContratos))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,11 +80,13 @@ func EnviarCorreoPrueba(email string, p *models.PlantillaCorreo) error {
|
|||||||
func EnviarCorreoManual(contrato *models.Contrato) error {
|
func EnviarCorreoManual(contrato *models.Contrato) error {
|
||||||
// Intentar obtener plantilla y pasarela desde la primera regla activa
|
// Intentar obtener plantilla y pasarela desde la primera regla activa
|
||||||
var p *models.PlantillaCorreo
|
var p *models.PlantillaCorreo
|
||||||
|
var reglaID uint
|
||||||
gateway := "bold"
|
gateway := "bold"
|
||||||
|
|
||||||
reglas, err := models.GetReglasActivas()
|
reglas, err := models.GetReglasActivas()
|
||||||
if err == nil && len(reglas) > 0 {
|
if err == nil && len(reglas) > 0 {
|
||||||
regla := reglas[0]
|
regla := reglas[0]
|
||||||
|
reglaID = regla.ID
|
||||||
gateway = regla.PasarelaEnlace
|
gateway = regla.PasarelaEnlace
|
||||||
p, err = models.GetPlantillaByID(regla.PlantillaID)
|
p, err = models.GetPlantillaByID(regla.PlantillaID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -107,7 +109,7 @@ func EnviarCorreoManual(contrato *models.Contrato) error {
|
|||||||
// Generar o reutilizar el enlace de pago
|
// Generar o reutilizar el enlace de pago
|
||||||
enlacePago := ObtenerOCrearEnlacePago(contrato, gateway)
|
enlacePago := ObtenerOCrearEnlacePago(contrato, gateway)
|
||||||
|
|
||||||
dias := int(contrato.FechaVencimiento.Sub(time.Now()).Hours() / 24)
|
dias := int(time.Until(contrato.FechaVencimiento).Hours() / 24)
|
||||||
var items []ItemServicio
|
var items []ItemServicio
|
||||||
for _, s := range contrato.Servicios {
|
for _, s := range contrato.Servicios {
|
||||||
items = append(items, ItemServicio{
|
items = append(items, ItemServicio{
|
||||||
@@ -131,7 +133,34 @@ func EnviarCorreoManual(contrato *models.Contrato) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return app.Http.Mail.Send(contrato.Cliente.Email, p.Asunto, html)
|
|
||||||
|
// Crear log previo (pendiente)
|
||||||
|
idsJSON, _ := json.Marshal([]uint{contrato.ID})
|
||||||
|
logEntry := models.NotificacionLog{
|
||||||
|
ClienteID: contrato.ClienteID,
|
||||||
|
ReglaID: reglaID,
|
||||||
|
ContratosIDs: string(idsJSON),
|
||||||
|
FechaEnvio: time.Now(),
|
||||||
|
Estado: "pendiente",
|
||||||
|
Asunto: p.Asunto,
|
||||||
|
PreviewHTML: html,
|
||||||
|
}
|
||||||
|
savedLog, _ := models.CreateNotificacionLog(logEntry)
|
||||||
|
|
||||||
|
sendErr := app.Http.Mail.Send(contrato.Cliente.Email, p.Asunto, html)
|
||||||
|
|
||||||
|
// Actualizar estado del log
|
||||||
|
if savedLog != nil {
|
||||||
|
if sendErr != nil {
|
||||||
|
savedLog.Estado = "fallido"
|
||||||
|
savedLog.ErrorMsg = sendErr.Error()
|
||||||
|
} else {
|
||||||
|
savedLog.Estado = "enviado"
|
||||||
|
}
|
||||||
|
models.UpdateNotificacionLog(*savedLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnviarNotificacionGrupo envía un correo agrupado para un cliente con múltiples contratos
|
// EnviarNotificacionGrupo envía un correo agrupado para un cliente con múltiples contratos
|
||||||
@@ -160,7 +189,7 @@ func EnviarNotificacionGrupo(regla *models.NotificacionRegla, cliente *models.Cl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dias := int(fechaVenc.Sub(time.Now()).Hours() / 24)
|
dias := int(time.Until(fechaVenc).Hours() / 24)
|
||||||
|
|
||||||
// Generar o reutilizar el enlace de pago para el primer contrato del grupo
|
// Generar o reutilizar el enlace de pago para el primer contrato del grupo
|
||||||
enlacePago := ""
|
enlacePago := ""
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
<thead class="text-left border-b border-gray-200 bg-gray-50">
|
<thead class="text-left border-b border-gray-200 bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="py-2 px-3">Nombre</th>
|
<th class="py-2 px-3">Nombre</th>
|
||||||
<th class="py-2 px-3">Días antes</th>
|
<th class="py-2 px-3">Evento</th>
|
||||||
|
<th class="py-2 px-3">Días</th>
|
||||||
<th class="py-2 px-3">Plantilla</th>
|
<th class="py-2 px-3">Plantilla</th>
|
||||||
<th class="py-2 px-3">Aplica a</th>
|
<th class="py-2 px-3">Aplica a</th>
|
||||||
<th class="py-2 px-3">Pasarela</th>
|
<th class="py-2 px-3">Pasarela</th>
|
||||||
@@ -30,7 +31,19 @@
|
|||||||
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
||||||
<td class="py-2 px-3 font-medium" x-text="d.nombre"></td>
|
<td class="py-2 px-3 font-medium" x-text="d.nombre"></td>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3">
|
||||||
<span class="px-2 py-0.5 bg-orange-100 text-orange-700 rounded text-xs font-semibold" x-text="d.dias_antes+' días'"></span>
|
<span class="px-2 py-0.5 rounded text-xs font-medium"
|
||||||
|
:class="{
|
||||||
|
'bg-orange-100 text-orange-700': d.tipo_evento==='vencimiento_proximo',
|
||||||
|
'bg-red-100 text-red-700': d.tipo_evento==='ya_vencido',
|
||||||
|
'bg-green-100 text-green-700': d.tipo_evento==='bienvenida',
|
||||||
|
'bg-blue-100 text-blue-700': d.tipo_evento==='pago_recibido',
|
||||||
|
'bg-gray-100 text-gray-600': d.tipo_evento==='manual'
|
||||||
|
}"
|
||||||
|
x-text="tipoEventoLabel(d.tipo_evento)"></span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
<span x-show="d.tipo_evento==='vencimiento_proximo'" class="px-2 py-0.5 bg-orange-100 text-orange-700 rounded text-xs font-semibold" x-text="d.dias_antes+' días'"></span>
|
||||||
|
<span x-show="d.tipo_evento!=='vencimiento_proximo'" class="text-gray-300 text-xs">—</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 px-3 text-gray-500" x-text="d.plantilla?.nombre||'—'"></td>
|
<td class="py-2 px-3 text-gray-500" x-text="d.plantilla?.nombre||'—'"></td>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3">
|
||||||
@@ -62,7 +75,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
<tr x-show="!loading && datos.length===0">
|
<tr x-show="!loading && datos.length===0">
|
||||||
<td colspan="6" class="text-center text-gray-400 py-8">Sin reglas configuradas</td>
|
<td colspan="8" class="text-center text-gray-400 py-8">Sin reglas configuradas</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -80,26 +93,43 @@
|
|||||||
<input x-model="form.nombre" required class="mt-1 w-full border rounded px-3 py-2 text-sm" />
|
<input x-model="form.nombre" required class="mt-1 w-full border rounded px-3 py-2 text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<label class="text-xs font-medium text-gray-600">Tipo de evento *</label>
|
||||||
|
<select x-model="form.tipo_evento" required class="mt-1 w-full border rounded px-3 py-2 text-sm">
|
||||||
|
<option value="vencimiento_proximo">⏰ Próximo a vencer (X días antes)</option>
|
||||||
|
<option value="ya_vencido">🔴 Servicio ya vencido</option>
|
||||||
|
<option value="bienvenida">🎉 Bienvenida (al activar contrato)</option>
|
||||||
|
<option value="pago_recibido">✅ Pago recibido (al confirmar pago)</option>
|
||||||
|
<option value="manual">✋ Manual (sin disparo automático)</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">
|
||||||
|
<span x-show="form.tipo_evento==='bienvenida'">Se dispara automáticamente cuando se activa un contrato nuevo.</span>
|
||||||
|
<span x-show="form.tipo_evento==='pago_recibido'">Se dispara automáticamente cuando se confirma un pago (Bold / dLocal).</span>
|
||||||
|
<span x-show="form.tipo_evento==='manual'">Solo se envía desde el historial o de forma programada manualmente.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div x-show="form.tipo_evento==='vencimiento_proximo'">
|
||||||
<label class="text-xs font-medium text-gray-600">Días antes del vencimiento *</label>
|
<label class="text-xs font-medium text-gray-600">Días antes del vencimiento *</label>
|
||||||
<input x-model="form.dias_antes" type="number" min="1" max="365" required class="mt-1 w-full border rounded px-3 py-2 text-sm" />
|
<input x-model="form.dias_antes" type="number" min="1" max="365"
|
||||||
|
:required="form.tipo_evento==='vencimiento_proximo'"
|
||||||
|
class="mt-1 w-full border rounded px-3 py-2 text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs font-medium text-gray-600">Plantilla de correo *</label>
|
<label class="text-xs font-medium text-gray-600">Plantilla de correo *</label>
|
||||||
<select x-model="form.plantilla_id" required class="mt-1 w-full border rounded px-3 py-2 text-sm">
|
<select x-model="form.plantilla_id" required class="mt-1 w-full border rounded px-3 py-2 text-sm">
|
||||||
<option value="">Seleccionar...</option>
|
<option value="">Seleccionar...</option>
|
||||||
<template x-for="p in plantillas" :key="p.ID">
|
<template x-for="p in plantillas" :key="p.ID">
|
||||||
<option :value="p.ID" x-text="p.nombre"></option>
|
<option :value="p.ID" x-text="p.nombre + ' (' + p.tipo + ')'"></option>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div x-show="['vencimiento_proximo','ya_vencido','manual'].includes(form.tipo_evento)">
|
||||||
<label class="text-xs font-medium text-gray-600">Pasarela de pago (enlace)</label>
|
<label class="text-xs font-medium text-gray-600">Pasarela de pago (enlace)</label>
|
||||||
<select x-model="form.pasarela_enlace" class="mt-1 w-full border rounded px-3 py-2 text-sm">
|
<select x-model="form.pasarela_enlace" class="mt-1 w-full border rounded px-3 py-2 text-sm">
|
||||||
<option value="bold">Bold (Colombia)</option>
|
<option value="bold">Bold (Colombia)</option>
|
||||||
<option value="dlocal">dLocal</option>
|
<option value="dlocal">dLocal</option>
|
||||||
<option value="ninguna">Ninguna (sin enlace de pago)</option>
|
<option value="ninguna">Ninguna (sin enlace de pago)</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="text-xs text-gray-400 mt-1">Genera automáticamente <code>{{.EnlacePago}}</code> al enviar</p>
|
<p class="text-xs text-gray-400 mt-1">Genera automáticamente <code>{{"{{"}.EnlacePago{{"}}"}}</code> en la plantilla</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs font-medium text-gray-600">Aplica a</label>
|
<label class="text-xs font-medium text-gray-600">Aplica a</label>
|
||||||
@@ -148,7 +178,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
loading: false, datos: [], plantillas: [],
|
loading: false, datos: [], plantillas: [],
|
||||||
addModal: false, editModal: false, deleteModal: false,
|
addModal: false, editModal: false, deleteModal: false,
|
||||||
selectedId: null,
|
selectedId: null,
|
||||||
form: { nombre:'', dias_antes:30, plantilla_id:'', aplica_a:'todos', pasarela_enlace:'bold', activo:true },
|
form: { nombre:'', tipo_evento:'vencimiento_proximo', dias_antes:30, plantilla_id:'', aplica_a:'todos', pasarela_enlace:'bold', activo:true },
|
||||||
toast: { show:false, msg:'', type:'ok' },
|
toast: { show:false, msg:'', type:'ok' },
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -166,7 +196,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openEdit(d) {
|
openEdit(d) {
|
||||||
this.form = { nombre: d.nombre, dias_antes: d.dias_antes, plantilla_id: d.plantilla_id, aplica_a: d.aplica_a, pasarela_enlace: d.pasarela_enlace||'bold', activo: d.activo };
|
this.form = { nombre: d.nombre, tipo_evento: d.tipo_evento||'vencimiento_proximo', dias_antes: d.dias_antes, plantilla_id: d.plantilla_id, aplica_a: d.aplica_a, pasarela_enlace: d.pasarela_enlace||'bold', activo: d.activo };
|
||||||
this.selectedId = d.ID;
|
this.selectedId = d.ID;
|
||||||
this.editModal = true;
|
this.editModal = true;
|
||||||
},
|
},
|
||||||
@@ -174,7 +204,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
closeModals() {
|
closeModals() {
|
||||||
this.addModal = this.editModal = this.deleteModal = false;
|
this.addModal = this.editModal = this.deleteModal = false;
|
||||||
this.selectedId = null;
|
this.selectedId = null;
|
||||||
this.form = { nombre:'', dias_antes:30, plantilla_id:'', aplica_a:'todos', pasarela_enlace:'bold', activo:true };
|
this.form = { nombre:'', tipo_evento:'vencimiento_proximo', dias_antes:30, plantilla_id:'', aplica_a:'todos', pasarela_enlace:'bold', activo:true };
|
||||||
},
|
},
|
||||||
|
|
||||||
async toggleActivo(d) {
|
async toggleActivo(d) {
|
||||||
@@ -189,8 +219,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
...this.form,
|
...this.form,
|
||||||
dias_antes: parseInt(this.form.dias_antes) || 0,
|
dias_antes: parseInt(this.form.dias_antes) || 0,
|
||||||
plantilla_id: parseInt(this.form.plantilla_id) || 0,
|
plantilla_id: parseInt(this.form.plantilla_id) || 0,
|
||||||
};
|
}; if (this.editModal) {
|
||||||
if (this.editModal) {
|
|
||||||
await axios.put(`/app/api/reglas-notificacion/${this.selectedId}`, payload);
|
await axios.put(`/app/api/reglas-notificacion/${this.selectedId}`, payload);
|
||||||
} else {
|
} else {
|
||||||
await axios.post('/app/api/reglas-notificacion', payload);
|
await axios.post('/app/api/reglas-notificacion', payload);
|
||||||
@@ -216,6 +245,17 @@ document.addEventListener('alpine:init', () => {
|
|||||||
showToast(msg, type='ok') {
|
showToast(msg, type='ok') {
|
||||||
this.toast = { show:true, msg, type };
|
this.toast = { show:true, msg, type };
|
||||||
setTimeout(() => this.toast.show = false, 3000);
|
setTimeout(() => this.toast.show = false, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
tipoEventoLabel(tipo) {
|
||||||
|
const map = {
|
||||||
|
'vencimiento_proximo': 'Próximo a vencer',
|
||||||
|
'ya_vencido': 'Ya vencido',
|
||||||
|
'bienvenida': 'Bienvenida',
|
||||||
|
'pago_recibido': 'Pago recibido',
|
||||||
|
'manual': 'Manual',
|
||||||
|
};
|
||||||
|
return map[tipo] || tipo;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user