This commit is contained in:
Tomas Dvorak
2026-03-13 14:34:19 +01:00
parent 84a8acf944
commit 30d70a6aeb
126 changed files with 27297 additions and 29069 deletions
+319 -305
View File
@@ -1,364 +1,378 @@
package controllers
import (
"encoding/xml"
"fmt"
"net/http"
"strings"
"time"
"encoding/xml"
"fmt"
"net/http"
"strings"
"time"
"fotbal-club/internal/models"
"fotbal-club/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// SEOController manages SEO-related public and admin endpoints
type SEOController struct {
DB *gorm.DB
DB *gorm.DB
}
func NewSEOController(db *gorm.DB) *SEOController {
// Ensure settings table has the SEO fields
_ = db.AutoMigrate(&models.Settings{})
return &SEOController{DB: db}
return &SEOController{DB: db}
}
// -------- Public Endpoints --------
// GetPublicSEO returns site-wide SEO defaults derived from Settings
func (s *SEOController) GetPublicSEO(c *gin.Context) {
var settings models.Settings
_ = s.DB.First(&settings).Error // ignore not found; return defaults
c.JSON(http.StatusOK, gin.H{
"site_title": settings.SiteTitle,
"site_description": settings.SiteDescription,
"meta_keywords": settings.MetaKeywords,
"default_og_image_url": settings.DefaultOGImageURL,
"twitter_handle": settings.TwitterHandle,
"canonical_base_url": settings.CanonicalBaseURL,
"additional_meta": settings.AdditionalMeta,
"enable_indexing": settings.EnableIndexing,
})
var settings models.Settings
_ = s.DB.First(&settings).Error // ignore not found; return defaults
c.JSON(http.StatusOK, gin.H{
"site_title": settings.SiteTitle,
"site_description": settings.SiteDescription,
"meta_keywords": settings.MetaKeywords,
"default_og_image_url": settings.DefaultOGImageURL,
"twitter_handle": settings.TwitterHandle,
"canonical_base_url": settings.CanonicalBaseURL,
"additional_meta": settings.AdditionalMeta,
"enable_indexing": settings.EnableIndexing,
})
}
// robots.txt dynamic based on settings.EnableIndexing
func (s *SEOController) GetRobotsTXT(c *gin.Context) {
var settings models.Settings
_ = s.DB.First(&settings).Error
allow := settings.EnableIndexing
var b strings.Builder
b.WriteString("# robots.txt for " + c.Request.Host + "\n")
b.WriteString("# Generated: " + time.Now().UTC().Format(time.RFC1123) + "\n\n")
if allow {
// Allow general crawlers
b.WriteString("User-agent: *\n")
b.WriteString("Allow: /\n")
b.WriteString("Disallow: /admin/\n")
b.WriteString("Disallow: /api/\n")
b.WriteString("Disallow: /login\n")
b.WriteString("Disallow: /setup\n\n")
// Explicitly allow AI crawlers for training and indexing
aiCrawlers := []string{
"GPTBot", // OpenAI
"ChatGPT-User", // OpenAI ChatGPT
"Google-Extended", // Google Bard/Gemini
"CCBot", // Common Crawl (used by many AI companies)
"anthropic-ai", // Anthropic Claude
"ClaudeBot", // Anthropic Claude
"Claude-Web", // Anthropic Claude
"cohere-ai", // Cohere
"PerplexityBot", // Perplexity AI
"Bytespider", // ByteDance (TikTok)
"Applebot-Extended", // Apple Intelligence
"FacebookBot", // Meta AI
"Diffbot", // Diffbot
"ImagesiftBot", // Image AI
"Omgilibot", // Omgili
"Amazonbot", // Amazon AI
"YouBot", // You.com
}
for _, bot := range aiCrawlers {
b.WriteString("User-agent: " + bot + "\n")
b.WriteString("Allow: /\n")
b.WriteString("Disallow: /admin/\n")
b.WriteString("Disallow: /api/\n\n")
}
} else {
b.WriteString("User-agent: *\n")
b.WriteString("Disallow: /\n\n")
}
if settings.CanonicalBaseURL != "" {
base := strings.TrimRight(settings.CanonicalBaseURL, "/")
b.WriteString("Sitemap: ")
b.WriteString(base)
b.WriteString("/sitemap.xml\n")
}
// Conditional GET based on settings update time
last := settings.UpdatedAt
if last.IsZero() {
last = time.Now().Add(-1 * time.Hour)
}
ifModifiedSince := c.GetHeader("If-Modified-Since")
if ifModifiedSince != "" {
if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil {
if !last.IsZero() && !last.After(t) {
c.Status(http.StatusNotModified)
return
}
}
}
c.Header("Last-Modified", last.UTC().Format(http.TimeFormat))
c.Header("ETag", fmt.Sprintf("W/\"%d\"", last.Unix()))
c.Header("Content-Type", "text/plain; charset=utf-8")
c.Header("Cache-Control", "public, max-age=3600")
c.String(http.StatusOK, b.String())
var settings models.Settings
_ = s.DB.First(&settings).Error
allow := settings.EnableIndexing
var b strings.Builder
b.WriteString("# robots.txt for " + c.Request.Host + "\n")
b.WriteString("# Generated: " + time.Now().UTC().Format(time.RFC1123) + "\n\n")
if allow {
// Allow general crawlers
b.WriteString("User-agent: *\n")
b.WriteString("Allow: /\n")
b.WriteString("Disallow: /admin/\n")
b.WriteString("Disallow: /api/\n")
b.WriteString("Disallow: /login\n")
b.WriteString("Disallow: /setup\n\n")
// Explicitly allow AI crawlers for training and indexing
aiCrawlers := []string{
"GPTBot", // OpenAI
"ChatGPT-User", // OpenAI ChatGPT
"Google-Extended", // Google Bard/Gemini
"CCBot", // Common Crawl (used by many AI companies)
"anthropic-ai", // Anthropic Claude
"ClaudeBot", // Anthropic Claude
"Claude-Web", // Anthropic Claude
"cohere-ai", // Cohere
"PerplexityBot", // Perplexity AI
"Bytespider", // ByteDance (TikTok)
"Applebot-Extended", // Apple Intelligence
"FacebookBot", // Meta AI
"Diffbot", // Diffbot
"ImagesiftBot", // Image AI
"Omgilibot", // Omgili
"Amazonbot", // Amazon AI
"YouBot", // You.com
}
for _, bot := range aiCrawlers {
b.WriteString("User-agent: " + bot + "\n")
b.WriteString("Allow: /\n")
b.WriteString("Disallow: /admin/\n")
b.WriteString("Disallow: /api/\n\n")
}
} else {
b.WriteString("User-agent: *\n")
b.WriteString("Disallow: /\n\n")
}
if settings.CanonicalBaseURL != "" {
base := strings.TrimRight(settings.CanonicalBaseURL, "/")
b.WriteString("Sitemap: ")
b.WriteString(base)
b.WriteString("/sitemap.xml\n")
}
// Conditional GET based on settings update time
last := settings.UpdatedAt
if last.IsZero() {
last = time.Now().Add(-1 * time.Hour)
}
ifModifiedSince := c.GetHeader("If-Modified-Since")
if ifModifiedSince != "" {
if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil {
if !last.IsZero() && !last.After(t) {
c.Status(http.StatusNotModified)
return
}
}
}
c.Header("Last-Modified", last.UTC().Format(http.TimeFormat))
c.Header("ETag", fmt.Sprintf("W/\"%d\"", last.Unix()))
c.Header("Content-Type", "text/plain; charset=utf-8")
c.Header("Cache-Control", "public, max-age=3600")
c.String(http.StatusOK, b.String())
}
// sitemap.xml built from content in DB
func (s *SEOController) GetSitemapXML(c *gin.Context) {
type imageEntry struct {
XMLName xml.Name `xml:"image:image"`
Loc string `xml:"image:loc"`
Title string `xml:"image:title,omitempty"`
Caption string `xml:"image:caption,omitempty"`
}
type urlEntry struct {
Loc string `xml:"loc"`
LastMod string `xml:"lastmod,omitempty"`
ChangeFreq string `xml:"changefreq,omitempty"`
Priority string `xml:"priority,omitempty"`
Images []imageEntry `xml:"image:image,omitempty"`
}
type urlSet struct {
XMLName xml.Name `xml:"urlset"`
Xmlns string `xml:"xmlns,attr"`
XmlnsImg string `xml:"xmlns:image,attr"`
URLs []urlEntry `xml:"url"`
}
type imageEntry struct {
XMLName xml.Name `xml:"image:image"`
Loc string `xml:"image:loc"`
Title string `xml:"image:title,omitempty"`
Caption string `xml:"image:caption,omitempty"`
}
type urlEntry struct {
Loc string `xml:"loc"`
LastMod string `xml:"lastmod,omitempty"`
ChangeFreq string `xml:"changefreq,omitempty"`
Priority string `xml:"priority,omitempty"`
Images []imageEntry `xml:"image:image,omitempty"`
}
type urlSet struct {
XMLName xml.Name `xml:"urlset"`
Xmlns string `xml:"xmlns,attr"`
XmlnsImg string `xml:"xmlns:image,attr"`
URLs []urlEntry `xml:"url"`
}
var settings models.Settings
_ = s.DB.First(&settings).Error
base := strings.TrimRight(settings.CanonicalBaseURL, "/")
if base == "" {
// fallback to request scheme+host
sch := "http"
if c.Request.TLS != nil {
sch = "https"
}
base = sch + "://" + c.Request.Host
}
var settings models.Settings
_ = s.DB.First(&settings).Error
base := strings.TrimRight(settings.CanonicalBaseURL, "/")
if base == "" {
// fallback to request scheme+host
sch := "http"
if c.Request.TLS != nil {
sch = "https"
}
base = sch + "://" + c.Request.Host
}
// Home
urls := []urlEntry{{
Loc: base + "/",
ChangeFreq: "daily",
Priority: "0.9",
}}
// Blog listing and key static pages
staticPaths := []string{
"/blog",
"/o-klubu",
"/kalendar",
"/tabulky",
"/sponzori",
"/kontakt",
}
for _, p := range staticPaths {
urls = append(urls, urlEntry{
Loc: base + p,
ChangeFreq: "weekly",
Priority: "0.6",
})
}
// Home
urls := []urlEntry{{
Loc: base + "/",
ChangeFreq: "daily",
Priority: "0.9",
}}
// Blog listing and key static pages
staticPaths := []string{
"/blog",
"/o-klubu",
"/kalendar",
"/tabulky",
"/sponzori",
"/kontakt",
}
for _, p := range staticPaths {
urls = append(urls, urlEntry{
Loc: base + p,
ChangeFreq: "weekly",
Priority: "0.6",
})
}
// Articles (published)
var articles []models.Article
_ = s.DB.Where("published = ?", true).Order("updated_at DESC").Limit(5000).Find(&articles).Error
for _, a := range articles {
last := a.UpdatedAt
if a.PublishedAt != nil && a.PublishedAt.After(last) {
last = *a.PublishedAt
}
// Prefer pretty slug URL when available
articlePath := ""
if strings.TrimSpace(a.Slug) != "" {
articlePath = "/blog/" + strings.TrimSpace(a.Slug)
} else {
articlePath = "/articles/" + intToString(int(a.ID))
}
img := strings.TrimSpace(a.ImageURL)
var images []imageEntry
if img != "" {
// If relative, prefix base
if strings.HasPrefix(img, "/") {
img = base + img
}
images = []imageEntry{{
Loc: img,
Title: a.SEOTitle,
}}
}
urls = append(urls, urlEntry{
Loc: base + articlePath,
LastMod: last.UTC().Format(time.RFC3339),
ChangeFreq: "weekly",
Priority: "0.7",
Images: images,
})
}
// Articles (published)
var articles []models.Article
_ = s.DB.Where("published = ?", true).Order("updated_at DESC").Limit(5000).Find(&articles).Error
for _, a := range articles {
last := a.UpdatedAt
if a.PublishedAt != nil && a.PublishedAt.After(last) {
last = *a.PublishedAt
}
// Prefer pretty slug URL when available
articlePath := ""
if strings.TrimSpace(a.Slug) != "" {
articlePath = "/blog/" + strings.TrimSpace(a.Slug)
} else {
articlePath = "/articles/" + intToString(int(a.ID))
}
img := strings.TrimSpace(a.ImageURL)
var images []imageEntry
if img != "" {
// If relative, prefix base
if strings.HasPrefix(img, "/") {
img = base + img
}
images = []imageEntry{{
Loc: img,
Title: a.SEOTitle,
}}
}
urls = append(urls, urlEntry{
Loc: base + articlePath,
LastMod: last.UTC().Format(time.RFC3339),
ChangeFreq: "weekly",
Priority: "0.7",
Images: images,
})
}
// Categories: include as filtered blog listing if categories exist
var categories []models.Category
_ = s.DB.Order("updated_at DESC").Limit(2000).Find(&categories).Error
for _, cat := range categories {
urls = append(urls, urlEntry{
Loc: base + "/blog?category=" + intToString(int(cat.ID)),
ChangeFreq: "weekly",
Priority: "0.5",
})
}
// Categories: include as filtered blog listing if categories exist
var categories []models.Category
_ = s.DB.Order("updated_at DESC").Limit(2000).Find(&categories).Error
for _, cat := range categories {
urls = append(urls, urlEntry{
Loc: base + "/blog?category=" + intToString(int(cat.ID)),
ChangeFreq: "weekly",
Priority: "0.5",
})
}
// Teams
var teams []models.Team
_ = s.DB.Where("is_active = ?", true).Find(&teams).Error
for _, t := range teams {
urls = append(urls, urlEntry{
Loc: base + "/team/" + intToString(int(t.ID)),
ChangeFreq: "weekly",
Priority: "0.5",
})
}
// Teams
var teams []models.Team
_ = s.DB.Where("is_active = ?", true).Find(&teams).Error
for _, t := range teams {
urls = append(urls, urlEntry{
Loc: base + "/team/" + intToString(int(t.ID)),
ChangeFreq: "weekly",
Priority: "0.5",
})
}
out := urlSet{
Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
XmlnsImg: "http://www.google.com/schemas/sitemap-image/1.1",
URLs: urls,
}
out := urlSet{
Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
XmlnsImg: "http://www.google.com/schemas/sitemap-image/1.1",
URLs: urls,
}
// Conditional GET: based on latest of settings/articles/categories update time
latest := settings.UpdatedAt
if len(articles) > 0 && articles[0].UpdatedAt.After(latest) {
latest = articles[0].UpdatedAt
}
if len(categories) > 0 && categories[0].UpdatedAt.After(latest) {
latest = categories[0].UpdatedAt
}
if latest.IsZero() {
latest = time.Now().Add(-1 * time.Hour)
}
ifModifiedSince := c.GetHeader("If-Modified-Since")
if ifModifiedSince != "" {
if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil {
if !latest.After(t) {
c.Status(http.StatusNotModified)
return
}
}
}
c.Header("Last-Modified", latest.UTC().Format(http.TimeFormat))
c.Header("ETag", fmt.Sprintf("W/\"%d\"", latest.Unix()))
c.Header("Content-Type", "application/xml; charset=utf-8")
c.Header("Cache-Control", "public, max-age=3600")
c.XML(http.StatusOK, out)
// Conditional GET: based on latest of settings/articles/categories update time
latest := settings.UpdatedAt
if len(articles) > 0 && articles[0].UpdatedAt.After(latest) {
latest = articles[0].UpdatedAt
}
if len(categories) > 0 && categories[0].UpdatedAt.After(latest) {
latest = categories[0].UpdatedAt
}
if latest.IsZero() {
latest = time.Now().Add(-1 * time.Hour)
}
ifModifiedSince := c.GetHeader("If-Modified-Since")
if ifModifiedSince != "" {
if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil {
if !latest.After(t) {
c.Status(http.StatusNotModified)
return
}
}
}
c.Header("Last-Modified", latest.UTC().Format(http.TimeFormat))
c.Header("ETag", fmt.Sprintf("W/\"%d\"", latest.Unix()))
c.Header("Content-Type", "application/xml; charset=utf-8")
c.Header("Cache-Control", "public, max-age=3600")
c.XML(http.StatusOK, out)
}
// -------- Admin Endpoints --------
type seoUpdate struct {
SiteTitle *string `json:"site_title"`
SiteDescription *string `json:"site_description"`
MetaKeywords *string `json:"meta_keywords"`
DefaultOGImageURL *string `json:"default_og_image_url"`
TwitterHandle *string `json:"twitter_handle"`
CanonicalBaseURL *string `json:"canonical_base_url"`
AdditionalMeta *string `json:"additional_meta"`
EnableIndexing *bool `json:"enable_indexing"`
SiteTitle *string `json:"site_title"`
SiteDescription *string `json:"site_description"`
MetaKeywords *string `json:"meta_keywords"`
DefaultOGImageURL *string `json:"default_og_image_url"`
TwitterHandle *string `json:"twitter_handle"`
CanonicalBaseURL *string `json:"canonical_base_url"`
AdditionalMeta *string `json:"additional_meta"`
EnableIndexing *bool `json:"enable_indexing"`
}
// GetSEOSettings returns only the SEO-related fields
func (s *SEOController) GetSEOSettings(c *gin.Context) {
var settings models.Settings
_ = s.DB.First(&settings).Error
c.JSON(http.StatusOK, gin.H{
"site_title": settings.SiteTitle,
"site_description": settings.SiteDescription,
"meta_keywords": settings.MetaKeywords,
"default_og_image_url": settings.DefaultOGImageURL,
"twitter_handle": settings.TwitterHandle,
"canonical_base_url": settings.CanonicalBaseURL,
"additional_meta": settings.AdditionalMeta,
"enable_indexing": settings.EnableIndexing,
})
var settings models.Settings
_ = s.DB.First(&settings).Error
c.JSON(http.StatusOK, gin.H{
"site_title": settings.SiteTitle,
"site_description": settings.SiteDescription,
"meta_keywords": settings.MetaKeywords,
"default_og_image_url": settings.DefaultOGImageURL,
"twitter_handle": settings.TwitterHandle,
"canonical_base_url": settings.CanonicalBaseURL,
"additional_meta": settings.AdditionalMeta,
"enable_indexing": settings.EnableIndexing,
})
}
// UpdateSEOSettings upserts SEO fields on the singleton Settings record
func (s *SEOController) UpdateSEOSettings(c *gin.Context) {
var body seoUpdate
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var body seoUpdate
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var settings models.Settings
if err := s.DB.First(&settings).Error; err != nil {
// create empty
settings = models.Settings{}
_ = s.DB.Create(&settings).Error
}
var settings models.Settings
if err := s.DB.First(&settings).Error; err != nil {
// create empty
settings = models.Settings{}
_ = s.DB.Create(&settings).Error
}
// Update only provided fields
updates := map[string]interface{}{}
if body.SiteTitle != nil { updates["site_title"] = strings.TrimSpace(*body.SiteTitle) }
if body.SiteDescription != nil { updates["site_description"] = strings.TrimSpace(*body.SiteDescription) }
if body.MetaKeywords != nil { updates["meta_keywords"] = strings.TrimSpace(*body.MetaKeywords) }
if body.DefaultOGImageURL != nil { updates["default_og_image_url"] = strings.TrimSpace(*body.DefaultOGImageURL) }
if body.TwitterHandle != nil { updates["twitter_handle"] = strings.TrimSpace(*body.TwitterHandle) }
if body.CanonicalBaseURL != nil { updates["canonical_base_url"] = strings.TrimSpace(*body.CanonicalBaseURL) }
if body.AdditionalMeta != nil { updates["additional_meta"] = *body.AdditionalMeta }
if body.EnableIndexing != nil { updates["enable_indexing"] = *body.EnableIndexing }
// Update only provided fields
updates := map[string]interface{}{}
if body.SiteTitle != nil {
updates["site_title"] = strings.TrimSpace(*body.SiteTitle)
}
if body.SiteDescription != nil {
updates["site_description"] = strings.TrimSpace(*body.SiteDescription)
}
if body.MetaKeywords != nil {
updates["meta_keywords"] = strings.TrimSpace(*body.MetaKeywords)
}
if body.DefaultOGImageURL != nil {
updates["default_og_image_url"] = strings.TrimSpace(*body.DefaultOGImageURL)
}
if body.TwitterHandle != nil {
updates["twitter_handle"] = strings.TrimSpace(*body.TwitterHandle)
}
if body.CanonicalBaseURL != nil {
updates["canonical_base_url"] = strings.TrimSpace(*body.CanonicalBaseURL)
}
if body.AdditionalMeta != nil {
updates["additional_meta"] = *body.AdditionalMeta
}
if body.EnableIndexing != nil {
updates["enable_indexing"] = *body.EnableIndexing
}
if len(updates) > 0 {
if err := s.DB.Model(&settings).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Nelze uložit SEO nastavení"})
return
}
}
if len(updates) > 0 {
if err := s.DB.Model(&settings).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Nelze uložit SEO nastavení"})
return
}
}
c.JSON(http.StatusOK, gin.H{"ok": true})
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// helper: fast int to string
func intToString(i int) string {
// simple and fast conversion
return strconvItoa(i)
// simple and fast conversion
return strconvItoa(i)
}
// local minimal itoa to avoid importing fmt
func strconvItoa(i int) string {
// handle zero
if i == 0 {
return "0"
}
neg := false
if i < 0 {
neg = true
i = -i
}
var b [20]byte
pos := len(b)
for i > 0 {
pos--
b[pos] = byte('0' + i%10)
i /= 10
}
if neg {
pos--
b[pos] = '-'
}
return string(b[pos:])
// handle zero
if i == 0 {
return "0"
}
neg := false
if i < 0 {
neg = true
i = -i
}
var b [20]byte
pos := len(b)
for i > 0 {
pos--
b[pos] = byte('0' + i%10)
i /= 10
}
if neg {
pos--
b[pos] = '-'
}
return string(b[pos:])
}
+31
View File
@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package eshopreporting
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}
@@ -0,0 +1,159 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: eshop_reporting.sql
package eshopreporting
import (
"context"
"database/sql"
)
const listOrderStatusBreakdown = `-- name: ListOrderStatusBreakdown :many
SELECT
status,
COUNT(*)::bigint AS order_count
FROM eshop_orders
WHERE deleted_at IS NULL
GROUP BY status
ORDER BY order_count DESC, status ASC
`
type ListOrderStatusBreakdownRow struct {
Status sql.NullString `json:"status"`
OrderCount int64 `json:"order_count"`
}
func (q *Queries) ListOrderStatusBreakdown(ctx context.Context) ([]ListOrderStatusBreakdownRow, error) {
rows, err := q.db.QueryContext(ctx, listOrderStatusBreakdown)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListOrderStatusBreakdownRow
for rows.Next() {
var i ListOrderStatusBreakdownRow
if err := rows.Scan(&i.Status, &i.OrderCount); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listRecentOrderSummaries = `-- name: ListRecentOrderSummaries :many
SELECT
o.id,
o.order_number,
o.status,
o.total_amount_cents,
o.currency,
o.shipping_method,
o.shipping_price_cents,
o.created_at,
COALESCE(p.status, '') AS payment_status,
COALESCE(s.status, '') AS shipping_status
FROM eshop_orders AS o
LEFT JOIN LATERAL (
SELECT ep.status
FROM eshop_payments AS ep
WHERE ep.order_id = o.id
AND ep.deleted_at IS NULL
ORDER BY ep.created_at DESC
LIMIT 1
) AS p ON TRUE
LEFT JOIN LATERAL (
SELECT sl.status
FROM eshop_shipping_labels AS sl
WHERE sl.order_id = o.id
AND sl.deleted_at IS NULL
ORDER BY sl.created_at DESC
LIMIT 1
) AS s ON TRUE
WHERE o.deleted_at IS NULL
ORDER BY o.created_at DESC
LIMIT $1
`
type ListRecentOrderSummariesRow struct {
ID int32 `json:"id"`
OrderNumber string `json:"order_number"`
Status sql.NullString `json:"status"`
TotalAmountCents int64 `json:"total_amount_cents"`
Currency sql.NullString `json:"currency"`
ShippingMethod sql.NullString `json:"shipping_method"`
ShippingPriceCents sql.NullInt64 `json:"shipping_price_cents"`
CreatedAt sql.NullTime `json:"created_at"`
PaymentStatus string `json:"payment_status"`
ShippingStatus string `json:"shipping_status"`
}
func (q *Queries) ListRecentOrderSummaries(ctx context.Context, limit int32) ([]ListRecentOrderSummariesRow, error) {
rows, err := q.db.QueryContext(ctx, listRecentOrderSummaries, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListRecentOrderSummariesRow
for rows.Next() {
var i ListRecentOrderSummariesRow
if err := rows.Scan(
&i.ID,
&i.OrderNumber,
&i.Status,
&i.TotalAmountCents,
&i.Currency,
&i.ShippingMethod,
&i.ShippingPriceCents,
&i.CreatedAt,
&i.PaymentStatus,
&i.ShippingStatus,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const summarizeOrderRevenue = `-- name: SummarizeOrderRevenue :one
SELECT
COUNT(*)::bigint AS order_count,
COALESCE(SUM(total_amount_cents), 0)::bigint AS gross_revenue_cents,
COALESCE(SUM(shipping_price_cents), 0)::bigint AS shipping_revenue_cents
FROM eshop_orders
WHERE deleted_at IS NULL
AND created_at >= $1
AND created_at < $2
`
type SummarizeOrderRevenueParams struct {
CreatedAt sql.NullTime `json:"created_at"`
CreatedAt_2 sql.NullTime `json:"created_at_2"`
}
type SummarizeOrderRevenueRow struct {
OrderCount int64 `json:"order_count"`
GrossRevenueCents int64 `json:"gross_revenue_cents"`
ShippingRevenueCents int64 `json:"shipping_revenue_cents"`
}
func (q *Queries) SummarizeOrderRevenue(ctx context.Context, arg SummarizeOrderRevenueParams) (SummarizeOrderRevenueRow, error) {
row := q.db.QueryRowContext(ctx, summarizeOrderRevenue, arg.CreatedAt, arg.CreatedAt_2)
var i SummarizeOrderRevenueRow
err := row.Scan(&i.OrderCount, &i.GrossRevenueCents, &i.ShippingRevenueCents)
return i, err
}
+165
View File
@@ -0,0 +1,165 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package eshopreporting
import (
"database/sql"
)
type EshopCart struct {
ID int32 `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at"`
UserID sql.NullInt32 `json:"user_id"`
SessionToken sql.NullString `json:"session_token"`
Currency sql.NullString `json:"currency"`
Completed sql.NullBool `json:"completed"`
}
type EshopCartItem struct {
ID int32 `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at"`
CartID int32 `json:"cart_id"`
ProductID int32 `json:"product_id"`
VariantID sql.NullInt32 `json:"variant_id"`
Quantity int32 `json:"quantity"`
UnitPriceCents int64 `json:"unit_price_cents"`
Currency sql.NullString `json:"currency"`
}
type EshopOrder struct {
ID int32 `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at"`
OrderNumber string `json:"order_number"`
UserID sql.NullInt32 `json:"user_id"`
SessionToken sql.NullString `json:"session_token"`
Email sql.NullString `json:"email"`
FirstName sql.NullString `json:"first_name"`
LastName sql.NullString `json:"last_name"`
BillingAddressJson sql.NullString `json:"billing_address_json"`
ShippingAddressJson sql.NullString `json:"shipping_address_json"`
Status sql.NullString `json:"status"`
TotalAmountCents int64 `json:"total_amount_cents"`
Currency sql.NullString `json:"currency"`
ShippingMethod sql.NullString `json:"shipping_method"`
ShippingPriceCents sql.NullInt64 `json:"shipping_price_cents"`
ShippingDataJson sql.NullString `json:"shipping_data_json"`
MetadataJson sql.NullString `json:"metadata_json"`
}
type EshopOrderItem struct {
ID int32 `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at"`
OrderID int32 `json:"order_id"`
ProductID int32 `json:"product_id"`
VariantID sql.NullInt32 `json:"variant_id"`
Name string `json:"name"`
Sku sql.NullString `json:"sku"`
Quantity int32 `json:"quantity"`
UnitPriceCents int64 `json:"unit_price_cents"`
Currency sql.NullString `json:"currency"`
VatRate sql.NullString `json:"vat_rate"`
}
type EshopPayment struct {
ID int32 `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at"`
OrderID int32 `json:"order_id"`
Provider sql.NullString `json:"provider"`
ProviderPaymentID sql.NullString `json:"provider_payment_id"`
Status sql.NullString `json:"status"`
AmountCents int64 `json:"amount_cents"`
Currency sql.NullString `json:"currency"`
RawPayloadJson sql.NullString `json:"raw_payload_json"`
}
type EshopProduct struct {
ID int32 `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at"`
Slug string `json:"slug"`
Name string `json:"name"`
ShortDescription sql.NullString `json:"short_description"`
DescriptionHtml sql.NullString `json:"description_html"`
PriceCents int64 `json:"price_cents"`
Currency sql.NullString `json:"currency"`
VatRate sql.NullString `json:"vat_rate"`
Active sql.NullBool `json:"active"`
StockMode sql.NullString `json:"stock_mode"`
DefaultImageUrl sql.NullString `json:"default_image_url"`
GalleryJson sql.NullString `json:"gallery_json"`
Tags sql.NullString `json:"tags"`
MetadataJson sql.NullString `json:"metadata_json"`
CategoryID sql.NullInt32 `json:"category_id"`
}
type EshopProductCategory struct {
ID int32 `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at"`
Slug string `json:"slug"`
Name string `json:"name"`
ParentID sql.NullInt32 `json:"parent_id"`
DisplayOrder sql.NullInt32 `json:"display_order"`
Active sql.NullBool `json:"active"`
}
type EshopProductVariant struct {
ID int32 `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at"`
ProductID int32 `json:"product_id"`
Sku sql.NullString `json:"sku"`
Name sql.NullString `json:"name"`
AttributesJson sql.NullString `json:"attributes_json"`
StockQty sql.NullInt32 `json:"stock_qty"`
Barcode sql.NullString `json:"barcode"`
ImageUrl sql.NullString `json:"image_url"`
}
type EshopSetting struct {
ID int32 `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at"`
DefaultCurrency sql.NullString `json:"default_currency"`
SupportedCurrencies sql.NullString `json:"supported_currencies"`
DefaultCountry sql.NullString `json:"default_country"`
ShippingOptionsJson sql.NullString `json:"shipping_options_json"`
TermsUrl sql.NullString `json:"terms_url"`
ReturnsPolicyUrl sql.NullString `json:"returns_policy_url"`
SupportEmail sql.NullString `json:"support_email"`
SupportPhone sql.NullString `json:"support_phone"`
}
type EshopShippingLabel struct {
ID int32 `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at"`
OrderID int32 `json:"order_id"`
Carrier sql.NullString `json:"carrier"`
PacketaPacketID sql.NullString `json:"packeta_packet_id"`
TrackingNumber sql.NullString `json:"tracking_number"`
LabelUrl sql.NullString `json:"label_url"`
Status sql.NullString `json:"status"`
HistoryJson sql.NullString `json:"history_json"`
}
type User struct {
ID int32 `json:"id"`
}
@@ -0,0 +1,17 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package eshopreporting
import (
"context"
)
type Querier interface {
ListOrderStatusBreakdown(ctx context.Context) ([]ListOrderStatusBreakdownRow, error)
ListRecentOrderSummaries(ctx context.Context, limit int32) ([]ListRecentOrderSummariesRow, error)
SummarizeOrderRevenue(ctx context.Context, arg SummarizeOrderRevenueParams) (SummarizeOrderRevenueRow, error)
}
var _ Querier = (*Queries)(nil)
+121
View File
@@ -0,0 +1,121 @@
package dbschema
import (
"fotbal-club/internal/models"
"gorm.io/gorm"
)
// AllModels returns the complete set of persisted models used to bootstrap the schema.
func AllModels() []interface{} {
return []interface{}{
&models.SetupInfo{},
&models.ClubInfo{},
&models.Settings{},
&models.User{},
&models.UserProfile{},
&models.Article{},
&models.Category{},
&models.ArticleTeamLink{},
&models.ArticleMatchLink{},
&models.Team{},
&models.Player{},
&models.Club{},
&models.Sponsor{},
&models.Banner{},
&models.ScoreboardState{},
&models.ContactCategory{},
&models.Contact{},
&models.ContactMessage{},
&models.NewsletterSubscription{},
&models.PasswordReset{},
&models.VisitorEvent{},
&models.AboutPage{},
&models.EmailLog{},
&models.EmailEvent{},
&models.NewsletterSentLog{},
&models.MatchNotification{},
&models.BlogNotification{},
&models.MatchOverride{},
&models.TeamLogoOverride{},
&models.NavigationItem{},
&models.SocialLink{},
&models.PageElementConfig{},
&models.ShortLink{},
&models.LinkClick{},
&models.Poll{},
&models.PollOption{},
&models.PollVote{},
&models.Comment{},
&models.CommentReaction{},
&models.CommentBan{},
&models.UnbanRequest{},
&models.CommentReport{},
&models.PointsTransaction{},
&models.Achievement{},
&models.UserAchievement{},
&models.RewardItem{},
&models.RewardRedemption{},
&models.Sweepstake{},
&models.SweepstakePrize{},
&models.SweepstakeEntry{},
&models.SweepstakeWinner{},
&models.UploadedFile{},
&models.FileUsage{},
&models.ErrorEvent{},
&models.CompetitionAlias{},
&models.Clothing{},
&models.Language{},
&models.Translation{},
&models.ContentTranslation{},
&models.UserLanguagePreference{},
&models.ManualCompetition{},
&models.ManualMatch{},
&models.ManualTableRow{},
&models.Event{},
&models.EventAttachment{},
&models.QRCode{},
&models.EshopProductCategory{},
&models.EshopProduct{},
&models.EshopProductVariant{},
&models.EshopCart{},
&models.EshopCartItem{},
&models.EshopOrder{},
&models.EshopOrderItem{},
&models.EshopPayment{},
&models.EshopShippingLabel{},
&models.EshopSettings{},
&models.Facility{},
&models.FacilityAvailabilityRule{},
&models.FacilityBooking{},
&models.FacilityEquipment{},
&models.FacilityMaintenance{},
&models.WeatherCondition{},
&models.FacilityBookingTemplate{},
&models.Budget{},
&models.Sponsorship{},
&models.SponsorshipPayment{},
&models.SponsorshipDocument{},
&models.Expense{},
&models.ExpenseDocument{},
&models.FinancialReport{},
&models.FinancialSettings{},
&models.Invoice{},
&models.InvoiceItem{},
&models.InvoicePayment{},
&models.InvoiceCustomer{},
&models.InvoiceTemplate{},
&models.InvoiceSettings{},
&models.InvoiceSequence{},
&models.AuditLog{},
&models.TicketType{},
&models.TicketCampaign{},
&models.CampaignTicketType{},
&models.Ticket{},
&models.TicketAvailability{},
}
}
func AutoMigrate(db *gorm.DB) error {
return db.AutoMigrate(AllModels()...)
}