feat(notif): integrar notificaciones de tareas con NotifEventoConfig
- SeedNotifDefaults: poblar 10 eventos por defecto (tickets, servidores, tareas) - notif_dispatch: NotificarTareaAsignada/Estado/Comentario ahora respetan canal_email, canal_telegram y canal_sistema de la config de notificaciones - mail_service: añadir SendTareaAsignadaEmail y SendTareaNotifAdmin - notif_config.html: agregar eventos tarea_asignada, tarea_estado, tarea_comentario al panel de configuración para que el admin pueda activar/desactivar canales Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
107178aa97
commit
37648724f8
+27
-2
@@ -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.
|
||||
|
||||
@@ -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(`<!DOCTYPE html>
|
||||
<html><body style="font-family:Inter,sans-serif;background:#f1f5f9;padding:32px">
|
||||
<div style="max-width:520px;margin:0 auto;background:#fff;border-radius:16px;padding:32px;border:1px solid #e2e8f0">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
|
||||
<div style="width:40px;height:40px;border-radius:50%%;background:#8eb02f;display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px">📋</div>
|
||||
<div><h2 style="margin:0;color:#1e293b;font-size:17px">Nueva tarea asignada</h2>
|
||||
<p style="margin:0;color:#64748b;font-size:13px">Prioridad: <strong>%s</strong></p></div>
|
||||
</div>
|
||||
<p style="color:#334155;font-size:15px">Hola <strong>%s</strong>,</p>
|
||||
<p style="color:#64748b;font-size:14px">Se te asignó la siguiente tarea:</p>
|
||||
<div style="background:#f8fafc;border-radius:10px;padding:16px;margin:16px 0;border:1px solid #e2e8f0">
|
||||
<p style="margin:0 0 8px;font-size:15px;font-weight:600;color:#1e293b">%s</p>
|
||||
<p style="margin:0;font-size:13px;color:#64748b">%s</p>
|
||||
</div>
|
||||
<a href="%s" style="display:block;text-align:center;background:#8eb02f;color:#fff;padding:12px 24px;border-radius:10px;font-weight:600;text-decoration:none;font-size:15px">Ver tarea</a>
|
||||
<p style="font-size:12px;color:#94a3b8;margin-top:24px;text-align:center">© U-site — Panel de administración</p>
|
||||
</div></body></html>`, 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(`<!DOCTYPE html>
|
||||
<html><body style="font-family:Inter,sans-serif;background:#f1f5f9;padding:32px">
|
||||
<div style="max-width:520px;margin:0 auto;background:#fff;border-radius:16px;padding:32px;border:1px solid #e2e8f0">
|
||||
<h2 style="margin:0 0 12px;color:#1e293b;font-size:17px">%s</h2>
|
||||
<p style="color:#334155;font-size:14px;white-space:pre-line">%s</p>
|
||||
<a href="%s" style="display:block;text-align:center;background:#8eb02f;color:#fff;padding:12px 24px;border-radius:10px;font-weight:600;text-decoration:none;font-size:15px;margin-top:20px">Ver tablero de tareas</a>
|
||||
<p style="font-size:12px;color:#94a3b8;margin-top:24px;text-align:center">© U-site — Panel de administración</p>
|
||||
</div></body></html>`, 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() {
|
||||
|
||||
@@ -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("📋 <b>Nueva tarea asignada</b>\n<b>%s</b>\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("📋 <b>Nueva tarea asignada</b>\n<b>%s</b>\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("🔀 <b>Tarea movida → %s</b>\n<b>%s</b>%s",
|
||||
label,
|
||||
escapeTelegramHTML(t.Titulo),
|
||||
escapeTelegramHTML(asignadoSuffix))
|
||||
sendTelegramAdmin(msg)
|
||||
}
|
||||
msg := fmt.Sprintf("🔀 <b>Tarea movida → %s</b>\n<b>%s</b>%s",
|
||||
label,
|
||||
escapeTelegramHTML(t.Titulo),
|
||||
asignado)
|
||||
sendTelegramAdmin(msg)
|
||||
}
|
||||
|
||||
func NotificarTareaComentario(t *models.Tarea, contenido string) {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf("💬 <b>Nuevo comentario en tarea</b>\n<b>%s</b>\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("💬 <b>Nuevo comentario en tarea</b>\n<b>%s</b>\n\n%s",
|
||||
escapeTelegramHTML(t.Titulo),
|
||||
escapeTelegramHTML(contenido))
|
||||
sendTelegramAdmin(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func sendTelegramAdmin(mensaje string) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user