package controllers import ( "math" "strconv" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) // DocPaginasIndex renderiza el panel de gestión de páginas de documentación. func DocPaginasIndex(c *fiber.Ctx) error { data := fiber.Map{ "user": c.Locals("user").(map[string]interface{}), "modules": c.Locals("userModules"), } if err := c.Render("docs/paginas", data, "layouts/main"); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return nil } // GetDocPaginas devuelve la lista paginada de páginas en JSON. func GetDocPaginas(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) if page < 1 { page = 1 } search := c.Query("search", "") limit := 10 offset := (page - 1) * limit var saasID, catID uint if v, err := strconv.ParseUint(c.Query("saas_id", "0"), 10, 32); err == nil { saasID = uint(v) } if v, err := strconv.ParseUint(c.Query("categoria_id", "0"), 10, 32); err == nil { catID = uint(v) } items, total, err := models.GetAllDocPaginas(limit, offset, search, saasID, catID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } // Selects para el formulario saasItems, _ := models.GetAllSaasProductosSelect() categorias, _ := models.GetAllDocCategoriasSelect() roles, _ := models.AllRolesSelect() return c.JSON(fiber.Map{ "items": items, "saas": saasItems, "categorias": categorias, "roles": roles, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, "limit": limit, }) } // GetDocPaginaDetalle devuelve una página por ID con todos sus datos para edición. func GetDocPaginaDetalle(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 32) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ID inválido"}) } item, err := models.GetDocPaginaByID(uint(id)) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Página no encontrada"}) } return c.JSON(item) } // CreateDocPagina crea una nueva página de documentación. func CreateDocPagina(c *fiber.Ctx) error { type Req struct { SaasID uint `json:"saas_id"` CategoriaID uint `json:"categoria_id"` Titulo string `json:"titulo"` Slug string `json:"slug"` Contenido string `json:"contenido"` TipoContenido string `json:"tipo_contenido"` Visibilidad string `json:"visibilidad"` Orden int `json:"orden"` Publicado bool `json:"publicado"` RolIDs []uint `json:"rol_ids"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Datos inválidos"}) } if req.Titulo == "" || req.Slug == "" || req.SaasID == 0 || req.CategoriaID == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Título, slug, saas y categoría son obligatorios"}) } if req.TipoContenido == "" { req.TipoContenido = "markdown" } if req.Visibilidad == "" { req.Visibilidad = "public" } userMap, _ := c.Locals("user").(map[string]interface{}) var creadoPor uint if userMap != nil { if idVal, ok := userMap["id"]; ok { switch v := idVal.(type) { case float64: creadoPor = uint(v) case uint: creadoPor = v } } } item := &models.DocPagina{ SaasID: req.SaasID, CategoriaID: req.CategoriaID, Titulo: req.Titulo, Slug: req.Slug, Contenido: req.Contenido, TipoContenido: req.TipoContenido, Visibilidad: req.Visibilidad, Orden: req.Orden, Publicado: req.Publicado, CreadoPor: creadoPor, } if err := models.CreateDocPagina(item, req.RolIDs); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"message": "Página creada", "item": item}) } // UpdateDocPagina actualiza una página existente. func UpdateDocPagina(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 32) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ID inválido"}) } type Req struct { SaasID uint `json:"saas_id"` CategoriaID uint `json:"categoria_id"` Titulo string `json:"titulo"` Slug string `json:"slug"` Contenido string `json:"contenido"` TipoContenido string `json:"tipo_contenido"` Visibilidad string `json:"visibilidad"` Orden int `json:"orden"` Publicado bool `json:"publicado"` RolIDs []uint `json:"rol_ids"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Datos inválidos"}) } item := &models.DocPagina{ SaasID: req.SaasID, CategoriaID: req.CategoriaID, Titulo: req.Titulo, Slug: req.Slug, Contenido: req.Contenido, TipoContenido: req.TipoContenido, Visibilidad: req.Visibilidad, Orden: req.Orden, Publicado: req.Publicado, } item.ID = uint(id) if err := models.UpdateDocPagina(item, req.RolIDs); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"message": "Página actualizada"}) } // DeleteDocPagina elimina una página de documentación. func DeleteDocPagina(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 32) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ID inválido"}) } if err := models.DeleteDocPagina(uint(id)); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"message": "Página eliminada"}) } // ─── Rutas públicas / privadas de lectura ───────────────────────────────────── // DocsPublicoIndex renderiza el índice público de un SaaS (sin login). func DocsPublicoIndex(c *fiber.Ctx) error { saasSlug := c.Params("saas") saas, err := models.GetSaasProductoBySlug(saasSlug) if err != nil { return c.Status(fiber.StatusNotFound).Render("errors/404", fiber.Map{}, "layouts/public") } paginas, err := models.GetDocPaginasPublicas(saas.ID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } // Agrupar por categoría grupos := agruparPorCategoria(paginas) return c.Render("docs/public-index", fiber.Map{ "saas": saas, "grupos": grupos, }, "layouts/public") } // DocsPublicaPagina renderiza una página pública individual. func DocsPublicaPagina(c *fiber.Ctx) error { saasSlug := c.Params("saas") slug := c.Params("slug") saas, err := models.GetSaasProductoBySlug(saasSlug) if err != nil { return c.Status(fiber.StatusNotFound).Render("errors/404", fiber.Map{}, "layouts/public") } pagina, err := models.GetDocPaginaBySlug(saas.ID, slug) if err != nil || !pagina.Publicado { return c.Status(fiber.StatusNotFound).Render("errors/404", fiber.Map{}, "layouts/public") } if pagina.Visibilidad == "private" { return c.Status(fiber.StatusForbidden).Render("errors/403", fiber.Map{}, "layouts/public") } // Índice lateral: páginas públicas del mismo SaaS todas, _ := models.GetDocPaginasPublicas(saas.ID) grupos := agruparPorCategoria(todas) return c.Render("docs/public-page", fiber.Map{ "saas": saas, "pagina": pagina, "grupos": grupos, }, "layouts/public") } // DocsPrivadoIndex renderiza el índice de un SaaS para usuarios autenticados (incluye privadas con rol). func DocsPrivadoIndex(c *fiber.Ctx) error { saasSlug := c.Params("saas") saas, err := models.GetSaasProductoBySlug(saasSlug) if err != nil { return c.Status(fiber.StatusNotFound).Render("errors/404", fiber.Map{}, "layouts/main") } rolNames := getUserRolNames(c) paginas, err := models.GetDocPaginasParaRoles(saas.ID, rolNames) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } grupos := agruparPorCategoria(paginas) return c.Render("docs/private-index", fiber.Map{ "user": c.Locals("user"), "saas": saas, "grupos": grupos, }, "layouts/main") } // DocsPrivadaPagina renderiza una página con verificación de rol. func DocsPrivadaPagina(c *fiber.Ctx) error { saasSlug := c.Params("saas") slug := c.Params("slug") saas, err := models.GetSaasProductoBySlug(saasSlug) if err != nil { return c.Status(fiber.StatusNotFound).Render("errors/404", fiber.Map{}, "layouts/main") } pagina, err := models.GetDocPaginaBySlug(saas.ID, slug) if err != nil || !pagina.Publicado { return c.Status(fiber.StatusNotFound).Render("errors/404", fiber.Map{}, "layouts/main") } if pagina.Visibilidad == "private" { // Verificar que el usuario tenga al menos un rol asignado a la página rolNames := getUserRolNames(c) if !paginaAccesible(pagina, rolNames) { return c.Status(fiber.StatusForbidden).Render("errors/403", fiber.Map{ "user": c.Locals("user"), }, "layouts/main") } } rolNames := getUserRolNames(c) todas, _ := models.GetDocPaginasParaRoles(saas.ID, rolNames) grupos := agruparPorCategoria(todas) return c.Render("docs/private-page", fiber.Map{ "user": c.Locals("user"), "modules": c.Locals("userModules"), "saas": saas, "pagina": pagina, "grupos": grupos, }, "layouts/main") } // ─── helpers ────────────────────────────────────────────────────────────────── type GrupoCategoria struct { Categoria models.DocCategoria Paginas []models.DocPagina } func agruparPorCategoria(paginas []models.DocPagina) []GrupoCategoria { orden := []uint{} mapa := map[uint]*GrupoCategoria{} for _, p := range paginas { if _, ok := mapa[p.CategoriaID]; !ok { orden = append(orden, p.CategoriaID) mapa[p.CategoriaID] = &GrupoCategoria{Categoria: p.Categoria} } mapa[p.CategoriaID].Paginas = append(mapa[p.CategoriaID].Paginas, p) } var result []GrupoCategoria for _, id := range orden { result = append(result, *mapa[id]) } return result } func getUserRolNames(c *fiber.Ctx) []string { userMap, ok := c.Locals("user").(map[string]interface{}) if !ok { return nil } raw, ok := userMap["roles"] if !ok { return nil } var names []string switch v := raw.(type) { case []string: names = v case []interface{}: for _, r := range v { if s, ok := r.(string); ok { names = append(names, s) } } } return names } func paginaAccesible(p *models.DocPagina, userRoles []string) bool { if p.Visibilidad == "public" { return true } for _, pr := range p.Roles { for _, ur := range userRoles { if pr.Name == ur { return true } } } return false }