`, proyectoNombre, autorNombre, titulo, respuesta, ticketURL)
+ go func() {
+ if err := app.Http.Mail.Send(adminEmail, subject, htmlBody, ""); err != nil {
+ log.Printf("[Notif] Error enviando respuesta cliente a admin: %v", err)
+ }
+ }()
+}
diff --git a/pkg/services/notif_dispatch.go b/pkg/services/notif_dispatch.go
new file mode 100644
index 0000000..eb5a732
--- /dev/null
+++ b/pkg/services/notif_dispatch.go
@@ -0,0 +1,162 @@
+package services
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/sujit-baniya/fiber-boilerplate/app"
+ "github.com/sujit-baniya/fiber-boilerplate/pkg/models"
+)
+
+// ─── DispatchTicketNuevo ──────────────────────────────────────────────────────
+// Llamar cuando el portal user crea un nuevo ticket.
+// Notifica al ADMIN según su configuración de canales.
+func DispatchTicketNuevo(ticket *models.ProyectoTicket, portalUser *models.PortalUser, proyectoNombre string) {
+ cfg := models.GetNotifConfig("ticket_nuevo", "admin")
+ if cfg == nil {
+ return
+ }
+ ticketURL := fmt.Sprintf("%s/app/tickets", app.Http.Server.Url)
+ titulo := fmt.Sprintf("Nuevo ticket: %s", ticket.Titulo)
+ cuerpo := fmt.Sprintf("Cliente %s abrió: %s", ticket.AutorNombre, ticket.Titulo)
+
+ if cfg.CanalSistema {
+ _ = models.CreateSistemaNotif(&models.SistemaNotificacion{
+ TipoUsuario: "admin",
+ UsuarioID: 0,
+ Titulo: titulo,
+ Cuerpo: cuerpo,
+ Url: ticketURL,
+ Icono: "🎫",
+ })
+ }
+ if cfg.CanalEmail {
+ if adminEmail := getAdminEmail(); adminEmail != "" {
+ SendTicketNuevoAdmin(adminEmail, proyectoNombre, ticket.AutorNombre, ticket.Titulo, ticket.Descripcion, ticketURL)
+ }
+ }
+ if cfg.CanalTelegram {
+ msg := fmt.Sprintf("🎫 Nuevo ticket\nProyecto: %s\nCliente: %s\nTítulo: %s\n\n%s\n\n🔗 %s",
+ proyectoNombre, ticket.AutorNombre, ticket.Titulo, ticket.Descripcion, ticketURL)
+ sendTelegramAdmin(msg)
+ }
+}
+
+// ─── DispatchTicketRespuestaCliente ──────────────────────────────────────────
+// Llamar cuando el portal user responde un ticket.
+// Notifica al ADMIN según su configuración de canales.
+func DispatchTicketRespuestaCliente(ticket *models.ProyectoTicket, contenido string, proyectoNombre string) {
+ cfg := models.GetNotifConfig("ticket_respuesta_cliente", "admin")
+ if cfg == nil {
+ return
+ }
+ ticketURL := fmt.Sprintf("%s/app/tickets", app.Http.Server.Url)
+ titulo := fmt.Sprintf("Respuesta de cliente: %s", ticket.Titulo)
+ cuerpo := fmt.Sprintf("%s respondió: %s", ticket.AutorNombre, contenido)
+
+ if cfg.CanalSistema {
+ _ = models.CreateSistemaNotif(&models.SistemaNotificacion{
+ TipoUsuario: "admin",
+ UsuarioID: 0,
+ Titulo: titulo,
+ Cuerpo: cuerpo,
+ Url: ticketURL,
+ Icono: "💬",
+ })
+ }
+ if cfg.CanalEmail {
+ if adminEmail := getAdminEmail(); adminEmail != "" {
+ SendTicketRespuestaAdmin(adminEmail, proyectoNombre, ticket.AutorNombre, ticket.Titulo, contenido, ticketURL)
+ }
+ }
+ if cfg.CanalTelegram {
+ msg := fmt.Sprintf("💬 Respuesta de cliente\nProyecto: %s\nCliente: %s\n\n%s\n\n🔗 %s",
+ proyectoNombre, ticket.AutorNombre, contenido, ticketURL)
+ sendTelegramAdmin(msg)
+ }
+}
+
+// ─── DispatchTicketRespuestaAdmin ─────────────────────────────────────────────
+// Llamar cuando el admin responde un ticket.
+// Notifica al PORTAL USER según su configuración de canales.
+func DispatchTicketRespuestaAdmin(ticket *models.ProyectoTicket, contenido string, proyectoNombre string) {
+ cfg := models.GetNotifConfig("ticket_respuesta_admin", "portal_user")
+ if cfg == nil {
+ return
+ }
+ // Obtener portal user para saber email y telegram
+ portalUser, err := models.GetPortalUserByID(ticket.PortalUserID)
+ if err != nil || portalUser == nil {
+ log.Printf("[Notif] PortalUser %d no encontrado: %v", ticket.PortalUserID, err)
+ return
+ }
+
+ portalURL := fmt.Sprintf("%s/portal/proyecto/%s", app.Http.Server.Url, ticket.Proyecto.Slug)
+ titulo := fmt.Sprintf("Respuesta en tu ticket: %s", ticket.Titulo)
+ cuerpo := fmt.Sprintf("El equipo respondió: %s", contenido)
+
+ if cfg.CanalSistema {
+ _ = models.CreateSistemaNotif(&models.SistemaNotificacion{
+ TipoUsuario: "portal_user",
+ UsuarioID: portalUser.ID,
+ Titulo: titulo,
+ Cuerpo: cuerpo,
+ Url: portalURL,
+ Icono: "💬",
+ })
+ }
+ if cfg.CanalEmail && portalUser.Email != "" {
+ SendTicketRespuestaPortalUser(portalUser.Email, portalUser.Nombre, proyectoNombre, ticket.Titulo, contenido, portalURL)
+ }
+ if cfg.CanalTelegram && portalUser.TelegramChatID != "" {
+ ts := NewTelegramService()
+ msg := fmt.Sprintf("💬 El equipo respondió tu ticket\nProyecto: %s\nTicket: %s\n\n%s\n\n🔗 %s",
+ proyectoNombre, ticket.Titulo, contenido, portalURL)
+ if err := ts.SendMessageWithToken(portalUser.TelegramChatID, msg, getAdminBotToken()); err != nil {
+ log.Printf("[Notif] Error telegram portal_user %d: %v", portalUser.ID, err)
+ }
+ }
+}
+
+// ─── helpers internos ─────────────────────────────────────────────────────────
+
+// getAdminEmail devuelve el from_address del SMTP activo como email del admin.
+func getAdminEmail() string {
+ cfg, err := models.GetSmtpConfig()
+ if err != nil || cfg == nil {
+ return ""
+ }
+ return cfg.FromAddress
+}
+
+// getAdminBotToken devuelve el token del primer TelegramConfig activo.
+func getAdminBotToken() string {
+ configs, err := models.GetAllTelegramConfigs()
+ if err != nil {
+ return ""
+ }
+ for _, c := range configs {
+ if c.Activo {
+ return c.BotToken
+ }
+ }
+ return ""
+}
+
+// sendTelegramAdmin envía un mensaje usando el primer TelegramConfig activo.
+func sendTelegramAdmin(mensaje string) {
+ configs, err := models.GetAllTelegramConfigs()
+ if err != nil {
+ log.Printf("[Notif] Error obteniendo configs telegram: %v", err)
+ return
+ }
+ for _, cfg := range configs {
+ if !cfg.Activo {
+ continue
+ }
+ ts := &TelegramService{BotToken: cfg.BotToken}
+ if err := ts.SendMessage(cfg.ChatID, mensaje); err != nil {
+ log.Printf("[Notif] Error enviando telegram admin (cfg %d): %v", cfg.ID, err)
+ }
+ }
+}
diff --git a/pkg/services/telegram_service.go b/pkg/services/telegram_service.go
index 98efdfd..7f6812b 100755
--- a/pkg/services/telegram_service.go
+++ b/pkg/services/telegram_service.go
@@ -56,3 +56,9 @@ func (ts *TelegramService) SendMessage(chatID interface{}, message string) error
return nil
}
+
+// SendMessageWithToken envía un mensaje usando un bot token explícito (útil para notificar a usuarios con su propio chat_id).
+func (ts *TelegramService) SendMessageWithToken(chatID interface{}, message, botToken string) error {
+ svc := &TelegramService{BotToken: botToken}
+ return svc.SendMessage(chatID, message)
+}
diff --git a/resources/views/notif_config.html b/resources/views/notif_config.html
new file mode 100644
index 0000000..b491e65
--- /dev/null
+++ b/resources/views/notif_config.html
@@ -0,0 +1,169 @@
+
+
+
+
Configuración de notificaciones
+
+ Define por qué canales se envían las alertas a cada destinatario. Más eventos se agregarán conforme el sistema crezca.
+
+
+
+
+
+
+
+
+
Evento
+
Destinatario
+
+ 📧 Email
+
+
+ ✈️ Telegram
+
+
+ 🔔 Sistema
+
+
Guardar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sin bot config.
+ Requiere Chat ID en usuario
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📱 Telegram para clientes del portal
+
+ Para notificar a un cliente por Telegram, el cliente debe abrir una conversación con el bot y compartirte su Chat ID.
+ Luego ve a Portal Usuarios y agrega el Chat ID en su perfil.
+ El bot a usar es el mismo configurado en Configuración Telegram.
+