mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #69
This commit is contained in:
@@ -40,6 +40,31 @@ type BaseController struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func normalizePhone(raw, country string) string {
|
||||
s := strings.TrimSpace(raw)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
re := regexp.MustCompile(`[\s\-\.\(\)]`)
|
||||
s = re.ReplaceAllString(s, "")
|
||||
if strings.HasPrefix(s, "00") {
|
||||
s = "+" + s[2:]
|
||||
}
|
||||
if strings.HasPrefix(s, "+") {
|
||||
return s
|
||||
}
|
||||
if matched, _ := regexp.MatchString(`^420\d{9}$`, s); matched {
|
||||
return "+" + s
|
||||
}
|
||||
if matched, _ := regexp.MatchString(`^\d{9}$`, s); matched {
|
||||
c := strings.ToLower(country)
|
||||
if strings.Contains(c, "česk") || strings.Contains(c, "czech") {
|
||||
return "+420" + s
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// GetMatchesHistory returns cached past matches with overrides applied (public)
|
||||
// Optional query: q= filters by home/away/venue/competition
|
||||
func (bc *BaseController) GetMatchesHistory(c *gin.Context) {
|
||||
@@ -600,6 +625,24 @@ func (bc *BaseController) CreateCategory(c *gin.Context) {
|
||||
Name: name,
|
||||
Description: strings.TrimSpace(body.Description),
|
||||
}
|
||||
// Ensure category slug is set and unique
|
||||
s := makeSlug(cat.Name)
|
||||
if s == "" {
|
||||
s = "category"
|
||||
}
|
||||
orig := s
|
||||
for i := 0; i < 50; i++ {
|
||||
var cnt int64
|
||||
if err := bc.DB.Model(&models.Category{}).Where("slug = ?", s).Count(&cnt).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Chyba při kontrole jedinečnosti URL"})
|
||||
return
|
||||
}
|
||||
if cnt == 0 {
|
||||
break
|
||||
}
|
||||
s = fmt.Sprintf("%s-%d", orig, i+1)
|
||||
}
|
||||
cat.Slug = s
|
||||
|
||||
if err := bc.DB.Create(&cat).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze vytvořit kategorii"})
|
||||
@@ -1715,10 +1758,31 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
ClubName string `json:"club_name"`
|
||||
ClubLogoURL string `json:"club_logo_url"`
|
||||
ClubURL string `json:"club_url"`
|
||||
|
||||
// Social profiles (optional)
|
||||
FacebookURL string `json:"facebook_url"`
|
||||
InstagramURL string `json:"instagram_url"`
|
||||
YoutubeURL string `json:"youtube_url"`
|
||||
|
||||
// Gallery (optional)
|
||||
GalleryURL string `json:"gallery_url"`
|
||||
GalleryLabel string `json:"gallery_label"`
|
||||
|
||||
// Location/Contact (optional)
|
||||
ContactAddress string `json:"contact_address"`
|
||||
ContactCity string `json:"contact_city"`
|
||||
ContactZip string `json:"contact_zip"`
|
||||
ContactCountry string `json:"contact_country"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
ContactEmail string `json:"contact_email"`
|
||||
LocationLatitude float64 `json:"location_latitude"`
|
||||
LocationLongitude float64 `json:"location_longitude"`
|
||||
MapStyle string `json:"map_style"`
|
||||
|
||||
// Frontpage style (optional)
|
||||
FrontpageStyle string `json:"frontpage_style"`
|
||||
|
||||
// Theme (optional, can set later)
|
||||
PrimaryColor string `json:"primary_color"`
|
||||
SecondaryColor string `json:"secondary_color"`
|
||||
AccentColor string `json:"accent_color"`
|
||||
@@ -1726,7 +1790,9 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
TextColor string `json:"text_color"`
|
||||
FontHeading string `json:"font_heading"`
|
||||
FontBody string `json:"font_body"`
|
||||
SMTP *struct {
|
||||
|
||||
// SMTP optional
|
||||
SMTP *struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
@@ -1789,24 +1855,36 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
s.YoutubeURL = v
|
||||
}
|
||||
|
||||
// Always allow updating SMTP via setup when admin already exists (idempotent)
|
||||
// Gallery
|
||||
if body.GalleryURL != "" {
|
||||
s.GalleryURL = strings.TrimSpace(body.GalleryURL)
|
||||
}
|
||||
if body.GalleryLabel != "" {
|
||||
s.GalleryLabel = strings.TrimSpace(body.GalleryLabel)
|
||||
}
|
||||
// Frontpage style
|
||||
if body.FrontpageStyle != "" {
|
||||
s.FrontpageStyle = body.FrontpageStyle
|
||||
}
|
||||
// SMTP overrides from initial setup
|
||||
if body.SMTP != nil {
|
||||
if host := strings.TrimSpace(body.SMTP.Host); host != "" {
|
||||
s.SMTPHost = host
|
||||
if v := strings.TrimSpace(body.SMTP.Host); v != "" {
|
||||
s.SMTPHost = v
|
||||
}
|
||||
if body.SMTP.Port > 0 {
|
||||
s.SMTPPort = body.SMTP.Port
|
||||
}
|
||||
if u := strings.TrimSpace(body.SMTP.Username); u != "" {
|
||||
s.SMTPUser = u
|
||||
if v := strings.TrimSpace(body.SMTP.Username); v != "" {
|
||||
s.SMTPUser = v
|
||||
s.SMTPAuth = true
|
||||
}
|
||||
if p := body.SMTP.Password; p != "" {
|
||||
s.SMTPPassword = p
|
||||
if v := body.SMTP.Password; v != "" {
|
||||
s.SMTPPassword = v
|
||||
}
|
||||
if from := strings.TrimSpace(body.SMTP.From); from != "" {
|
||||
s.SMTPFrom = from
|
||||
if v := strings.TrimSpace(body.SMTP.From); v != "" {
|
||||
s.SMTPFrom = v
|
||||
}
|
||||
// Default FromName if empty
|
||||
if s.SMTPFromName == "" {
|
||||
s.SMTPFromName = "Fotbal Club"
|
||||
}
|
||||
@@ -1834,7 +1912,6 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger background prefetch and YouTube cache refresh when settings are updated post-setup
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
@@ -2035,7 +2112,7 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
s.ContactCountry = v
|
||||
}
|
||||
if v := strings.TrimSpace(body.ContactPhone); v != "" {
|
||||
s.ContactPhone = v
|
||||
s.ContactPhone = normalizePhone(v, body.ContactCountry)
|
||||
}
|
||||
if v := strings.TrimSpace(body.ContactEmail); v != "" {
|
||||
s.ContactEmail = v
|
||||
@@ -2530,7 +2607,25 @@ func (bc *BaseController) CreateArticle(c *gin.Context) {
|
||||
var cat models.Category
|
||||
if err := bc.DB.Where("name = ?", name).First(&cat).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// Create category with unique slug
|
||||
cat = models.Category{Name: name}
|
||||
s := makeSlug(cat.Name)
|
||||
if s == "" {
|
||||
s = "category"
|
||||
}
|
||||
orig := s
|
||||
for i := 0; i < 50; i++ {
|
||||
var cnt int64
|
||||
if err := bc.DB.Model(&models.Category{}).Where("slug = ?", s).Count(&cnt).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Chyba při kontrole jedinečnosti URL (kategorie)"})
|
||||
return
|
||||
}
|
||||
if cnt == 0 {
|
||||
break
|
||||
}
|
||||
s = fmt.Sprintf("%s-%d", orig, i+1)
|
||||
}
|
||||
cat.Slug = s
|
||||
if err := bc.DB.Create(&cat).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze vytvořit kategorii"})
|
||||
return
|
||||
@@ -2685,6 +2780,8 @@ func (bc *BaseController) UpdateArticle(c *gin.Context) {
|
||||
YouTubeVideoTitle *string `json:"youtube_video_title"`
|
||||
YouTubeVideoURL *string `json:"youtube_video_url"`
|
||||
YouTubeVideoThumbnail *string `json:"youtube_video_thumbnail"`
|
||||
// Attachments array from frontend, stored as JSON string in model
|
||||
Attachments []map[string]any `json:"attachments"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
@@ -2729,7 +2826,25 @@ func (bc *BaseController) UpdateArticle(c *gin.Context) {
|
||||
var cat models.Category
|
||||
if err := bc.DB.Where("name = ?", name).First(&cat).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// Create category with unique slug
|
||||
cat = models.Category{Name: name}
|
||||
s := makeSlug(cat.Name)
|
||||
if s == "" {
|
||||
s = "category"
|
||||
}
|
||||
orig := s
|
||||
for i := 0; i < 50; i++ {
|
||||
var cnt int64
|
||||
if err := bc.DB.Model(&models.Category{}).Where("slug = ?", s).Count(&cnt).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Chyba při kontrole jedinečnosti URL (kategorie)"})
|
||||
return
|
||||
}
|
||||
if cnt == 0 {
|
||||
break
|
||||
}
|
||||
s = fmt.Sprintf("%s-%d", orig, i+1)
|
||||
}
|
||||
cat.Slug = s
|
||||
if err := bc.DB.Create(&cat).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze vytvořit kategorii"})
|
||||
return
|
||||
@@ -2758,8 +2873,25 @@ func (bc *BaseController) UpdateArticle(c *gin.Context) {
|
||||
art.Featured = *body.Featured
|
||||
}
|
||||
if body.Slug != nil {
|
||||
art.Slug = strings.TrimSpace(*body.Slug)
|
||||
}
|
||||
s := strings.TrimSpace(*body.Slug)
|
||||
if s == "" {
|
||||
s = makeSlug(art.Title)
|
||||
}
|
||||
// Ensure slug is unique across other articles
|
||||
orig := s
|
||||
for i := 0; i < 50; i++ {
|
||||
var cnt int64
|
||||
if err := bc.DB.Model(&models.Article{}).Where("slug = ? AND id != ?", s, art.ID).Count(&cnt).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Chyba při kontrole jedinečnosti URL"})
|
||||
return
|
||||
}
|
||||
if cnt == 0 {
|
||||
break
|
||||
}
|
||||
s = fmt.Sprintf("%s-%d", orig, i+1)
|
||||
}
|
||||
art.Slug = s
|
||||
}
|
||||
if body.SeoTitle != nil {
|
||||
art.SEOTitle = strings.TrimSpace(*body.SeoTitle)
|
||||
}
|
||||
@@ -2791,6 +2923,12 @@ func (bc *BaseController) UpdateArticle(c *gin.Context) {
|
||||
if body.YouTubeVideoThumbnail != nil {
|
||||
art.YouTubeVideoThumbnail = strings.TrimSpace(*body.YouTubeVideoThumbnail)
|
||||
}
|
||||
// Attachments
|
||||
if len(body.Attachments) > 0 {
|
||||
if b, err := json.Marshal(body.Attachments); err == nil {
|
||||
art.Attachments = string(b)
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-fill SEO if still empty after updates
|
||||
if strings.TrimSpace(art.SEOTitle) == "" && strings.TrimSpace(art.Title) != "" {
|
||||
@@ -2801,7 +2939,7 @@ func (bc *BaseController) UpdateArticle(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := bc.DB.Save(&art).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze uložit změny"})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze uložit změny", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if art.ImageURL == "" {
|
||||
@@ -3108,13 +3246,46 @@ func (bc *BaseController) CreatePlayer(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": err.Error()})
|
||||
return
|
||||
}
|
||||
// Auto-split full name if last name missing and first contains spaces; drop middle names
|
||||
first := strings.TrimSpace(body.FirstName)
|
||||
last := strings.TrimSpace(body.LastName)
|
||||
if last == "" && strings.Contains(first, " ") {
|
||||
parts := strings.Fields(first)
|
||||
if len(parts) >= 2 {
|
||||
first = parts[0]
|
||||
last = parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
if first == "" || last == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Jméno a příjmení jsou povinné"})
|
||||
return
|
||||
}
|
||||
// Validate numeric limits
|
||||
if body.JerseyNumber != nil {
|
||||
if *body.JerseyNumber < 0 || *body.JerseyNumber > 99 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Číslo dresu musí být v rozmezí 0–99"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.Height != nil {
|
||||
if *body.Height < 50 || *body.Height > 250 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Výška musí být v rozmezí 50–250 cm"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.Weight != nil {
|
||||
if *body.Weight < 30 || *body.Weight > 200 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Váha musí být v rozmezí 30–200 kg"})
|
||||
return
|
||||
}
|
||||
}
|
||||
p := models.Player{
|
||||
FirstName: strings.TrimSpace(body.FirstName),
|
||||
LastName: strings.TrimSpace(body.LastName),
|
||||
FirstName: first,
|
||||
LastName: last,
|
||||
Position: strings.TrimSpace(body.Position),
|
||||
Nationality: strings.TrimSpace(body.Nationality),
|
||||
Email: strings.TrimSpace(body.Email),
|
||||
Phone: strings.TrimSpace(body.Phone),
|
||||
Phone: normalizePhone(body.Phone, ""),
|
||||
ImageURL: strings.TrimSpace(body.ImageURL),
|
||||
}
|
||||
if body.TeamID != nil {
|
||||
@@ -3204,6 +3375,18 @@ func (bc *BaseController) UpdatePlayer(c *gin.Context) {
|
||||
if body.LastName != nil {
|
||||
p.LastName = strings.TrimSpace(*body.LastName)
|
||||
}
|
||||
// Auto-split if last name empty and first contains spaces; ensure both present
|
||||
if strings.TrimSpace(p.LastName) == "" && strings.Contains(strings.TrimSpace(p.FirstName), " ") {
|
||||
parts := strings.Fields(strings.TrimSpace(p.FirstName))
|
||||
if len(parts) >= 2 {
|
||||
p.FirstName = parts[0]
|
||||
p.LastName = parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(p.FirstName) == "" || strings.TrimSpace(p.LastName) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Jméno a příjmení jsou povinné"})
|
||||
return
|
||||
}
|
||||
if body.Position != nil {
|
||||
p.Position = strings.TrimSpace(*body.Position)
|
||||
}
|
||||
@@ -3220,15 +3403,27 @@ func (bc *BaseController) UpdatePlayer(c *gin.Context) {
|
||||
p.TeamID = *body.TeamID
|
||||
}
|
||||
if body.JerseyNumber != nil {
|
||||
if *body.JerseyNumber < 0 || *body.JerseyNumber > 99 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Číslo dresu musí být v rozmezí 0–99"})
|
||||
return
|
||||
}
|
||||
p.JerseyNumber = *body.JerseyNumber
|
||||
}
|
||||
if body.Nationality != nil {
|
||||
p.Nationality = strings.TrimSpace(*body.Nationality)
|
||||
}
|
||||
if body.Height != nil {
|
||||
if *body.Height < 50 || *body.Height > 250 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Výška musí být v rozmezí 50–250 cm"})
|
||||
return
|
||||
}
|
||||
p.Height = *body.Height
|
||||
}
|
||||
if body.Weight != nil {
|
||||
if *body.Weight < 30 || *body.Weight > 200 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Váha musí být v rozmezí 30–200 kg"})
|
||||
return
|
||||
}
|
||||
p.Weight = *body.Weight
|
||||
}
|
||||
if body.IsActive != nil {
|
||||
@@ -3238,7 +3433,7 @@ func (bc *BaseController) UpdatePlayer(c *gin.Context) {
|
||||
p.Email = strings.TrimSpace(*body.Email)
|
||||
}
|
||||
if body.Phone != nil {
|
||||
p.Phone = strings.TrimSpace(*body.Phone)
|
||||
p.Phone = normalizePhone(*body.Phone, "")
|
||||
}
|
||||
if body.ImageURL != nil {
|
||||
p.ImageURL = strings.TrimSpace(*body.ImageURL)
|
||||
@@ -4067,7 +4262,7 @@ func (bc *BaseController) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
var body reqBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": err.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Neplatná data", "detail": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4336,7 +4531,12 @@ func (bc *BaseController) UpdateSettings(c *gin.Context) {
|
||||
s.ContactCountry = strings.TrimSpace(*body.ContactCountry)
|
||||
}
|
||||
if body.ContactPhone != nil {
|
||||
s.ContactPhone = strings.TrimSpace(*body.ContactPhone)
|
||||
v := strings.TrimSpace(*body.ContactPhone)
|
||||
country := s.ContactCountry
|
||||
if body.ContactCountry != nil {
|
||||
country = strings.TrimSpace(*body.ContactCountry)
|
||||
}
|
||||
s.ContactPhone = normalizePhone(v, country)
|
||||
}
|
||||
if body.ContactEmail != nil {
|
||||
s.ContactEmail = strings.TrimSpace(*body.ContactEmail)
|
||||
|
||||
Reference in New Issue
Block a user