package controllers import ( "fotbal-club/internal/models" "net/http" "strconv" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type NavigationController struct { DB *gorm.DB } func NewNavigationController(db *gorm.DB) *NavigationController { return &NavigationController{DB: db} } // GetNavigationItems returns all navigation items (public endpoint) // @Summary Get all navigation items // @Description Returns all visible navigation items with their children (excludes admin-only items) // @Tags navigation // @Produce json // @Success 200 {array} models.NavigationItem // @Router /api/v1/navigation [get] func (nc *NavigationController) GetNavigationItems(c *gin.Context) { var items []models.NavigationItem // Get only top-level items (no parent) that are visible and NOT admin-only if err := nc.DB.Where("parent_id IS NULL AND visible = ? AND requires_admin = ?", true, false). Order("display_order ASC"). Preload("Children", func(db *gorm.DB) *gorm.DB { return db.Where("visible = ? AND requires_admin = ?", true, false).Order("display_order ASC") }). Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch navigation items"}) return } // Compute URLs for items for i := range items { if items[i].URL == "" { items[i].URL = items[i].GetURL() } for j := range items[i].Children { if items[i].Children[j].URL == "" { items[i].Children[j].URL = items[i].Children[j].GetURL() } } } c.JSON(http.StatusOK, items) } // GetAllNavigationItems returns all navigation items including hidden ones (admin only) // @Summary Get all navigation items (admin) // @Description Returns all navigation items for admin management // @Tags navigation // @Produce json // @Security BearerAuth // @Success 200 {array} models.NavigationItem // @Router /api/v1/admin/navigation [get] func (nc *NavigationController) GetAllNavigationItems(c *gin.Context) { var items []models.NavigationItem if err := nc.DB.Where("parent_id IS NULL"). Order("requires_admin ASC, display_order ASC"). Preload("Children", func(db *gorm.DB) *gorm.DB { return db.Order("display_order ASC") }). Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch navigation items"}) return } // Compute URLs for items for i := range items { if items[i].URL == "" { items[i].URL = items[i].GetURL() } for j := range items[i].Children { if items[i].Children[j].URL == "" { items[i].Children[j].URL = items[i].Children[j].GetURL() } } } c.JSON(http.StatusOK, items) } // CreateNavigationItem creates a new navigation item // @Summary Create navigation item // @Description Creates a new navigation item // @Tags navigation // @Accept json // @Produce json // @Security BearerAuth // @Param item body models.NavigationItem true "Navigation item data" // @Success 201 {object} models.NavigationItem // @Router /api/v1/admin/navigation [post] func (nc *NavigationController) CreateNavigationItem(c *gin.Context) { var item models.NavigationItem if err := c.ShouldBindJSON(&item); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // If no display order is set, put it at the end if item.DisplayOrder == 0 { var maxOrder int query := nc.DB.Model(&models.NavigationItem{}) // Calculate max order for items at the same level (same parent) and same admin status if item.ParentID == nil { query = query.Where("parent_id IS NULL") } else { query = query.Where("parent_id = ?", *item.ParentID) } // Also consider requires_admin to keep frontend and admin items separate query = query.Where("requires_admin = ?", item.RequiresAdmin) query.Select("COALESCE(MAX(display_order), -1) + 1").Scan(&maxOrder) item.DisplayOrder = maxOrder } if err := nc.DB.Create(&item).Error; err != nil { // Log the actual error for debugging c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to create navigation item", "details": err.Error(), }) return } c.JSON(http.StatusCreated, item) } // UpdateNavigationItem updates an existing navigation item // @Summary Update navigation item // @Description Updates an existing navigation item // @Tags navigation // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Navigation item ID" // @Param item body models.NavigationItem true "Updated navigation item data" // @Success 200 {object} models.NavigationItem // @Router /api/v1/admin/navigation/{id} [put] func (nc *NavigationController) UpdateNavigationItem(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) return } var item models.NavigationItem if err := nc.DB.First(&item, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Navigation item not found"}) return } var updates models.NavigationItem if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update fields item.Label = updates.Label item.URL = updates.URL item.Icon = updates.Icon item.Type = updates.Type item.PageType = updates.PageType item.PageID = updates.PageID item.Visible = updates.Visible item.DisplayOrder = updates.DisplayOrder item.ParentID = updates.ParentID item.Target = updates.Target item.CSSClass = updates.CSSClass item.RequiresAuth = updates.RequiresAuth item.RequiresAdmin = updates.RequiresAdmin if err := nc.DB.Save(&item).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to update navigation item", "details": err.Error(), }) return } c.JSON(http.StatusOK, item) } // DeleteNavigationItem deletes a navigation item // @Summary Delete navigation item // @Description Deletes a navigation item // @Tags navigation // @Produce json // @Security BearerAuth // @Param id path int true "Navigation item ID" // @Success 200 {object} map[string]string // @Router /api/v1/admin/navigation/{id} [delete] func (nc *NavigationController) DeleteNavigationItem(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) return } if err := nc.DB.Delete(&models.NavigationItem{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete navigation item"}) return } c.JSON(http.StatusOK, gin.H{"message": "Navigation item deleted successfully"}) } // ReorderNavigationItems updates the display order of multiple items // @Summary Reorder navigation items // @Description Updates the display order of navigation items // @Tags navigation // @Accept json // @Produce json // @Security BearerAuth // @Param orders body []map[string]int true "Array of {id, display_order}" // @Success 200 {object} map[string]string // @Router /api/v1/admin/navigation/reorder [post] func (nc *NavigationController) ReorderNavigationItems(c *gin.Context) { var orders []map[string]int if err := c.ShouldBindJSON(&orders); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update each item's order in a transaction err := nc.DB.Transaction(func(tx *gorm.DB) error { for _, order := range orders { id, ok1 := order["id"] displayOrder, ok2 := order["display_order"] if !ok1 || !ok2 { continue } if err := tx.Model(&models.NavigationItem{}). Where("id = ?", id). Update("display_order", displayOrder).Error; err != nil { return err } } return nil }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reorder navigation items"}) return } c.JSON(http.StatusOK, gin.H{"message": "Navigation items reordered successfully"}) } // GetSocialLinks returns all social links (public endpoint) // @Summary Get all social links // @Description Returns all visible social links // @Tags navigation // @Produce json // @Success 200 {array} models.SocialLink // @Router /api/v1/social-links [get] func (nc *NavigationController) GetSocialLinks(c *gin.Context) { var links []models.SocialLink if err := nc.DB.Where("visible = ?", true). Order("display_order ASC"). Find(&links).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch social links"}) return } c.JSON(http.StatusOK, links) } // GetAllSocialLinks returns all social links including hidden ones (admin only) // @Summary Get all social links (admin) // @Description Returns all social links for admin management // @Tags navigation // @Produce json // @Security BearerAuth // @Success 200 {array} models.SocialLink // @Router /api/v1/admin/social-links [get] func (nc *NavigationController) GetAllSocialLinks(c *gin.Context) { var links []models.SocialLink if err := nc.DB.Order("display_order ASC").Find(&links).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch social links"}) return } c.JSON(http.StatusOK, links) } // CreateSocialLink creates a new social link // @Summary Create social link // @Description Creates a new social link // @Tags navigation // @Accept json // @Produce json // @Security BearerAuth // @Param link body models.SocialLink true "Social link data" // @Success 201 {object} models.SocialLink // @Router /api/v1/admin/social-links [post] func (nc *NavigationController) CreateSocialLink(c *gin.Context) { var link models.SocialLink if err := c.ShouldBindJSON(&link); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // If no display order is set, put it at the end if link.DisplayOrder == 0 { var maxOrder int nc.DB.Model(&models.SocialLink{}). Select("COALESCE(MAX(display_order), -1) + 1"). Scan(&maxOrder) link.DisplayOrder = maxOrder } if err := nc.DB.Create(&link).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create social link"}) return } c.JSON(http.StatusCreated, link) } // UpdateSocialLink updates an existing social link // @Summary Update social link // @Description Updates an existing social link // @Tags navigation // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Social link ID" // @Param link body models.SocialLink true "Updated social link data" // @Success 200 {object} models.SocialLink // @Router /api/v1/admin/social-links/{id} [put] func (nc *NavigationController) UpdateSocialLink(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) return } var link models.SocialLink if err := nc.DB.First(&link, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Social link not found"}) return } var updates models.SocialLink if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } link.Platform = updates.Platform link.URL = updates.URL link.DisplayOrder = updates.DisplayOrder link.Visible = updates.Visible link.Icon = updates.Icon if err := nc.DB.Save(&link).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update social link"}) return } c.JSON(http.StatusOK, link) } // DeleteSocialLink deletes a social link // @Summary Delete social link // @Description Deletes a social link // @Tags navigation // @Produce json // @Security BearerAuth // @Param id path int true "Social link ID" // @Success 200 {object} map[string]string // @Router /api/v1/admin/social-links/{id} [delete] func (nc *NavigationController) DeleteSocialLink(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) return } if err := nc.DB.Delete(&models.SocialLink{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete social link"}) return } c.JSON(http.StatusOK, gin.H{"message": "Social link deleted successfully"}) } // ReorderSocialLinks updates the display order of multiple social links // @Summary Reorder social links // @Description Updates the display order of social links // @Tags navigation // @Accept json // @Produce json // @Security BearerAuth // @Param orders body []map[string]int true "Array of {id, display_order}" // @Success 200 {object} map[string]string // @Router /api/v1/admin/social-links/reorder [post] func (nc *NavigationController) ReorderSocialLinks(c *gin.Context) { var orders []map[string]int if err := c.ShouldBindJSON(&orders); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err := nc.DB.Transaction(func(tx *gorm.DB) error { for _, order := range orders { id, ok1 := order["id"] displayOrder, ok2 := order["display_order"] if !ok1 || !ok2 { continue } if err := tx.Model(&models.SocialLink{}). Where("id = ?", id). Update("display_order", displayOrder).Error; err != nil { return err } } return nil }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reorder social links"}) return } c.JSON(http.StatusOK, gin.H{"message": "Social links reordered successfully"}) } // SeedDefaultNavigation creates default navigation items if none exist // @Summary Seed default navigation // @Description Creates default navigation items if the database is empty // @Tags navigation // @Security BearerAuth // @Success 200 {object} map[string]interface{} // @Router /api/v1/admin/navigation/seed [post] func (nc *NavigationController) SeedDefaultNavigation(c *gin.Context) { // Check existing counts for frontend and admin separately var frontendCount int64 var adminCount int64 nc.DB.Model(&models.NavigationItem{}).Where("requires_admin = ?", false).Count(&frontendCount) nc.DB.Model(&models.NavigationItem{}).Where("requires_admin = ?", true).Count(&adminCount) // Default frontend navigation items frontendItems := []models.NavigationItem{ {Label: "Domů", Type: models.NavTypePage, PageType: "home", DisplayOrder: 0, Visible: true, RequiresAdmin: false}, {Label: "O klubu", Type: models.NavTypePage, PageType: "about", DisplayOrder: 1, Visible: true, RequiresAdmin: false}, {Label: "Kalendář", Type: models.NavTypePage, PageType: "calendar", DisplayOrder: 2, Visible: true, RequiresAdmin: false}, {Label: "Zápasy", Type: models.NavTypePage, PageType: "matches", DisplayOrder: 3, Visible: true, RequiresAdmin: false}, {Label: "Aktivity", Type: models.NavTypePage, PageType: "activities", DisplayOrder: 4, Visible: true, RequiresAdmin: false}, {Label: "Hráči", Type: models.NavTypePage, PageType: "players", DisplayOrder: 5, Visible: true, RequiresAdmin: false}, {Label: "Tabulky", Type: models.NavTypePage, PageType: "tables", DisplayOrder: 6, Visible: true, RequiresAdmin: false}, {Label: "Články", Type: models.NavTypePage, PageType: "blog", DisplayOrder: 7, Visible: true, RequiresAdmin: false}, {Label: "Videa", Type: models.NavTypePage, PageType: "videos", DisplayOrder: 8, Visible: true, RequiresAdmin: false}, {Label: "Fotogalerie", Type: models.NavTypePage, PageType: "gallery", DisplayOrder: 9, Visible: true, RequiresAdmin: false}, {Label: "Sponzoři", Type: models.NavTypePage, PageType: "sponsors", DisplayOrder: 10, Visible: true, RequiresAdmin: false}, {Label: "Kontakt", Type: models.NavTypePage, PageType: "contact", DisplayOrder: 11, Visible: true, RequiresAdmin: false}, } // Create items in a transaction with admin categories and children (seed missing parts only) seededFrontend := false seededAdmin := false err := nc.DB.Transaction(func(tx *gorm.DB) error { if frontendCount == 0 { for _, item := range frontendItems { if err := tx.Create(&item).Error; err != nil { return err } } seededFrontend = true } if adminCount == 0 { catOrder := 0 createCategory := func(label string) (*models.NavigationItem, error) { cat := &models.NavigationItem{Label: label, Type: models.NavTypeDropdown, DisplayOrder: catOrder, Visible: true, RequiresAdmin: true} catOrder++ if err := tx.Create(cat).Error; err != nil { return nil, err } return cat, nil } createChild := func(parent *models.NavigationItem, label, pageType string, order int) error { pid := parent.ID child := &models.NavigationItem{Label: label, Type: models.NavTypeInternal, PageType: pageType, DisplayOrder: order, Visible: true, RequiresAdmin: true} child.ParentID = &pid return tx.Create(child).Error } zakladni, err := createCategory("Základní") if err != nil { return err } if err := createChild(zakladni, "Nástěnka", "dashboard", 0); err != nil { return err } if err := createChild(zakladni, "Analytika", "analytics", 1); err != nil { return err } sport, err := createCategory("Sport") if err != nil { return err } if err := createChild(sport, "Týmy", "teams", 0); err != nil { return err } if err := createChild(sport, "Zápasy", "matches", 1); err != nil { return err } if err := createChild(sport, "Hráči", "players", 2); err != nil { return err } if err := createChild(sport, "Alias soutěží", "competition_aliases", 3); err != nil { return err } if err := createChild(sport, "Tabule (Scoreboard)", "scoreboard", 4); err != nil { return err } if err := createChild(sport, "Scoreboard Remote", "scoreboard_remote", 5); err != nil { return err } obsah, err := createCategory("Obsah") if err != nil { return err } if err := createChild(obsah, "Články", "articles", 0); err != nil { return err } if err := createChild(obsah, "Aktivity", "activities", 1); err != nil { return err } if err := createChild(obsah, "Kategorie", "categories", 2); err != nil { return err } if err := createChild(obsah, "Komentáře", "comments", 3); err != nil { return err } media, err := createCategory("Média") if err != nil { return err } if err := createChild(media, "Videa", "videos", 0); err != nil { return err } if err := createChild(media, "Galerie (Zonerama)", "gallery", 1); err != nil { return err } if err := createChild(media, "Soubory", "files", 2); err != nil { return err } kom, err := createCategory("Komunikace") if err != nil { return err } if err := createChild(kom, "Zprávy", "messages", 0); err != nil { return err } if err := createChild(kom, "Zpravodaj", "newsletter", 1); err != nil { return err } if err := createChild(kom, "Kontakty", "contacts", 2); err != nil { return err } marketing, err := createCategory("Marketing") if err != nil { return err } if err := createChild(marketing, "Sponzoři", "sponsors", 0); err != nil { return err } if err := createChild(marketing, "Bannery", "banners", 1); err != nil { return err } if err := createChild(marketing, "Oblečení", "clothing", 2); err != nil { return err } if err := createChild(marketing, "Ankety", "polls", 3); err != nil { return err } if err := createChild(marketing, "Soutěže", "sweepstakes", 4); err != nil { return err } if err := createChild(marketing, "Odměny & Úspěchy", "engagement", 5); err != nil { return err } if err := createChild(marketing, "Zkrácené odkazy", "shortlinks", 6); err != nil { return err } nastroje, err := createCategory("Nástroje") if err != nil { return err } if err := createChild(nastroje, "Prefetch & Cache", "prefetch", 0); err != nil { return err } nastaveni, err := createCategory("Nastavení") if err != nil { return err } if err := createChild(nastaveni, "Nastavení", "settings", 0); err != nil { return err } if err := createChild(nastaveni, "Uživatelé", "users", 1); err != nil { return err } if err := createChild(nastaveni, "Navigace", "navigation", 2); err != nil { return err } napoveda, err := createCategory("Nápověda") if err != nil { return err } if err := createChild(napoveda, "Dokumentace", "docs", 0); err != nil { return err } seededAdmin = true } return nil }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to seed navigation items"}) return } // Since creation is split, compute counts again var total int64 nc.DB.Model(&models.NavigationItem{}).Count(&total) nc.DB.Model(&models.NavigationItem{}).Where("requires_admin = ?", false).Count(&frontendCount) nc.DB.Model(&models.NavigationItem{}).Where("requires_admin = ?", true).Count(&adminCount) message := "Navigation items already exist" if seededFrontend && seededAdmin { message = "Default frontend and admin navigation created successfully" } else if seededFrontend { message = "Default frontend navigation created successfully" } else if seededAdmin { message = "Default admin navigation created successfully" } c.JSON(http.StatusOK, gin.H{ "message": message, "count": total, "frontend_count": frontendCount, "admin_count": adminCount, "seeded": seededFrontend || seededAdmin, "seeded_frontend": seededFrontend, "seeded_admin": seededAdmin, }) }