From 2b7458b612a2d54ac1a42ade55cde642ecf96891 Mon Sep 17 00:00:00 2001
From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com>
Date: Sat, 16 May 2026 20:01:36 -0500
Subject: [PATCH] up
---
migrations/migrate.go | 45 ++
pkg/models/partner_recurso.go | 91 ++++
resources/views/partner_recursos.html | 387 ++++++++++++++++++
resources/views/portal/dashboard.html | 132 ++++++
.../controllers/partner_recurso_controller.go | 285 +++++++++++++
rest/middlewares/portal_auth.go | 11 +
rest/routes/portal.go | 5 +
rest/routes/user.go | 16 +
8 files changed, 972 insertions(+)
create mode 100644 pkg/models/partner_recurso.go
create mode 100644 resources/views/partner_recursos.html
create mode 100644 rest/controllers/partner_recurso_controller.go
diff --git a/migrations/migrate.go b/migrations/migrate.go
index 2fc7f2c..c9f2455 100755
--- a/migrations/migrate.go
+++ b/migrations/migrate.go
@@ -85,6 +85,9 @@ func Migrate() {
// Sistema de notificaciones por evento
&models.NotifEventoConfig{},
&models.SistemaNotificacion{},
+ // Submódulo Partner
+ &models.PartnerRecurso{},
+ &models.PartnerComunicado{},
); err != nil {
log.Fatalf("Error during main migration: %v", err)
}
@@ -128,6 +131,9 @@ func Migrate() {
// Crear módulo y submódulos de Portal de Clientes
SeedPortalClientes()
+ // Agregar submódulo "Partner Recursos" al módulo Portal de Clientes
+ SeedPartnerRecursos()
+
log.Println("Migration Completed...")
}
@@ -846,3 +852,42 @@ func SeedNotifDefaults() {
func SeedShield() {
log.Println("[SEED] SeedShield: sin entradas por defecto definidas aún.")
}
+
+// SeedPartnerRecursos agrega el submódulo "Partner Recursos" al módulo "Portal de Clientes". Es idempotente.
+func SeedPartnerRecursos() {
+ db := app.Http.Database.DB
+
+ var modulo models.Modules
+ if err := db.Where("title = ?", "Portal de Clientes").First(&modulo).Error; err != nil {
+ log.Printf("[SEED] Módulo 'Portal de Clientes' no encontrado para SeedPartnerRecursos: %v", err)
+ return
+ }
+
+ url := "/app/partner-recursos"
+ var sub models.Submodules
+ if err := db.Where("url = ?", url).First(&sub).Error; err != nil {
+ sub = models.Submodules{
+ Title: "Partner Recursos",
+ Description: "Documentación y comunicados exclusivos para partners",
+ Url: url,
+ ModuleId: modulo.ID,
+ ModifiedAt: time.Now(),
+ }
+ if err2 := db.Create(&sub).Error; err2 != nil {
+ log.Printf("[SEED] Error creando submódulo Partner Recursos: %v", err2)
+ return
+ }
+ log.Printf("[SEED] Submódulo 'Partner Recursos' creado (ID %d)", sub.ID)
+ } else {
+ log.Printf("[SEED] Submódulo 'Partner Recursos' ya existe (ID %d)", sub.ID)
+ }
+
+ var roles []models.Roles
+ if err := db.Find(&roles).Error; err != nil {
+ return
+ }
+ for _, rol := range roles {
+ db.Model(&rol).Association("Submodules").Append(&sub)
+ }
+ log.Println("[SEED] Seed de Partner Recursos completado.")
+}
diff --git a/pkg/models/partner_recurso.go b/pkg/models/partner_recurso.go
new file mode 100644
index 0000000..f441ee9
--- /dev/null
+++ b/pkg/models/partner_recurso.go
@@ -0,0 +1,91 @@
+package models
+
+import (
+ "github.com/sujit-baniya/fiber-boilerplate/app"
+ "gorm.io/gorm"
+)
+
+// ─── PartnerRecurso ───────────────────────────────────────────────────────────
+// Archivos/documentos que el admin sube para que los partners descarguen.
+
+type PartnerRecurso struct {
+ gorm.Model
+ Nombre string `json:"nombre" gorm:"column:nombre"`
+ Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
+ Categoria string `json:"categoria" gorm:"column:categoria;default:'general'"` // general|tecnico|comercial|marketing
+ Archivo string `json:"archivo" gorm:"column:archivo"`
+ NombreOrig string `json:"nombre_orig" gorm:"column:nombre_orig"`
+ Activo bool `json:"activo" gorm:"column:activo;default:true"`
+}
+
+func (PartnerRecurso) TableName() string { return "partner_recursos" }
+
+// ─── PartnerComunicado ────────────────────────────────────────────────────────
+// Anuncios/novedades publicados por el admin exclusivamente para partners.
+
+type PartnerComunicado struct {
+ gorm.Model
+ Titulo string `json:"titulo" gorm:"column:titulo"`
+ Contenido string `json:"contenido" gorm:"column:contenido;type:text"`
+ Archivo string `json:"archivo" gorm:"column:archivo"`
+ NombreOrig string `json:"nombre_orig" gorm:"column:nombre_orig"`
+ Activo bool `json:"activo" gorm:"column:activo;default:true"`
+}
+
+func (PartnerComunicado) TableName() string { return "partner_comunicados" }
+
+// ─── CRUD PartnerRecurso ──────────────────────────────────────────────────────
+
+func GetAllPartnerRecursos(soloActivos bool) ([]PartnerRecurso, error) {
+ var items []PartnerRecurso
+ db := app.Http.Database.DB.Order("created_at DESC")
+ if soloActivos {
+ db = db.Where("activo = true")
+ }
+ return items, db.Find(&items).Error
+}
+
+func GetPartnerRecursoByID(id uint) (*PartnerRecurso, error) {
+ var item PartnerRecurso
+ return &item, app.Http.Database.DB.First(&item, id).Error
+}
+
+func CreatePartnerRecurso(r *PartnerRecurso) error {
+ return app.Http.Database.DB.Create(r).Error
+}
+
+func UpdatePartnerRecurso(r *PartnerRecurso) error {
+ return app.Http.Database.DB.Save(r).Error
+}
+
+func DeletePartnerRecurso(id uint) error {
+ return app.Http.Database.DB.Delete(&PartnerRecurso{}, id).Error
+}
+
+// ─── CRUD PartnerComunicado ───────────────────────────────────────────────────
+
+func GetAllPartnerComunicados(soloActivos bool) ([]PartnerComunicado, error) {
+ var items []PartnerComunicado
+ db := app.Http.Database.DB.Order("created_at DESC")
+ if soloActivos {
+ db = db.Where("activo = true")
+ }
+ return items, db.Find(&items).Error
+}
+
+func GetPartnerComunicadoByID(id uint) (*PartnerComunicado, error) {
+ var item PartnerComunicado
+ return &item, app.Http.Database.DB.First(&item, id).Error
+}
+
+func CreatePartnerComunicado(c *PartnerComunicado) error {
+ return app.Http.Database.DB.Create(c).Error
+}
+
+func UpdatePartnerComunicado(c *PartnerComunicado) error {
+ return app.Http.Database.DB.Save(c).Error
+}
+
+func DeletePartnerComunicado(id uint) error {
+ return app.Http.Database.DB.Delete(&PartnerComunicado{}, id).Error
+}
diff --git a/resources/views/partner_recursos.html b/resources/views/partner_recursos.html
new file mode 100644
index 0000000..30b6f97
--- /dev/null
+++ b/resources/views/partner_recursos.html
@@ -0,0 +1,387 @@
+
+
+
+
+
+
Partner Recursos
+
Documentación y comunicados exclusivos para los usuarios partner del portal.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sin comunicados publicados.
+
+
+
+
+
+
+
+
diff --git a/resources/views/portal/dashboard.html b/resources/views/portal/dashboard.html
index 0f3de9d..aadbef1 100644
--- a/resources/views/portal/dashboard.html
+++ b/resources/views/portal/dashboard.html
@@ -80,3 +80,135 @@
{{ end }}
+
+{{ if .isPartner }}
+
+
+
+
+
P
+
+
Zona Partner
+
Recursos y comunicados exclusivos para ti
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sin comunicados por ahora.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sin recursos disponibles.
+
+
+
+{{ end }}
+
+
diff --git a/rest/controllers/partner_recurso_controller.go b/rest/controllers/partner_recurso_controller.go
new file mode 100644
index 0000000..19ffe6e
--- /dev/null
+++ b/rest/controllers/partner_recurso_controller.go
@@ -0,0 +1,285 @@
+package controllers
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/sujit-baniya/fiber-boilerplate/pkg/models"
+)
+
+// ─── Admin: vista principal ───────────────────────────────────────────────────
+
+func PartnerRecursosIndex(c *fiber.Ctx) error {
+ return c.Render("partner_recursos", fiber.Map{
+ "user": c.Locals("user"),
+ "modules": c.Locals("userModules"),
+ }, "layouts/main")
+}
+
+// ─── Recursos ─────────────────────────────────────────────────────────────────
+
+func LoadPartnerRecursos(c *fiber.Ctx) error {
+ recursos, err := models.GetAllPartnerRecursos(false)
+ if err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"recursos": recursos})
+}
+
+func CreatePartnerRecurso(c *fiber.Ctx) error {
+ type Req struct {
+ Nombre string `json:"nombre"`
+ Descripcion string `json:"descripcion"`
+ Categoria string `json:"categoria"`
+ Activo bool `json:"activo"`
+ }
+ var req Req
+ if err := c.BodyParser(&req); err != nil {
+ return c.Status(400).JSON(fiber.Map{"error": "Datos inválidos"})
+ }
+ r := &models.PartnerRecurso{
+ Nombre: req.Nombre,
+ Descripcion: req.Descripcion,
+ Categoria: req.Categoria,
+ Activo: req.Activo,
+ }
+ if err := models.CreatePartnerRecurso(r); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.Status(201).JSON(r)
+}
+
+func UpdatePartnerRecurso(c *fiber.Ctx) error {
+ id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
+ type Req struct {
+ Nombre string `json:"nombre"`
+ Descripcion string `json:"descripcion"`
+ Categoria string `json:"categoria"`
+ Activo bool `json:"activo"`
+ }
+ var req Req
+ if err := c.BodyParser(&req); err != nil {
+ return c.Status(400).JSON(fiber.Map{"error": "Datos inválidos"})
+ }
+ r, err := models.GetPartnerRecursoByID(uint(id))
+ if err != nil {
+ return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
+ }
+ r.Nombre = req.Nombre
+ r.Descripcion = req.Descripcion
+ r.Categoria = req.Categoria
+ r.Activo = req.Activo
+ if err := models.UpdatePartnerRecurso(r); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(r)
+}
+
+func DeletePartnerRecurso(c *fiber.Ctx) error {
+ id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
+ if err := models.DeletePartnerRecurso(uint(id)); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"ok": true})
+}
+
+func UploadPartnerRecursoArchivo(c *fiber.Ctx) error {
+ id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
+ file, err := c.FormFile("archivo")
+ if err != nil {
+ return c.Status(400).JSON(fiber.Map{"error": "Archivo requerido"})
+ }
+ if file.Size > 50*1024*1024 {
+ return c.Status(400).JSON(fiber.Map{"error": "Máximo 50MB"})
+ }
+ dir := "uploads/partner_recursos"
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": "Error al crear directorio"})
+ }
+ safeFile := safeFilenameProyecto(file.Filename)
+ savePath := filepath.Join(dir, fmt.Sprintf("%d_%s", id, safeFile))
+ // Validar que la ruta quede dentro de uploads/
+ clean := filepath.Clean(savePath)
+ if !strings.HasPrefix(clean, "uploads/") {
+ return c.Status(400).JSON(fiber.Map{"error": "Ruta inválida"})
+ }
+ if err := c.SaveFile(file, savePath); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": "Error al guardar archivo"})
+ }
+ r, err := models.GetPartnerRecursoByID(uint(id))
+ if err != nil {
+ return c.Status(404).JSON(fiber.Map{"error": "Recurso no encontrado"})
+ }
+ r.Archivo = savePath
+ r.NombreOrig = file.Filename
+ if err := models.UpdatePartnerRecurso(r); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"ok": true, "archivo": savePath, "nombre_orig": file.Filename})
+}
+
+func DownloadPartnerRecurso(c *fiber.Ctx) error {
+ id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
+ r, err := models.GetPartnerRecursoByID(uint(id))
+ if err != nil || r.Archivo == "" {
+ return c.Status(404).JSON(fiber.Map{"error": "Archivo no encontrado"})
+ }
+ clean := filepath.Clean(r.Archivo)
+ if !strings.HasPrefix(clean, "uploads/") {
+ return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
+ }
+ nombre := r.NombreOrig
+ if nombre == "" {
+ nombre = filepath.Base(r.Archivo)
+ }
+ c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, nombre))
+ return c.SendFile(clean)
+}
+
+// ─── Comunicados ──────────────────────────────────────────────────────────────
+
+func LoadPartnerComunicados(c *fiber.Ctx) error {
+ items, err := models.GetAllPartnerComunicados(false)
+ if err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"comunicados": items})
+}
+
+func CreatePartnerComunicado(c *fiber.Ctx) error {
+ type Req struct {
+ Titulo string `json:"titulo"`
+ Contenido string `json:"contenido"`
+ Activo bool `json:"activo"`
+ }
+ var req Req
+ if err := c.BodyParser(&req); err != nil {
+ return c.Status(400).JSON(fiber.Map{"error": "Datos inválidos"})
+ }
+ item := &models.PartnerComunicado{
+ Titulo: req.Titulo,
+ Contenido: req.Contenido,
+ Activo: req.Activo,
+ }
+ if err := models.CreatePartnerComunicado(item); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.Status(201).JSON(item)
+}
+
+func UpdatePartnerComunicado(c *fiber.Ctx) error {
+ id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
+ type Req struct {
+ Titulo string `json:"titulo"`
+ Contenido string `json:"contenido"`
+ Activo bool `json:"activo"`
+ }
+ var req Req
+ if err := c.BodyParser(&req); err != nil {
+ return c.Status(400).JSON(fiber.Map{"error": "Datos inválidos"})
+ }
+ item, err := models.GetPartnerComunicadoByID(uint(id))
+ if err != nil {
+ return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
+ }
+ item.Titulo = req.Titulo
+ item.Contenido = req.Contenido
+ item.Activo = req.Activo
+ if err := models.UpdatePartnerComunicado(item); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(item)
+}
+
+func DeletePartnerComunicado(c *fiber.Ctx) error {
+ id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
+ if err := models.DeletePartnerComunicado(uint(id)); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"ok": true})
+}
+
+func UploadPartnerComunicadoArchivo(c *fiber.Ctx) error {
+ id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
+ file, err := c.FormFile("archivo")
+ if err != nil {
+ return c.Status(400).JSON(fiber.Map{"error": "Archivo requerido"})
+ }
+ if file.Size > 50*1024*1024 {
+ return c.Status(400).JSON(fiber.Map{"error": "Máximo 50MB"})
+ }
+ dir := "uploads/partner_comunicados"
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": "Error al crear directorio"})
+ }
+ safeFile := safeFilenameProyecto(file.Filename)
+ savePath := filepath.Join(dir, fmt.Sprintf("%d_%s", id, safeFile))
+ clean := filepath.Clean(savePath)
+ if !strings.HasPrefix(clean, "uploads/") {
+ return c.Status(400).JSON(fiber.Map{"error": "Ruta inválida"})
+ }
+ if err := c.SaveFile(file, savePath); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": "Error al guardar archivo"})
+ }
+ item, err := models.GetPartnerComunicadoByID(uint(id))
+ if err != nil {
+ return c.Status(404).JSON(fiber.Map{"error": "Comunicado no encontrado"})
+ }
+ item.Archivo = savePath
+ item.NombreOrig = file.Filename
+ if err := models.UpdatePartnerComunicado(item); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"ok": true, "archivo": savePath, "nombre_orig": file.Filename})
+}
+
+func DownloadPartnerComunicadoArchivo(c *fiber.Ctx) error {
+ id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
+ item, err := models.GetPartnerComunicadoByID(uint(id))
+ if err != nil || item.Archivo == "" {
+ return c.Status(404).JSON(fiber.Map{"error": "Archivo no encontrado"})
+ }
+ clean := filepath.Clean(item.Archivo)
+ if !strings.HasPrefix(clean, "uploads/") {
+ return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
+ }
+ nombre := item.NombreOrig
+ if nombre == "" {
+ nombre = filepath.Base(item.Archivo)
+ }
+ c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, nombre))
+ return c.SendFile(clean)
+}
+
+// ─── Portal: acceso para partners ────────────────────────────────────────────
+
+func PortalPartnerRecursos(c *fiber.Ctx) error {
+ recursos, _ := models.GetAllPartnerRecursos(true)
+ comunicados, _ := models.GetAllPartnerComunicados(true)
+ return c.JSON(fiber.Map{
+ "recursos": recursos,
+ "comunicados": comunicados,
+ })
+}
+
+func PortalDownloadPartnerRecurso(c *fiber.Ctx) error {
+ id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
+ r, err := models.GetPartnerRecursoByID(uint(id))
+ if err != nil || r.Archivo == "" || !r.Activo {
+ return c.Status(404).JSON(fiber.Map{"error": "Archivo no encontrado"})
+ }
+ clean := filepath.Clean(r.Archivo)
+ if !strings.HasPrefix(clean, "uploads/") {
+ return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
+ }
+ nombre := r.NombreOrig
+ if nombre == "" {
+ nombre = filepath.Base(r.Archivo)
+ }
+ c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, nombre))
+ return c.SendFile(clean)
+}
diff --git a/rest/middlewares/portal_auth.go b/rest/middlewares/portal_auth.go
index bf41fc4..44e799f 100644
--- a/rest/middlewares/portal_auth.go
+++ b/rest/middlewares/portal_auth.go
@@ -35,3 +35,14 @@ func PortalUserFromLocals(c *fiber.Ctx) *models.PortalUser {
u, _ := c.Locals("portalUser").(*models.PortalUser)
return u
}
+
+// PortalPartnerOnly permite el acceso solo a usuarios con rol "partner".
+func PortalPartnerOnly() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ u := PortalUserFromLocals(c)
+ if u == nil || u.Rol != "partner" {
+ return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Acceso exclusivo para partners"})
+ }
+ return c.Next()
+ }
+}
diff --git a/rest/routes/portal.go b/rest/routes/portal.go
index f3acc9f..7c7fe9f 100644
--- a/rest/routes/portal.go
+++ b/rest/routes/portal.go
@@ -35,4 +35,9 @@ func PortalRoutes(app fiber.Router) {
portal.Get("/mis-notifs", controllers.PortalGetMisNotifs)
portal.Put("/mis-notifs/:id/leida", controllers.PortalMarcarNotifLeida)
portal.Post("/mis-notifs/marcar-todas", controllers.PortalMarcarTodasLeidas)
+
+ // Partner: recursos y comunicados (solo usuarios con rol partner)
+ portal.Get("/partner-recursos", middlewares.PortalPartnerOnly(), controllers.PortalPartnerRecursos)
+ portal.Get("/partner-recursos/:id/download", middlewares.PortalPartnerOnly(), controllers.PortalDownloadPartnerRecurso)
+ portal.Get("/partner-comunicados/:id/download", middlewares.PortalPartnerOnly(), controllers.DownloadPartnerComunicadoArchivo)
}
diff --git a/rest/routes/user.go b/rest/routes/user.go
index 0dcf6de..479b9f8 100755
--- a/rest/routes/user.go
+++ b/rest/routes/user.go
@@ -295,6 +295,22 @@ func UserRoutes(app fiber.Router) {
protected.Get("/notif-config/data", controllers.GetNotifConfigs)
protected.Post("/notif-config", controllers.SaveNotifConfig)
+ // Partner Recursos (documentación y comunicados para partners)
+ protected.Get("/partner-recursos", middlewares.MenuMiddleware, controllers.PartnerRecursosIndex)
+ protected.Get("/partner-recursos/data", controllers.LoadPartnerRecursos)
+ protected.Post("/partner-recursos", controllers.CreatePartnerRecurso)
+ protected.Put("/partner-recursos/:id", controllers.UpdatePartnerRecurso)
+ protected.Delete("/partner-recursos/:id", controllers.DeletePartnerRecurso)
+ protected.Post("/partner-recursos/:id/upload", controllers.UploadPartnerRecursoArchivo)
+ protected.Get("/partner-recursos/:id/download", controllers.DownloadPartnerRecurso)
+
+ protected.Get("/partner-comunicados/data", controllers.LoadPartnerComunicados)
+ protected.Post("/partner-comunicados", controllers.CreatePartnerComunicado)
+ protected.Put("/partner-comunicados/:id", controllers.UpdatePartnerComunicado)
+ protected.Delete("/partner-comunicados/:id", controllers.DeletePartnerComunicado)
+ protected.Post("/partner-comunicados/:id/upload", controllers.UploadPartnerComunicadoArchivo)
+ protected.Get("/partner-comunicados/:id/download", controllers.DownloadPartnerComunicadoArchivo)
+
// Notificaciones in-app del admin (bell)
protected.Get("/mis-notifs", controllers.GetMisNotifs)
protected.Put("/mis-notifs/:id/leida", controllers.MarcarNotifLeida)