295 lines
9.3 KiB
Go
295 lines
9.3 KiB
Go
package v1
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/service"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
// CategoryHandler handles Category CRUD endpoints.
|
|
type CategoryHandler struct {
|
|
svc *service.CategoryService
|
|
portalSvc *service.PortalService
|
|
}
|
|
|
|
func NewCategoryHandler(svc *service.CategoryService, portalSvc ...*service.PortalService) *CategoryHandler {
|
|
h := &CategoryHandler{svc: svc}
|
|
if len(portalSvc) > 0 {
|
|
h.portalSvc = portalSvc[0]
|
|
}
|
|
return h
|
|
}
|
|
|
|
// Create creates a new category.
|
|
// POST /api/v1/accounts/:account_id/portals/:portal_id/categories
|
|
func (h *CategoryHandler) Create(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
portal, ok := h.resolvePortal(c, accountID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req service.CreateCategoryRequest
|
|
if err := bindChatwootPayload(c, "category", &req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
category, err := h.svc.Create(c.Request.Context(), portal.ID, accountID, &req)
|
|
if err != nil {
|
|
applogger.L().Errorf("Create category: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create category")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": categoryPayload(category, c.Query("locale"))})
|
|
}
|
|
|
|
// Get retrieves a category by ID.
|
|
// GET /api/v1/accounts/:account_id/portals/:portal_id/categories/:category_id
|
|
func (h *CategoryHandler) Get(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
portal, ok := h.resolvePortal(c, accountID)
|
|
if !ok {
|
|
return
|
|
}
|
|
categoryID, err := strconv.ParseUint(c.Param("category_id"), 10, 64)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid category_id")
|
|
return
|
|
}
|
|
|
|
category, err := h.svc.GetByPortalAndID(c.Request.Context(), portal.ID, uint(categoryID))
|
|
if err != nil {
|
|
applogger.L().Errorf("Get category: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "category not found")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": categoryPayload(category, c.Query("locale"))})
|
|
}
|
|
|
|
// Update modifies an existing category.
|
|
// PUT /api/v1/accounts/:account_id/portals/:portal_id/categories/:category_id
|
|
func (h *CategoryHandler) Update(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
portal, ok := h.resolvePortal(c, accountID)
|
|
if !ok {
|
|
return
|
|
}
|
|
categoryID, err := strconv.ParseUint(c.Param("category_id"), 10, 64)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid category_id")
|
|
return
|
|
}
|
|
|
|
var req service.UpdateCategoryRequest
|
|
if err := bindChatwootPayload(c, "category", &req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
category, err := h.svc.UpdateScoped(c.Request.Context(), portal.ID, uint(categoryID), &req)
|
|
if err != nil {
|
|
applogger.L().Errorf("Update category: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to update category")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": categoryPayload(category, c.Query("locale"))})
|
|
}
|
|
|
|
// Delete removes a category.
|
|
// DELETE /api/v1/accounts/:account_id/portals/:portal_id/categories/:category_id
|
|
func (h *CategoryHandler) Delete(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
portal, ok := h.resolvePortal(c, accountID)
|
|
if !ok {
|
|
return
|
|
}
|
|
categoryID, err := strconv.ParseUint(c.Param("category_id"), 10, 64)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid category_id")
|
|
return
|
|
}
|
|
|
|
if err := h.svc.DeleteScoped(c.Request.Context(), portal.ID, uint(categoryID)); err != nil {
|
|
applogger.L().Errorf("Delete category: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to delete category")
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// List returns all categories for a portal.
|
|
// GET /api/v1/accounts/:account_id/portals/:portal_id/categories
|
|
func (h *CategoryHandler) List(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
portal, ok := h.resolvePortal(c, accountID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
locale := c.Query("locale")
|
|
|
|
categories, _, err := h.svc.ListByPortalID(c.Request.Context(), portal.ID, locale, 0, 0)
|
|
if err != nil {
|
|
applogger.L().Errorf("List categories: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list categories")
|
|
return
|
|
}
|
|
|
|
payload := make([]gin.H, 0, len(categories))
|
|
for i := range categories {
|
|
payload = append(payload, categoryPayload(&categories[i], locale))
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"payload": payload, "meta": gin.H{"current_page": c.DefaultQuery("page", "1"), "categories_count": len(categories)}})
|
|
}
|
|
|
|
// Reorder updates the display order of categories.
|
|
// POST /api/v1/accounts/:account_id/portals/:portal_id/categories/reorder
|
|
func (h *CategoryHandler) Reorder(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
portal, ok := h.resolvePortal(c, accountID)
|
|
if !ok {
|
|
return
|
|
}
|
|
positions, err := bindCategoryReorder(c)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.svc.ReorderScoped(c.Request.Context(), portal.ID, positions); err != nil {
|
|
applogger.L().Errorf("Reorder categories: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to reorder categories")
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
func (h *CategoryHandler) resolvePortal(c *gin.Context, accountID uint) (*model.Portal, bool) {
|
|
if h.portalSvc != nil {
|
|
portal, err := h.portalSvc.ResolveByAccountAndRouteID(c.Request.Context(), accountID, c.Param("portal_id"))
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
|
|
return nil, false
|
|
}
|
|
return portal, true
|
|
}
|
|
portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id")
|
|
return nil, false
|
|
}
|
|
return &model.Portal{Base: model.Base{ID: uint(portalID)}, AccountID: accountID}, true
|
|
}
|
|
|
|
func categoryPayload(category *model.Category, currentLocale string) gin.H {
|
|
if category == nil {
|
|
return gin.H{}
|
|
}
|
|
payload := gin.H{
|
|
"id": category.ID,
|
|
"name": category.Name,
|
|
"slug": category.Slug,
|
|
"locale": category.Locale,
|
|
"description": category.Description,
|
|
"position": category.Position,
|
|
"account_id": category.AccountID,
|
|
"icon": category.Icon,
|
|
"related_categories": relatedCategoryPayloads(category.RelatedCategories),
|
|
"meta": gin.H{"articles_count": categoryArticleCount(category.Articles, currentLocale)},
|
|
}
|
|
if category.Parent != nil && category.Parent.ID != 0 {
|
|
payload["parent_category"] = associatedCategoryPayload(category.Parent)
|
|
payload["root_category"] = associatedCategoryPayload(category.Parent)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func relatedCategoryPayloads(related []model.RelatedCategory) []gin.H {
|
|
payload := make([]gin.H, 0, len(related))
|
|
for _, item := range related {
|
|
if item.RelatedCategory.ID != 0 {
|
|
payload = append(payload, associatedCategoryPayload(&item.RelatedCategory))
|
|
}
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func associatedCategoryPayload(category *model.Category) gin.H {
|
|
return gin.H{
|
|
"id": category.ID,
|
|
"name": category.Name,
|
|
"slug": category.Slug,
|
|
"locale": category.Locale,
|
|
"description": category.Description,
|
|
"position": category.Position,
|
|
"account_id": category.AccountID,
|
|
}
|
|
}
|
|
|
|
func categoryArticleCount(articles []model.Article, locale string) int {
|
|
count := 0
|
|
for _, article := range articles {
|
|
if locale == "" || article.Locale == locale {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func bindCategoryReorder(c *gin.Context) (map[uint]int, error) {
|
|
var req struct {
|
|
PositionsHash map[string]int `json:"positions_hash"`
|
|
Positions []positionEntry `json:"positions"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
return nil, err
|
|
}
|
|
positions := make(map[uint]int, len(req.PositionsHash)+len(req.Positions))
|
|
for id, position := range req.PositionsHash {
|
|
parsed, err := strconv.ParseUint(id, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
positions[uint(parsed)] = position
|
|
}
|
|
for _, entry := range req.Positions {
|
|
positions[entry.ID] = entry.Position
|
|
}
|
|
return positions, nil
|
|
}
|