diff --git a/migrations/migrate.go b/migrations/migrate.go index 0bd28bc..0ac06ab 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -872,9 +872,34 @@ func SeedPortalClientes() { log.Println("[SEED] Seed de Portal de Clientes completado.") } -// SeedNotifDefaults es un stub idempotente para configuraciones de notificaciones por defecto. +// SeedNotifDefaults inserta configuraciones de notificación por defecto (idempotente). +// Solo crea registros que no existen; no sobreescribe cambios del admin. func SeedNotifDefaults() { - log.Println("[SEED] SeedNotifDefaults: sin entradas por defecto definidas aún.") + db := app.Http.Database.DB + defaults := []models.NotifEventoConfig{ + // Tickets + {Evento: "ticket_nuevo", Destinatario: "admin", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Admin recibe cuando el cliente abre un ticket"}, + {Evento: "ticket_respuesta_cliente", Destinatario: "admin", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Admin recibe cuando el cliente responde un ticket"}, + {Evento: "ticket_respuesta_admin", Destinatario: "portal_user", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Portal user recibe cuando el admin responde"}, + {Evento: "factura_subida", Destinatario: "portal_user", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Portal user recibe cuando se sube una factura"}, + // Servidores + {Evento: "servidor_caido", Destinatario: "admin", CanalEmail: false, CanalTelegram: true, CanalSistema: true, Descripcion: "Admin recibe cuando un servidor deja de reportar"}, + {Evento: "servidor_vence_pronto", Destinatario: "admin", CanalEmail: true, CanalTelegram: true, CanalSistema: true, Descripcion: "Admin recibe cuando un VPS está próximo a vencer"}, + {Evento: "servidor_recurso_alto", Destinatario: "admin", CanalEmail: false, CanalTelegram: true, CanalSistema: true, Descripcion: "Admin recibe cuando CPU/RAM/Disco supera el umbral"}, + // Tareas + {Evento: "tarea_asignada", Destinatario: "admin", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Notifica cuando se asigna una tarea a un usuario"}, + {Evento: "tarea_estado", Destinatario: "admin", CanalEmail: false, CanalTelegram: false, CanalSistema: true, Descripcion: "Notifica cuando cambia el estado de una tarea"}, + {Evento: "tarea_comentario", Destinatario: "admin", CanalEmail: false, CanalTelegram: false, CanalSistema: true, Descripcion: "Notifica cuando hay un nuevo comentario en una tarea"}, + } + created := 0 + for _, d := range defaults { + var existing models.NotifEventoConfig + if err := db.Where("evento = ? AND destinatario = ?", d.Evento, d.Destinatario).First(&existing).Error; err != nil { + db.Create(&d) + created++ + } + } + log.Printf("[SEED] SeedNotifDefaults: %d nuevas configuraciones creadas.", created) } // SeedShield es un stub idempotente para la configuración de Shield. diff --git a/pkg/services/mail_service.go b/pkg/services/mail_service.go index 83ffc5f..51140ba 100755 --- a/pkg/services/mail_service.go +++ b/pkg/services/mail_service.go @@ -220,6 +220,50 @@ func SendTicketRespuestaAdmin(adminEmail, proyectoNombre, autorNombre, titulo, r }() } +// SendTareaAsignadaEmail notifica al usuario asignado sobre una nueva tarea. +func SendTareaAsignadaEmail(toEmail, toName, tareaTitle, prioridad, descripcion, tareaURL string) { + subject := fmt.Sprintf("📋 Nueva tarea asignada: %s", tareaTitle) + htmlBody := fmt.Sprintf(` + +
+
+
📋
+

Nueva tarea asignada

+

Prioridad: %s

+
+

Hola %s,

+

Se te asignó la siguiente tarea:

+
+

%s

+

%s

+
+ Ver tarea +

© U-site — Panel de administración

