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:
+
+
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(`
+
+`, 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() {