This commit is contained in:
Tomas Dvorak
2025-10-17 17:39:11 +02:00
parent 35d0954afd
commit e9a63073e5
61 changed files with 3824 additions and 1061 deletions
+130 -1
View File
@@ -11,6 +11,7 @@ import (
"strings"
"fotbal-club/internal/models"
"fotbal-club/internal/services"
"fotbal-club/pkg/logger"
"github.com/gin-gonic/gin"
@@ -366,13 +367,37 @@ func calculateFileMD5(filePath string) (string, error) {
func detectMimeType(filePath string) string {
ext := strings.ToLower(filepath.Ext(filePath))
mimeTypes := map[string]string{
// Images
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".svg": "image/svg+xml",
".pdf": "application/pdf",
".webp": "image/webp",
".bmp": "image/bmp",
".ico": "image/x-icon",
// Documents
".pdf": "application/pdf",
".doc": "application/msword",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".ppt": "application/vnd.ms-powerpoint",
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".txt": "text/plain",
".csv": "text/csv",
// Archives
".zip": "application/zip",
".rar": "application/x-rar-compressed",
".7z": "application/x-7z-compressed",
".tar": "application/x-tar",
".gz": "application/gzip",
// Media
".mp4": "video/mp4",
".avi": "video/x-msvideo",
".mov": "video/quicktime",
".mp3": "audio/mpeg",
".wav": "audio/wav",
}
if mime, ok := mimeTypes[ext]; ok {
@@ -423,3 +448,107 @@ func getEntityInfo(db *gorm.DB, entityType string, entityID uint) map[string]int
return info
}
// RefreshFileTracking re-scans all entities and updates file usage tracking
func (fc *FilesController) RefreshFileTracking(c *gin.Context) {
entityType := c.Query("entity_type") // Optional: "article", "event", "player", etc.
stats := map[string]int{
"articles_scanned": 0,
"events_scanned": 0,
"players_scanned": 0,
"sponsors_scanned": 0,
"contacts_scanned": 0,
"teams_scanned": 0,
"settings_scanned": 0,
}
fileTracker := services.NewFileTracker(fc.DB)
// Refresh articles
if entityType == "" || entityType == "article" {
var articles []models.Article
if err := fc.DB.Find(&articles).Error; err == nil {
for _, article := range articles {
fileTracker.TrackArticleFiles(&article)
stats["articles_scanned"]++
}
logger.Info("Refreshed file tracking for %d articles", len(articles))
}
}
// Refresh events
if entityType == "" || entityType == "event" {
var events []models.Event
if err := fc.DB.Preload("Attachments").Find(&events).Error; err == nil {
for _, event := range events {
fileTracker.TrackEventFiles(&event)
stats["events_scanned"]++
}
logger.Info("Refreshed file tracking for %d events", len(events))
}
}
// Refresh players
if entityType == "" || entityType == "player" {
var players []models.Player
if err := fc.DB.Find(&players).Error; err == nil {
for _, player := range players {
fileTracker.TrackPlayerFiles(&player)
stats["players_scanned"]++
}
logger.Info("Refreshed file tracking for %d players", len(players))
}
}
// Refresh sponsors
if entityType == "" || entityType == "sponsor" {
var sponsors []models.Sponsor
if err := fc.DB.Find(&sponsors).Error; err == nil {
for _, sponsor := range sponsors {
fileTracker.TrackSponsorFiles(&sponsor)
stats["sponsors_scanned"]++
}
logger.Info("Refreshed file tracking for %d sponsors", len(sponsors))
}
}
// Refresh contacts
if entityType == "" || entityType == "contact" {
var contacts []models.Contact
if err := fc.DB.Find(&contacts).Error; err == nil {
for _, contact := range contacts {
fileTracker.TrackContactFiles(&contact)
stats["contacts_scanned"]++
}
logger.Info("Refreshed file tracking for %d contacts", len(contacts))
}
}
// Refresh teams
if entityType == "" || entityType == "team" {
var teams []models.Team
if err := fc.DB.Find(&teams).Error; err == nil {
for _, team := range teams {
fileTracker.TrackTeamFiles(&team)
stats["teams_scanned"]++
}
logger.Info("Refreshed file tracking for %d teams", len(teams))
}
}
// Refresh settings
if entityType == "" || entityType == "settings" {
var settings models.Settings
if err := fc.DB.First(&settings).Error; err == nil {
fileTracker.TrackSettingsFiles(&settings)
stats["settings_scanned"]++
logger.Info("Refreshed file tracking for settings")
}
}
c.JSON(http.StatusOK, gin.H{
"message": "File tracking refreshed successfully",
"stats": stats,
})
}
+22 -6
View File
@@ -108,15 +108,28 @@ func (nc *NavigationController) CreateNavigationItem(c *gin.Context) {
// 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)
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 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create navigation item"})
// Log the actual error for debugging
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to create navigation item",
"details": err.Error(),
})
return
}
@@ -169,7 +182,10 @@ func (nc *NavigationController) UpdateNavigationItem(c *gin.Context) {
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"})
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to update navigation item",
"details": err.Error(),
})
return
}
+1
View File
@@ -326,6 +326,7 @@ func SetupRoutes(api *gin.RouterGroup, db *gorm.DB) {
files.GET("/:id/usages", filesController.GetFileUsages)
files.DELETE("/:id", filesController.DeleteFile)
files.POST("/scan", filesController.ScanAndSyncFiles)
files.POST("/refresh-tracking", filesController.RefreshFileTracking)
}
// Navigation management (admin)
+64 -12
View File
@@ -1,7 +1,10 @@
package services
import (
"encoding/json"
"fotbal-club/internal/models"
"path/filepath"
"strconv"
"strings"
"gorm.io/gorm"
@@ -120,19 +123,35 @@ func (ft *FileTracker) TrackArticleFiles(article *models.Article) error {
// Track attachments if present
if article.Attachments != "" {
// Attachments is a JSON array of URLs
// For simplicity, we'll track each attachment URL separately
// You might want to parse the JSON properly in production
attachments := strings.Split(article.Attachments, ",")
for i, attachment := range attachments {
attachment = strings.Trim(attachment, `[]" `)
if attachment != "" {
fieldName := "attachments"
if i > 0 {
// If multiple attachments, differentiate them
fieldName = "attachments"
// Attachments is a JSON array of URLs - parse properly
var attachmentURLs []string
if err := json.Unmarshal([]byte(article.Attachments), &attachmentURLs); err == nil {
// Successfully parsed as JSON array
for i, attachmentURL := range attachmentURLs {
if attachmentURL != "" {
// Extract filename from URL for better field naming
filename := filepath.Base(attachmentURL)
fieldName := "attachment_" + filename
// Ensure unique field names if same filename appears multiple times
if _, exists := fieldURLMap[fieldName]; exists {
fieldName = "attachment_" + filename + "_" + strconv.Itoa(i)
}
fieldURLMap[fieldName] = attachmentURL
}
}
} else {
// Fallback to simple comma-separated parsing
attachments := strings.Split(article.Attachments, ",")
for i, attachment := range attachments {
attachment = strings.Trim(attachment, `[]" `)
if attachment != "" {
filename := filepath.Base(attachment)
fieldName := "attachment_" + filename
if _, exists := fieldURLMap[fieldName]; exists {
fieldName = "attachment_" + filename + "_" + strconv.Itoa(i)
}
fieldURLMap[fieldName] = attachment
}
fieldURLMap[fieldName] = attachment
}
}
}
@@ -162,6 +181,39 @@ func (ft *FileTracker) TrackEventFiles(event *models.Event) error {
"image_url": event.ImageURL,
"file_url": event.FileURL,
}
// Track each attachment separately
for i, attachment := range event.Attachments {
if attachment.URL != "" {
// Generate field name from attachment name or filename
fieldName := ""
if attachment.Name != "" {
// Use attachment name if available
fieldName = "attachment_" + strings.ReplaceAll(attachment.Name, " ", "_")
} else {
// Fall back to filename from URL
filename := filepath.Base(attachment.URL)
fieldName = "attachment_" + filename
}
// Ensure unique field names if duplicates exist
originalFieldName := fieldName
for counter := 0; ; counter++ {
if _, exists := fieldURLMap[fieldName]; !exists {
break
}
// Add counter suffix if field name already exists
fieldName = originalFieldName + "_" + strconv.Itoa(counter)
if counter > 999 { // Prevent infinite loop
fieldName = originalFieldName + "_" + strconv.Itoa(i)
break
}
}
fieldURLMap[fieldName] = attachment.URL
}
}
return ft.UpdateFileUsages("event", event.ID, fieldURLMap)
}