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", "visible = ? AND requires_admin = ?", true, false). 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("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 nc.DB.Model(&models.NavigationItem{}). Where("parent_id IS NULL"). Select("COALESCE(MAX(display_order), -1) + 1"). Scan(&maxOrder) item.DisplayOrder = maxOrder } if err := nc.DB.Create(&item).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create navigation item"}) 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"}) 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 // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]interface{} // @Router /api/v1/admin/navigation/seed [post] func (nc *NavigationController) SeedDefaultNavigation(c *gin.Context) { // Check if navigation items already exist var count int64 nc.DB.Model(&models.NavigationItem{}).Count(&count) if count > 0 { c.JSON(http.StatusOK, gin.H{ "message": "Navigation items already exist", "count": count, "seeded": false, }) return } // 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}, } // Default admin panel navigation items adminItems := []models.NavigationItem{ // Main section {Label: "Nástěnka", Type: models.NavTypeInternal, PageType: "dashboard", DisplayOrder: 0, Visible: true, RequiresAdmin: true}, {Label: "Analytika", Type: models.NavTypeInternal, PageType: "analytics", DisplayOrder: 1, Visible: true, RequiresAdmin: true}, // Content section {Label: "Týmy", Type: models.NavTypeInternal, PageType: "teams", DisplayOrder: 2, Visible: true, RequiresAdmin: true}, {Label: "Zápasy", Type: models.NavTypeInternal, PageType: "matches", DisplayOrder: 3, Visible: true, RequiresAdmin: true}, {Label: "Aktivity", Type: models.NavTypeInternal, PageType: "activities", DisplayOrder: 4, Visible: true, RequiresAdmin: true}, {Label: "Hráči", Type: models.NavTypeInternal, PageType: "players", DisplayOrder: 5, Visible: true, RequiresAdmin: true}, {Label: "Články", Type: models.NavTypeInternal, PageType: "articles", DisplayOrder: 6, Visible: true, RequiresAdmin: true}, {Label: "Kategorie", Type: models.NavTypeInternal, PageType: "categories", DisplayOrder: 7, Visible: true, RequiresAdmin: true}, {Label: "O klubu", Type: models.NavTypeInternal, PageType: "about", DisplayOrder: 8, Visible: true, RequiresAdmin: true}, {Label: "Videa", Type: models.NavTypeInternal, PageType: "videos", DisplayOrder: 9, Visible: true, RequiresAdmin: true}, {Label: "Galerie (Zonerama)", Type: models.NavTypeInternal, PageType: "gallery", DisplayOrder: 10, Visible: true, RequiresAdmin: true}, {Label: "Tabule (Scoreboard)", Type: models.NavTypeInternal, PageType: "scoreboard", DisplayOrder: 11, Visible: true, RequiresAdmin: true}, {Label: "Scoreboard Remote", Type: models.NavTypeInternal, PageType: "scoreboard_remote", DisplayOrder: 12, Visible: true, RequiresAdmin: true}, {Label: "Oblečení", Type: models.NavTypeInternal, PageType: "clothing", DisplayOrder: 13, Visible: true, RequiresAdmin: true}, {Label: "Sponzoři", Type: models.NavTypeInternal, PageType: "sponsors", DisplayOrder: 14, Visible: true, RequiresAdmin: true}, {Label: "Bannery", Type: models.NavTypeInternal, PageType: "banners", DisplayOrder: 15, Visible: true, RequiresAdmin: true}, {Label: "Zprávy", Type: models.NavTypeInternal, PageType: "messages", DisplayOrder: 16, Visible: true, RequiresAdmin: true}, {Label: "Kontakty", Type: models.NavTypeInternal, PageType: "contacts", DisplayOrder: 17, Visible: true, RequiresAdmin: true}, {Label: "Zpravodaj", Type: models.NavTypeInternal, PageType: "newsletter", DisplayOrder: 18, Visible: true, RequiresAdmin: true}, {Label: "Ankety", Type: models.NavTypeInternal, PageType: "polls", DisplayOrder: 19, Visible: true, RequiresAdmin: true}, // Settings section {Label: "Navigace", Type: models.NavTypeInternal, PageType: "navigation", DisplayOrder: 20, Visible: true, RequiresAdmin: true}, {Label: "Alias soutěží", Type: models.NavTypeInternal, PageType: "competition_aliases", DisplayOrder: 21, Visible: true, RequiresAdmin: true}, {Label: "Prefetch & Cache", Type: models.NavTypeInternal, PageType: "prefetch", DisplayOrder: 22, Visible: true, RequiresAdmin: true}, {Label: "Uživatelé", Type: models.NavTypeInternal, PageType: "users", DisplayOrder: 23, Visible: true, RequiresAdmin: true}, {Label: "Nastavení", Type: models.NavTypeInternal, PageType: "settings", DisplayOrder: 24, Visible: true, RequiresAdmin: true}, {Label: "Soubory", Type: models.NavTypeInternal, PageType: "files", DisplayOrder: 25, Visible: true, RequiresAdmin: true}, // Help section {Label: "Dokumentace", Type: models.NavTypeInternal, PageType: "docs", DisplayOrder: 26, Visible: true, RequiresAdmin: true}, } // Combine all items allItems := append(frontendItems, adminItems...) // Create items in a transaction err := nc.DB.Transaction(func(tx *gorm.DB) error { for _, item := range allItems { if err := tx.Create(&item).Error; err != nil { return err } } return nil }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to seed navigation items"}) return } c.JSON(http.StatusOK, gin.H{ "message": "Default navigation items created successfully", "count": len(allItems), "frontend_count": len(frontendItems), "admin_count": len(adminItems), "seeded": true, }) }