+
`, prioridad, toName, tareaTitle, descripcion, tareaURL) + go func() { + if err := app.Http.Mail.Send(toEmail, subject, htmlBody, ""); err != nil { + log.Printf("[Notif] Error enviando tarea asignada a %s: %v", toEmail, err) + } + }() +} + +// SendTareaNotifAdmin notifica al admin sobre cambios en tareas (estado, comentarios). +func SendTareaNotifAdmin(adminEmail, titulo, cuerpo, tareaURL string) { + htmlBody := fmt.Sprintf(` + +
+

%s

+

%s

+ Ver tablero de tareas +

© U-site — Panel de administración

+
`, titulo, cuerpo, tareaURL) + go func() { + if err := app.Http.Mail.Send(adminEmail, titulo, htmlBody, ""); err != nil { + log.Printf("[Notif] Error enviando notif tarea a admin: %v", err) + } + }() +} + // SendFacturaSubidaPortalUser notifica a un portal user que tiene una nueva factura disponible. func SendFacturaSubidaPortalUser(toEmail, userName, clienteNombre, numero string, monto float64, moneda, portalURL string) { go func() { diff --git a/pkg/services/notif_dispatch.go b/pkg/services/notif_dispatch.go index afefdb7..d834543 100644 --- a/pkg/services/notif_dispatch.go +++ b/pkg/services/notif_dispatch.go @@ -260,22 +260,54 @@ func NotificarTareaAsignada(t *models.Tarea) { if t == nil || t.AsignadoID == nil { return } + cfg := models.GetNotifConfig("tarea_asignada", "admin") + if cfg == nil { + return + } asignado := "" if t.Asignado != nil { asignado = t.Asignado.Name } - msg := fmt.Sprintf("📋 Nueva tarea asignada\n%s\nAsignado a: %s\nPrioridad: %s\n\n%s", - escapeTelegramHTML(t.Titulo), - escapeTelegramHTML(asignado), - escapeTelegramHTML(t.Prioridad), - escapeTelegramHTML(t.Descripcion)) - sendTelegramAdmin(msg) + titulo := fmt.Sprintf("📋 Nueva tarea asignada: %s", t.Titulo) + cuerpo := fmt.Sprintf("Asignado a: %s | Prioridad: %s\n%s", asignado, t.Prioridad, t.Descripcion) + tareaPath := "/app/tareas" + + if cfg.CanalSistema { + _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ + TipoUsuario: "admin", + UsuarioID: 0, + Titulo: titulo, + Cuerpo: cuerpo, + Url: tareaPath, + Icono: "📋", + }) + } + if cfg.CanalEmail { + tareaURL := absAppURL(tareaPath) + if t.Asignado != nil && t.Asignado.Email != "" { + go SendTareaAsignadaEmail(t.Asignado.Email, t.Asignado.Name, t.Titulo, t.Prioridad, t.Descripcion, tareaURL) + } else if adminEmail := getAdminEmail(); adminEmail != "" { + go SendTareaNotifAdmin(adminEmail, titulo, cuerpo, tareaURL) + } + } + if cfg.CanalTelegram { + msg := fmt.Sprintf("📋 Nueva tarea asignada\n%s\nAsignado a: %s\nPrioridad: %s\n\n%s", + escapeTelegramHTML(t.Titulo), + escapeTelegramHTML(asignado), + escapeTelegramHTML(t.Prioridad), + escapeTelegramHTML(t.Descripcion)) + sendTelegramAdmin(msg) + } } func NotificarTareaEstado(t *models.Tarea) { if t == nil { return } + cfg := models.GetNotifConfig("tarea_estado", "admin") + if cfg == nil { + return + } etiquetas := map[string]string{ "por_hacer": "📥 Por hacer", "en_progreso": "🔄 En progreso", @@ -287,24 +319,71 @@ func NotificarTareaEstado(t *models.Tarea) { label = t.Estado } asignado := "" + asignadoSuffix := "" if t.Asignado != nil { - asignado = "\nAsignado: " + escapeTelegramHTML(t.Asignado.Name) + asignado = t.Asignado.Name + asignadoSuffix = "\nAsignado: " + asignado + } + titulo := fmt.Sprintf("🔀 Tarea → %s: %s", label, t.Titulo) + cuerpo := fmt.Sprintf("Estado: %s%s", label, asignadoSuffix) + tareaPath := "/app/tareas" + + if cfg.CanalSistema { + _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ + TipoUsuario: "admin", + UsuarioID: 0, + Titulo: titulo, + Cuerpo: cuerpo, + Url: tareaPath, + Icono: "🔀", + }) + } + if cfg.CanalEmail { + if adminEmail := getAdminEmail(); adminEmail != "" { + go SendTareaNotifAdmin(adminEmail, titulo, cuerpo, absAppURL(tareaPath)) + } + } + if cfg.CanalTelegram { + msg := fmt.Sprintf("🔀 Tarea movida → %s\n%s%s", + label, + escapeTelegramHTML(t.Titulo), + escapeTelegramHTML(asignadoSuffix)) + sendTelegramAdmin(msg) } - msg := fmt.Sprintf("🔀 Tarea movida → %s\n%s%s", - label, - escapeTelegramHTML(t.Titulo), - asignado) - sendTelegramAdmin(msg) } func NotificarTareaComentario(t *models.Tarea, contenido string) { if t == nil { return } - msg := fmt.Sprintf("💬 Nuevo comentario en tarea\n%s\n\n%s", - escapeTelegramHTML(t.Titulo), - escapeTelegramHTML(contenido)) - sendTelegramAdmin(msg) + cfg := models.GetNotifConfig("tarea_comentario", "admin") + if cfg == nil { + return + } + titulo := fmt.Sprintf("💬 Nuevo comentario en tarea: %s", t.Titulo) + tareaPath := "/app/tareas" + + if cfg.CanalSistema { + _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ + TipoUsuario: "admin", + UsuarioID: 0, + Titulo: titulo, + Cuerpo: contenido, + Url: tareaPath, + Icono: "💬", + }) + } + if cfg.CanalEmail { + if adminEmail := getAdminEmail(); adminEmail != "" { + go SendTareaNotifAdmin(adminEmail, titulo, contenido, absAppURL(tareaPath)) + } + } + if cfg.CanalTelegram { + msg := fmt.Sprintf("💬 Nuevo comentario en tarea\n%s\n\n%s", + escapeTelegramHTML(t.Titulo), + escapeTelegramHTML(contenido)) + sendTelegramAdmin(msg) + } } func sendTelegramAdmin(mensaje string) { diff --git a/resources/views/notif_config.html b/resources/views/notif_config.html index 333f111..9d1fc0f 100644 --- a/resources/views/notif_config.html +++ b/resources/views/notif_config.html @@ -185,9 +185,10 @@ const EVENTOS = [ { evento: 'servidor_caido', destinatario: 'admin', label: 'Servidor sin señal (agente caído)', icon: '🔴' }, { evento: 'servidor_vence_pronto', destinatario: 'admin', label: 'VPS próximo a vencer', icon: '📅' }, { evento: 'servidor_recurso_alto', destinatario: 'admin', label: 'Recurso al límite (CPU/RAM/Disco)', icon: '⚠️' }, - // Próximos eventos (deshabilitados por ahora): - // { evento: 'factura_emitida', destinatario: 'portal_user', label: 'Factura emitida', icon: '🧾' }, - // { evento: 'avance_publicado', destinatario: 'portal_user', label: 'Avance publicado', icon: '📦' }, + // Tareas + { evento: 'tarea_asignada', destinatario: 'admin', label: 'Tarea asignada', icon: '📋' }, + { evento: 'tarea_estado', destinatario: 'admin', label: 'Tarea cambio de estado', icon: '🔀' }, + { evento: 'tarea_comentario', destinatario: 'admin', label: 'Comentario en tarea', icon: '💬' }, ]; function notifConfig() {