Commit a8f03535 by Augusto

first Commit

parents
root = "."
[build]
pre_cmd = ["swag init -g cmd/server/main.go -o docs --parseInternal --parseDependency --quiet"]
cmd = "go build -o ./bin/server ./cmd/server"
bin = "./bin/server"
include_ext = ["go"]
exclude_dir = ["vendor", "tmp", "docs"]
exclude_regex = ["_test.go"]
[log]
time = true
[color]
main = "magenta"
.env
.env.*
# Air
/tmp
.air.toml.tmp
# Go build artifacts
/bin/
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
# Go workspace
/go.work
/go.work.sum
# IDE
.vscode/
.idea/
*.swp
.DS_Store
# Logs
*.log
# Docker
**/.docker/
FROM golang:1.25-alpine AS builder
WORKDIR /src
RUN apk add --no-cache ca-certificates tzdata
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/server ./cmd/server
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /out/server /app/server
EXPOSE 3003
CMD ["/app/server"]
package main
import (
"log"
"os"
"github.com/joho/godotenv"
"ScoutSystemElite/internal/config"
"ScoutSystemElite/internal/database"
"ScoutSystemElite/internal/router"
)
// @title Scout System Elite API
// @version 1.0
// @description Football Scouting Platform API - Backend for Scout System Elite
// @host localhost:3000
// @BasePath /api
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
// @securityDefinitions.basic BasicAuth
func main() {
if err := godotenv.Load(".env"); err != nil {
log.Printf("warning: could not load .env file: %v", err)
}
cfg := config.Load()
db, err := database.Connect(cfg)
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}
r := router.New(db, cfg)
port := cfg.Port
if port == "" {
port = os.Getenv("PORT")
}
if port == "" {
port = "3000"
}
if err := r.Run(":" + port); err != nil {
log.Fatalf("failed to run server: %v", err)
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
module ScoutSystemElite
go 1.25.0
require (
github.com/gin-contrib/cors v1.7.0
github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/joho/godotenv v1.5.1
github.com/pquerna/otp v1.5.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.46.0
gorm.io/datatypes v1.2.7
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.2 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.29.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
)
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cors v1.7.0 h1:wZX2wuZ0o7rV2/1i7gb4Jn+gW7HBqaP91fizJkBUJOA=
github.com/gin-contrib/cors v1.7.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc=
github.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk=
github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
package config
import "os"
type Config struct {
Port string
DatabaseURL string
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
SSLMode string
JWTSecret string
JWTAccessTokenTTLMinutes string
ProviderUser string
ProviderSecret string
}
func Load() Config {
return Config{
Port: os.Getenv("APP_PORT"),
DatabaseURL: os.Getenv("DATABASE_URL"),
DBHost: os.Getenv("DB_HOST"),
DBPort: os.Getenv("DB_PORT"),
DBUser: os.Getenv("DB_USER"),
DBPassword: os.Getenv("DB_PASSWORD"),
DBName: os.Getenv("DB_NAME"),
SSLMode: envOrDefault("DB_SSLMODE", "disable"),
JWTSecret: os.Getenv("JWT_SECRET"),
JWTAccessTokenTTLMinutes: envOrDefault("JWT_ACCESS_TOKEN_TTL_MINUTES", "1440"),
ProviderUser: os.Getenv("ProviderUser"),
ProviderSecret: os.Getenv("ProviderSecret"),
}
}
func envOrDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
package database
import (
"fmt"
"reflect"
"strings"
gonanoid "github.com/matoous/go-nanoid/v2"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"ScoutSystemElite/internal/config"
"ScoutSystemElite/internal/models"
)
const nanoidAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
func registerNanoIDHook(db *gorm.DB) {
db.Callback().Create().Before("gorm:create").Register("sselite:nanoid_pk", func(tx *gorm.DB) {
stmt := tx.Statement
if stmt == nil || stmt.Schema == nil {
return
}
pk := stmt.Schema.PrioritizedPrimaryField
if pk == nil || pk.FieldType == nil || pk.FieldType.Kind() != reflect.String {
return
}
// Only for empty string PKs.
current, isZero := pk.ValueOf(tx.Statement.Context, stmt.ReflectValue)
if !isZero {
// ValueOf's isZero already handles "".
_ = current
return
}
id, err := gonanoid.Generate(nanoidAlphabet, 15)
if err != nil {
_ = tx.AddError(err)
return
}
_ = pk.Set(tx.Statement.Context, stmt.ReflectValue, id)
})
}
func ensureEnumType(db *gorm.DB, typeName string, values []string) error {
// Uses Postgres duplicate_object exception to make this idempotent.
quoted := make([]string, 0, len(values))
for _, v := range values {
v = strings.ReplaceAll(v, "'", "''")
quoted = append(quoted, "'"+v+"'")
}
sql := fmt.Sprintf(
"DO $$ BEGIN CREATE TYPE %s AS ENUM (%s); EXCEPTION WHEN duplicate_object THEN null; END $$;",
typeName,
strings.Join(quoted, ","),
)
return db.Exec(sql).Error
}
func Connect(cfg config.Config) (*gorm.DB, error) {
dsn := cfg.DatabaseURL
if dsn == "" {
dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
cfg.DBHost,
cfg.DBPort,
cfg.DBUser,
cfg.DBPassword,
cfg.DBName,
cfg.SSLMode,
)
}
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
registerNanoIDHook(db)
// Ensure enum types exist before AutoMigrate creates columns that reference them.
if err := ensureEnumType(db, "foot", []string{"left", "right", "both"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "gender", []string{"male", "female", "other"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "status", []string{"active", "inactive"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "coach_position", []string{"head_coach", "assistant_coach", "analyst"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "api_sync_status", []string{"pending", "synced", "error"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "report_status", []string{"saved", "finished"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "calendar_event_type", []string{"match", "travel", "player_observation", "meeting", "training", "other"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "list_type", []string{"shortlist", "shadow_team", "target_list"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "position_category", []string{"Forward", "Goalkeeper", "Defender", "Midfield"}); err != nil {
return nil, err
}
if err := ensureEnumType(db, "audit_log_action", []string{"create", "update", "delete", "soft_delete", "restore"}); err != nil {
return nil, err
}
if err := db.AutoMigrate(
&models.User{},
&models.UserSession{},
&models.Area{},
&models.Position{},
&models.Player{},
&models.Coach{},
&models.Agent{},
&models.PlayerAgent{},
&models.CoachAgent{},
&models.Report{},
&models.CalendarEvent{},
&models.List{},
&models.ListShare{},
&models.File{},
&models.Category{},
&models.GlobalSetting{},
&models.UserSetting{},
&models.ClientSubscription{},
&models.ClientModule{},
&models.PlayerFeatureCategory{},
&models.PlayerFeatureType{},
&models.PlayerFeatureRating{},
&models.PlayerFeatureSelection{},
&models.ProfileDescription{},
&models.ProfileLink{},
&models.AuditLog{},
); err != nil {
return nil, err
}
return db, nil
}
package errors
import "errors"
type Code string
const (
CodeNotFound Code = "NOT_FOUND"
CodeInvalidInput Code = "INVALID_INPUT"
CodeConflict Code = "CONFLICT"
CodeInternal Code = "INTERNAL"
)
type AppError struct {
Code Code
Message string
Err error
}
func (e *AppError) Error() string {
return e.Message
}
func Wrap(code Code, msg string, err error) *AppError {
return &AppError{Code: code, Message: msg, Err: err}
}
func New(code Code, msg string) *AppError {
return &AppError{Code: code, Message: msg}
}
func IsCode(err error, code Code) bool {
var ae *AppError
if errors.As(err, &ae) {
return ae.Code == code
}
return false
}
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type AgentHandler struct {
service *services.AgentService
}
func NewAgentHandler(db *gorm.DB) *AgentHandler {
return &AgentHandler{
service: services.NewAgentService(db),
}
}
// Create godoc
// @Summary Create a new agent
// @Description Create a new agent
// @Tags Agents
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateAgentRequest true "Agent data"
// @Success 201 {object} services.AgentResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Router /agents [post]
func (h *AgentHandler) Create(c *gin.Context) {
var req services.CreateAgentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
agent, err := h.service.Create(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, agent)
}
// GetByID godoc
// @Summary Get agent by ID
// @Description Get a single agent by their ID
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param agentId path int true "Agent ID"
// @Success 200 {object} services.AgentResponse
// @Failure 404 {object} map[string]interface{} "Agent not found"
// @Router /agents/{agentId} [get]
func (h *AgentHandler) GetByID(c *gin.Context) {
id := c.Param("agentId")
agent, err := h.service.FindByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, agent)
}
// FindAll godoc
// @Summary Get all agents
// @Description Get paginated list of agents with optional filters
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param name query string false "Filter by name"
// @Param type query string false "Filter by type"
// @Param status query string false "Filter by status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedAgentsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /agents [get]
func (h *AgentHandler) FindAll(c *gin.Context) {
var query services.QueryAgentsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update an agent
// @Description Update an existing agent's information
// @Tags Agents
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param agentId path int true "Agent ID"
// @Param request body services.UpdateAgentRequest true "Agent data"
// @Success 200 {object} services.AgentResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Agent not found"
// @Router /agents/{agentId} [patch]
func (h *AgentHandler) Update(c *gin.Context) {
id := c.Param("agentId")
var req services.UpdateAgentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
agent, err := h.service.Update(id, req)
if err != nil {
if err.Error() == "agent not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, agent)
}
// Delete godoc
// @Summary Delete an agent
// @Description Soft delete an agent
// @Tags Agents
// @Security BearerAuth
// @Param agentId path int true "Agent ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Agent not found"
// @Router /agents/{agentId} [delete]
func (h *AgentHandler) Delete(c *gin.Context) {
id := c.Param("agentId")
if err := h.service.Delete(id); err != nil {
if err.Error() == "agent not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// AssociateWithPlayer godoc
// @Summary Associate agent with player
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param agentId path int true "Agent ID"
// @Param playerId path string true "Player ID"
// @Success 201 {object} services.AgentPlayerAssociationResponse
// @Router /agents/{agentId}/players/{playerId} [post]
func (h *AgentHandler) AssociateWithPlayer(c *gin.Context) {
agentID := c.Param("agentId")
playerID := c.Param("playerId")
res, err := h.service.AssociateWithPlayer(agentID, playerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, res)
}
// AssociateWithCoach godoc
// @Summary Associate agent with coach
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param agentId path int true "Agent ID"
// @Param coachId path string true "Coach ID"
// @Success 201 {object} services.AgentCoachAssociationResponse
// @Router /agents/{agentId}/coaches/{coachId} [post]
func (h *AgentHandler) AssociateWithCoach(c *gin.Context) {
agentID := c.Param("agentId")
coachID := c.Param("coachId")
res, err := h.service.AssociateWithCoach(agentID, coachID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, res)
}
// RemoveFromPlayer godoc
// @Summary Remove agent from player
// @Tags Agents
// @Security BearerAuth
// @Param agentId path int true "Agent ID"
// @Param playerId path string true "Player ID"
// @Success 204
// @Router /agents/{agentId}/players/{playerId} [delete]
func (h *AgentHandler) RemoveFromPlayer(c *gin.Context) {
agentID := c.Param("agentId")
playerID := c.Param("playerId")
if err := h.service.RemoveFromPlayer(agentID, playerID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// RemoveFromCoach godoc
// @Summary Remove agent from coach
// @Tags Agents
// @Security BearerAuth
// @Param agentId path int true "Agent ID"
// @Param coachId path string true "Coach ID"
// @Success 204
// @Router /agents/{agentId}/coaches/{coachId} [delete]
func (h *AgentHandler) RemoveFromCoach(c *gin.Context) {
agentID := c.Param("agentId")
coachID := c.Param("coachId")
if err := h.service.RemoveFromCoach(agentID, coachID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// GetByPlayer godoc
// @Summary Get agents associated with a player
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param playerId path string true "Player ID"
// @Success 200 {array} services.AgentResponse
// @Router /agents/by-player/{playerId} [get]
func (h *AgentHandler) GetByPlayer(c *gin.Context) {
playerID := c.Param("playerId")
res, err := h.service.GetAgentsByPlayer(playerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, res)
}
// GetByCoach godoc
// @Summary Get agents associated with a coach
// @Tags Agents
// @Security BearerAuth
// @Produce json
// @Param coachId path string true "Coach ID"
// @Success 200 {array} services.AgentResponse
// @Router /agents/by-coach/{coachId} [get]
func (h *AgentHandler) GetByCoach(c *gin.Context) {
coachID := c.Param("coachId")
res, err := h.service.GetAgentsByCoach(coachID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, res)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type AreaHandler struct {
service *services.AreaService
}
func NewAreaHandler(db *gorm.DB) *AreaHandler {
return &AreaHandler{
service: services.NewAreaService(db),
}
}
// Create godoc
// @Summary Create a new area
// @Description Create a new geographical area
// @Tags Areas
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateAreaRequest true "Area data"
// @Success 201 {object} services.AreaResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 409 {object} map[string]interface{} "Area already exists"
// @Router /areas [post]
func (h *AreaHandler) Create(c *gin.Context) {
var req services.CreateAreaRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
area, err := h.service.Create(req)
if err != nil {
if err.Error() == "area with same wyId already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, area)
}
// GetByID godoc
// @Summary Get area by ID
// @Description Get a single area by its ID
// @Tags Areas
// @Security BearerAuth
// @Produce json
// @Param id path int true "Area ID"
// @Success 200 {object} services.AreaResponse
// @Failure 404 {object} map[string]interface{} "Area not found"
// @Router /areas/id/{id} [get]
func (h *AreaHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid area ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
area, err := h.service.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, area)
}
// GetByWyIDAlias godoc
// @Summary Get area by wyId
// @Description Get a single area by its Wyscout ID
// @Tags Areas
// @Security BearerAuth
// @Produce json
// @Param wyId path int true "Wyscout Area ID"
// @Success 200 {object} services.AreaResponse
// @Failure 404 {object} map[string]interface{} "Area not found"
// @Router /areas/{wyId} [get]
func (h *AreaHandler) GetByWyIDAlias(c *gin.Context) {
wyID, err := strconv.Atoi(c.Param("wyId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid wyId",
"errorCode": "VALIDATION_ERROR",
})
return
}
area, err := h.service.FindByWyID(wyID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, area)
}
// GetByWyID godoc
// @Summary Get area by WyID
// @Description Get a single area by its Wyscout ID
// @Tags Areas
// @Security BearerAuth
// @Produce json
// @Param wyId path int true "Wyscout Area ID"
// @Success 200 {object} services.AreaResponse
// @Failure 404 {object} map[string]interface{} "Area not found"
// @Router /areas/wy/{wyId} [get]
func (h *AreaHandler) GetByWyID(c *gin.Context) {
wyID, err := strconv.Atoi(c.Param("wyId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid wyId",
"errorCode": "VALIDATION_ERROR",
})
return
}
area, err := h.service.FindByWyID(wyID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, area)
}
// FindAll godoc
// @Summary Get all areas
// @Description Get paginated list of areas with optional filters
// @Tags Areas
// @Security BearerAuth
// @Produce json
// @Param name query string false "Filter by name"
// @Param alpha2 query string false "Filter by alpha2 code"
// @Param alpha3 query string false "Filter by alpha3 code"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedAreasResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /areas [get]
func (h *AreaHandler) FindAll(c *gin.Context) {
var query services.QueryAreasRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update an area
// @Description Update an existing area's information
// @Tags Areas
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Area ID"
// @Param request body services.UpdateAreaRequest true "Area data"
// @Success 200 {object} services.AreaResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Area not found"
// @Router /areas/{id} [patch]
func (h *AreaHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid area ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateAreaRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
area, err := h.service.Update(uint(id), req)
if err != nil {
if err.Error() == "area not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, area)
}
// Delete godoc
// @Summary Delete an area
// @Description Soft delete an area
// @Tags Areas
// @Security BearerAuth
// @Param id path int true "Area ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Area not found"
// @Router /areas/{id} [delete]
func (h *AreaHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid area ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Delete(uint(id)); err != nil {
if err.Error() == "area not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/config"
"ScoutSystemElite/internal/services"
)
type AuthHandler struct {
service *services.AuthService
}
func NewAuthHandler(db *gorm.DB, cfg config.Config) *AuthHandler {
return &AuthHandler{
service: services.NewAuthService(db, cfg),
}
}
// Login godoc
// @Summary User login
// @Description Authenticate user with email and password
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body services.LoginRequest true "Login credentials"
// @Success 200 {object} services.AuthResponse
// @Failure 400 {object} map[string]interface{} "Invalid request body"
// @Failure 401 {object} map[string]interface{} "Invalid credentials"
// @Router /auth/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
var req services.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
deviceInfo := services.DeviceInfo{
UserAgent: c.GetHeader("User-Agent"),
IPAddress: c.ClientIP(),
Platform: c.GetHeader("Sec-Ch-Ua-Platform"),
}
resp, err := h.service.Login(req, deviceInfo)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": err.Error(),
"errorCode": "UNAUTHORIZED",
})
return
}
c.JSON(http.StatusOK, resp)
}
// Logout godoc
// @Summary User logout
// @Description Logout user from current device
// @Tags Auth
// @Security BearerAuth
// @Produce json
// @Param X-Device-Id header string false "Device ID"
// @Success 204 "No Content"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/logout [post]
func (h *AuthHandler) Logout(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
deviceID := c.GetHeader("X-Device-Id")
if err := h.service.Logout(userID.(uint), deviceID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// LogoutAll godoc
// @Summary Logout from all devices
// @Description Logout user from all devices
// @Tags Auth
// @Security BearerAuth
// @Produce json
// @Success 204 "No Content"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/logout-all [post]
func (h *AuthHandler) LogoutAll(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
if err := h.service.LogoutFromAllDevices(userID.(uint)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// Setup2FA godoc
// @Summary Setup two-factor authentication
// @Description Generate 2FA secret and QR code for user
// @Tags Auth
// @Security BearerAuth
// @Produce json
// @Success 200 {object} services.Setup2FAResponse
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/2fa/setup [post]
func (h *AuthHandler) Setup2FA(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
resp, err := h.service.Setup2FA(userID.(uint))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": err.Error(),
"errorCode": "BAD_REQUEST",
})
return
}
c.JSON(http.StatusOK, resp)
}
// Verify2FA godoc
// @Summary Verify and enable 2FA
// @Description Verify 2FA code and enable two-factor authentication
// @Tags Auth
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body object{code=string} true "2FA verification code"
// @Success 204 "No Content"
// @Failure 400 {object} map[string]interface{} "Invalid code"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/2fa/verify [post]
func (h *AuthHandler) Verify2FA(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req struct {
Code string `json:"code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.VerifyAndEnable2FA(userID.(uint), req.Code); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": err.Error(),
"errorCode": "BAD_REQUEST",
})
return
}
c.Status(http.StatusNoContent)
}
// Disable2FA godoc
// @Summary Disable two-factor authentication
// @Description Disable 2FA for user account
// @Tags Auth
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body object{password=string} true "User password for verification"
// @Success 204 "No Content"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/2fa/disable [post]
func (h *AuthHandler) Disable2FA(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req struct {
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Disable2FA(userID.(uint), req.Password); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": err.Error(),
"errorCode": "UNAUTHORIZED",
})
return
}
c.Status(http.StatusNoContent)
}
// ChangePassword godoc
// @Summary Change user password
// @Description Change the current user's password
// @Tags Auth
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body object{currentPassword=string,newPassword=string} true "Password change request"
// @Success 204 "No Content"
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /auth/change-password [post]
func (h *AuthHandler) ChangePassword(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req struct {
CurrentPassword string `json:"currentPassword" binding:"required,min=6"`
NewPassword string `json:"newPassword" binding:"required,min=6"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.ChangePassword(userID.(uint), req.CurrentPassword, req.NewPassword); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": err.Error(),
"errorCode": "UNAUTHORIZED",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type CalendarHandler struct {
service *services.CalendarService
}
func NewCalendarHandler(db *gorm.DB) *CalendarHandler {
return &CalendarHandler{
service: services.NewCalendarService(db),
}
}
// Create godoc
// @Summary Create a new calendar event
// @Description Create a new calendar event for the authenticated user
// @Tags Calendar
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateCalendarEventRequest true "Event data"
// @Success 201 {object} services.CalendarEventResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /calendar [post]
func (h *CalendarHandler) Create(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreateCalendarEventRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
event, err := h.service.Create(userID.(uint), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, event)
}
// GetByID godoc
// @Summary Get calendar event by ID
// @Description Get a single calendar event by its ID
// @Tags Calendar
// @Security BearerAuth
// @Produce json
// @Param id path int true "Event ID"
// @Success 200 {object} services.CalendarEventResponse
// @Failure 404 {object} map[string]interface{} "Event not found"
// @Router /calendar/{id} [get]
func (h *CalendarHandler) GetByID(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
event, err := h.service.FindByID(userID.(uint), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, event)
}
// FindAll godoc
// @Summary Get all calendar events
// @Description Get paginated list of calendar events with optional filters
// @Tags Calendar
// @Security BearerAuth
// @Produce json
// @Param eventType query string false "Filter by event type"
// @Param fromDate query string false "Filter from date (YYYY-MM-DD)"
// @Param toDate query string false "Filter to date (YYYY-MM-DD)"
// @Param matchWyId query int false "Filter by match WyID"
// @Param playerWyId query int false "Filter by player WyID"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedCalendarEventsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /calendar [get]
func (h *CalendarHandler) FindAll(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var query services.QueryCalendarEventsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(userID.(uint), query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update a calendar event
// @Description Update an existing calendar event
// @Tags Calendar
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path string true "Event ID"
// @Param request body services.UpdateCalendarEventRequest true "Event data"
// @Success 200 {object} services.CalendarEventResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Event not found"
// @Router /calendar/{id} [patch]
func (h *CalendarHandler) Update(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
var req services.UpdateCalendarEventRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
event, err := h.service.Update(userID.(uint), id, req)
if err != nil {
if err.Error() == "calendar event not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, event)
}
// Delete godoc
// @Summary Delete a calendar event
// @Description Soft delete a calendar event
// @Tags Calendar
// @Security BearerAuth
// @Param id path int true "Event ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Event not found"
// @Router /calendar/{id} [delete]
func (h *CalendarHandler) Delete(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
if err := h.service.Delete(userID.(uint), id); err != nil {
if err.Error() == "calendar event not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type CoachHandler struct {
service *services.CoachService
}
func NewCoachHandler(db *gorm.DB) *CoachHandler {
return &CoachHandler{
service: services.NewCoachService(db),
}
}
// Save godoc
// @Summary Create or update a coach
// @Description Create a new coach or update existing by WyID
// @Tags Coaches
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateCoachRequest true "Coach data"
// @Success 200 {object} services.CoachResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Router /coaches [post]
func (h *CoachHandler) Save(c *gin.Context) {
var req services.CreateCoachRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
coach, err := h.service.UpsertByWyID(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, coach)
}
// GetByWyIDAlias godoc
// @Summary Get coach by wyId
// @Description Get a single coach by their Wyscout ID
// @Tags Coaches
// @Security BearerAuth
// @Produce json
// @Param wyId path int true "Wyscout Coach ID"
// @Success 200 {object} services.CoachResponse
// @Failure 404 {object} map[string]interface{} "Coach not found"
// @Router /coaches/{wyId} [get]
func (h *CoachHandler) GetByWyIDAlias(c *gin.Context) {
wyID, err := strconv.Atoi(c.Param("wyId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid wyId",
"errorCode": "VALIDATION_ERROR",
})
return
}
coach, err := h.service.FindByWyID(wyID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if coach == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Coach not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, coach)
}
// GetByID godoc
// @Summary Get coach by ID
// @Description Get a single coach by their internal ID
// @Tags Coaches
// @Security BearerAuth
// @Produce json
// @Param id path string true "Coach ID"
// @Success 200 {object} services.CoachResponse
// @Failure 404 {object} map[string]interface{} "Coach not found"
// @Router /coaches/id/{id} [get]
func (h *CoachHandler) GetByID(c *gin.Context) {
id := c.Param("id")
coach, err := h.service.FindByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if coach == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Coach not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, coach)
}
// GetByWyID godoc
// @Summary Get coach by WyID
// @Description Get a single coach by their Wyscout ID
// @Tags Coaches
// @Security BearerAuth
// @Produce json
// @Param wyId path int true "Wyscout Coach ID"
// @Success 200 {object} services.CoachResponse
// @Failure 404 {object} map[string]interface{} "Coach not found"
// @Router /coaches/wy/{wyId} [get]
func (h *CoachHandler) GetByWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid wyId",
"errorCode": "VALIDATION_ERROR",
})
return
}
coach, err := h.service.FindByWyID(wyID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if coach == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Coach not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, coach)
}
// FindAll godoc
// @Summary Get all coaches
// @Description Get paginated list of coaches with optional filters
// @Tags Coaches
// @Security BearerAuth
// @Produce json
// @Param name query string false "Filter by name"
// @Param teamWyId query int false "Filter by team WyID"
// @Param nationalityWyId query int false "Filter by nationality WyID"
// @Param position query string false "Filter by position"
// @Param status query string false "Filter by status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedCoachesResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /coaches [get]
func (h *CoachHandler) FindAll(c *gin.Context) {
var query services.QueryCoachesRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// UpdateByID godoc
// @Summary Update a coach
// @Description Update an existing coach's information
// @Tags Coaches
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path string true "Coach ID"
// @Param request body services.UpdateCoachRequest true "Coach data"
// @Success 200 {object} services.CoachResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Coach not found"
// @Router /coaches/{id} [patch]
func (h *CoachHandler) UpdateByID(c *gin.Context) {
id := c.Param("id")
var req services.UpdateCoachRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
coach, err := h.service.UpdateByID(id, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if coach == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Coach not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, coach)
}
// DeleteByID godoc
// @Summary Delete a coach
// @Description Soft delete a coach
// @Tags Coaches
// @Security BearerAuth
// @Param id path string true "Coach ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Coach not found"
// @Router /coaches/{id} [delete]
func (h *CoachHandler) DeleteByID(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteByID(id); err != nil {
if err.Error() == "coach not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type FileHandler struct {
service *services.FileService
}
func NewFileHandler(db *gorm.DB) *FileHandler {
return &FileHandler{
service: services.NewFileService(db),
}
}
// Create godoc
// @Summary Create a new file record
// @Description Create a new file metadata record
// @Tags Files
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateFileRequest true "File data"
// @Success 201 {object} services.FileResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /files [post]
func (h *FileHandler) Create(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreateFileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
file, err := h.service.Create(userID.(uint), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, file)
}
// GetByID godoc
// @Summary Get file by ID
// @Description Get a single file record by its ID
// @Tags Files
// @Security BearerAuth
// @Produce json
// @Param id path int true "File ID"
// @Success 200 {object} services.FileResponse
// @Failure 404 {object} map[string]interface{} "File not found"
// @Router /files/{id} [get]
func (h *FileHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid file ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
file, err := h.service.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, file)
}
// FindAll godoc
// @Summary Get all files
// @Description Get paginated list of files with optional filters
// @Tags Files
// @Security BearerAuth
// @Produce json
// @Param entityType query string false "Filter by entity type"
// @Param entityId query int false "Filter by entity ID"
// @Param entityWyId query int false "Filter by entity WyID"
// @Param category query string false "Filter by category"
// @Param mimeType query string false "Filter by MIME type"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedFilesResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /files [get]
func (h *FileHandler) FindAll(c *gin.Context) {
var query services.QueryFilesRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// FindByEntity godoc
// @Summary Get files by entity
// @Description Get files associated with a specific entity type
// @Tags Files
// @Security BearerAuth
// @Produce json
// @Param entityType path string true "Entity type (player, coach, etc.)"
// @Param entityId query int false "Entity ID"
// @Param entityWyId query int false "Entity WyID"
// @Success 200 {array} services.FileResponse
// @Router /files/entity/{entityType} [get]
func (h *FileHandler) FindByEntity(c *gin.Context) {
entityType := c.Param("entityType")
var entityID *int
var entityWyID *int
if idStr := c.Query("entityId"); idStr != "" {
if id, err := strconv.Atoi(idStr); err == nil {
entityID = &id
}
}
if wyIDStr := c.Query("entityWyId"); wyIDStr != "" {
if wyID, err := strconv.Atoi(wyIDStr); err == nil {
entityWyID = &wyID
}
}
files, err := h.service.FindByEntity(entityType, entityID, entityWyID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, files)
}
// Update godoc
// @Summary Update a file record
// @Description Update an existing file metadata record
// @Tags Files
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "File ID"
// @Param request body services.UpdateFileRequest true "File data"
// @Success 200 {object} services.FileResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "File not found"
// @Router /files/{id} [patch]
func (h *FileHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid file ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateFileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
file, err := h.service.Update(uint(id), req)
if err != nil {
if err.Error() == "file not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, file)
}
// Delete godoc
// @Summary Delete a file record
// @Description Soft delete a file record
// @Tags Files
// @Security BearerAuth
// @Param id path int true "File ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "File not found"
// @Router /files/{id} [delete]
func (h *FileHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid file ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Delete(uint(id)); err != nil {
if err.Error() == "file not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type ListHandler struct {
service *services.ListService
}
func NewListHandler(db *gorm.DB) *ListHandler {
return &ListHandler{
service: services.NewListService(db),
}
}
// Create godoc
// @Summary Create a new list
// @Description Create a new player list for the authenticated user
// @Tags Lists
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateListRequest true "List data"
// @Success 201 {object} services.ListResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /lists [post]
func (h *ListHandler) Create(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreateListRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
list, err := h.service.Create(userID.(uint), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, list)
}
// GetByID godoc
// @Summary Get list by ID
// @Description Get a single list by its ID
// @Tags Lists
// @Security BearerAuth
// @Produce json
// @Param id path int true "List ID"
// @Success 200 {object} services.ListResponse
// @Failure 404 {object} map[string]interface{} "List not found"
// @Router /lists/{id} [get]
func (h *ListHandler) GetByID(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
list, err := h.service.FindByID(userID.(uint), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, list)
}
// FindAll godoc
// @Summary Get all lists
// @Description Get paginated list of player lists with optional filters
// @Tags Lists
// @Security BearerAuth
// @Produce json
// @Param type query string false "Filter by list type"
// @Param name query string false "Filter by name"
// @Param season query string false "Filter by season"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedListsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /lists [get]
func (h *ListHandler) FindAll(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var query services.QueryListsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(userID.(uint), query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// FindSharedWithMe godoc
// @Summary Get lists shared with the authenticated user
// @Description Get paginated list of player lists that have been shared with the authenticated user
// @Tags Lists
// @Security BearerAuth
// @Produce json
// @Param type query string false "Filter by list type"
// @Param name query string false "Filter by name"
// @Param season query string false "Filter by season"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedListsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /lists/shared-with-me [get]
func (h *ListHandler) FindSharedWithMe(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var query services.QueryListsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindSharedWithUser(userID.(uint), query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update a list
// @Description Update an existing list
// @Tags Lists
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "List ID"
// @Param request body services.UpdateListRequest true "List data"
// @Success 200 {object} services.ListResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "List not found"
// @Router /lists/{id} [patch]
func (h *ListHandler) Update(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
var req services.UpdateListRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
list, err := h.service.Update(userID.(uint), id, req)
if err != nil {
if err.Error() == "list not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, list)
}
// Delete godoc
// @Summary Delete a list
// @Description Soft delete a list
// @Tags Lists
// @Security BearerAuth
// @Param id path int true "List ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "List not found"
// @Router /lists/{id} [delete]
func (h *ListHandler) Delete(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id")
if err := h.service.Delete(userID.(uint), id); err != nil {
if err.Error() == "list not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type PlayerAgentHandler struct {
service *services.PlayerAgentService
}
func NewPlayerAgentHandler(db *gorm.DB) *PlayerAgentHandler {
return &PlayerAgentHandler{
service: services.NewPlayerAgentService(db),
}
}
func (h *PlayerAgentHandler) Create(c *gin.Context) {
var req services.CreatePlayerAgentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
relation, err := h.service.Create(req)
if err != nil {
if err.Error() == "player-agent relation already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, relation)
}
func (h *PlayerAgentHandler) FindByPlayerID(c *gin.Context) {
playerID := c.Param("playerId")
relations, err := h.service.FindByPlayerID(playerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, relations)
}
func (h *PlayerAgentHandler) FindByAgentID(c *gin.Context) {
agentID := c.Param("agentId")
relations, err := h.service.FindByAgentID(agentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, relations)
}
func (h *PlayerAgentHandler) Delete(c *gin.Context) {
playerID := c.Param("playerId")
agentID := c.Param("agentId")
if err := h.service.Delete(playerID, agentID); err != nil {
if err.Error() == "player-agent relation not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
type CoachAgentHandler struct {
service *services.CoachAgentService
}
func NewCoachAgentHandler(db *gorm.DB) *CoachAgentHandler {
return &CoachAgentHandler{
service: services.NewCoachAgentService(db),
}
}
func (h *CoachAgentHandler) Create(c *gin.Context) {
var req services.CreateCoachAgentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
relation, err := h.service.Create(req)
if err != nil {
if err.Error() == "coach-agent relation already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, relation)
}
func (h *CoachAgentHandler) FindByCoachID(c *gin.Context) {
coachID := c.Param("coachId")
relations, err := h.service.FindByCoachID(coachID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, relations)
}
func (h *CoachAgentHandler) FindByAgentID(c *gin.Context) {
agentID := c.Param("agentId")
relations, err := h.service.FindByAgentID(agentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, relations)
}
func (h *CoachAgentHandler) Delete(c *gin.Context) {
coachID := c.Param("coachId")
agentID := c.Param("agentId")
if err := h.service.Delete(coachID, agentID); err != nil {
if err.Error() == "coach-agent relation not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type PlayerFeatureHandler struct {
service *services.PlayerFeatureService
}
func NewPlayerFeatureHandler(db *gorm.DB) *PlayerFeatureHandler {
return &PlayerFeatureHandler{
service: services.NewPlayerFeatureService(db),
}
}
// CreateCategory godoc
// @Summary Create player feature category
// @Tags Player Features
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateFeatureCategoryRequest true "Category"
// @Success 201 {object} services.PlayerFeatureCategoryResponse
// @Failure 400 {object} map[string]interface{}
// @Router /player-features/categories [post]
func (h *PlayerFeatureHandler) CreateCategory(c *gin.Context) {
var req services.CreateFeatureCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
cat, err := h.service.CreateCategory(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, cat)
}
// GetCategories godoc
// @Summary List player feature categories
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Success 200 {array} services.PlayerFeatureCategoryResponse
// @Router /player-features/categories [get]
func (h *PlayerFeatureHandler) GetCategories(c *gin.Context) {
categories, err := h.service.GetCategories()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, categories)
}
// GetCategoryByID godoc
// @Summary Get player feature category by ID
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param id path int true "Category ID"
// @Success 200 {object} services.PlayerFeatureCategoryResponse
// @Failure 404 {object} map[string]interface{}
// @Router /player-features/categories/{id} [get]
func (h *PlayerFeatureHandler) GetCategoryByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid category ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
cat, err := h.service.GetCategoryByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, cat)
}
// UpdateCategory godoc
// @Summary Update player feature category
// @Tags Player Features
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Category ID"
// @Param request body services.CreateFeatureCategoryRequest true "Category"
// @Success 200 {object} services.PlayerFeatureCategoryResponse
// @Failure 404 {object} map[string]interface{}
// @Router /player-features/categories/{id} [patch]
func (h *PlayerFeatureHandler) UpdateCategory(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid category ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.CreateFeatureCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
cat, err := h.service.UpdateCategory(uint(id), req)
if err != nil {
if err.Error() == "category not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, cat)
}
// DeleteCategory godoc
// @Summary Delete player feature category
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param id path int true "Category ID"
// @Success 204
// @Router /player-features/categories/{id} [delete]
func (h *PlayerFeatureHandler) DeleteCategory(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid category ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.DeleteCategory(uint(id)); err != nil {
if err.Error() == "category not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// CreateFeatureType godoc
// @Summary Create player feature type
// @Tags Player Features
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateFeatureTypeRequest true "Feature type"
// @Success 201 {object} services.PlayerFeatureTypeResponse
// @Failure 400 {object} map[string]interface{}
// @Router /player-features/types [post]
func (h *PlayerFeatureHandler) CreateFeatureType(c *gin.Context) {
var req services.CreateFeatureTypeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
ft, err := h.service.CreateFeatureType(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, ft)
}
// GetFeatureTypes godoc
// @Summary List player feature types
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param categoryId query int false "Filter by category ID"
// @Success 200 {array} services.PlayerFeatureTypeResponse
// @Router /player-features/types [get]
func (h *PlayerFeatureHandler) GetFeatureTypes(c *gin.Context) {
var categoryID *uint
if catStr := c.Query("categoryId"); catStr != "" {
if id, err := strconv.ParseUint(catStr, 10, 32); err == nil {
catID := uint(id)
categoryID = &catID
}
}
types, err := h.service.GetFeatureTypes(categoryID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, types)
}
// GetFeatureTypesByCategory godoc
// @Summary List player feature types by category
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param categoryId path int true "Category ID"
// @Success 200 {array} services.PlayerFeatureTypeResponse
// @Router /player-features/types/category/{categoryId} [get]
func (h *PlayerFeatureHandler) GetFeatureTypesByCategory(c *gin.Context) {
categoryID, err := strconv.ParseUint(c.Param("categoryId"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid category ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
types, err := h.service.GetFeatureTypesByCategory(uint(categoryID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, types)
}
// GetFeatureTypeByID godoc
// @Summary Get player feature type by ID
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param id path int true "Feature type ID"
// @Success 200 {object} services.PlayerFeatureTypeResponse
// @Failure 404 {object} map[string]interface{}
// @Router /player-features/types/{id} [get]
func (h *PlayerFeatureHandler) GetFeatureTypeByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid feature type ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
ft, err := h.service.GetFeatureTypeByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, ft)
}
// UpdateFeatureType godoc
// @Summary Update player feature type
// @Tags Player Features
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Feature type ID"
// @Param request body services.CreateFeatureTypeRequest true "Feature type"
// @Success 200 {object} services.PlayerFeatureTypeResponse
// @Failure 404 {object} map[string]interface{}
// @Router /player-features/types/{id} [patch]
func (h *PlayerFeatureHandler) UpdateFeatureType(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid feature type ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.CreateFeatureTypeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
ft, err := h.service.UpdateFeatureType(uint(id), req)
if err != nil {
if err.Error() == "feature type not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, ft)
}
// DeleteFeatureType godoc
// @Summary Delete player feature type
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param id path int true "Feature type ID"
// @Success 204
// @Router /player-features/types/{id} [delete]
func (h *PlayerFeatureHandler) DeleteFeatureType(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid feature type ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.DeleteFeatureType(uint(id)); err != nil {
if err.Error() == "feature type not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// CreateRating godoc
// @Summary Create player feature rating
// @Tags Player Features
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateFeatureRatingRequest true "Rating"
// @Success 201 {object} services.PlayerFeatureRatingResponse
// @Failure 400 {object} map[string]interface{}
// @Router /player-features/ratings [post]
func (h *PlayerFeatureHandler) CreateRating(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreateFeatureRatingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
rating, err := h.service.CreateRating(userID.(uint), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, rating)
}
// GetRatingsByPlayer godoc
// @Summary List ratings by player
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param playerId path string true "Player ID"
// @Success 200 {array} services.PlayerFeatureRatingWithUserNameResponse
// @Router /player-features/ratings/player/{playerId} [get]
func (h *PlayerFeatureHandler) GetRatingsByPlayer(c *gin.Context) {
playerID := c.Param("playerId")
ratings, err := h.service.GetRatingsByPlayer(playerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, ratings)
}
// GetRatingByID godoc
// @Summary Get rating by ID
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param id path int true "Rating ID"
// @Success 200 {object} services.PlayerFeatureRatingResponse
// @Failure 404 {object} map[string]interface{}
// @Router /player-features/ratings/{id} [get]
func (h *PlayerFeatureHandler) GetRatingByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid rating ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
rating, err := h.service.GetRatingByID(uint(id))
if err != nil {
if err.Error() == "rating not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, rating)
}
// GetRatingsByPlayerAndScout godoc
// @Summary List ratings by player and scout
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param playerId path string true "Player ID"
// @Param userId path int true "User ID"
// @Success 200 {array} services.PlayerFeatureRatingResponse
// @Router /player-features/ratings/player/{playerId}/scout/{userId} [get]
func (h *PlayerFeatureHandler) GetRatingsByPlayerAndScout(c *gin.Context) {
playerID := c.Param("playerId")
userID, err := strconv.ParseUint(c.Param("userId"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
ratings, err := h.service.GetRatingsByPlayerAndScout(playerID, uint(userID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, ratings)
}
// GetRatingsByFeature godoc
// @Summary List ratings by feature type
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param featureTypeId path int true "Feature type ID"
// @Success 200 {array} services.PlayerFeatureRatingResponse
// @Router /player-features/ratings/feature/{featureTypeId} [get]
func (h *PlayerFeatureHandler) GetRatingsByFeature(c *gin.Context) {
featureTypeID, err := strconv.ParseUint(c.Param("featureTypeId"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid feature type ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
ratings, err := h.service.GetRatingsByFeatureType(uint(featureTypeID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, ratings)
}
// CreateSelection godoc
// @Summary Create player feature selection
// @Tags Player Features
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateFeatureSelectionRequest true "Selection"
// @Success 201 {object} services.PlayerFeatureSelectionResponse
// @Failure 400 {object} map[string]interface{}
// @Router /player-features/selections [post]
func (h *PlayerFeatureHandler) CreateSelection(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreateFeatureSelectionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
sel, err := h.service.CreateSelection(userID.(uint), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, sel)
}
// UpdateSelection godoc
// @Summary Update player feature selection
// @Tags Player Features
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Selection ID"
// @Param request body services.UpdateFeatureSelectionRequest true "Selection"
// @Success 200 {object} services.PlayerFeatureSelectionResponse
// @Failure 404 {object} map[string]interface{}
// @Router /player-features/selections/{id} [patch]
func (h *PlayerFeatureHandler) UpdateSelection(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid selection ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateFeatureSelectionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
sel, err := h.service.UpdateSelection(uint(id), userID.(uint), req)
if err != nil {
if err.Error() == "selection not found or not owned by user" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, sel)
}
// GetSelectionByID godoc
// @Summary Get selection by ID
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param id path int true "Selection ID"
// @Success 200 {object} services.PlayerFeatureSelectionResponse
// @Failure 404 {object} map[string]interface{}
// @Router /player-features/selections/{id} [get]
func (h *PlayerFeatureHandler) GetSelectionByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid selection ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
sel, err := h.service.GetSelectionByID(uint(id))
if err != nil {
if err.Error() == "selection not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, sel)
}
// GetSelectionsByPlayer godoc
// @Summary List selections by player
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param playerId path string true "Player ID"
// @Success 200 {array} services.PlayerFeatureSelectionWithUserNameResponse
// @Router /player-features/selections/player/{playerId} [get]
func (h *PlayerFeatureHandler) GetSelectionsByPlayer(c *gin.Context) {
playerID := c.Param("playerId")
rows, err := h.service.GetSelectionsByPlayer(playerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, rows)
}
// GetSelectionsByPlayerAndScout godoc
// @Summary List selections by player and scout
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param playerId path string true "Player ID"
// @Param userId path int true "User ID"
// @Success 200 {array} services.PlayerFeatureSelectionResponse
// @Router /player-features/selections/player/{playerId}/scout/{userId} [get]
func (h *PlayerFeatureHandler) GetSelectionsByPlayerAndScout(c *gin.Context) {
playerID := c.Param("playerId")
userID, err := strconv.ParseUint(c.Param("userId"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
rows, err := h.service.GetSelectionsByPlayerAndScout(playerID, uint(userID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, rows)
}
// GetSelectionsByFeature godoc
// @Summary List selections by feature type
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param featureTypeId path int true "Feature type ID"
// @Success 200 {array} services.PlayerFeatureSelectionResponse
// @Router /player-features/selections/feature/{featureTypeId} [get]
func (h *PlayerFeatureHandler) GetSelectionsByFeature(c *gin.Context) {
featureTypeID, err := strconv.ParseUint(c.Param("featureTypeId"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid feature type ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
rows, err := h.service.GetSelectionsByFeatureType(uint(featureTypeID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, rows)
}
// DeleteSelection godoc
// @Summary Delete player feature selection
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param id path int true "Selection ID"
// @Success 204
// @Router /player-features/selections/{id} [delete]
func (h *PlayerFeatureHandler) DeleteSelection(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid selection ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.DeleteSelection(uint(id), userID.(uint)); err != nil {
if err.Error() == "selection not found or not owned by user" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// GetPlayerFeaturesWithRatings godoc
// @Summary Get aggregated player features
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param playerId path string true "Player ID"
// @Success 200 {object} services.PlayerFeaturesAggregatedResponse
// @Router /player-features/player/{playerId}/all [get]
func (h *PlayerFeatureHandler) GetPlayerFeaturesWithRatings(c *gin.Context) {
playerID := c.Param("playerId")
res, err := h.service.GetPlayerFeaturesWithRatings(playerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, res)
}
// UpdateRating godoc
// @Summary Update player feature rating
// @Tags Player Features
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Rating ID"
// @Param request body services.UpdateFeatureRatingRequest true "Rating"
// @Success 200 {object} services.PlayerFeatureRatingResponse
// @Failure 404 {object} map[string]interface{}
// @Router /player-features/ratings/{id} [patch]
func (h *PlayerFeatureHandler) UpdateRating(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid rating ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateFeatureRatingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
rating, err := h.service.UpdateRating(uint(id), userID.(uint), req)
if err != nil {
if err.Error() == "rating not found or not owned by user" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, rating)
}
// DeleteRating godoc
// @Summary Delete player feature rating
// @Tags Player Features
// @Security BearerAuth
// @Produce json
// @Param id path int true "Rating ID"
// @Success 204
// @Router /player-features/ratings/{id} [delete]
func (h *PlayerFeatureHandler) DeleteRating(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid rating ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.DeleteRating(uint(id), userID.(uint)); err != nil {
if err.Error() == "rating not found or not owned by user" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
type ListShareHandler struct {
service *services.ListShareService
}
func NewListShareHandler(db *gorm.DB) *ListShareHandler {
return &ListShareHandler{
service: services.NewListShareService(db),
}
}
func (h *ListShareHandler) Create(c *gin.Context) {
var req services.CreateListShareRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
share, err := h.service.Create(req)
if err != nil {
if err.Error() == "list share already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, share)
}
func (h *ListShareHandler) FindByListID(c *gin.Context) {
listID := c.Param("listId")
shares, err := h.service.FindByListID(listID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, shares)
}
func (h *ListShareHandler) Delete(c *gin.Context) {
listID := c.Param("listId")
userID, err := strconv.ParseUint(c.Param("userId"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Delete(listID, uint(userID)); err != nil {
if err.Error() == "list share not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type PlayerHandler struct {
service *services.PlayerService
}
func NewPlayerHandler(db *gorm.DB) *PlayerHandler {
return &PlayerHandler{
service: services.NewPlayerService(db),
}
}
// Save godoc
// @Summary Create or update a player
// @Description Create a new player or update existing by WyID
// @Tags Players
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreatePlayerRequest true "Player data"
// @Success 200 {object} services.PlayerResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Router /players [post]
func (h *PlayerHandler) Save(c *gin.Context) {
var req services.CreatePlayerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
player, err := h.service.UpsertByWyID(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, player)
}
// GetByID godoc
// @Summary Get player by ID
// @Description Get a single player by their internal ID
// @Tags Players
// @Security BearerAuth
// @Produce json
// @Param id path string true "Player ID"
// @Success 200 {object} services.PlayerResponse
// @Failure 404 {object} map[string]interface{} "Player not found"
// @Router /players/{id} [get]
func (h *PlayerHandler) GetByID(c *gin.Context) {
id := c.Param("id")
player, err := h.service.FindByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if player == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Player not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, player)
}
// GetByWyID godoc
// @Summary Get player by WyID
// @Description Get a single player by their Wyscout ID
// @Tags Players
// @Security BearerAuth
// @Produce json
// @Param wyId path int true "Wyscout Player ID"
// @Success 200 {object} services.PlayerResponse
// @Failure 404 {object} map[string]interface{} "Player not found"
// @Router /players/wy/{wyId} [get]
func (h *PlayerHandler) GetByWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid wyId",
"errorCode": "VALIDATION_ERROR",
})
return
}
player, err := h.service.FindByWyID(wyID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if player == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Player not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, player)
}
// FindAll godoc
// @Summary Get all players
// @Description Get paginated list of players with optional filters
// @Tags Players
// @Security BearerAuth
// @Produce json
// @Param name query string false "Filter by name"
// @Param foot query string false "Filter by preferred foot"
// @Param teamWyId query int false "Filter by team WyID"
// @Param nationalityWyId query int false "Filter by nationality WyID"
// @Param positions query string false "Filter by positions (comma-separated)"
// @Param minAge query int false "Minimum age"
// @Param maxAge query int false "Maximum age"
// @Param minHeight query int false "Minimum height in cm"
// @Param maxHeight query int false "Maximum height in cm"
// @Param gender query string false "Filter by gender"
// @Param status query string false "Filter by status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedPlayersResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /players [get]
func (h *PlayerHandler) FindAll(c *gin.Context) {
var query services.QueryPlayersRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// UpdateByID godoc
// @Summary Update a player
// @Description Update an existing player's information
// @Tags Players
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path string true "Player ID"
// @Param request body services.UpdatePlayerRequest true "Player data"
// @Success 200 {object} services.PlayerResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Player not found"
// @Router /players/{id} [patch]
func (h *PlayerHandler) UpdateByID(c *gin.Context) {
id := c.Param("id")
var req services.UpdatePlayerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
player, err := h.service.UpdateByID(id, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if player == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Player not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, player)
}
// DeleteByID godoc
// @Summary Delete a player
// @Description Soft delete a player
// @Tags Players
// @Security BearerAuth
// @Param id path string true "Player ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Player not found"
// @Router /players/{id} [delete]
func (h *PlayerHandler) DeleteByID(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteByID(id); err != nil {
if err.Error() == "player not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type PositionHandler struct {
service *services.PositionService
}
func NewPositionHandler(db *gorm.DB) *PositionHandler {
return &PositionHandler{
service: services.NewPositionService(db),
}
}
// Create godoc
// @Summary Create a new position
// @Description Create a new player position
// @Tags Positions
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreatePositionRequest true "Position data"
// @Success 201 {object} services.PositionResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Router /positions [post]
func (h *PositionHandler) Create(c *gin.Context) {
var req services.CreatePositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
position, err := h.service.Create(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, position)
}
// GetByID godoc
// @Summary Get position by ID
// @Description Get a single position by its ID
// @Tags Positions
// @Security BearerAuth
// @Produce json
// @Param id path int true "Position ID"
// @Success 200 {object} services.PositionResponse
// @Failure 404 {object} map[string]interface{} "Position not found"
// @Router /positions/{id} [get]
func (h *PositionHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid position ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
position, err := h.service.FindByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, position)
}
// FindAll godoc
// @Summary Get all positions
// @Description Get paginated list of positions with optional filters
// @Tags Positions
// @Security BearerAuth
// @Produce json
// @Param name query string false "Filter by name"
// @Param category query string false "Filter by category"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedPositionsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /positions [get]
func (h *PositionHandler) FindAll(c *gin.Context) {
var query services.QueryPositionsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update a position
// @Description Update an existing position
// @Tags Positions
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Position ID"
// @Param request body services.UpdatePositionRequest true "Position data"
// @Success 200 {object} services.PositionResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Position not found"
// @Router /positions/{id} [patch]
func (h *PositionHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid position ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdatePositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
position, err := h.service.Update(uint(id), req)
if err != nil {
if err.Error() == "position not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, position)
}
// Delete godoc
// @Summary Delete a position
// @Description Soft delete a position
// @Tags Positions
// @Security BearerAuth
// @Param id path int true "Position ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Position not found"
// @Router /positions/{id} [delete]
func (h *PositionHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid position ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Delete(uint(id)); err != nil {
if err.Error() == "position not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type ProfileHandler struct {
service *services.ProfileService
}
func NewProfileHandler(db *gorm.DB) *ProfileHandler {
return &ProfileHandler{service: services.NewProfileService(db)}
}
// CreateDescription godoc
// @Summary Create profile description
// @Tags Profiles
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateProfileDescriptionRequest true "Profile description"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Router /profiles/descriptions [post]
func (h *ProfileHandler) CreateDescription(c *gin.Context) {
var req services.CreateProfileDescriptionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
d, err := h.service.CreateDescription(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, d)
}
// UpdateDescription godoc
// @Summary Update profile description by ID
// @Tags Profiles
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Description ID"
// @Param request body services.UpdateProfileDescriptionRequest true "Update profile description"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]interface{}
// @Router /profiles/descriptions/{id} [patch]
func (h *ProfileHandler) UpdateDescription(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid description ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateProfileDescriptionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
d, err := h.service.UpdateDescription(uint(id), req)
if err != nil {
if err.Error() == "description not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, d)
}
// GetDescriptionByID godoc
// @Summary Get profile description by ID
// @Tags Profiles
// @Security BearerAuth
// @Produce json
// @Param id path int true "Description ID"
// @Success 200 {object} map[string]interface{}
// @Router /profiles/descriptions/{id} [get]
func (h *ProfileHandler) GetDescriptionByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid description ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
d, err := h.service.GetDescriptionByID(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, d)
}
// GetDescriptionByPlayerID godoc
// @Summary Get profile description for a player
// @Tags Profiles
// @Security BearerAuth
// @Produce json
// @Param playerId path int true "Player ID"
// @Success 200 {object} map[string]interface{}
// @Router /profiles/descriptions/player/{playerId} [get]
func (h *ProfileHandler) GetDescriptionByPlayerID(c *gin.Context) {
playerID, err := strconv.Atoi(c.Param("playerId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid player ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
d, err := h.service.GetDescriptionByPlayerID(playerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, d)
}
// GetDescriptionByCoachID godoc
// @Summary Get profile description for a coach
// @Tags Profiles
// @Security BearerAuth
// @Produce json
// @Param coachId path int true "Coach ID"
// @Success 200 {object} map[string]interface{}
// @Router /profiles/descriptions/coach/{coachId} [get]
func (h *ProfileHandler) GetDescriptionByCoachID(c *gin.Context) {
coachID, err := strconv.Atoi(c.Param("coachId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid coach ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
d, err := h.service.GetDescriptionByCoachID(coachID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, d)
}
// DeleteDescription godoc
// @Summary Delete profile description by ID
// @Tags Profiles
// @Security BearerAuth
// @Produce json
// @Param id path int true "Description ID"
// @Success 204
// @Failure 404 {object} map[string]interface{}
// @Router /profiles/descriptions/{id} [delete]
func (h *ProfileHandler) DeleteDescription(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid description ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
ok, err := h.service.DeleteDescription(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if !ok {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Description not found",
"errorCode": "NOT_FOUND",
})
return
}
c.Status(http.StatusNoContent)
}
// CreateLink godoc
// @Summary Create profile link
// @Tags Profiles
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateProfileLinkRequest true "Profile link"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Router /profiles/links [post]
func (h *ProfileHandler) CreateLink(c *gin.Context) {
var req services.CreateProfileLinkRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
link, err := h.service.CreateLink(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, link)
}
// UpdateLink godoc
// @Summary Update profile link by ID
// @Tags Profiles
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Link ID"
// @Param request body services.UpdateProfileLinkRequest true "Update profile link"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]interface{}
// @Router /profiles/links/{id} [patch]
func (h *ProfileHandler) UpdateLink(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid link ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateProfileLinkRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
link, err := h.service.UpdateLink(uint(id), req)
if err != nil {
if err.Error() == "link not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, link)
}
// GetLinkByID godoc
// @Summary Get profile link by ID
// @Tags Profiles
// @Security BearerAuth
// @Produce json
// @Param id path int true "Link ID"
// @Success 200 {object} map[string]interface{}
// @Router /profiles/links/{id} [get]
func (h *ProfileHandler) GetLinkByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid link ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
link, err := h.service.GetLinkByID(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, link)
}
// GetLinksByPlayerID godoc
// @Summary Get all profile links for a player
// @Tags Profiles
// @Security BearerAuth
// @Produce json
// @Param playerId path int true "Player ID"
// @Success 200 {array} map[string]interface{}
// @Router /profiles/links/player/{playerId} [get]
func (h *ProfileHandler) GetLinksByPlayerID(c *gin.Context) {
playerID, err := strconv.Atoi(c.Param("playerId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid player ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
links, err := h.service.GetLinksByPlayerID(playerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, links)
}
// GetLinksByCoachID godoc
// @Summary Get all profile links for a coach
// @Tags Profiles
// @Security BearerAuth
// @Produce json
// @Param coachId path int true "Coach ID"
// @Success 200 {array} map[string]interface{}
// @Router /profiles/links/coach/{coachId} [get]
func (h *ProfileHandler) GetLinksByCoachID(c *gin.Context) {
coachID, err := strconv.Atoi(c.Param("coachId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid coach ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
links, err := h.service.GetLinksByCoachID(coachID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, links)
}
// DeleteLink godoc
// @Summary Delete profile link by ID
// @Tags Profiles
// @Security BearerAuth
// @Produce json
// @Param id path int true "Link ID"
// @Success 204
// @Failure 404 {object} map[string]interface{}
// @Router /profiles/links/{id} [delete]
func (h *ProfileHandler) DeleteLink(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid link ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
ok, err := h.service.DeleteLink(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if !ok {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Link not found",
"errorCode": "NOT_FOUND",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type ReportHandler struct {
service *services.ReportService
}
func NewReportHandler(db *gorm.DB) *ReportHandler {
return &ReportHandler{
service: services.NewReportService(db),
}
}
// Create godoc
// @Summary Create a new report
// @Description Create a new scouting report
// @Tags Reports
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateReportRequest true "Report data"
// @Success 201 {object} services.ReportResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /reports [post]
func (h *ReportHandler) Create(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreateReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("POST /reports invalid request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"details": err.Error(),
"errorCode": "VALIDATION_ERROR",
})
return
}
report, err := h.service.Create(userID.(uint), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, report)
}
// GetByID godoc
// @Summary Get report by ID
// @Description Get a single report by its ID
// @Tags Reports
// @Security BearerAuth
// @Produce json
// @Param id path int true "Report ID"
// @Success 200 {object} services.ReportResponse
// @Failure 404 {object} map[string]interface{} "Report not found"
// @Router /reports/{id} [get]
func (h *ReportHandler) GetByID(c *gin.Context) {
id := c.Param("id")
report, err := h.service.FindByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, report)
}
// FindAll godoc
// @Summary Get all reports
// @Description Get paginated list of reports with optional filters
// @Tags Reports
// @Security BearerAuth
// @Produce json
// @Param playerWyId query int false "Filter by player WyID"
// @Param coachWyId query int false "Filter by coach WyID"
// @Param matchWyId query int false "Filter by match WyID"
// @Param type query string false "Filter by report type"
// @Param status query string false "Filter by status"
// @Param userId query int false "Filter by user ID"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedReportsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /reports [get]
func (h *ReportHandler) FindAll(c *gin.Context) {
var query services.QueryReportsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// Update godoc
// @Summary Update a report
// @Description Update an existing report's information
// @Tags Reports
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Report ID"
// @Param request body services.UpdateReportRequest true "Report data"
// @Success 200 {object} services.ReportResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Report not found"
// @Router /reports/{id} [patch]
func (h *ReportHandler) Update(c *gin.Context) {
id := c.Param("id")
var req services.UpdateReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
report, err := h.service.Update(id, req)
if err != nil {
if err.Error() == "report not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, report)
}
// Delete godoc
// @Summary Delete a report
// @Description Soft delete a report
// @Tags Reports
// @Security BearerAuth
// @Param id path int true "Report ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Report not found"
// @Router /reports/{id} [delete]
func (h *ReportHandler) Delete(c *gin.Context) {
id := c.Param("id")
if err := h.service.Delete(id); err != nil {
if err.Error() == "report not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type SettingsHandler struct {
service *services.SettingsService
}
func NewSettingsHandler(db *gorm.DB) *SettingsHandler {
return &SettingsHandler{
service: services.NewSettingsService(db),
}
}
// Global Settings
// CreateGlobalSetting godoc
// @Summary Create global setting
// @Tags Settings
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateGlobalSettingRequest true "Global setting"
// @Success 201 {object} services.GlobalSettingResponse
// @Failure 400 {object} map[string]interface{}
// @Failure 409 {object} map[string]interface{}
// @Router /settings/global [post]
func (h *SettingsHandler) CreateGlobalSetting(c *gin.Context) {
var req services.CreateGlobalSettingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
setting, err := h.service.CreateGlobalSetting(req)
if err != nil {
if err.Error() == "setting with same category and key already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, setting)
}
// GetGlobalSettings godoc
// @Summary List global settings
// @Tags Settings
// @Security BearerAuth
// @Produce json
// @Param category query string false "Filter by category"
// @Success 200 {array} services.GlobalSettingResponse
// @Router /settings/global [get]
func (h *SettingsHandler) GetGlobalSettings(c *gin.Context) {
category := c.Query("category")
settings, err := h.service.GetGlobalSettings(category)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, settings)
}
// GetGlobalSettingByID godoc
// @Summary Get global setting by ID
// @Tags Settings
// @Security BearerAuth
// @Produce json
// @Param id path int true "Setting ID"
// @Success 200 {object} services.GlobalSettingResponse
// @Failure 404 {object} map[string]interface{}
// @Router /settings/global/{id} [get]
func (h *SettingsHandler) GetGlobalSettingByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid setting ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
setting, err := h.service.GetGlobalSettingByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, setting)
}
// UpdateGlobalSetting godoc
// @Summary Update global setting
// @Tags Settings
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Setting ID"
// @Param request body services.UpdateGlobalSettingRequest true "Update global setting"
// @Success 200 {object} services.GlobalSettingResponse
// @Failure 404 {object} map[string]interface{}
// @Router /settings/global/{id} [patch]
func (h *SettingsHandler) UpdateGlobalSetting(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid setting ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateGlobalSettingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
setting, err := h.service.UpdateGlobalSetting(uint(id), req)
if err != nil {
if err.Error() == "global setting not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, setting)
}
// DeleteGlobalSetting godoc
// @Summary Delete global setting
// @Tags Settings
// @Security BearerAuth
// @Produce json
// @Param id path int true "Setting ID"
// @Success 200 {object} services.GlobalSettingResponse
// @Failure 404 {object} map[string]interface{}
// @Router /settings/global/{id} [delete]
func (h *SettingsHandler) DeleteGlobalSetting(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid setting ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
setting, err := h.service.DeleteGlobalSetting(uint(id))
if err != nil {
if err.Error() == "global setting not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, setting)
}
// GetGlobalSettingsByCategory godoc
// @Summary Get global settings by category
// @Tags Settings
// @Security BearerAuth
// @Produce json
// @Param category path string true "Category"
// @Success 200 {array} services.GlobalSettingResponse
// @Router /settings/global/category/{category} [get]
func (h *SettingsHandler) GetGlobalSettingsByCategory(c *gin.Context) {
category := c.Param("category")
settings, err := h.service.GetGlobalSettingsByCategory(category)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, settings)
}
// User Settings
// CreateUserSetting godoc
// @Summary Create user setting
// @Tags Settings
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateUserSettingRequest true "User setting"
// @Success 201 {object} services.UserSettingResponse
// @Failure 400 {object} map[string]interface{}
// @Failure 409 {object} map[string]interface{}
// @Router /settings/user [post]
func (h *SettingsHandler) CreateUserSetting(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreateUserSettingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
setting, err := h.service.CreateUserSetting(userID.(uint), req)
if err != nil {
if err.Error() == "setting with same category and key already exists for this user" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, setting)
}
// GetUserSettings godoc
// @Summary List user settings
// @Tags Settings
// @Security BearerAuth
// @Produce json
// @Param category query string false "Filter by category"
// @Success 200 {array} services.UserSettingResponse
// @Router /settings/user [get]
func (h *SettingsHandler) GetUserSettings(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
category := c.Query("category")
settings, err := h.service.GetUserSettings(userID.(uint), category)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, settings)
}
// GetUserSettingByID godoc
// @Summary Get user setting by ID
// @Tags Settings
// @Security BearerAuth
// @Produce json
// @Param id path int true "Setting ID"
// @Success 200 {object} services.UserSettingResponse
// @Failure 404 {object} map[string]interface{}
// @Router /settings/user/{id} [get]
func (h *SettingsHandler) GetUserSettingByID(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid setting ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
setting, err := h.service.GetUserSettingByID(userID.(uint), uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, setting)
}
// UpdateUserSetting godoc
// @Summary Update user setting
// @Tags Settings
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Setting ID"
// @Param request body services.UpdateUserSettingRequest true "Update user setting"
// @Success 200 {object} services.UserSettingResponse
// @Failure 404 {object} map[string]interface{}
// @Router /settings/user/{id} [patch]
func (h *SettingsHandler) UpdateUserSetting(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid setting ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateUserSettingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
setting, err := h.service.UpdateUserSetting(userID.(uint), uint(id), req)
if err != nil {
if err.Error() == "user setting not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, setting)
}
// DeleteUserSetting godoc
// @Summary Delete user setting
// @Tags Settings
// @Security BearerAuth
// @Produce json
// @Param id path int true "Setting ID"
// @Success 200 {object} services.UserSettingResponse
// @Failure 404 {object} map[string]interface{}
// @Router /settings/user/{id} [delete]
func (h *SettingsHandler) DeleteUserSetting(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid setting ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
setting, err := h.service.DeleteUserSetting(userID.(uint), uint(id))
if err != nil {
if err.Error() == "user setting not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, setting)
}
// BulkUpdateUserSettings godoc
// @Summary Bulk update user settings
// @Tags Settings
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.BulkUpdateUserSettingsRequest true "Bulk update payload"
// @Success 200 {array} services.UserSettingResponse
// @Failure 400 {object} map[string]interface{}
// @Router /settings/user/bulk [put]
func (h *SettingsHandler) BulkUpdateUserSettings(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.BulkUpdateUserSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
settings, err := h.service.BulkUpdateUserSettings(userID.(uint), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, settings)
}
// GetUserSettingsByCategory godoc
// @Summary Get user settings by category
// @Tags Settings
// @Security BearerAuth
// @Produce json
// @Param category path string true "Category"
// @Success 200 {array} services.UserSettingResponse
// @Router /settings/user/category/{category} [get]
func (h *SettingsHandler) GetUserSettingsByCategory(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
category := c.Param("category")
settings, err := h.service.GetUserSettingsByCategory(userID.(uint), category)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, settings)
}
// Categories
// CreateCategory godoc
// @Summary Create settings category
// @Tags Settings
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateSettingsCategoryRequest true "Category"
// @Success 201 {object} services.CategoryResponse
// @Failure 400 {object} map[string]interface{}
// @Failure 409 {object} map[string]interface{}
// @Router /settings/categories [post]
func (h *SettingsHandler) CreateCategory(c *gin.Context) {
var req services.CreateSettingsCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
category, err := h.service.CreateCategory(req)
if err != nil {
if err.Error() == "category with same name already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, category)
}
// GetCategories godoc
// @Summary List settings categories
// @Tags Settings
// @Security BearerAuth
// @Produce json
// @Param type query string false "Filter by type"
// @Success 200 {array} services.CategoryResponse
// @Router /settings/categories [get]
func (h *SettingsHandler) GetCategories(c *gin.Context) {
catType := c.Query("type")
categories, err := h.service.GetCategories(catType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, categories)
}
// GetCategoryByID godoc
// @Summary Get settings category by ID
// @Tags Settings
// @Security BearerAuth
// @Produce json
// @Param id path int true "Category ID"
// @Success 200 {object} services.CategoryResponse
// @Failure 404 {object} map[string]interface{}
// @Router /settings/categories/{id} [get]
func (h *SettingsHandler) GetCategoryByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid category ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
category, err := h.service.GetCategoryByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, category)
}
// UpdateCategory godoc
// @Summary Update settings category
// @Tags Settings
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Category ID"
// @Param request body services.CreateSettingsCategoryRequest true "Category"
// @Success 200 {object} services.CategoryResponse
// @Failure 404 {object} map[string]interface{}
// @Router /settings/categories/{id} [patch]
func (h *SettingsHandler) UpdateCategory(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid category ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.CreateSettingsCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
category, err := h.service.UpdateCategory(uint(id), req)
if err != nil {
if err.Error() == "category not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, category)
}
// DeleteCategory godoc
// @Summary Delete settings category
// @Tags Settings
// @Security BearerAuth
// @Produce json
// @Param id path int true "Category ID"
// @Success 200 {object} services.CategoryResponse
// @Failure 404 {object} map[string]interface{}
// @Router /settings/categories/{id} [delete]
func (h *SettingsHandler) DeleteCategory(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid category ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
category, err := h.service.DeleteCategory(uint(id))
if err != nil {
if err.Error() == "category not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, category)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type SuperAdminHandler struct {
service *services.SuperAdminService
}
func NewSuperAdminHandler(db *gorm.DB) *SuperAdminHandler {
return &SuperAdminHandler{service: services.NewSuperAdminService(db)}
}
// GetServerStatus godoc
// @Summary Get server status
// @Tags SuperAdmin
// @Security BasicAuth
// @Produce json
// @Success 200 {object} services.ServerStatusResponse
// @Router /superadmin/status [get]
func (h *SuperAdminHandler) GetServerStatus(c *gin.Context) {
res, err := h.service.GetServerStatus()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, res)
}
// CreateUser godoc
// @Summary Create a new user
// @Tags SuperAdmin
// @Security BasicAuth
// @Accept json
// @Produce json
// @Param request body services.CreateUserRequest true "User"
// @Success 201 {object} services.UserResponse
// @Failure 400 {object} map[string]interface{}
// @Failure 409 {object} map[string]interface{}
// @Router /superadmin/users [post]
func (h *SuperAdminHandler) CreateUser(c *gin.Context) {
var req services.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.CreateUser(req)
if err != nil {
if err.Error() == "email already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, user)
}
// GetAllUsers godoc
// @Summary Get all users
// @Tags SuperAdmin
// @Security BasicAuth
// @Produce json
// @Success 200 {array} services.UserResponse
// @Router /superadmin/users [get]
func (h *SuperAdminHandler) GetAllUsers(c *gin.Context) {
users, err := h.service.GetAllUsers()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, users)
}
// GetUserByID godoc
// @Summary Get user by ID
// @Tags SuperAdmin
// @Security BasicAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} services.UserResponse
// @Failure 404 {object} map[string]interface{}
// @Router /superadmin/users/{id} [get]
func (h *SuperAdminHandler) GetUserByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.GetUserByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, user)
}
// UpdateUser godoc
// @Summary Update user
// @Tags SuperAdmin
// @Security BasicAuth
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Param request body services.UpdateUserRequest true "Update user"
// @Success 200 {object} services.UserResponse
// @Failure 404 {object} map[string]interface{}
// @Failure 409 {object} map[string]interface{}
// @Router /superadmin/users/{id} [patch]
func (h *SuperAdminHandler) UpdateUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.UpdateUser(uint(id), req)
if err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
if err.Error() == "email already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, user)
}
// DeleteUser godoc
// @Summary Delete user
// @Tags SuperAdmin
// @Security BasicAuth
// @Param id path int true "User ID"
// @Success 204
// @Failure 404 {object} map[string]interface{}
// @Router /superadmin/users/{id} [delete]
func (h *SuperAdminHandler) DeleteUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.DeleteUser(uint(id)); err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// ActivateUser godoc
// @Summary Activate user
// @Tags SuperAdmin
// @Security BasicAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} services.UserResponse
// @Failure 404 {object} map[string]interface{}
// @Router /superadmin/users/{id}/activate [patch]
func (h *SuperAdminHandler) ActivateUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.ActivateUser(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, user)
}
// DeactivateUser godoc
// @Summary Deactivate user
// @Tags SuperAdmin
// @Security BasicAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} services.UserResponse
// @Failure 404 {object} map[string]interface{}
// @Router /superadmin/users/{id}/deactivate [patch]
func (h *SuperAdminHandler) DeactivateUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.DeactivateUser(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, user)
}
// UnlockAccount godoc
// @Summary Unlock user account
// @Tags SuperAdmin
// @Security BasicAuth
// @Param id path int true "User ID"
// @Success 204
// @Failure 404 {object} map[string]interface{}
// @Router /superadmin/users/{id}/unlock [patch]
func (h *SuperAdminHandler) UnlockAccount(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.UnlockAccount(uint(id)); err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// CreateClientModule godoc
// @Summary Create client module configuration
// @Tags SuperAdmin
// @Security BasicAuth
// @Accept json
// @Produce json
// @Param request body services.CreateClientModuleRequest true "Client module configuration"
// @Success 201 {object} models.ClientModule
// @Failure 409 {object} map[string]interface{}
// @Router /superadmin/client-modules [post]
func (h *SuperAdminHandler) CreateClientModule(c *gin.Context) {
var req services.CreateClientModuleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
cm, err := h.service.CreateClientModule(req)
if err != nil {
if err.Error() == "client module configuration already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, cm)
}
// GetClientModules godoc
// @Summary Get all client module configurations
// @Tags SuperAdmin
// @Security BasicAuth
// @Produce json
// @Success 200 {array} models.ClientModule
// @Router /superadmin/client-modules [get]
func (h *SuperAdminHandler) GetClientModules(c *gin.Context) {
rows, err := h.service.GetClientModules()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, rows)
}
// GetClientModuleByID godoc
// @Summary Get client module configuration by ID
// @Tags SuperAdmin
// @Security BasicAuth
// @Produce json
// @Param id path int true "Client module configuration ID"
// @Success 200 {object} models.ClientModule
// @Failure 404 {object} map[string]interface{}
// @Router /superadmin/client-modules/{id} [get]
func (h *SuperAdminHandler) GetClientModuleByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
row, err := h.service.GetClientModuleByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, row)
}
// GetClientModuleByClientID godoc
// @Summary Get client module configuration by client ID
// @Tags SuperAdmin
// @Security BasicAuth
// @Produce json
// @Param clientId path string true "Client ID"
// @Success 200 {object} models.ClientModule
// @Failure 404 {object} map[string]interface{}
// @Router /superadmin/client-modules/client/{clientId} [get]
func (h *SuperAdminHandler) GetClientModuleByClientID(c *gin.Context) {
clientID := c.Param("clientId")
row, err := h.service.GetClientModuleByClientID(clientID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, row)
}
// UpdateClientModule godoc
// @Summary Update client module configuration
// @Tags SuperAdmin
// @Security BasicAuth
// @Accept json
// @Produce json
// @Param id path int true "Client module configuration ID"
// @Param request body services.UpdateClientModuleRequest true "Update client module configuration"
// @Success 200 {object} models.ClientModule
// @Failure 404 {object} map[string]interface{}
// @Router /superadmin/client-modules/{id} [patch]
func (h *SuperAdminHandler) UpdateClientModule(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateClientModuleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
row, err := h.service.UpdateClientModule(uint(id), req)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, row)
}
// DeleteClientModule godoc
// @Summary Delete client module configuration
// @Tags SuperAdmin
// @Security BasicAuth
// @Produce json
// @Param id path int true "Client module configuration ID"
// @Success 200 {object} models.ClientModule
// @Failure 404 {object} map[string]interface{}
// @Router /superadmin/client-modules/{id} [delete]
func (h *SuperAdminHandler) DeleteClientModule(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
row, err := h.service.DeleteClientModule(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, row)
}
// GetModuleStatus godoc
// @Summary Get module status and role mappings
// @Tags SuperAdmin
// @Security BasicAuth
// @Produce json
// @Success 200 {object} services.ModulesStatusResponse
// @Router /superadmin/modules/status [get]
func (h *SuperAdminHandler) GetModuleStatus(c *gin.Context) {
res, err := h.service.GetModuleStatus()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, res)
}
// GetClientModuleStatus godoc
// @Summary Get client module status
// @Tags SuperAdmin
// @Security BasicAuth
// @Produce json
// @Param id path string true "Client ID"
// @Success 200 {object} services.ClientModulesStatusResponse
// @Failure 404 {object} map[string]interface{}
// @Router /superadmin/client-modules/{id}/status [get]
func (h *SuperAdminHandler) GetClientModuleStatus(c *gin.Context) {
clientID := c.Param("id")
res, err := h.service.GetClientModuleStatus(clientID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, res)
}
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type UserHandler struct {
service *services.UserService
}
func NewUserHandler(db *gorm.DB) *UserHandler {
return &UserHandler{
service: services.NewUserService(db),
}
}
// FindAll godoc
// @Summary Get all users
// @Description Get paginated list of users with optional filters
// @Tags Users
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param name query string false "Filter by name"
// @Param email query string false "Filter by email"
// @Param role query string false "Filter by role"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {array} services.UserResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /users [get]
func (h *UserHandler) FindAll(c *gin.Context) {
var query services.QueryUsersRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
users, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, users)
}
// FindOne godoc
// @Summary Get user by ID
// @Description Get a single user by their ID
// @Tags Users
// @Security BearerAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} services.UserResponse
// @Failure 404 {object} map[string]interface{} "User not found"
// @Router /users/{id} [get]
func (h *UserHandler) FindOne(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.FindOne(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, user)
}
// Create godoc
// @Summary Create a new user
// @Description Create a new user account
// @Tags Users
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateUserRequest true "User data"
// @Success 201 {object} services.UserResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 409 {object} map[string]interface{} "Email already exists"
// @Router /users [post]
func (h *UserHandler) Create(c *gin.Context) {
var req services.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.Create(req)
if err != nil {
if err.Error() == "email already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, user)
}
// Update godoc
// @Summary Update a user
// @Description Update an existing user's information
// @Tags Users
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Param request body services.UpdateUserRequest true "User data"
// @Success 200 {object} services.UserResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "User not found"
// @Failure 409 {object} map[string]interface{} "Email already exists"
// @Router /users/{id} [patch]
func (h *UserHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
var req services.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.Update(uint(id), req)
if err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
if err.Error() == "email already exists" {
c.JSON(http.StatusConflict, gin.H{
"statusCode": http.StatusConflict,
"message": err.Error(),
"errorCode": "CONFLICT",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, user)
}
// Remove godoc
// @Summary Delete a user
// @Description Soft delete a user account
// @Tags Users
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "User not found"
// @Router /users/{id} [delete]
func (h *UserHandler) Remove(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Remove(uint(id)); err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.Status(http.StatusNoContent)
}
// Activate godoc
// @Summary Activate a user
// @Description Activate a user account
// @Tags Users
// @Security BearerAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} services.UserResponse
// @Failure 404 {object} map[string]interface{} "User not found"
// @Router /users/{id}/activate [post]
func (h *UserHandler) Activate(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.Activate(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, user)
}
// Deactivate godoc
// @Summary Deactivate a user
// @Description Deactivate a user account
// @Tags Users
// @Security BearerAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} services.UserResponse
// @Failure 404 {object} map[string]interface{} "User not found"
// @Router /users/{id}/deactivate [post]
func (h *UserHandler) Deactivate(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
user, err := h.service.Deactivate(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": err.Error(),
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, user)
}
package middleware
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type Claims struct {
UserID uint `json:"userId"`
jwt.RegisteredClaims
}
func JWTAuth(db *gorm.DB, jwtSecret string) gin.HandlerFunc {
secret := []byte(jwtSecret)
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
tokenStr := ""
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
tokenStr = strings.TrimSpace(authHeader[len("Bearer "):])
} else {
// Swagger UI / manual clients sometimes send the raw JWT without the "Bearer " prefix.
// Accept it if it looks like a JWT (three dot-separated segments).
parts := strings.Split(authHeader, ".")
if len(parts) == 3 {
tokenStr = strings.TrimSpace(authHeader)
}
}
if tokenStr == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
claims := &Claims{}
tkn, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
return secret, nil
})
if err != nil || !tkn.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
var session models.UserSession
if err := db.Where("token = ?", tokenStr).First(&session).Error; err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
if time.Now().After(session.ExpiresAt) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
var user models.User
if err := db.Where("id = ?", claims.UserID).First(&user).Error; err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
if user.IsActive != nil && !*user.IsActive {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
c.Set("user", user)
c.Set("userId", user.ID)
c.Set("session", session)
c.Next()
}
}
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
"ScoutSystemElite/internal/models"
)
var AllRoles = []string{
"superadmin",
"admin",
"groupmanager",
"president",
"sportsdirector",
"chiefscout",
"staffscout",
"chiefdataanalyst",
"staffdataanalyst",
"chieftransfermarket",
"stafftransfermarket",
"viewer",
}
func RequireRoles(allowedRoles ...string) gin.HandlerFunc {
roleSet := make(map[string]bool)
for _, r := range allowedRoles {
roleSet[r] = true
}
return func(c *gin.Context) {
userVal, exists := c.Get("user")
if !exists {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
user, ok := userVal.(models.User)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
if !roleSet[user.Role] {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"statusCode": http.StatusForbidden,
"message": "Forbidden - insufficient permissions",
"errorCode": "FORBIDDEN",
})
return
}
c.Next()
}
}
func AdminOnly() gin.HandlerFunc {
return RequireRoles("superadmin", "admin")
}
func SuperAdminOnly() gin.HandlerFunc {
return RequireRoles("superadmin")
}
func ManagersAndAbove() gin.HandlerFunc {
return RequireRoles(
"superadmin",
"admin",
"groupmanager",
"president",
"sportsdirector",
"chiefscout",
"chiefdataanalyst",
"chieftransfermarket",
)
}
func AllAuthenticated() gin.HandlerFunc {
return RequireRoles(AllRoles...)
}
package models
import (
"time"
"gorm.io/datatypes"
)
type Foot string
type Gender string
type Status string
type CoachPosition string
type APISyncStatus string
type MatchType string
type MatchStatus string
type ReportStatus string
type CalendarEventType string
type ListType string
type PositionCategory string
type AuditLogAction string
// USERS & AUTHENTICATION
type User struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
Name string `gorm:"column:name;not null" json:"name"`
Email string `gorm:"column:email;not null;uniqueIndex:users_email_idx" json:"email"`
PasswordHash string `gorm:"column:password_hash;not null" json:"passwordHash"`
Role string `gorm:"column:role;not null;default:viewer;index:users_role_idx" json:"role"`
IsActive *bool `gorm:"column:is_active;default:true;index:users_is_active_idx" json:"isActive"`
EmailVerifiedAt *time.Time `gorm:"column:email_verified_at" json:"emailVerifiedAt"`
LastLoginAt *time.Time `gorm:"column:last_login_at" json:"lastLoginAt"`
TwoFactorEnabled *bool `gorm:"column:two_factor_enabled;default:false;index:users_two_factor_enabled_idx" json:"twoFactorEnabled"`
TwoFactorSecret *string `gorm:"column:two_factor_secret" json:"twoFactorSecret"`
FailedLoginAttempts *int `gorm:"column:failed_login_attempts;default:0" json:"failedLoginAttempts"`
LockedUntil *time.Time `gorm:"column:locked_until;index:users_locked_until_idx" json:"lockedUntil"`
CreatedAt time.Time `gorm:"column:created_at;not null;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"deletedAt"`
}
func (User) TableName() string { return "users" }
type UserSession struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
UserID uint `gorm:"column:user_id;not null;index:user_sessions_user_id_idx" json:"userId"`
Token string `gorm:"column:token;not null;uniqueIndex:user_sessions_token_idx" json:"token"`
DeviceID string `gorm:"column:device_id;not null;index:user_sessions_device_id_idx" json:"deviceId"`
DeviceInfo datatypes.JSON `gorm:"column:device_info;type:jsonb" json:"deviceInfo"`
ExpiresAt time.Time `gorm:"column:expires_at;not null;index:user_sessions_expires_at_idx" json:"expiresAt"`
CreatedAt time.Time `gorm:"column:created_at;not null;autoCreateTime" json:"createdAt"`
}
func (UserSession) TableName() string { return "user_sessions" }
// CORE DATA
type Area struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
WyID int `gorm:"column:wy_id;not null;uniqueIndex:idx_areas_wy_id" json:"wyId"`
Name string `gorm:"column:name;not null" json:"name"`
Alpha2Code *string `gorm:"column:alpha2code" json:"alpha2code"`
Alpha3Code *string `gorm:"column:alpha3code" json:"alpha3code"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"deletedAt"`
}
func (Area) TableName() string { return "areas" }
type Position struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
Name string `gorm:"column:name;not null;index:positions_name_idx" json:"name"`
Code2 *string `gorm:"column:code2;uniqueIndex:positions_code2_unique" json:"code2"`
Code3 *string `gorm:"column:code3;uniqueIndex:positions_code3_unique" json:"code3"`
Order *int `gorm:"column:order;default:0" json:"order"`
LocationX *int `gorm:"column:location_x" json:"locationX"`
LocationY *int `gorm:"column:location_y" json:"locationY"`
BGColor *string `gorm:"column:bg_color" json:"bgColor"`
TextColor *string `gorm:"column:text_color" json:"textColor"`
Category PositionCategory `gorm:"column:category;not null;index:positions_category_idx" json:"category"`
IsActive *bool `gorm:"column:is_active;default:true;index:positions_is_active_idx" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index:positions_deleted_at_idx" json:"deletedAt"`
}
func (Position) TableName() string { return "positions" }
type Player struct {
ID string `gorm:"primaryKey;size:16;column:id" json:"id"`
WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"`
TsID *string `gorm:"column:ts_id;size:64" json:"tsId"`
TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"`
TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"`
CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"`
FirstName string `gorm:"column:first_name" json:"firstName"`
LastName string `gorm:"column:last_name" json:"lastName"`
MiddleName *string `gorm:"column:middle_name" json:"middleName"`
ShortName *string `gorm:"column:short_name" json:"shortName"`
GSMID *int `gorm:"column:gsm_id" json:"gsmId"`
CurrentTeamID *int `gorm:"column:current_team_id" json:"currentTeamId"`
CurrentNationalTeamID *int `gorm:"column:current_national_team_id" json:"currentNationalTeamId"`
DateOfBirth *time.Time `gorm:"column:date_of_birth" json:"dateOfBirth"`
HeightCM *int `gorm:"column:height_cm" json:"heightCm"`
WeightKG *float64 `gorm:"column:weight_kg" json:"weightKg"`
Foot *string `gorm:"column:foot;type:foot" json:"foot"`
Gender *string `gorm:"column:gender;type:gender" json:"gender"`
PositionID *uint `gorm:"column:position_id;index:idx_players_position_id" json:"positionId"`
OtherPositionIDs datatypes.JSON `gorm:"column:other_position_ids;type:jsonb" json:"otherPositionIds"`
RoleName *string `gorm:"column:role_name" json:"roleName"`
BirthAreaWyID *int `gorm:"column:birth_area_wy_id" json:"birthAreaWyId"`
SecondBirthAreaWyID *int `gorm:"column:second_birth_area_wy_id" json:"secondBirthAreaWyId"`
PassportAreaWyID *int `gorm:"column:passport_area_wy_id" json:"passportAreaWyId"`
SecondPassportAreaWyID *int `gorm:"column:second_passport_area_wy_id" json:"secondPassportAreaWyId"`
MarketValue *int `gorm:"column:market_value" json:"marketValue"`
MarketValueCurrency *string `gorm:"column:market_value_currency" json:"marketValueCurrency"`
ContractUntil *time.Time `gorm:"column:contract_until" json:"contractUntil"`
Email *string `gorm:"column:email" json:"email"`
Phone *string `gorm:"column:phone" json:"phone"`
OnLoan bool `gorm:"column:on_loan;default:false" json:"onLoan"`
AgentName *string `gorm:"column:agent_name" json:"agent"`
Ranking *string `gorm:"column:ranking" json:"ranking"`
ROI *string `gorm:"column:roi" json:"roi"`
ValueRange *string `gorm:"column:value_range" json:"valueRange"`
TransferValue *float64 `gorm:"column:transfer_value" json:"transferValue"`
Salary *float64 `gorm:"column:salary" json:"salary"`
Feasible bool `gorm:"column:feasible;default:false" json:"feasible"`
Morphology *string `gorm:"column:morphology" json:"morphology"`
AbilityJSON *string `gorm:"column:ability_json;type:jsonb" json:"abilityJson"`
CharacteristicsJSON *string `gorm:"column:characteristics_json;type:jsonb" json:"characteristicsJson"`
UID *string `gorm:"column:uid" json:"uid"`
Deathday *time.Time `gorm:"column:deathday" json:"deathday"`
RetireTime *time.Time `gorm:"column:retire_time" json:"retireTime"`
Status string `gorm:"column:status;type:status;default:active" json:"status"`
ImageDataURL *string `gorm:"column:image_data_url" json:"imageDataUrl"`
JerseyNumber *int `gorm:"column:jersey_number" json:"jerseyNumber"`
APILastSyncedAt *time.Time `gorm:"column:api_last_synced_at" json:"apiLastSyncedAt"`
APISyncStatus string `gorm:"column:api_sync_status;type:api_sync_status;default:pending" json:"apiSyncStatus"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
func (Player) TableName() string { return "players" }
type Coach struct {
ID string `gorm:"primaryKey;size:16;column:id" json:"id"`
WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"`
TsID string `gorm:"column:ts_id;size:64" json:"tsId"`
CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"`
TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"`
FirstName string `gorm:"column:first_name" json:"firstName"`
LastName string `gorm:"column:last_name" json:"lastName"`
MiddleName *string `gorm:"column:middle_name" json:"middleName"`
ShortName *string `gorm:"column:short_name" json:"shortName"`
DateOfBirth *time.Time `gorm:"column:date_of_birth" json:"dateOfBirth"`
NationalityWyID *int `gorm:"column:nationality_wy_id" json:"nationalityWyId"`
CurrentTeamWyID *int `gorm:"column:current_team_wy_id" json:"currentTeamWyId"`
Position string `gorm:"column:position;type:coach_position;default:head_coach" json:"position"`
CoachingLicense *string `gorm:"column:coaching_license" json:"coachingLicense"`
YearsExperience *int `gorm:"column:years_experience" json:"yearsExperience"`
PreferredFormation *string `gorm:"column:preferred_formation" json:"preferredFormation"`
JoinedAt *time.Time `gorm:"column:joined_at" json:"joinedAt"`
ContractUntil *time.Time `gorm:"column:contract_until" json:"contractUntil"`
UID *string `gorm:"column:uid" json:"uid"`
Deathday *time.Time `gorm:"column:deathday" json:"deathday"`
Status string `gorm:"column:status;type:status;default:active" json:"status"`
ImageDataURL *string `gorm:"column:image_data_url" json:"imageDataUrl"`
APILastSyncedAt *time.Time `gorm:"column:api_last_synced_at" json:"apiLastSyncedAt"`
APISyncStatus string `gorm:"column:api_sync_status;type:api_sync_status;default:pending" json:"apiSyncStatus"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deletedAt"`
}
func (Coach) TableName() string { return "coaches" }
// AGENTS
type Agent struct {
ID string `gorm:"primaryKey;size:16;column:id" json:"id"`
Name string `gorm:"column:name;not null" json:"name"`
Description *string `gorm:"column:description" json:"description"`
Type string `gorm:"column:type;not null" json:"type"`
Email string `gorm:"column:email;not null" json:"email"`
Phone string `gorm:"column:phone;not null" json:"phone"`
Status string `gorm:"column:status;not null" json:"status"`
Address string `gorm:"column:address;not null" json:"address"`
CountryWyID int `gorm:"column:country_wy_id;not null" json:"countryWyId"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}
func (Agent) TableName() string { return "agents" }
type PlayerAgent struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
PlayerID string `gorm:"column:player_id;not null;size:16;uniqueIndex:player_agents_player_id_agent_id_idx,priority:1" json:"playerId"`
AgentID string `gorm:"column:agent_id;not null;size:16;uniqueIndex:player_agents_player_id_agent_id_idx,priority:2" json:"agentId"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}
func (PlayerAgent) TableName() string { return "player_agents" }
type CoachAgent struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
CoachID string `gorm:"column:coach_id;not null;size:16;uniqueIndex:coach_agents_coach_id_agent_id_idx,priority:1" json:"coachId"`
AgentID string `gorm:"column:agent_id;not null;size:16;uniqueIndex:coach_agents_coach_id_agent_id_idx,priority:2" json:"agentId"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}
func (CoachAgent) TableName() string { return "coach_agents" }
// REPORTS
type Report struct {
ID string `gorm:"primaryKey;size:16;column:id" json:"id"`
Name string `gorm:"column:name;not null" json:"name"`
PlayerDataID *string `gorm:"column:player_data_id" json:"playerDataId"`
CoachDataID *string `gorm:"column:coach_data_id" json:"coachDataId"`
MatchDataID *string `gorm:"column:match_data_id" json:"matchDataId"`
PlayerWyID *int `gorm:"column:player_wy_id" json:"playerWyId"`
CoachWyID *int `gorm:"column:coach_wy_id" json:"coachWyId"`
MatchWyID *int `gorm:"column:match_wy_id" json:"matchWyId"`
Type string `gorm:"column:type;not null" json:"type"`
Description datatypes.JSON `gorm:"column:description;type:jsonb" json:"description"`
Grade *string `gorm:"column:grade" json:"grade"`
Rating *float64 `gorm:"column:rating;type:numeric(5,2)" json:"rating"`
Decision *string `gorm:"column:decision" json:"decision"`
Status *ReportStatus `gorm:"column:status;default:saved" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
UserID *uint `gorm:"column:user_id" json:"userId"`
}
func (Report) TableName() string { return "reports" }
// CALENDAR
type CalendarEvent struct {
ID string `gorm:"primaryKey;size:16;column:id" json:"id"`
UserID uint `gorm:"column:user_id;not null;index:idx_calendar_events_user_id" json:"userId"`
Title string `gorm:"column:title;not null" json:"title"`
Description *string `gorm:"column:description" json:"description"`
EventType CalendarEventType `gorm:"column:event_type;not null;index:idx_calendar_events_event_type" json:"eventType"`
StartDate time.Time `gorm:"column:start_date;not null;index:idx_calendar_events_start_date" json:"startDate"`
EndDate *time.Time `gorm:"column:end_date" json:"endDate"`
MatchWyID *int `gorm:"column:match_wy_id;index:idx_calendar_events_match_wy_id" json:"matchWyId"`
PlayerWyID *int `gorm:"column:player_wy_id;index:idx_calendar_events_player_wy_id" json:"playerWyId"`
Metadata datatypes.JSON `gorm:"column:metadata;type:jsonb" json:"metadata"`
IsActive *bool `gorm:"column:is_active;default:true;index:idx_calendar_events_is_active" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index:idx_calendar_events_deleted_at" json:"deletedAt"`
}
func (CalendarEvent) TableName() string { return "calendar_events" }
// LISTS
type List struct {
ID string `gorm:"primaryKey;size:16;column:id" json:"id"`
UserID uint `gorm:"column:user_id;not null;index:idx_lists_user_id" json:"userId"`
Name string `gorm:"column:name;not null" json:"name"`
Season string `gorm:"column:season;not null;index:idx_lists_season" json:"season"`
Type ListType `gorm:"column:type;not null;index:idx_lists_type" json:"type"`
PlayersByPosition datatypes.JSON `gorm:"column:players_by_position;type:jsonb" json:"playersByPosition"`
IsActive *bool `gorm:"column:is_active;default:true;index:idx_lists_is_active" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index:idx_lists_deleted_at" json:"deletedAt"`
}
func (List) TableName() string { return "lists" }
type ListShare struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
ListID string `gorm:"column:list_id;not null;size:16;uniqueIndex:idx_list_shares_list_user,priority:1;index:idx_list_shares_list_id" json:"listId"`
UserID uint `gorm:"column:user_id;not null;uniqueIndex:idx_list_shares_list_user,priority:2;index:idx_list_shares_user_id" json:"userId"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}
func (ListShare) TableName() string { return "list_shares" }
// FILES
type File struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
FileName string `gorm:"column:file_name;not null" json:"fileName"`
OriginalFileName string `gorm:"column:original_file_name;not null" json:"originalFileName"`
FilePath string `gorm:"column:file_path;not null" json:"filePath"`
MimeType string `gorm:"column:mime_type;not null" json:"mimeType"`
FileSize int `gorm:"column:file_size;not null" json:"fileSize"`
EntityType string `gorm:"column:entity_type;not null;index:idx_files_entity_type" json:"entityType"`
EntityID *int `gorm:"column:entity_id;index:idx_files_entity_id" json:"entityId"`
EntityWyID *int `gorm:"column:entity_wy_id;index:idx_files_entity_wy_id" json:"entityWyId"`
Category *string `gorm:"column:category;index:idx_files_category" json:"category"`
Description *string `gorm:"column:description" json:"description"`
UploadedBy *uint `gorm:"column:uploaded_by;index:idx_files_uploaded_by" json:"uploadedBy"`
IsActive *bool `gorm:"column:is_active;default:true;index:idx_files_is_active" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index:idx_files_deleted_at" json:"deletedAt"`
}
func (File) TableName() string { return "files" }
// SETTINGS
type Category struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
Name string `gorm:"column:name;not null;uniqueIndex:categories_name_idx" json:"name"`
Description *string `gorm:"column:description" json:"description"`
Type string `gorm:"column:type;not null;index:categories_type_idx" json:"type"`
IsActive *bool `gorm:"column:is_active;default:true;index:categories_is_active_idx" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}
func (Category) TableName() string { return "categories" }
type GlobalSetting struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
Category string `gorm:"column:category;not null;index:global_settings_category_idx" json:"category"`
Key string `gorm:"column:key;not null;index:global_settings_key_idx" json:"key"`
Name string `gorm:"column:name;not null" json:"name"`
Description *string `gorm:"column:description" json:"description"`
Value datatypes.JSON `gorm:"column:value;not null;type:jsonb" json:"value"`
Color *string `gorm:"column:color" json:"color"`
IsActive *bool `gorm:"column:is_active;default:true;index:global_settings_is_active_idx" json:"isActive"`
SortOrder *int `gorm:"column:sort_order;default:0;index:global_settings_sort_order_idx" json:"sortOrder"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}
func (GlobalSetting) TableName() string { return "global_settings" }
type UserSetting struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
UserID uint `gorm:"column:user_id;not null;uniqueIndex:user_settings_user_category_key_idx,priority:1;index:user_settings_user_id_idx" json:"userId"`
Category string `gorm:"column:category;not null;uniqueIndex:user_settings_user_category_key_idx,priority:2;index:user_settings_category_idx" json:"category"`
Key string `gorm:"column:key;not null;uniqueIndex:user_settings_user_category_key_idx,priority:3;index:user_settings_key_idx" json:"key"`
Value datatypes.JSON `gorm:"column:value;not null;type:jsonb" json:"value"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}
func (UserSetting) TableName() string { return "user_settings" }
// CLIENT SUBSCRIPTIONS / MODULES
type ClientSubscription struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
ClientID string `gorm:"column:client_id;not null;uniqueIndex:client_subscriptions_client_id_idx" json:"clientId"`
Features datatypes.JSON `gorm:"column:features;not null;type:jsonb" json:"features"`
ExpiresAt *time.Time `gorm:"column:expires_at" json:"expiresAt"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}
func (ClientSubscription) TableName() string { return "client_subscriptions" }
type ClientModule struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
ClientID string `gorm:"column:client_id;not null;uniqueIndex:client_modules_client_id_idx" json:"clientId"`
Scouting *bool `gorm:"column:scouting;default:false" json:"scouting"`
DataAnalytics *bool `gorm:"column:data_analytics;default:false" json:"dataAnalytics"`
Transfers *bool `gorm:"column:transfers;default:false" json:"transfers"`
IsActive *bool `gorm:"column:is_active;default:true;index:client_modules_is_active_idx" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}
func (ClientModule) TableName() string { return "client_modules" }
// PLAYER FEATURES
type PlayerFeatureCategory struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
Name string `gorm:"column:name;not null;uniqueIndex:player_feature_categories_name_idx" json:"name"`
Description *string `gorm:"column:description" json:"description"`
Order *int `gorm:"column:order;default:0" json:"order"`
IsActive *bool `gorm:"column:is_active;default:true" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"deletedAt"`
}
func (PlayerFeatureCategory) TableName() string { return "player_feature_categories" }
type PlayerFeatureType struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
CategoryID uint `gorm:"column:category_id;not null;index:player_feature_types_category_id_idx" json:"categoryId"`
Name string `gorm:"column:name;not null;index:player_feature_types_name_idx" json:"name"`
Description *string `gorm:"column:description" json:"description"`
Order *int `gorm:"column:order;default:0;index:player_feature_types_order_idx" json:"order"`
IsActive *bool `gorm:"column:is_active;default:true;index:player_feature_types_is_active_idx" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index:player_feature_types_deleted_at_idx" json:"deletedAt"`
}
func (PlayerFeatureType) TableName() string { return "player_feature_types" }
type PlayerFeatureRating struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
PlayerID string `gorm:"column:player_id;not null;size:16;uniqueIndex:player_feature_ratings_player_feature_user_idx,priority:1;index:player_feature_ratings_player_id_idx" json:"playerId"`
FeatureTypeID uint `gorm:"column:feature_type_id;not null;uniqueIndex:player_feature_ratings_player_feature_user_idx,priority:2;index:player_feature_ratings_feature_type_id_idx" json:"featureTypeId"`
UserID uint `gorm:"column:user_id;not null;uniqueIndex:player_feature_ratings_player_feature_user_idx,priority:3;index:player_feature_ratings_user_id_idx" json:"userId"`
Rating *int `gorm:"column:rating;index:player_feature_ratings_rating_idx" json:"rating"`
Notes *string `gorm:"column:notes" json:"notes"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index:player_feature_ratings_deleted_at_idx" json:"deletedAt"`
}
func (PlayerFeatureRating) TableName() string { return "player_feature_ratings" }
type PlayerFeatureSelection struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
PlayerID string `gorm:"column:player_id;not null;size:16;uniqueIndex:player_feature_selections_player_feature_user_idx,priority:1;index:player_feature_selections_player_id_idx" json:"playerId"`
FeatureTypeID uint `gorm:"column:feature_type_id;not null;uniqueIndex:player_feature_selections_player_feature_user_idx,priority:2;index:player_feature_selections_feature_type_id_idx" json:"featureTypeId"`
UserID uint `gorm:"column:user_id;not null;uniqueIndex:player_feature_selections_player_feature_user_idx,priority:3;index:player_feature_selections_user_id_idx" json:"userId"`
Notes *string `gorm:"column:notes" json:"notes"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index:player_feature_selections_deleted_at_idx" json:"deletedAt"`
}
func (PlayerFeatureSelection) TableName() string { return "player_feature_selections" }
// PROFILES
type ProfileDescription struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
PlayerID *string `gorm:"column:player_id;size:16;index:profile_descriptions_player_id_idx" json:"playerId"`
CoachID *string `gorm:"column:coach_id;size:16;index:profile_descriptions_coach_id_idx" json:"coachId"`
Description string `gorm:"column:description;not null" json:"description"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index:profile_descriptions_deleted_at_idx" json:"deletedAt"`
}
func (ProfileDescription) TableName() string { return "profile_descriptions" }
type ProfileLink struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
PlayerID *string `gorm:"column:player_id;size:16;index:profile_links_player_id_idx" json:"playerId"`
CoachID *string `gorm:"column:coach_id;size:16;index:profile_links_coach_id_idx" json:"coachId"`
Title string `gorm:"column:title;not null" json:"title"`
URL string `gorm:"column:url;not null" json:"url"`
Icon *string `gorm:"column:icon" json:"icon"`
Order *int `gorm:"column:order;default:0" json:"order"`
IsActive *bool `gorm:"column:is_active;default:true;index:profile_links_is_active_idx" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index:profile_links_deleted_at_idx" json:"deletedAt"`
}
func (ProfileLink) TableName() string { return "profile_links" }
// AUDIT LOGS
type AuditLog struct {
ID uint `gorm:"primaryKey;column:id" json:"id"`
UserID *uint `gorm:"column:user_id;index:idx_audit_logs_user_id" json:"userId"`
Action AuditLogAction `gorm:"column:action;not null;index:idx_audit_logs_action" json:"action"`
EntityType string `gorm:"column:entity_type;not null;index:idx_audit_logs_entity_type" json:"entityType"`
EntityID *int `gorm:"column:entity_id;index:idx_audit_logs_entity_id" json:"entityId"`
EntityWyID *int `gorm:"column:entity_wy_id;index:idx_audit_logs_entity_wy_id" json:"entityWyId"`
OldValues datatypes.JSON `gorm:"column:old_values;type:jsonb" json:"oldValues"`
NewValues datatypes.JSON `gorm:"column:new_values;type:jsonb" json:"newValues"`
Changes datatypes.JSON `gorm:"column:changes;type:jsonb" json:"changes"`
IPAddress *string `gorm:"column:ip_address" json:"ipAddress"`
UserAgent *string `gorm:"column:user_agent" json:"userAgent"`
Metadata datatypes.JSON `gorm:"column:metadata;type:jsonb" json:"metadata"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
}
func (AuditLog) TableName() string { return "audit_logs" }
package router
import (
"encoding/base64"
"net/http"
"os"
"strconv"
"strings"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"gorm.io/gorm"
"ScoutSystemElite/internal/config"
"ScoutSystemElite/internal/handlers"
"ScoutSystemElite/internal/middleware"
"ScoutSystemElite/docs"
)
func decodeEnvMaybeBase64(raw string) string {
raw = strings.TrimSpace(raw)
raw = strings.Trim(raw, "\"'")
if raw == "" {
return raw
}
decoded, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return raw
}
return string(decoded)
}
func basicAuthMiddleware() gin.HandlerFunc {
rawUser := os.Getenv("API_USERNAME")
rawPass := os.Getenv("API_PASSWORD")
username := decodeEnvMaybeBase64(rawUser)
password := decodeEnvMaybeBase64(rawPass)
if username == "" && password == "" {
return func(c *gin.Context) { c.Next() }
}
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if !strings.HasPrefix(authHeader, "Basic ") {
c.Header("WWW-Authenticate", "Basic")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Basic authentication required"})
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[1] == "" {
c.Header("WWW-Authenticate", "Basic")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid authentication credentials"})
return
}
decoded, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
c.Header("WWW-Authenticate", "Basic")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid authentication credentials"})
return
}
creds := string(decoded)
up := strings.SplitN(creds, ":", 2)
if len(up) != 2 {
c.Header("WWW-Authenticate", "Basic")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid authentication credentials"})
return
}
if up[0] != username || up[1] != password {
c.Header("WWW-Authenticate", "Basic")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
return
}
c.Next()
}
}
func New(db *gorm.DB, cfg config.Config) *gin.Engine {
r := gin.Default()
port := cfg.Port
if port == "" {
port = os.Getenv("PORT")
}
if port == "" {
port = "3000"
}
if _, err := strconv.Atoi(port); err != nil {
port = "3000"
}
docs.SwaggerInfo.Host = "localhost:" + port
docs.SwaggerInfo.BasePath = "/api"
docs.SwaggerInfo.Schemes = []string{"http"}
r.Use(cors.New(cors.Config{
AllowAllOrigins: true,
AllowMethods: []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodOptions,
},
AllowHeaders: []string{"*"},
ExposeHeaders: []string{"*"},
AllowCredentials: false,
}))
r.GET("/health", func(c *gin.Context) {
if sqlDB, err := db.DB(); err == nil {
if err := sqlDB.Ping(); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"status": "unhealthy", "error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// Swagger documentation
swaggerHandler := ginSwagger.WrapHandler(
swaggerFiles.Handler,
ginSwagger.URL("/swagger/doc.json"),
ginSwagger.PersistAuthorization(true),
)
r.GET("/swagger/*any", func(c *gin.Context) {
// When Swagger UI is accessed via LAN IP (e.g. http://192.168.x.x:3000/swagger/index.html),
// the spec must not point to localhost, otherwise the browser will call its own localhost.
if c.Request != nil && c.Request.Host != "" {
docs.SwaggerInfo.Host = c.Request.Host
}
swaggerHandler(c)
})
jwtMiddleware := middleware.JWTAuth(db, cfg.JWTSecret)
api := r.Group("/api")
// Auth routes
authHandler := handlers.NewAuthHandler(db, cfg)
authGroup := api.Group("/auth")
{
authGroup.POST("/login", authHandler.Login)
authProtected := authGroup.Group("")
authProtected.Use(jwtMiddleware)
{
authProtected.POST("/logout", authHandler.Logout)
authProtected.POST("/logout-all", authHandler.LogoutAll)
authProtected.POST("/2fa/setup", authHandler.Setup2FA)
authProtected.POST("/2fa/verify", authHandler.Verify2FA)
authProtected.DELETE("/2fa/disable", authHandler.Disable2FA)
authProtected.PATCH("/change-password", authHandler.ChangePassword)
}
}
// Users routes
userHandler := handlers.NewUserHandler(db)
usersGroup := api.Group("/users")
usersGroup.Use(jwtMiddleware)
{
usersGroup.GET("", userHandler.FindAll)
usersGroup.GET("/:id", userHandler.FindOne)
usersGroup.POST("", userHandler.Create)
usersGroup.PATCH("/:id", userHandler.Update)
usersGroup.DELETE("/:id", userHandler.Remove)
usersGroup.PATCH("/:id/activate", userHandler.Activate)
usersGroup.PATCH("/:id/deactivate", userHandler.Deactivate)
}
// Players routes
playerHandler := handlers.NewPlayerHandler(db)
playersGroup := api.Group("/players")
playersGroup.Use(jwtMiddleware)
{
playersGroup.POST("", playerHandler.Save)
playersGroup.GET("", playerHandler.FindAll)
playersGroup.GET("/:id", playerHandler.GetByID)
playersGroup.GET("/wy/:wyId", playerHandler.GetByWyID)
playersGroup.PATCH("/:id", playerHandler.UpdateByID)
playersGroup.DELETE("/:id", playerHandler.DeleteByID)
}
// Coaches routes
coachHandler := handlers.NewCoachHandler(db)
coachesGroup := api.Group("/coaches")
coachesGroup.Use(jwtMiddleware)
{
coachesGroup.POST("", coachHandler.Save)
coachesGroup.GET("", coachHandler.FindAll)
coachesGroup.GET("/id/:id", coachHandler.GetByID)
coachesGroup.GET("/:wyId", coachHandler.GetByWyIDAlias)
coachesGroup.GET("/wy/:wyId", coachHandler.GetByWyID)
coachesGroup.PATCH("/:id", coachHandler.UpdateByID)
coachesGroup.DELETE("/:id", coachHandler.DeleteByID)
}
// Settings routes
settingsHandler := handlers.NewSettingsHandler(db)
settingsGroup := api.Group("/settings")
settingsGroup.Use(jwtMiddleware)
{
// Global settings
settingsGroup.GET("/global", settingsHandler.GetGlobalSettings)
settingsGroup.POST("/global", settingsHandler.CreateGlobalSetting)
settingsGroup.GET("/global/:id", settingsHandler.GetGlobalSettingByID)
settingsGroup.PATCH("/global/:id", settingsHandler.UpdateGlobalSetting)
settingsGroup.PUT("/global/:id", settingsHandler.UpdateGlobalSetting)
settingsGroup.DELETE("/global/:id", settingsHandler.DeleteGlobalSetting)
settingsGroup.GET("/global/category/:category", settingsHandler.GetGlobalSettingsByCategory)
// User settings
settingsGroup.GET("/user", settingsHandler.GetUserSettings)
settingsGroup.POST("/user", settingsHandler.CreateUserSetting)
settingsGroup.GET("/user/:id", settingsHandler.GetUserSettingByID)
settingsGroup.PATCH("/user/:id", settingsHandler.UpdateUserSetting)
settingsGroup.PUT("/user/:id", settingsHandler.UpdateUserSetting)
settingsGroup.DELETE("/user/:id", settingsHandler.DeleteUserSetting)
settingsGroup.PUT("/user/bulk", settingsHandler.BulkUpdateUserSettings)
settingsGroup.GET("/user/category/:category", settingsHandler.GetUserSettingsByCategory)
// Categories
settingsGroup.GET("/categories", settingsHandler.GetCategories)
settingsGroup.POST("/categories", settingsHandler.CreateCategory)
settingsGroup.GET("/categories/:id", settingsHandler.GetCategoryByID)
settingsGroup.PATCH("/categories/:id", settingsHandler.UpdateCategory)
settingsGroup.PUT("/categories/:id", settingsHandler.UpdateCategory)
settingsGroup.DELETE("/categories/:id", settingsHandler.DeleteCategory)
}
// Agents routes
agentHandler := handlers.NewAgentHandler(db)
agentsGroup := api.Group("/agents")
agentsGroup.Use(jwtMiddleware)
{
agentsGroup.POST("", agentHandler.Create)
agentsGroup.POST("/:agentId/players/:playerId", agentHandler.AssociateWithPlayer)
agentsGroup.POST("/:agentId/coaches/:coachId", agentHandler.AssociateWithCoach)
agentsGroup.DELETE("/:agentId/players/:playerId", agentHandler.RemoveFromPlayer)
agentsGroup.DELETE("/:agentId/coaches/:coachId", agentHandler.RemoveFromCoach)
agentsGroup.GET("/by-player/:playerId", agentHandler.GetByPlayer)
agentsGroup.GET("/by-coach/:coachId", agentHandler.GetByCoach)
agentsGroup.GET("", agentHandler.FindAll)
agentsGroup.GET("/:agentId", agentHandler.GetByID)
agentsGroup.PATCH("/:agentId", agentHandler.Update)
agentsGroup.DELETE("/:agentId", agentHandler.Delete)
}
// Areas routes
areaHandler := handlers.NewAreaHandler(db)
areasGroup := api.Group("/areas")
areasGroup.Use(jwtMiddleware)
{
areasGroup.POST("", areaHandler.Create)
areasGroup.GET("", areaHandler.FindAll)
areasGroup.GET("/id/:id", areaHandler.GetByID)
areasGroup.GET("/:wyId", areaHandler.GetByWyIDAlias)
areasGroup.GET("/wy/:wyId", areaHandler.GetByWyID)
areasGroup.PATCH("/:id", areaHandler.Update)
areasGroup.DELETE("/:id", areaHandler.Delete)
}
// Reports routes
reportHandler := handlers.NewReportHandler(db)
reportsGroup := api.Group("/reports")
reportsGroup.Use(jwtMiddleware)
{
reportsGroup.POST("", reportHandler.Create)
reportsGroup.GET("", reportHandler.FindAll)
reportsGroup.GET("/:id", reportHandler.GetByID)
reportsGroup.PATCH("/:id", reportHandler.Update)
reportsGroup.DELETE("/:id", reportHandler.Delete)
}
// Calendar routes
calendarHandler := handlers.NewCalendarHandler(db)
calendarGroup := api.Group("/calendar")
calendarGroup.Use(jwtMiddleware)
{
calendarGroup.POST("", calendarHandler.Create)
calendarGroup.GET("", calendarHandler.FindAll)
calendarGroup.GET("/:id", calendarHandler.GetByID)
calendarGroup.PATCH("/:id", calendarHandler.Update)
calendarGroup.DELETE("/:id", calendarHandler.Delete)
}
// Lists routes
listHandler := handlers.NewListHandler(db)
listsGroup := api.Group("/lists")
listsGroup.Use(jwtMiddleware)
{
listsGroup.POST("", listHandler.Create)
listsGroup.GET("", listHandler.FindAll)
listsGroup.GET("/shared-with-me", listHandler.FindSharedWithMe)
listsGroup.GET("/:id", listHandler.GetByID)
listsGroup.PATCH("/:id", listHandler.Update)
listsGroup.DELETE("/:id", listHandler.Delete)
}
// Positions routes
positionHandler := handlers.NewPositionHandler(db)
positionsGroup := api.Group("/positions")
positionsGroup.Use(jwtMiddleware)
{
positionsGroup.POST("", positionHandler.Create)
positionsGroup.GET("", positionHandler.FindAll)
positionsGroup.GET("/:id", positionHandler.GetByID)
positionsGroup.PATCH("/:id", positionHandler.Update)
positionsGroup.DELETE("/:id", positionHandler.Delete)
}
// Files routes
fileHandler := handlers.NewFileHandler(db)
filesGroup := api.Group("/files")
filesGroup.Use(jwtMiddleware)
{
filesGroup.POST("", fileHandler.Create)
filesGroup.GET("", fileHandler.FindAll)
filesGroup.GET("/:id", fileHandler.GetByID)
filesGroup.GET("/entity/:entityType", fileHandler.FindByEntity)
filesGroup.PATCH("/:id", fileHandler.Update)
filesGroup.DELETE("/:id", fileHandler.Delete)
}
// Player-Agent relations
playerAgentHandler := handlers.NewPlayerAgentHandler(db)
playerAgentsGroup := api.Group("/player-agents")
playerAgentsGroup.Use(jwtMiddleware)
{
playerAgentsGroup.POST("", playerAgentHandler.Create)
playerAgentsGroup.GET("/player/:playerId", playerAgentHandler.FindByPlayerID)
playerAgentsGroup.GET("/agent/:agentId", playerAgentHandler.FindByAgentID)
playerAgentsGroup.DELETE("/:playerId/:agentId", playerAgentHandler.Delete)
}
// Coach-Agent relations
coachAgentHandler := handlers.NewCoachAgentHandler(db)
coachAgentsGroup := api.Group("/coach-agents")
coachAgentsGroup.Use(jwtMiddleware)
{
coachAgentsGroup.POST("", coachAgentHandler.Create)
coachAgentsGroup.GET("/coach/:coachId", coachAgentHandler.FindByCoachID)
coachAgentsGroup.GET("/agent/:agentId", coachAgentHandler.FindByAgentID)
coachAgentsGroup.DELETE("/:coachId/:agentId", coachAgentHandler.Delete)
}
// Profiles routes
profileHandler := handlers.NewProfileHandler(db)
profilesGroup := api.Group("/profiles")
profilesGroup.Use(jwtMiddleware)
{
// Descriptions
profilesGroup.POST("/descriptions", profileHandler.CreateDescription)
profilesGroup.PATCH("/descriptions/:id", profileHandler.UpdateDescription)
profilesGroup.GET("/descriptions/:id", profileHandler.GetDescriptionByID)
profilesGroup.GET("/descriptions/player/:playerId", profileHandler.GetDescriptionByPlayerID)
profilesGroup.GET("/descriptions/coach/:coachId", profileHandler.GetDescriptionByCoachID)
profilesGroup.DELETE("/descriptions/:id", profileHandler.DeleteDescription)
// Links
profilesGroup.POST("/links", profileHandler.CreateLink)
profilesGroup.PATCH("/links/:id", profileHandler.UpdateLink)
profilesGroup.GET("/links/:id", profileHandler.GetLinkByID)
profilesGroup.GET("/links/player/:playerId", profileHandler.GetLinksByPlayerID)
profilesGroup.GET("/links/coach/:coachId", profileHandler.GetLinksByCoachID)
profilesGroup.DELETE("/links/:id", profileHandler.DeleteLink)
}
// Player Features routes
playerFeatureHandler := handlers.NewPlayerFeatureHandler(db)
pfGroup := api.Group("/player-features")
pfGroup.Use(jwtMiddleware)
{
// Categories
pfGroup.POST("/categories", playerFeatureHandler.CreateCategory)
pfGroup.GET("/categories", playerFeatureHandler.GetCategories)
pfGroup.GET("/categories/:id", playerFeatureHandler.GetCategoryByID)
pfGroup.PATCH("/categories/:id", playerFeatureHandler.UpdateCategory)
pfGroup.DELETE("/categories/:id", playerFeatureHandler.DeleteCategory)
// Feature Types
pfGroup.POST("/types", playerFeatureHandler.CreateFeatureType)
pfGroup.GET("/types", playerFeatureHandler.GetFeatureTypes)
pfGroup.GET("/types/category/:categoryId", playerFeatureHandler.GetFeatureTypesByCategory)
pfGroup.GET("/types/:id", playerFeatureHandler.GetFeatureTypeByID)
pfGroup.PATCH("/types/:id", playerFeatureHandler.UpdateFeatureType)
pfGroup.DELETE("/types/:id", playerFeatureHandler.DeleteFeatureType)
// Ratings
pfGroup.POST("/ratings", playerFeatureHandler.CreateRating)
pfGroup.GET("/ratings/:id", playerFeatureHandler.GetRatingByID)
pfGroup.GET("/ratings/player/:playerId", playerFeatureHandler.GetRatingsByPlayer)
pfGroup.GET("/ratings/player/:playerId/scout/:userId", playerFeatureHandler.GetRatingsByPlayerAndScout)
pfGroup.GET("/ratings/feature/:featureTypeId", playerFeatureHandler.GetRatingsByFeature)
pfGroup.PATCH("/ratings/:id", playerFeatureHandler.UpdateRating)
pfGroup.DELETE("/ratings/:id", playerFeatureHandler.DeleteRating)
// Aggregated
pfGroup.GET("/player/:playerId/all", playerFeatureHandler.GetPlayerFeaturesWithRatings)
// Selections
pfGroup.POST("/selections", playerFeatureHandler.CreateSelection)
pfGroup.GET("/selections/:id", playerFeatureHandler.GetSelectionByID)
pfGroup.GET("/selections/player/:playerId", playerFeatureHandler.GetSelectionsByPlayer)
pfGroup.GET("/selections/player/:playerId/scout/:userId", playerFeatureHandler.GetSelectionsByPlayerAndScout)
pfGroup.GET("/selections/feature/:featureTypeId", playerFeatureHandler.GetSelectionsByFeature)
pfGroup.PATCH("/selections/:id", playerFeatureHandler.UpdateSelection)
pfGroup.DELETE("/selections/:id", playerFeatureHandler.DeleteSelection)
}
// List Shares routes
listShareHandler := handlers.NewListShareHandler(db)
listSharesGroup := api.Group("/list-shares")
listSharesGroup.Use(jwtMiddleware)
{
listSharesGroup.POST("", listShareHandler.Create)
listSharesGroup.GET("/list/:listId", listShareHandler.FindByListID)
listSharesGroup.DELETE("/:listId/:userId", listShareHandler.Delete)
}
superadmin := api.Group("/superadmin")
superadmin.Use(basicAuthMiddleware())
{
saHandler := handlers.NewSuperAdminHandler(db)
superadmin.GET("/status", saHandler.GetServerStatus)
// Users
superadmin.POST("/users", saHandler.CreateUser)
superadmin.GET("/users", saHandler.GetAllUsers)
superadmin.GET("/users/:id", saHandler.GetUserByID)
superadmin.PATCH("/users/:id", saHandler.UpdateUser)
superadmin.DELETE("/users/:id", saHandler.DeleteUser)
superadmin.PATCH("/users/:id/activate", saHandler.ActivateUser)
superadmin.PATCH("/users/:id/deactivate", saHandler.DeactivateUser)
superadmin.PATCH("/users/:id/unlock", saHandler.UnlockAccount)
// Client modules
superadmin.POST("/client-modules", saHandler.CreateClientModule)
superadmin.GET("/client-modules", saHandler.GetClientModules)
superadmin.GET("/modules/status", saHandler.GetModuleStatus)
superadmin.GET("/client-modules/:id/status", saHandler.GetClientModuleStatus)
superadmin.GET("/client-modules/client/:clientId", saHandler.GetClientModuleByClientID)
superadmin.GET("/client-modules/:id", saHandler.GetClientModuleByID)
superadmin.PATCH("/client-modules/:id", saHandler.UpdateClientModule)
superadmin.DELETE("/client-modules/:id", saHandler.DeleteClientModule)
}
return r
}
package services
import (
"errors"
"fmt"
"strings"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type AgentService struct {
db *gorm.DB
}
func NewAgentService(db *gorm.DB) *AgentService {
return &AgentService{db: db}
}
type AgentResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
Type string `json:"type"`
Email string `json:"email"`
Phone string `json:"phone"`
Status string `json:"status"`
Address string `json:"address"`
CountryWyID int `json:"countryWyId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type QueryAgentsRequest struct {
Name string `form:"name"`
Type string `form:"type"`
Status string `form:"status"`
CountryWyID *int `form:"countryWyId"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreateAgentRequest struct {
Name string `json:"name" binding:"required"`
Description *string `json:"description"`
Type string `json:"type" binding:"required"`
Email string `json:"email" binding:"required,email"`
Phone string `json:"phone" binding:"required"`
Status string `json:"status" binding:"required"`
Address string `json:"address" binding:"required"`
CountryWyID int `json:"countryWyId" binding:"required"`
}
type UpdateAgentRequest struct {
Name *string `json:"name"`
Description *string `json:"description"`
Type *string `json:"type"`
Email *string `json:"email"`
Phone *string `json:"phone"`
Status *string `json:"status"`
Address *string `json:"address"`
CountryWyID *int `json:"countryWyId"`
}
type PaginatedAgentsResponse struct {
Data []AgentResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
type AgentPlayerAssociationResponse struct {
ID uint `json:"id"`
AgentID string `json:"agentId"`
PlayerID string `json:"playerId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type AgentCoachAssociationResponse struct {
ID uint `json:"id"`
AgentID string `json:"agentId"`
CoachID string `json:"coachId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (s *AgentService) Create(req CreateAgentRequest) (*AgentResponse, error) {
agent := models.Agent{
Name: req.Name,
Description: req.Description,
Type: req.Type,
Email: req.Email,
Phone: req.Phone,
Status: req.Status,
Address: req.Address,
CountryWyID: req.CountryWyID,
}
if err := s.db.Create(&agent).Error; err != nil {
return nil, err
}
return toAgentResponse(agent), nil
}
func (s *AgentService) FindByID(id string) (*AgentResponse, error) {
var agent models.Agent
if err := s.db.First(&agent, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("agent not found")
}
return nil, err
}
return toAgentResponse(agent), nil
}
func (s *AgentService) FindAll(query QueryAgentsRequest) (*PaginatedAgentsResponse, error) {
q := s.db.Model(&models.Agent{})
if query.Name != "" {
q = q.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(query.Name)+"%")
}
if query.Type != "" {
q = q.Where("type = ?", query.Type)
}
if query.Status != "" {
q = q.Where("status = ?", query.Status)
}
if query.CountryWyID != nil {
q = q.Where("country_wy_id = ?", *query.CountryWyID)
}
var total int64
q.Count(&total)
sortBy := "name"
allowedSorts := map[string]string{
"name": "name",
"type": "type",
"status": "status",
"createdAt": "created_at",
"updatedAt": "updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var agents []models.Agent
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&agents).Error; err != nil {
return nil, err
}
data := make([]AgentResponse, len(agents))
for i, a := range agents {
data[i] = *toAgentResponse(a)
}
return &PaginatedAgentsResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(agents)) < total,
}, nil
}
func (s *AgentService) Update(id string, req UpdateAgentRequest) (*AgentResponse, error) {
var agent models.Agent
if err := s.db.First(&agent, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("agent not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Type != nil {
updates["type"] = *req.Type
}
if req.Email != nil {
updates["email"] = *req.Email
}
if req.Phone != nil {
updates["phone"] = *req.Phone
}
if req.Status != nil {
updates["status"] = *req.Status
}
if req.Address != nil {
updates["address"] = *req.Address
}
if req.CountryWyID != nil {
updates["country_wy_id"] = *req.CountryWyID
}
if len(updates) > 0 {
if err := s.db.Model(&agent).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&agent, "id = ?", id)
}
return toAgentResponse(agent), nil
}
func (s *AgentService) Delete(id string) error {
var agent models.Agent
if err := s.db.First(&agent, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("agent not found")
}
return err
}
return s.db.Delete(&agent).Error
}
func (s *AgentService) AssociateWithPlayer(agentID string, playerID string) (*AgentPlayerAssociationResponse, error) {
var existing models.PlayerAgent
if err := s.db.Where("agent_id = ? AND player_id = ?", agentID, playerID).First(&existing).Error; err == nil {
return toAgentPlayerAssociationResponse(existing), nil
}
pa := models.PlayerAgent{AgentID: agentID, PlayerID: playerID}
if err := s.db.Create(&pa).Error; err != nil {
return nil, err
}
return toAgentPlayerAssociationResponse(pa), nil
}
func (s *AgentService) AssociateWithCoach(agentID string, coachID string) (*AgentCoachAssociationResponse, error) {
var existing models.CoachAgent
if err := s.db.Where("agent_id = ? AND coach_id = ?", agentID, coachID).First(&existing).Error; err == nil {
return toAgentCoachAssociationResponse(existing), nil
}
ca := models.CoachAgent{AgentID: agentID, CoachID: coachID}
if err := s.db.Create(&ca).Error; err != nil {
return nil, err
}
return toAgentCoachAssociationResponse(ca), nil
}
func (s *AgentService) RemoveFromPlayer(agentID string, playerID string) error {
return s.db.Where("agent_id = ? AND player_id = ?", agentID, playerID).Delete(&models.PlayerAgent{}).Error
}
func (s *AgentService) RemoveFromCoach(agentID string, coachID string) error {
return s.db.Where("agent_id = ? AND coach_id = ?", agentID, coachID).Delete(&models.CoachAgent{}).Error
}
func (s *AgentService) GetAgentsByPlayer(playerID string) ([]AgentResponse, error) {
var agents []models.Agent
if err := s.db.
Model(&models.Agent{}).
Joins("JOIN player_agents ON player_agents.agent_id = agents.id").
Where("player_agents.player_id = ?", playerID).
Order("agents.name asc").
Find(&agents).Error; err != nil {
return nil, err
}
res := make([]AgentResponse, len(agents))
for i, a := range agents {
res[i] = *toAgentResponse(a)
}
return res, nil
}
func (s *AgentService) GetAgentsByCoach(coachID string) ([]AgentResponse, error) {
var agents []models.Agent
if err := s.db.
Model(&models.Agent{}).
Joins("JOIN coach_agents ON coach_agents.agent_id = agents.id").
Where("coach_agents.coach_id = ?", coachID).
Order("agents.name asc").
Find(&agents).Error; err != nil {
return nil, err
}
res := make([]AgentResponse, len(agents))
for i, a := range agents {
res[i] = *toAgentResponse(a)
}
return res, nil
}
func toAgentResponse(a models.Agent) *AgentResponse {
return &AgentResponse{
ID: a.ID,
Name: a.Name,
Description: a.Description,
Type: a.Type,
Email: a.Email,
Phone: a.Phone,
Status: a.Status,
Address: a.Address,
CountryWyID: a.CountryWyID,
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
}
}
func toAgentPlayerAssociationResponse(pa models.PlayerAgent) *AgentPlayerAssociationResponse {
return &AgentPlayerAssociationResponse{
ID: pa.ID,
AgentID: pa.AgentID,
PlayerID: pa.PlayerID,
CreatedAt: pa.CreatedAt,
UpdatedAt: pa.UpdatedAt,
}
}
func toAgentCoachAssociationResponse(ca models.CoachAgent) *AgentCoachAssociationResponse {
return &AgentCoachAssociationResponse{
ID: ca.ID,
AgentID: ca.AgentID,
CoachID: ca.CoachID,
CreatedAt: ca.CreatedAt,
UpdatedAt: ca.UpdatedAt,
}
}
package services
import (
"errors"
"fmt"
"strings"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type AreaService struct {
db *gorm.DB
}
func NewAreaService(db *gorm.DB) *AreaService {
return &AreaService{db: db}
}
type AreaResponse struct {
ID uint `json:"id"`
WyID int `json:"wyId"`
Name string `json:"name"`
Alpha2Code *string `json:"alpha2code"`
Alpha3Code *string `json:"alpha3code"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
}
type QueryAreasRequest struct {
Name string `form:"name"`
Alpha2 string `form:"alpha2"`
Alpha3 string `form:"alpha3"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreateAreaRequest struct {
WyID int `json:"wyId" binding:"required"`
Name string `json:"name" binding:"required"`
Alpha2Code *string `json:"alpha2code"`
Alpha3Code *string `json:"alpha3code"`
}
type UpdateAreaRequest struct {
WyID *int `json:"wyId"`
Name *string `json:"name"`
Alpha2Code *string `json:"alpha2code"`
Alpha3Code *string `json:"alpha3code"`
}
type PaginatedAreasResponse struct {
Data []AreaResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
func (s *AreaService) Create(req CreateAreaRequest) (*AreaResponse, error) {
var existing models.Area
if err := s.db.Where("wy_id = ?", req.WyID).First(&existing).Error; err == nil {
return nil, fmt.Errorf("area with same wyId already exists")
}
area := models.Area{
WyID: req.WyID,
Name: req.Name,
Alpha2Code: req.Alpha2Code,
Alpha3Code: req.Alpha3Code,
}
if err := s.db.Create(&area).Error; err != nil {
return nil, err
}
return toAreaResponse(area), nil
}
func (s *AreaService) FindByID(id uint) (*AreaResponse, error) {
var area models.Area
if err := s.db.First(&area, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("area not found")
}
return nil, err
}
return toAreaResponse(area), nil
}
func (s *AreaService) FindByWyID(wyID int) (*AreaResponse, error) {
var area models.Area
if err := s.db.Where("wy_id = ?", wyID).First(&area).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("area not found")
}
return nil, err
}
return toAreaResponse(area), nil
}
func (s *AreaService) FindAll(query QueryAreasRequest) (*PaginatedAreasResponse, error) {
q := s.db.Model(&models.Area{})
if query.Name != "" {
q = q.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(query.Name)+"%")
}
if query.Alpha2 != "" {
q = q.Where("alpha2code = ?", strings.ToUpper(query.Alpha2))
}
if query.Alpha3 != "" {
q = q.Where("alpha3code = ?", strings.ToUpper(query.Alpha3))
}
var total int64
q.Count(&total)
sortBy := "name"
allowedSorts := map[string]string{
"name": "name",
"wyId": "wy_id",
"alpha2": "alpha2code",
"alpha3": "alpha3code",
"createdAt": "created_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var areas []models.Area
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&areas).Error; err != nil {
return nil, err
}
data := make([]AreaResponse, len(areas))
for i, a := range areas {
data[i] = *toAreaResponse(a)
}
return &PaginatedAreasResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(areas)) < total,
}, nil
}
func (s *AreaService) Update(id uint, req UpdateAreaRequest) (*AreaResponse, error) {
var area models.Area
if err := s.db.First(&area, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("area not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.WyID != nil {
updates["wy_id"] = *req.WyID
}
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Alpha2Code != nil {
updates["alpha2code"] = *req.Alpha2Code
}
if req.Alpha3Code != nil {
updates["alpha3code"] = *req.Alpha3Code
}
if len(updates) > 0 {
if err := s.db.Model(&area).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&area, id)
}
return toAreaResponse(area), nil
}
func (s *AreaService) Delete(id uint) error {
var area models.Area
if err := s.db.First(&area, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("area not found")
}
return err
}
return s.db.Delete(&area).Error
}
func toAreaResponse(a models.Area) *AreaResponse {
return &AreaResponse{
ID: a.ID,
WyID: a.WyID,
Name: a.Name,
Alpha2Code: a.Alpha2Code,
Alpha3Code: a.Alpha3Code,
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
DeletedAt: a.DeletedAt,
}
}
package services
import (
"crypto/rand"
"encoding/base32"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/bcrypt"
"gorm.io/datatypes"
"gorm.io/gorm"
"ScoutSystemElite/internal/config"
"ScoutSystemElite/internal/models"
)
type AuthService struct {
db *gorm.DB
cfg config.Config
}
func NewAuthService(db *gorm.DB, cfg config.Config) *AuthService {
return &AuthService{db: db, cfg: cfg}
}
type DeviceInfo struct {
UserAgent string `json:"userAgent"`
IPAddress string `json:"ipAddress"`
Platform string `json:"platform"`
}
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
TwoFactorCode string `json:"twoFactorCode,omitempty"`
DeviceID string `json:"deviceId,omitempty"`
}
type UserInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
TwoFactorEnabled bool `json:"twoFactorEnabled"`
}
type AuthResponse struct {
AccessToken string `json:"accessToken"`
User UserInfo `json:"user"`
RequiresTwoFactor bool `json:"requiresTwoFactor,omitempty"`
}
type Setup2FAResponse struct {
QRCodeURL string `json:"qrCodeUrl"`
Secret string `json:"secret"`
BackupCodes []string `json:"backupCodes"`
}
func (s *AuthService) Login(req LoginRequest, deviceInfo DeviceInfo) (*AuthResponse, error) {
var user models.User
if err := s.db.Where("email = ?", req.Email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("invalid credentials")
}
return nil, err
}
if user.IsActive != nil && !*user.IsActive {
return nil, fmt.Errorf("invalid credentials")
}
if user.LockedUntil != nil && user.LockedUntil.After(time.Now()) {
return nil, fmt.Errorf("account is locked due to multiple failed login attempts")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
s.handleFailedLogin(user.ID)
return nil, fmt.Errorf("invalid credentials")
}
if user.FailedLoginAttempts != nil && *user.FailedLoginAttempts > 0 {
s.resetFailedLoginAttempts(user.ID)
}
twoFactorEnabled := user.TwoFactorEnabled != nil && *user.TwoFactorEnabled
if twoFactorEnabled {
if req.TwoFactorCode == "" {
return &AuthResponse{
AccessToken: "",
User: UserInfo{
ID: user.ID,
Name: user.Name,
Email: user.Email,
Role: user.Role,
TwoFactorEnabled: true,
},
RequiresTwoFactor: true,
}, nil
}
if user.TwoFactorSecret == nil || !totp.Validate(req.TwoFactorCode, *user.TwoFactorSecret) {
return nil, fmt.Errorf("invalid 2FA code")
}
}
deviceID := req.DeviceID
if deviceID == "" {
deviceID = generateDeviceID()
}
s.db.Where("user_id = ?", user.ID).Delete(&models.UserSession{})
token, err := s.createSession(user, deviceID, deviceInfo)
if err != nil {
return nil, err
}
now := time.Now()
s.db.Model(&user).Update("last_login_at", now)
return &AuthResponse{
AccessToken: token,
User: UserInfo{
ID: user.ID,
Name: user.Name,
Email: user.Email,
Role: user.Role,
TwoFactorEnabled: twoFactorEnabled,
},
}, nil
}
func (s *AuthService) Logout(userID uint, deviceID string) error {
if deviceID != "" {
return s.db.Where("user_id = ? AND device_id = ?", userID, deviceID).Delete(&models.UserSession{}).Error
}
return s.LogoutFromAllDevices(userID)
}
func (s *AuthService) LogoutFromAllDevices(userID uint) error {
return s.db.Where("user_id = ?", userID).Delete(&models.UserSession{}).Error
}
func (s *AuthService) Setup2FA(userID uint) (*Setup2FAResponse, error) {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
return nil, fmt.Errorf("user not found")
}
if user.TwoFactorEnabled != nil && *user.TwoFactorEnabled {
return nil, fmt.Errorf("2FA is already enabled for this user")
}
secret := make([]byte, 20)
if _, err := rand.Read(secret); err != nil {
return nil, err
}
secretBase32 := base32.StdEncoding.EncodeToString(secret)
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "ScoutingSystem",
AccountName: user.Email,
Secret: []byte(secretBase32),
})
if err != nil {
return nil, err
}
backupCodes := generateBackupCodes(8)
s.db.Model(&user).Update("two_factor_secret", key.Secret())
return &Setup2FAResponse{
QRCodeURL: key.URL(),
Secret: key.Secret(),
BackupCodes: backupCodes,
}, nil
}
func (s *AuthService) VerifyAndEnable2FA(userID uint, code string) error {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
return fmt.Errorf("user not found")
}
if user.TwoFactorSecret == nil || *user.TwoFactorSecret == "" {
return fmt.Errorf("2FA setup not initiated")
}
if !totp.Validate(code, *user.TwoFactorSecret) {
return fmt.Errorf("invalid 2FA code")
}
enabled := true
return s.db.Model(&user).Update("two_factor_enabled", enabled).Error
}
func (s *AuthService) Disable2FA(userID uint, password string) error {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
return fmt.Errorf("user not found")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return fmt.Errorf("invalid password")
}
s.db.Where("user_id = ?", userID).Delete(&models.UserSession{})
return s.db.Model(&user).Updates(map[string]interface{}{
"two_factor_enabled": false,
"two_factor_secret": nil,
}).Error
}
func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword string) error {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
return fmt.Errorf("user not found")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(currentPassword)); err != nil {
return fmt.Errorf("invalid current password")
}
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
return s.db.Model(&user).Update("password_hash", string(hash)).Error
}
func (s *AuthService) createSession(user models.User, deviceID string, deviceInfo DeviceInfo) (string, error) {
ttlMinutes, _ := strconv.Atoi(s.cfg.JWTAccessTokenTTLMinutes)
if ttlMinutes == 0 {
ttlMinutes = 1440
}
expiresAt := time.Now().Add(time.Duration(ttlMinutes) * time.Minute)
claims := jwt.MapClaims{
"userId": user.ID,
"email": user.Email,
"role": user.Role,
"exp": expiresAt.Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(s.cfg.JWTSecret))
if err != nil {
return "", err
}
deviceInfoJSON, _ := json.Marshal(deviceInfo)
session := models.UserSession{
UserID: user.ID,
Token: tokenString,
DeviceID: deviceID,
DeviceInfo: datatypes.JSON(deviceInfoJSON),
ExpiresAt: expiresAt,
}
if err := s.db.Create(&session).Error; err != nil {
return "", err
}
return tokenString, nil
}
func (s *AuthService) handleFailedLogin(userID uint) {
var user models.User
s.db.First(&user, userID)
attempts := 1
if user.FailedLoginAttempts != nil {
attempts = *user.FailedLoginAttempts + 1
}
updates := map[string]interface{}{
"failed_login_attempts": attempts,
}
if attempts >= 5 {
lockUntil := time.Now().Add(30 * time.Minute)
updates["locked_until"] = lockUntil
}
s.db.Model(&user).Updates(updates)
}
func (s *AuthService) resetFailedLoginAttempts(userID uint) {
s.db.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{
"failed_login_attempts": 0,
"locked_until": nil,
})
}
func generateDeviceID() string {
b := make([]byte, 16)
rand.Read(b)
return fmt.Sprintf("device_%x", b)
}
func generateBackupCodes(count int) []string {
codes := make([]string, count)
for i := 0; i < count; i++ {
b := make([]byte, 4)
rand.Read(b)
codes[i] = fmt.Sprintf("%08X", b)
}
return codes
}
package services
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type CalendarService struct {
db *gorm.DB
}
func NewCalendarService(db *gorm.DB) *CalendarService {
return &CalendarService{db: db}
}
type CalendarEventResponse struct {
ID string `json:"id"`
UserID uint `json:"userId"`
Title string `json:"title"`
Description *string `json:"description"`
EventType string `json:"eventType"`
StartDate time.Time `json:"startDate"`
EndDate *time.Time `json:"endDate"`
MatchWyID *int `json:"matchWyId"`
PlayerWyID *int `json:"playerWyId"`
Metadata interface{} `json:"metadata"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type QueryCalendarEventsRequest struct {
EventType string `form:"eventType"`
FromDate string `form:"fromDate"`
ToDate string `form:"toDate"`
MatchWyID *int `form:"matchWyId"`
PlayerWyID *int `form:"playerWyId"`
IsActive *bool `form:"isActive"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreateCalendarEventRequest struct {
Title string `json:"title" binding:"required"`
Description *string `json:"description"`
EventType string `json:"eventType" binding:"required"`
StartDate time.Time `json:"startDate" binding:"required"`
EndDate *time.Time `json:"endDate"`
MatchWyID *int `json:"matchWyId"`
PlayerWyID *int `json:"playerWyId"`
Metadata interface{} `json:"metadata"`
}
type UpdateCalendarEventRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
EventType *string `json:"eventType"`
StartDate *time.Time `json:"startDate"`
EndDate *time.Time `json:"endDate"`
MatchWyID *int `json:"matchWyId"`
PlayerWyID *int `json:"playerWyId"`
Metadata interface{} `json:"metadata"`
IsActive *bool `json:"isActive"`
}
type PaginatedCalendarEventsResponse struct {
Data []CalendarEventResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
func (s *CalendarService) Create(userID uint, req CreateCalendarEventRequest) (*CalendarEventResponse, error) {
eventType := models.CalendarEventType(req.EventType)
isActive := true
metaJSON, err := json.Marshal(req.Metadata)
if err != nil {
return nil, err
}
event := models.CalendarEvent{
UserID: userID,
Title: req.Title,
Description: req.Description,
EventType: eventType,
StartDate: req.StartDate,
EndDate: req.EndDate,
MatchWyID: req.MatchWyID,
PlayerWyID: req.PlayerWyID,
Metadata: datatypes.JSON(metaJSON),
IsActive: &isActive,
}
if err := s.db.Create(&event).Error; err != nil {
return nil, err
}
return toCalendarEventResponse(event), nil
}
func (s *CalendarService) FindByID(userID uint, id string) (*CalendarEventResponse, error) {
var event models.CalendarEvent
if err := s.db.Where("id = ? AND user_id = ? AND deleted_at IS NULL", id, userID).First(&event).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("calendar event not found")
}
return nil, err
}
return toCalendarEventResponse(event), nil
}
func (s *CalendarService) FindAll(userID uint, query QueryCalendarEventsRequest) (*PaginatedCalendarEventsResponse, error) {
q := s.db.Model(&models.CalendarEvent{}).Where("user_id = ? AND deleted_at IS NULL", userID)
if query.EventType != "" {
q = q.Where("event_type = ?", query.EventType)
}
if query.FromDate != "" {
if t, err := time.Parse("2006-01-02", query.FromDate); err == nil {
q = q.Where("start_date >= ?", t)
}
}
if query.ToDate != "" {
if t, err := time.Parse("2006-01-02", query.ToDate); err == nil {
q = q.Where("start_date <= ?", t)
}
}
if query.MatchWyID != nil {
q = q.Where("match_wy_id = ?", *query.MatchWyID)
}
if query.PlayerWyID != nil {
q = q.Where("player_wy_id = ?", *query.PlayerWyID)
}
if query.IsActive != nil {
q = q.Where("is_active = ?", *query.IsActive)
}
var total int64
q.Count(&total)
sortBy := "start_date"
allowedSorts := map[string]string{
"title": "title",
"eventType": "event_type",
"startDate": "start_date",
"endDate": "end_date",
"createdAt": "created_at",
"updatedAt": "updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var events []models.CalendarEvent
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&events).Error; err != nil {
return nil, err
}
data := make([]CalendarEventResponse, len(events))
for i, e := range events {
data[i] = *toCalendarEventResponse(e)
}
return &PaginatedCalendarEventsResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(events)) < total,
}, nil
}
func (s *CalendarService) Update(userID uint, id string, req UpdateCalendarEventRequest) (*CalendarEventResponse, error) {
var event models.CalendarEvent
if err := s.db.Where("id = ? AND user_id = ? AND deleted_at IS NULL", id, userID).First(&event).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("calendar event not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Title != nil {
updates["title"] = *req.Title
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.EventType != nil {
updates["event_type"] = *req.EventType
}
if req.StartDate != nil {
updates["start_date"] = *req.StartDate
}
if req.EndDate != nil {
updates["end_date"] = *req.EndDate
}
if req.MatchWyID != nil {
updates["match_wy_id"] = *req.MatchWyID
}
if req.PlayerWyID != nil {
updates["player_wy_id"] = *req.PlayerWyID
}
if req.Metadata != nil {
metaJSON, err := json.Marshal(req.Metadata)
if err != nil {
return nil, err
}
updates["metadata"] = datatypes.JSON(metaJSON)
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) > 0 {
if err := s.db.Model(&event).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&event, "id = ?", id)
}
return toCalendarEventResponse(event), nil
}
func (s *CalendarService) Delete(userID uint, id string) error {
var event models.CalendarEvent
if err := s.db.Where("id = ? AND user_id = ? AND deleted_at IS NULL", id, userID).First(&event).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("calendar event not found")
}
return err
}
now := time.Now()
return s.db.Model(&event).Update("deleted_at", now).Error
}
func toCalendarEventResponse(e models.CalendarEvent) *CalendarEventResponse {
isActive := e.IsActive != nil && *e.IsActive
var meta interface{}
if len(e.Metadata) > 0 {
_ = json.Unmarshal([]byte(e.Metadata), &meta)
}
return &CalendarEventResponse{
ID: e.ID,
UserID: e.UserID,
Title: e.Title,
Description: e.Description,
EventType: string(e.EventType),
StartDate: e.StartDate,
EndDate: e.EndDate,
MatchWyID: e.MatchWyID,
PlayerWyID: e.PlayerWyID,
Metadata: meta,
IsActive: isActive,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
package services
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type CoachService struct {
db *gorm.DB
}
func NewCoachService(db *gorm.DB) *CoachService {
return &CoachService{db: db}
}
type CoachResponse struct {
ID string `json:"id"`
WyID *int `json:"wyId"`
TsID string `json:"tsId"`
CountryTsID *string `json:"countryTsId"`
TeamTsID *string `json:"teamTsId"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
MiddleName *string `json:"middleName"`
ShortName *string `json:"shortName"`
DateOfBirth *time.Time `json:"dateOfBirth"`
NationalityWyID *int `json:"nationalityWyId"`
CurrentTeamWyID *int `json:"currentTeamWyId"`
Position string `json:"position"`
CoachingLicense *string `json:"coachingLicense"`
YearsExperience *int `json:"yearsExperience"`
PreferredFormation *string `json:"preferredFormation"`
JoinedAt *time.Time `json:"joinedAt"`
ContractUntil *time.Time `json:"contractUntil"`
Status string `json:"status"`
ImageDataURL *string `json:"imageDataUrl"`
APILastSyncedAt *time.Time `json:"apiLastSyncedAt"`
APISyncStatus string `json:"apiSyncStatus"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
}
type QueryCoachesRequest struct {
Name string `form:"name"`
TeamWyID *int `form:"teamWyId"`
NationalityWyID *int `form:"nationalityWyId"`
Position string `form:"position"`
Status string `form:"status"`
Archived *bool `form:"archived"`
MinAge *int `form:"minAge"`
MaxAge *int `form:"maxAge"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreateCoachRequest struct {
WyID *int `json:"wyId"`
TsID string `json:"tsId"`
TeamTsID *string `json:"teamTsId"`
CountryTsID *string `json:"countryTsId"`
FirstName string `json:"firstName" binding:"required"`
LastName string `json:"lastName" binding:"required"`
MiddleName *string `json:"middleName"`
ShortName *string `json:"shortName"`
DateOfBirth *time.Time `json:"dateOfBirth"`
NationalityWyID *int `json:"nationalityWyId"`
CurrentTeamWyID *int `json:"currentTeamWyId"`
Position *string `json:"position"`
PreferredFormation *string `json:"preferredFormation"`
JoinedAt *time.Time `json:"joinedAt"`
ContractUntil *time.Time `json:"contractUntil"`
Status *string `json:"status"`
}
type UpdateCoachRequest struct {
WyID *int `json:"wyId"`
TsID *string `json:"tsId"`
TeamTsID *string `json:"teamTsId"`
CountryTsID *string `json:"countryTsId"`
FirstName *string `json:"firstName"`
LastName *string `json:"lastName"`
MiddleName *string `json:"middleName"`
ShortName *string `json:"shortName"`
DateOfBirth *time.Time `json:"dateOfBirth"`
NationalityWyID *int `json:"nationalityWyId"`
CurrentTeamWyID *int `json:"currentTeamWyId"`
Position *string `json:"position"`
PreferredFormation *string `json:"preferredFormation"`
JoinedAt *time.Time `json:"joinedAt"`
ContractUntil *time.Time `json:"contractUntil"`
Status *string `json:"status"`
IsActive *bool `json:"isActive"`
}
type PaginatedCoachesResponse struct {
Data []CoachResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
func (s *CoachService) UpsertByWyID(req CreateCoachRequest) (*CoachResponse, error) {
if req.WyID != nil {
var existing models.Coach
if err := s.db.Where("wy_id = ?", *req.WyID).First(&existing).Error; err == nil {
return s.updateCoach(&existing, req)
}
}
id := generateCoachID()
coach := models.Coach{
ID: id,
WyID: req.WyID,
TsID: req.TsID,
TeamTsID: req.TeamTsID,
CountryTsID: req.CountryTsID,
FirstName: req.FirstName,
LastName: req.LastName,
MiddleName: req.MiddleName,
ShortName: req.ShortName,
DateOfBirth: req.DateOfBirth,
NationalityWyID: req.NationalityWyID,
CurrentTeamWyID: req.CurrentTeamWyID,
Position: derefStringCoach(req.Position, "head_coach"),
PreferredFormation: req.PreferredFormation,
JoinedAt: req.JoinedAt,
ContractUntil: req.ContractUntil,
Status: derefStringCoach(req.Status, "active"),
APISyncStatus: "pending",
IsActive: true,
}
if err := s.db.Create(&coach).Error; err != nil {
return nil, err
}
return toCoachResponse(coach), nil
}
func (s *CoachService) updateCoach(coach *models.Coach, req CreateCoachRequest) (*CoachResponse, error) {
updates := map[string]interface{}{
"ts_id": req.TsID,
"team_ts_id": req.TeamTsID,
"country_ts_id": req.CountryTsID,
"first_name": req.FirstName,
"last_name": req.LastName,
"middle_name": req.MiddleName,
"short_name": req.ShortName,
"date_of_birth": req.DateOfBirth,
"nationality_wy_id": req.NationalityWyID,
"current_team_wy_id": req.CurrentTeamWyID,
"preferred_formation": req.PreferredFormation,
"joined_at": req.JoinedAt,
"contract_until": req.ContractUntil,
}
if req.Position != nil {
updates["position"] = *req.Position
}
if req.Status != nil {
updates["status"] = *req.Status
}
if err := s.db.Model(coach).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(coach, "id = ?", coach.ID)
return toCoachResponse(*coach), nil
}
func (s *CoachService) FindByID(id string) (*CoachResponse, error) {
var coach models.Coach
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&coach).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return toCoachResponse(coach), nil
}
func (s *CoachService) FindByWyID(wyID int) (*CoachResponse, error) {
var coach models.Coach
if err := s.db.Where("wy_id = ? AND deleted_at IS NULL", wyID).First(&coach).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return toCoachResponse(coach), nil
}
func (s *CoachService) FindAll(query QueryCoachesRequest) (*PaginatedCoachesResponse, error) {
q := s.db.Model(&models.Coach{}).Where("deleted_at IS NULL")
if query.Name != "" {
nameLower := "%" + strings.ToLower(query.Name) + "%"
q = q.Where("LOWER(first_name) LIKE ? OR LOWER(last_name) LIKE ? OR LOWER(middle_name) LIKE ? OR LOWER(short_name) LIKE ?",
nameLower, nameLower, nameLower, nameLower)
}
if query.TeamWyID != nil {
q = q.Where("current_team_wy_id = ?", *query.TeamWyID)
}
if query.NationalityWyID != nil {
q = q.Where("nationality_wy_id = ?", *query.NationalityWyID)
}
if query.Position != "" {
q = q.Where("position = ?", query.Position)
}
if query.Status != "" {
q = q.Where("status = ?", query.Status)
}
if query.Archived != nil {
if *query.Archived {
q = q.Where("status = ?", "inactive")
} else {
q = q.Where("status = ? OR status IS NULL", "active")
}
}
if query.MinAge != nil || query.MaxAge != nil {
now := time.Now()
if query.MinAge != nil {
maxBirthDate := now.AddDate(-*query.MinAge, 0, 0)
q = q.Where("date_of_birth <= ?", maxBirthDate)
}
if query.MaxAge != nil {
minBirthDate := now.AddDate(-*query.MaxAge-1, 0, 0)
q = q.Where("date_of_birth > ?", minBirthDate)
}
}
var total int64
q.Count(&total)
sortBy := "last_name"
allowedSorts := map[string]string{
"firstName": "first_name",
"lastName": "last_name",
"shortName": "short_name",
"position": "position",
"status": "status",
"createdAt": "created_at",
"updatedAt": "updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var coaches []models.Coach
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&coaches).Error; err != nil {
return nil, err
}
data := make([]CoachResponse, len(coaches))
for i, c := range coaches {
data[i] = *toCoachResponse(c)
}
return &PaginatedCoachesResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(coaches)) < total,
}, nil
}
func (s *CoachService) UpdateByID(id string, req UpdateCoachRequest) (*CoachResponse, error) {
var coach models.Coach
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&coach).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
updates := make(map[string]interface{})
if req.WyID != nil {
updates["wy_id"] = *req.WyID
}
if req.TsID != nil {
updates["ts_id"] = *req.TsID
}
if req.TeamTsID != nil {
updates["team_ts_id"] = *req.TeamTsID
}
if req.CountryTsID != nil {
updates["country_ts_id"] = *req.CountryTsID
}
if req.FirstName != nil {
updates["first_name"] = *req.FirstName
}
if req.LastName != nil {
updates["last_name"] = *req.LastName
}
if req.MiddleName != nil {
updates["middle_name"] = *req.MiddleName
}
if req.ShortName != nil {
updates["short_name"] = *req.ShortName
}
if req.DateOfBirth != nil {
updates["date_of_birth"] = *req.DateOfBirth
}
if req.NationalityWyID != nil {
updates["nationality_wy_id"] = *req.NationalityWyID
}
if req.CurrentTeamWyID != nil {
updates["current_team_wy_id"] = *req.CurrentTeamWyID
}
if req.Position != nil {
updates["position"] = *req.Position
}
if req.PreferredFormation != nil {
updates["preferred_formation"] = *req.PreferredFormation
}
if req.JoinedAt != nil {
updates["joined_at"] = *req.JoinedAt
}
if req.ContractUntil != nil {
updates["contract_until"] = *req.ContractUntil
}
if req.Status != nil {
updates["status"] = *req.Status
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) > 0 {
if err := s.db.Model(&coach).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&coach, "id = ?", id)
}
return toCoachResponse(coach), nil
}
func (s *CoachService) DeleteByID(id string) error {
var coach models.Coach
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&coach).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("coach not found")
}
return err
}
now := time.Now()
return s.db.Model(&coach).Update("deleted_at", now).Error
}
func toCoachResponse(c models.Coach) *CoachResponse {
return &CoachResponse{
ID: c.ID,
WyID: c.WyID,
TsID: c.TsID,
CountryTsID: c.CountryTsID,
TeamTsID: c.TeamTsID,
FirstName: c.FirstName,
LastName: c.LastName,
MiddleName: c.MiddleName,
ShortName: c.ShortName,
DateOfBirth: c.DateOfBirth,
NationalityWyID: c.NationalityWyID,
CurrentTeamWyID: c.CurrentTeamWyID,
Position: c.Position,
CoachingLicense: c.CoachingLicense,
YearsExperience: c.YearsExperience,
PreferredFormation: c.PreferredFormation,
JoinedAt: c.JoinedAt,
ContractUntil: c.ContractUntil,
Status: c.Status,
ImageDataURL: c.ImageDataURL,
APILastSyncedAt: c.APILastSyncedAt,
APISyncStatus: c.APISyncStatus,
IsActive: c.IsActive,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
DeletedAt: c.DeletedAt,
}
}
func generateCoachID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}
func derefStringCoach(s *string, def string) string {
if s == nil {
return def
}
return *s
}
package services
import (
"errors"
"fmt"
"strings"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type FileService struct {
db *gorm.DB
}
func NewFileService(db *gorm.DB) *FileService {
return &FileService{db: db}
}
type FileResponse struct {
ID uint `json:"id"`
FileName string `json:"fileName"`
OriginalFileName string `json:"originalFileName"`
FilePath string `json:"filePath"`
MimeType string `json:"mimeType"`
FileSize int `json:"fileSize"`
EntityType string `json:"entityType"`
EntityID *int `json:"entityId"`
EntityWyID *int `json:"entityWyId"`
Category *string `json:"category"`
Description *string `json:"description"`
UploadedBy *uint `json:"uploadedBy"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type QueryFilesRequest struct {
EntityType string `form:"entityType"`
EntityID *int `form:"entityId"`
EntityWyID *int `form:"entityWyId"`
Category string `form:"category"`
MimeType string `form:"mimeType"`
IsActive *bool `form:"isActive"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreateFileRequest struct {
FileName string `json:"fileName" binding:"required"`
OriginalFileName string `json:"originalFileName" binding:"required"`
FilePath string `json:"filePath" binding:"required"`
MimeType string `json:"mimeType" binding:"required"`
FileSize int `json:"fileSize" binding:"required"`
EntityType string `json:"entityType" binding:"required"`
EntityID *int `json:"entityId"`
EntityWyID *int `json:"entityWyId"`
Category *string `json:"category"`
Description *string `json:"description"`
}
type UpdateFileRequest struct {
FileName *string `json:"fileName"`
OriginalFileName *string `json:"originalFileName"`
FilePath *string `json:"filePath"`
MimeType *string `json:"mimeType"`
FileSize *int `json:"fileSize"`
EntityType *string `json:"entityType"`
EntityID *int `json:"entityId"`
EntityWyID *int `json:"entityWyId"`
Category *string `json:"category"`
Description *string `json:"description"`
IsActive *bool `json:"isActive"`
}
type PaginatedFilesResponse struct {
Data []FileResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
func (s *FileService) Create(userID uint, req CreateFileRequest) (*FileResponse, error) {
isActive := true
file := models.File{
FileName: req.FileName,
OriginalFileName: req.OriginalFileName,
FilePath: req.FilePath,
MimeType: req.MimeType,
FileSize: req.FileSize,
EntityType: req.EntityType,
EntityID: req.EntityID,
EntityWyID: req.EntityWyID,
Category: req.Category,
Description: req.Description,
UploadedBy: &userID,
IsActive: &isActive,
}
if err := s.db.Create(&file).Error; err != nil {
return nil, err
}
return toFileResponse(file), nil
}
func (s *FileService) FindByID(id uint) (*FileResponse, error) {
var file models.File
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&file).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("file not found")
}
return nil, err
}
return toFileResponse(file), nil
}
func (s *FileService) FindAll(query QueryFilesRequest) (*PaginatedFilesResponse, error) {
q := s.db.Model(&models.File{}).Where("deleted_at IS NULL")
if query.EntityType != "" {
q = q.Where("entity_type = ?", query.EntityType)
}
if query.EntityID != nil {
q = q.Where("entity_id = ?", *query.EntityID)
}
if query.EntityWyID != nil {
q = q.Where("entity_wy_id = ?", *query.EntityWyID)
}
if query.Category != "" {
q = q.Where("category = ?", query.Category)
}
if query.MimeType != "" {
q = q.Where("mime_type LIKE ?", query.MimeType+"%")
}
if query.IsActive != nil {
q = q.Where("is_active = ?", *query.IsActive)
}
var total int64
q.Count(&total)
sortBy := "created_at"
allowedSorts := map[string]string{
"fileName": "file_name",
"fileSize": "file_size",
"mimeType": "mime_type",
"createdAt": "created_at",
"updatedAt": "updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "DESC"
if strings.ToLower(query.SortOrder) == "asc" {
sortOrder = "ASC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var files []models.File
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&files).Error; err != nil {
return nil, err
}
data := make([]FileResponse, len(files))
for i, f := range files {
data[i] = *toFileResponse(f)
}
return &PaginatedFilesResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(files)) < total,
}, nil
}
func (s *FileService) FindByEntity(entityType string, entityID *int, entityWyID *int) ([]FileResponse, error) {
q := s.db.Model(&models.File{}).Where("entity_type = ? AND deleted_at IS NULL", entityType)
if entityID != nil {
q = q.Where("entity_id = ?", *entityID)
}
if entityWyID != nil {
q = q.Where("entity_wy_id = ?", *entityWyID)
}
var files []models.File
if err := q.Order("created_at DESC").Find(&files).Error; err != nil {
return nil, err
}
result := make([]FileResponse, len(files))
for i, f := range files {
result[i] = *toFileResponse(f)
}
return result, nil
}
func (s *FileService) Update(id uint, req UpdateFileRequest) (*FileResponse, error) {
var file models.File
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&file).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("file not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.FileName != nil {
updates["file_name"] = *req.FileName
}
if req.OriginalFileName != nil {
updates["original_file_name"] = *req.OriginalFileName
}
if req.FilePath != nil {
updates["file_path"] = *req.FilePath
}
if req.MimeType != nil {
updates["mime_type"] = *req.MimeType
}
if req.FileSize != nil {
updates["file_size"] = *req.FileSize
}
if req.EntityType != nil {
updates["entity_type"] = *req.EntityType
}
if req.EntityID != nil {
updates["entity_id"] = *req.EntityID
}
if req.EntityWyID != nil {
updates["entity_wy_id"] = *req.EntityWyID
}
if req.Category != nil {
updates["category"] = *req.Category
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) > 0 {
if err := s.db.Model(&file).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&file, id)
}
return toFileResponse(file), nil
}
func (s *FileService) Delete(id uint) error {
var file models.File
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&file).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("file not found")
}
return err
}
now := time.Now()
return s.db.Model(&file).Update("deleted_at", now).Error
}
func toFileResponse(f models.File) *FileResponse {
isActive := f.IsActive != nil && *f.IsActive
return &FileResponse{
ID: f.ID,
FileName: f.FileName,
OriginalFileName: f.OriginalFileName,
FilePath: f.FilePath,
MimeType: f.MimeType,
FileSize: f.FileSize,
EntityType: f.EntityType,
EntityID: f.EntityID,
EntityWyID: f.EntityWyID,
Category: f.Category,
Description: f.Description,
UploadedBy: f.UploadedBy,
IsActive: isActive,
CreatedAt: f.CreatedAt,
UpdatedAt: f.UpdatedAt,
}
}
package services
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type ListService struct {
db *gorm.DB
}
func NewListService(db *gorm.DB) *ListService {
return &ListService{db: db}
}
type ListResponse struct {
ID string `json:"id"`
UserID uint `json:"userId"`
Name string `json:"name"`
Season string `json:"season"`
Type string `json:"type"`
PlayersByPosition interface{} `json:"playersByPosition"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type QueryListsRequest struct {
Type string `form:"type"`
Name string `form:"name"`
Season string `form:"season"`
IsActive *bool `form:"isActive"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreateListRequest struct {
Name string `json:"name" binding:"required"`
Season string `json:"season" binding:"required"`
Type string `json:"type" binding:"required"`
PlayersByPosition interface{} `json:"playersByPosition"`
}
type UpdateListRequest struct {
Name *string `json:"name"`
Season *string `json:"season"`
Type *string `json:"type"`
PlayersByPosition interface{} `json:"playersByPosition"`
IsActive *bool `json:"isActive"`
}
type PaginatedListsResponse struct {
Data []ListResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
func (s *ListService) Create(userID uint, req CreateListRequest) (*ListResponse, error) {
isActive := true
listType := models.ListType(req.Type)
playersJSON, err := json.Marshal(req.PlayersByPosition)
if err != nil {
return nil, err
}
list := models.List{
UserID: userID,
Name: req.Name,
Season: req.Season,
Type: listType,
PlayersByPosition: datatypes.JSON(playersJSON),
IsActive: &isActive,
}
if err := s.db.Create(&list).Error; err != nil {
return nil, err
}
return toListResponse(list), nil
}
func (s *ListService) FindByID(userID uint, id string) (*ListResponse, error) {
var list models.List
if err := s.db.Where("id = ? AND user_id = ? AND deleted_at IS NULL", id, userID).First(&list).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("list not found")
}
return nil, err
}
return toListResponse(list), nil
}
func (s *ListService) FindAll(userID uint, query QueryListsRequest) (*PaginatedListsResponse, error) {
q := s.db.Model(&models.List{}).Where("user_id = ? AND deleted_at IS NULL", userID)
if query.Type != "" {
q = q.Where("type = ?", query.Type)
}
if query.Name != "" {
q = q.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(query.Name)+"%")
}
if query.Season != "" {
q = q.Where("season = ?", query.Season)
}
if query.IsActive != nil {
q = q.Where("is_active = ?", *query.IsActive)
}
var total int64
q.Count(&total)
sortBy := "name"
allowedSorts := map[string]string{
"name": "name",
"type": "type",
"season": "season",
"createdAt": "created_at",
"updatedAt": "updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var lists []models.List
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&lists).Error; err != nil {
return nil, err
}
data := make([]ListResponse, len(lists))
for i, l := range lists {
data[i] = *toListResponse(l)
}
return &PaginatedListsResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(lists)) < total,
}, nil
}
func (s *ListService) FindSharedWithUser(userID uint, query QueryListsRequest) (*PaginatedListsResponse, error) {
q := s.db.
Model(&models.List{}).
Joins("JOIN list_shares ON list_shares.list_id = lists.id").
Where("list_shares.user_id = ? AND lists.deleted_at IS NULL", userID)
if query.Type != "" {
q = q.Where("lists.type = ?", query.Type)
}
if query.Name != "" {
q = q.Where("LOWER(lists.name) LIKE ?", "%"+strings.ToLower(query.Name)+"%")
}
if query.Season != "" {
q = q.Where("lists.season = ?", query.Season)
}
if query.IsActive != nil {
q = q.Where("lists.is_active = ?", *query.IsActive)
}
var total int64
q.Count(&total)
sortBy := "name"
allowedSorts := map[string]string{
"name": "lists.name",
"type": "lists.type",
"season": "lists.season",
"createdAt": "lists.created_at",
"updatedAt": "lists.updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var lists []models.List
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&lists).Error; err != nil {
return nil, err
}
data := make([]ListResponse, len(lists))
for i, l := range lists {
data[i] = *toListResponse(l)
}
return &PaginatedListsResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(lists)) < total,
}, nil
}
func (s *ListService) Update(userID uint, id string, req UpdateListRequest) (*ListResponse, error) {
var list models.List
if err := s.db.Where("id = ? AND user_id = ? AND deleted_at IS NULL", id, userID).First(&list).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("list not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Season != nil {
updates["season"] = *req.Season
}
if req.Type != nil {
updates["type"] = *req.Type
}
if req.PlayersByPosition != nil {
playersJSON, err := json.Marshal(req.PlayersByPosition)
if err != nil {
return nil, err
}
updates["players_by_position"] = datatypes.JSON(playersJSON)
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) > 0 {
if err := s.db.Model(&list).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&list, "id = ?", id)
}
return toListResponse(list), nil
}
func (s *ListService) Delete(userID uint, id string) error {
var list models.List
if err := s.db.Where("id = ? AND user_id = ? AND deleted_at IS NULL", id, userID).First(&list).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("list not found")
}
return err
}
now := time.Now()
return s.db.Model(&list).Update("deleted_at", now).Error
}
func toListResponse(l models.List) *ListResponse {
isActive := l.IsActive != nil && *l.IsActive
var players interface{}
if len(l.PlayersByPosition) > 0 {
_ = json.Unmarshal([]byte(l.PlayersByPosition), &players)
}
return &ListResponse{
ID: l.ID,
UserID: l.UserID,
Name: l.Name,
Season: l.Season,
Type: string(l.Type),
PlayersByPosition: players,
IsActive: isActive,
CreatedAt: l.CreatedAt,
UpdatedAt: l.UpdatedAt,
}
}
package services
import (
"errors"
"fmt"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type PlayerAgentService struct {
db *gorm.DB
}
func NewPlayerAgentService(db *gorm.DB) *PlayerAgentService {
return &PlayerAgentService{db: db}
}
type PlayerAgentResponse struct {
ID uint `json:"id"`
PlayerID string `json:"playerId"`
AgentID string `json:"agentId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type CreatePlayerAgentRequest struct {
PlayerID string `json:"playerId" binding:"required"`
AgentID string `json:"agentId" binding:"required"`
}
func (s *PlayerAgentService) Create(req CreatePlayerAgentRequest) (*PlayerAgentResponse, error) {
var existing models.PlayerAgent
if err := s.db.Where("player_id = ? AND agent_id = ?", req.PlayerID, req.AgentID).First(&existing).Error; err == nil {
return nil, fmt.Errorf("player-agent relation already exists")
}
pa := models.PlayerAgent{
PlayerID: req.PlayerID,
AgentID: req.AgentID,
}
if err := s.db.Create(&pa).Error; err != nil {
return nil, err
}
return toPlayerAgentResponse(pa), nil
}
func (s *PlayerAgentService) FindByPlayerID(playerID string) ([]PlayerAgentResponse, error) {
var relations []models.PlayerAgent
if err := s.db.Where("player_id = ?", playerID).Find(&relations).Error; err != nil {
return nil, err
}
result := make([]PlayerAgentResponse, len(relations))
for i, r := range relations {
result[i] = *toPlayerAgentResponse(r)
}
return result, nil
}
func (s *PlayerAgentService) FindByAgentID(agentID string) ([]PlayerAgentResponse, error) {
var relations []models.PlayerAgent
if err := s.db.Where("agent_id = ?", agentID).Find(&relations).Error; err != nil {
return nil, err
}
result := make([]PlayerAgentResponse, len(relations))
for i, r := range relations {
result[i] = *toPlayerAgentResponse(r)
}
return result, nil
}
func (s *PlayerAgentService) Delete(playerID string, agentID string) error {
var pa models.PlayerAgent
if err := s.db.Where("player_id = ? AND agent_id = ?", playerID, agentID).First(&pa).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("player-agent relation not found")
}
return err
}
return s.db.Delete(&pa).Error
}
func toPlayerAgentResponse(pa models.PlayerAgent) *PlayerAgentResponse {
return &PlayerAgentResponse{
ID: pa.ID,
PlayerID: pa.PlayerID,
AgentID: pa.AgentID,
CreatedAt: pa.CreatedAt,
UpdatedAt: pa.UpdatedAt,
}
}
type CoachAgentService struct {
db *gorm.DB
}
func NewCoachAgentService(db *gorm.DB) *CoachAgentService {
return &CoachAgentService{db: db}
}
type CoachAgentResponse struct {
ID uint `json:"id"`
CoachID string `json:"coachId"`
AgentID string `json:"agentId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type CreateCoachAgentRequest struct {
CoachID string `json:"coachId" binding:"required"`
AgentID string `json:"agentId" binding:"required"`
}
func (s *CoachAgentService) Create(req CreateCoachAgentRequest) (*CoachAgentResponse, error) {
var existing models.CoachAgent
if err := s.db.Where("coach_id = ? AND agent_id = ?", req.CoachID, req.AgentID).First(&existing).Error; err == nil {
return nil, fmt.Errorf("coach-agent relation already exists")
}
ca := models.CoachAgent{
CoachID: req.CoachID,
AgentID: req.AgentID,
}
if err := s.db.Create(&ca).Error; err != nil {
return nil, err
}
return toCoachAgentResponse(ca), nil
}
func (s *CoachAgentService) FindByCoachID(coachID string) ([]CoachAgentResponse, error) {
var relations []models.CoachAgent
if err := s.db.Where("coach_id = ?", coachID).Find(&relations).Error; err != nil {
return nil, err
}
result := make([]CoachAgentResponse, len(relations))
for i, r := range relations {
result[i] = *toCoachAgentResponse(r)
}
return result, nil
}
func (s *CoachAgentService) FindByAgentID(agentID string) ([]CoachAgentResponse, error) {
var relations []models.CoachAgent
if err := s.db.Where("agent_id = ?", agentID).Find(&relations).Error; err != nil {
return nil, err
}
result := make([]CoachAgentResponse, len(relations))
for i, r := range relations {
result[i] = *toCoachAgentResponse(r)
}
return result, nil
}
func (s *CoachAgentService) Delete(coachID string, agentID string) error {
var ca models.CoachAgent
if err := s.db.Where("coach_id = ? AND agent_id = ?", coachID, agentID).First(&ca).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("coach-agent relation not found")
}
return err
}
return s.db.Delete(&ca).Error
}
func toCoachAgentResponse(ca models.CoachAgent) *CoachAgentResponse {
return &CoachAgentResponse{
ID: ca.ID,
CoachID: ca.CoachID,
AgentID: ca.AgentID,
CreatedAt: ca.CreatedAt,
UpdatedAt: ca.UpdatedAt,
}
}
package services
import (
"errors"
"fmt"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type PlayerFeatureService struct {
db *gorm.DB
}
func NewPlayerFeatureService(db *gorm.DB) *PlayerFeatureService {
return &PlayerFeatureService{db: db}
}
type PlayerFeatureCategoryResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
Order *int `json:"order"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type PlayerFeatureTypeResponse struct {
ID uint `json:"id"`
CategoryID uint `json:"categoryId"`
Name string `json:"name"`
Description *string `json:"description"`
Order *int `json:"order"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type PlayerFeatureRatingResponse struct {
ID uint `json:"id"`
PlayerID string `json:"playerId"`
FeatureTypeID uint `json:"featureTypeId"`
UserID uint `json:"userId"`
Rating *int `json:"rating"`
Notes *string `json:"notes"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type PlayerFeatureRatingWithUserNameResponse struct {
ID uint `json:"id"`
PlayerID string `json:"playerId"`
FeatureTypeID uint `json:"featureTypeId"`
UserID uint `json:"userId"`
Rating *int `json:"rating"`
Notes *string `json:"notes"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
UserName string `json:"userName"`
}
type PlayerFeatureSelectionResponse struct {
ID uint `json:"id"`
PlayerID string `json:"playerId"`
FeatureTypeID uint `json:"featureTypeId"`
UserID uint `json:"userId"`
Notes *string `json:"notes"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type PlayerFeatureSelectionWithUserNameResponse struct {
ID uint `json:"id"`
PlayerID string `json:"playerId"`
FeatureTypeID uint `json:"featureTypeId"`
UserID uint `json:"userId"`
Notes *string `json:"notes"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
UserName string `json:"userName"`
}
type CreateFeatureCategoryRequest struct {
Name string `json:"name" binding:"required"`
Description *string `json:"description"`
Order *int `json:"order"`
}
type CreateFeatureTypeRequest struct {
CategoryID uint `json:"categoryId" binding:"required"`
Name string `json:"name" binding:"required"`
Description *string `json:"description"`
Order *int `json:"order"`
}
type CreateFeatureRatingRequest struct {
PlayerID string `json:"playerId" binding:"required"`
FeatureTypeID uint `json:"featureTypeId" binding:"required"`
Rating *int `json:"rating"`
Notes *string `json:"notes"`
}
type UpdateFeatureRatingRequest struct {
Rating *int `json:"rating"`
Notes *string `json:"notes"`
}
type CreateFeatureSelectionRequest struct {
PlayerID string `json:"playerId" binding:"required"`
FeatureTypeID uint `json:"featureTypeId" binding:"required"`
Notes *string `json:"notes"`
}
type UpdateFeatureSelectionRequest struct {
Notes *string `json:"notes"`
}
func (s *PlayerFeatureService) CreateCategory(req CreateFeatureCategoryRequest) (*PlayerFeatureCategoryResponse, error) {
isActive := true
cat := models.PlayerFeatureCategory{
Name: req.Name,
Description: req.Description,
Order: req.Order,
IsActive: &isActive,
}
if err := s.db.Create(&cat).Error; err != nil {
return nil, err
}
return toFeatureCategoryResponse(cat), nil
}
func (s *PlayerFeatureService) GetCategories() ([]PlayerFeatureCategoryResponse, error) {
var categories []models.PlayerFeatureCategory
if err := s.db.Where("deleted_at IS NULL").Order("\"order\", name").Find(&categories).Error; err != nil {
return nil, err
}
result := make([]PlayerFeatureCategoryResponse, len(categories))
for i, c := range categories {
result[i] = *toFeatureCategoryResponse(c)
}
return result, nil
}
func (s *PlayerFeatureService) GetFeatureTypesByCategory(categoryID uint) ([]PlayerFeatureTypeResponse, error) {
return s.GetFeatureTypes(&categoryID)
}
func (s *PlayerFeatureService) GetCategoryByID(id uint) (*PlayerFeatureCategoryResponse, error) {
var cat models.PlayerFeatureCategory
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&cat).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("category not found")
}
return nil, err
}
return toFeatureCategoryResponse(cat), nil
}
func (s *PlayerFeatureService) UpdateCategory(id uint, req CreateFeatureCategoryRequest) (*PlayerFeatureCategoryResponse, error) {
var cat models.PlayerFeatureCategory
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&cat).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("category not found")
}
return nil, err
}
updates := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"order": req.Order,
}
if err := s.db.Model(&cat).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&cat, id)
return toFeatureCategoryResponse(cat), nil
}
func (s *PlayerFeatureService) DeleteCategory(id uint) error {
var cat models.PlayerFeatureCategory
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&cat).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("category not found")
}
return err
}
now := time.Now()
return s.db.Model(&cat).Update("deleted_at", now).Error
}
func (s *PlayerFeatureService) CreateFeatureType(req CreateFeatureTypeRequest) (*PlayerFeatureTypeResponse, error) {
isActive := true
ft := models.PlayerFeatureType{
CategoryID: req.CategoryID,
Name: req.Name,
Description: req.Description,
Order: req.Order,
IsActive: &isActive,
}
if err := s.db.Create(&ft).Error; err != nil {
return nil, err
}
return toFeatureTypeResponse(ft), nil
}
func (s *PlayerFeatureService) GetFeatureTypes(categoryID *uint) ([]PlayerFeatureTypeResponse, error) {
q := s.db.Model(&models.PlayerFeatureType{}).Where("deleted_at IS NULL")
if categoryID != nil {
q = q.Where("category_id = ?", *categoryID)
}
var types []models.PlayerFeatureType
if err := q.Order("\"order\", name").Find(&types).Error; err != nil {
return nil, err
}
result := make([]PlayerFeatureTypeResponse, len(types))
for i, t := range types {
result[i] = *toFeatureTypeResponse(t)
}
return result, nil
}
func (s *PlayerFeatureService) GetFeatureTypeByID(id uint) (*PlayerFeatureTypeResponse, error) {
var ft models.PlayerFeatureType
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&ft).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("feature type not found")
}
return nil, err
}
return toFeatureTypeResponse(ft), nil
}
func (s *PlayerFeatureService) UpdateFeatureType(id uint, req CreateFeatureTypeRequest) (*PlayerFeatureTypeResponse, error) {
var ft models.PlayerFeatureType
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&ft).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("feature type not found")
}
return nil, err
}
updates := map[string]interface{}{
"category_id": req.CategoryID,
"name": req.Name,
"description": req.Description,
"order": req.Order,
}
if err := s.db.Model(&ft).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&ft, id)
return toFeatureTypeResponse(ft), nil
}
func (s *PlayerFeatureService) DeleteFeatureType(id uint) error {
var ft models.PlayerFeatureType
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&ft).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("feature type not found")
}
return err
}
now := time.Now()
return s.db.Model(&ft).Update("deleted_at", now).Error
}
func (s *PlayerFeatureService) CreateRating(userID uint, req CreateFeatureRatingRequest) (*PlayerFeatureRatingResponse, error) {
var existing models.PlayerFeatureRating
err := s.db.Where("player_id = ? AND feature_type_id = ? AND user_id = ?", req.PlayerID, req.FeatureTypeID, userID).First(&existing).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if err == nil {
updates := map[string]interface{}{
"rating": req.Rating,
"notes": req.Notes,
"deleted_at": nil,
}
if err := s.db.Model(&existing).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&existing, existing.ID)
return toFeatureRatingResponse(existing), nil
}
rating := models.PlayerFeatureRating{
PlayerID: req.PlayerID,
FeatureTypeID: req.FeatureTypeID,
UserID: userID,
Rating: req.Rating,
Notes: req.Notes,
}
if err := s.db.Create(&rating).Error; err != nil {
return nil, err
}
return toFeatureRatingResponse(rating), nil
}
func (s *PlayerFeatureService) GetRatingsByPlayer(playerID string) ([]PlayerFeatureRatingResponse, error) {
var ratings []models.PlayerFeatureRating
if err := s.db.Where("player_id = ? AND deleted_at IS NULL", playerID).Find(&ratings).Error; err != nil {
return nil, err
}
result := make([]PlayerFeatureRatingResponse, len(ratings))
for i, r := range ratings {
result[i] = *toFeatureRatingResponse(r)
}
return result, nil
}
func (s *PlayerFeatureService) GetRatingByID(id uint) (*PlayerFeatureRatingWithUserNameResponse, error) {
var row PlayerFeatureRatingWithUserNameResponse
err := s.db.Table("player_feature_ratings AS r").
Select("r.id, r.player_id AS player_id, r.feature_type_id AS feature_type_id, r.user_id AS user_id, r.rating, r.notes, r.created_at, r.updated_at, r.deleted_at, u.name AS user_name").
Joins("LEFT JOIN users u ON u.id = r.user_id").
Where("r.id = ?", id).
Take(&row).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("rating not found")
}
return nil, err
}
return &row, nil
}
func (s *PlayerFeatureService) GetRatingsByPlayerAndScout(playerID string, userID uint) ([]PlayerFeatureRatingResponse, error) {
var ratings []models.PlayerFeatureRating
if err := s.db.Where("player_id = ? AND user_id = ? AND deleted_at IS NULL", playerID, userID).Find(&ratings).Error; err != nil {
return nil, err
}
result := make([]PlayerFeatureRatingResponse, len(ratings))
for i, r := range ratings {
result[i] = *toFeatureRatingResponse(r)
}
return result, nil
}
func (s *PlayerFeatureService) GetRatingsByFeatureType(featureTypeID uint) ([]PlayerFeatureRatingResponse, error) {
var ratings []models.PlayerFeatureRating
if err := s.db.Where("feature_type_id = ? AND deleted_at IS NULL", featureTypeID).Find(&ratings).Error; err != nil {
return nil, err
}
result := make([]PlayerFeatureRatingResponse, len(ratings))
for i, r := range ratings {
result[i] = *toFeatureRatingResponse(r)
}
return result, nil
}
func (s *PlayerFeatureService) UpdateRating(id uint, userID uint, req UpdateFeatureRatingRequest) (*PlayerFeatureRatingResponse, error) {
var rating models.PlayerFeatureRating
if err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&rating).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("rating not found or not owned by user")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Rating != nil {
updates["rating"] = req.Rating
}
if req.Notes != nil {
updates["notes"] = *req.Notes
}
if len(updates) > 0 {
if err := s.db.Model(&rating).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&rating, id)
}
return toFeatureRatingResponse(rating), nil
}
func (s *PlayerFeatureService) DeleteRating(id uint, userID uint) error {
var rating models.PlayerFeatureRating
if err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&rating).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("rating not found or not owned by user")
}
return err
}
now := time.Now()
return s.db.Model(&rating).Update("deleted_at", now).Error
}
func (s *PlayerFeatureService) CreateSelection(userID uint, req CreateFeatureSelectionRequest) (*PlayerFeatureSelectionResponse, error) {
var existing models.PlayerFeatureSelection
err := s.db.Where("player_id = ? AND feature_type_id = ? AND user_id = ?", req.PlayerID, req.FeatureTypeID, userID).First(&existing).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if err == nil {
updates := map[string]interface{}{
"notes": req.Notes,
"deleted_at": nil,
}
if err := s.db.Model(&existing).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&existing, existing.ID)
return toFeatureSelectionResponse(existing), nil
}
sel := models.PlayerFeatureSelection{
PlayerID: req.PlayerID,
FeatureTypeID: req.FeatureTypeID,
UserID: userID,
Notes: req.Notes,
}
if err := s.db.Create(&sel).Error; err != nil {
return nil, err
}
return toFeatureSelectionResponse(sel), nil
}
func (s *PlayerFeatureService) UpdateSelection(id uint, userID uint, req UpdateFeatureSelectionRequest) (*PlayerFeatureSelectionResponse, error) {
var sel models.PlayerFeatureSelection
if err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&sel).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("selection not found or not owned by user")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Notes != nil {
updates["notes"] = *req.Notes
}
if len(updates) > 0 {
if err := s.db.Model(&sel).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&sel, id)
}
return toFeatureSelectionResponse(sel), nil
}
func (s *PlayerFeatureService) GetSelectionByID(id uint) (*PlayerFeatureSelectionWithUserNameResponse, error) {
var row PlayerFeatureSelectionWithUserNameResponse
err := s.db.Table("player_feature_selections AS s").
Select("s.id, s.player_id AS player_id, s.feature_type_id AS feature_type_id, s.user_id AS user_id, s.notes, s.created_at, s.updated_at, s.deleted_at, u.name AS user_name").
Joins("LEFT JOIN users u ON u.id = s.user_id").
Where("s.id = ?", id).
Take(&row).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("selection not found")
}
return nil, err
}
return &row, nil
}
func (s *PlayerFeatureService) GetSelectionsByPlayer(playerID string) ([]PlayerFeatureSelectionWithUserNameResponse, error) {
var rows []PlayerFeatureSelectionWithUserNameResponse
if err := s.db.Table("player_feature_selections AS s").
Select("s.id, s.player_id AS player_id, s.feature_type_id AS feature_type_id, s.user_id AS user_id, s.notes, s.created_at, s.updated_at, s.deleted_at, u.name AS user_name").
Joins("LEFT JOIN users u ON u.id = s.user_id").
Where("s.player_id = ? AND s.deleted_at IS NULL", playerID).
Find(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (s *PlayerFeatureService) GetSelectionsByPlayerAndScout(playerID string, userID uint) ([]PlayerFeatureSelectionResponse, error) {
var sels []models.PlayerFeatureSelection
if err := s.db.Where("player_id = ? AND user_id = ? AND deleted_at IS NULL", playerID, userID).Find(&sels).Error; err != nil {
return nil, err
}
result := make([]PlayerFeatureSelectionResponse, len(sels))
for i, sel := range sels {
result[i] = *toFeatureSelectionResponse(sel)
}
return result, nil
}
func (s *PlayerFeatureService) GetSelectionsByFeatureType(featureTypeID uint) ([]PlayerFeatureSelectionResponse, error) {
var sels []models.PlayerFeatureSelection
if err := s.db.Where("feature_type_id = ? AND deleted_at IS NULL", featureTypeID).Find(&sels).Error; err != nil {
return nil, err
}
result := make([]PlayerFeatureSelectionResponse, len(sels))
for i, sel := range sels {
result[i] = *toFeatureSelectionResponse(sel)
}
return result, nil
}
func (s *PlayerFeatureService) DeleteSelection(id uint, userID uint) error {
var sel models.PlayerFeatureSelection
if err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&sel).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("selection not found or not owned by user")
}
return err
}
now := time.Now()
return s.db.Model(&sel).Update("deleted_at", now).Error
}
type AggregatedFeatureRating struct {
ID uint `json:"id"`
UserID uint `json:"userId"`
Rating *int `json:"rating"`
Notes *string `json:"notes"`
}
type AggregatedFeature struct {
FeatureTypeID uint `json:"featureTypeId"`
FeatureName string `json:"featureName"`
Ratings []AggregatedFeatureRating `json:"ratings"`
AverageRating *float64 `json:"averageRating"`
RatingCount int `json:"ratingCount"`
}
type AggregatedCategory struct {
CategoryID uint `json:"categoryId"`
CategoryName string `json:"categoryName"`
Features []AggregatedFeature `json:"features"`
}
type PlayerFeaturesAggregatedResponse struct {
PlayerID string `json:"playerId"`
Categories []AggregatedCategory `json:"categories"`
}
func (s *PlayerFeatureService) GetPlayerFeaturesWithRatings(playerID string) (*PlayerFeaturesAggregatedResponse, error) {
var categories []models.PlayerFeatureCategory
if err := s.db.Where("deleted_at IS NULL").Order("\"order\", name").Find(&categories).Error; err != nil {
return nil, err
}
res := &PlayerFeaturesAggregatedResponse{PlayerID: playerID, Categories: make([]AggregatedCategory, 0, len(categories))}
for _, cat := range categories {
var types []models.PlayerFeatureType
if err := s.db.Where("category_id = ? AND deleted_at IS NULL", cat.ID).Order("\"order\", name").Find(&types).Error; err != nil {
return nil, err
}
catOut := AggregatedCategory{CategoryID: cat.ID, CategoryName: cat.Name, Features: make([]AggregatedFeature, 0, len(types))}
for _, t := range types {
var ratings []models.PlayerFeatureRating
if err := s.db.Where("player_id = ? AND feature_type_id = ? AND deleted_at IS NULL", playerID, t.ID).Find(&ratings).Error; err != nil {
return nil, err
}
rOut := make([]AggregatedFeatureRating, 0, len(ratings))
sum := 0
count := 0
for _, r := range ratings {
rOut = append(rOut, AggregatedFeatureRating{ID: r.ID, UserID: r.UserID, Rating: r.Rating, Notes: r.Notes})
if r.Rating != nil {
sum += *r.Rating
count++
}
}
var avg *float64
if count > 0 {
v := float64(sum) / float64(count)
avg = &v
}
catOut.Features = append(catOut.Features, AggregatedFeature{
FeatureTypeID: t.ID,
FeatureName: t.Name,
Ratings: rOut,
AverageRating: avg,
RatingCount: count,
})
}
res.Categories = append(res.Categories, catOut)
}
return res, nil
}
type ListShareService struct {
db *gorm.DB
}
func NewListShareService(db *gorm.DB) *ListShareService {
return &ListShareService{db: db}
}
type ListShareResponse struct {
ID uint `json:"id"`
ListID string `json:"listId"`
UserID uint `json:"userId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type CreateListShareRequest struct {
ListID string `json:"listId" binding:"required"`
UserID uint `json:"userId" binding:"required"`
}
func (s *ListShareService) Create(req CreateListShareRequest) (*ListShareResponse, error) {
var existing models.ListShare
if err := s.db.Where("list_id = ? AND user_id = ?", req.ListID, req.UserID).First(&existing).Error; err == nil {
return nil, fmt.Errorf("list share already exists")
}
share := models.ListShare{
ListID: req.ListID,
UserID: req.UserID,
}
if err := s.db.Create(&share).Error; err != nil {
return nil, err
}
return toListShareResponse(share), nil
}
func (s *ListShareService) FindByListID(listID string) ([]ListShareResponse, error) {
var shares []models.ListShare
if err := s.db.Where("list_id = ?", listID).Find(&shares).Error; err != nil {
return nil, err
}
result := make([]ListShareResponse, len(shares))
for i, sh := range shares {
result[i] = *toListShareResponse(sh)
}
return result, nil
}
func (s *ListShareService) Delete(listID string, userID uint) error {
var share models.ListShare
if err := s.db.Where("list_id = ? AND user_id = ?", listID, userID).First(&share).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("list share not found")
}
return err
}
return s.db.Delete(&share).Error
}
func toFeatureCategoryResponse(c models.PlayerFeatureCategory) *PlayerFeatureCategoryResponse {
isActive := c.IsActive != nil && *c.IsActive
return &PlayerFeatureCategoryResponse{
ID: c.ID,
Name: c.Name,
Description: c.Description,
Order: c.Order,
IsActive: isActive,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
func toFeatureTypeResponse(t models.PlayerFeatureType) *PlayerFeatureTypeResponse {
isActive := t.IsActive != nil && *t.IsActive
return &PlayerFeatureTypeResponse{
ID: t.ID,
CategoryID: t.CategoryID,
Name: t.Name,
Description: t.Description,
Order: t.Order,
IsActive: isActive,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
}
}
func toFeatureRatingResponse(r models.PlayerFeatureRating) *PlayerFeatureRatingResponse {
return &PlayerFeatureRatingResponse{
ID: r.ID,
PlayerID: r.PlayerID,
FeatureTypeID: r.FeatureTypeID,
UserID: r.UserID,
Rating: r.Rating,
Notes: r.Notes,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
}
func toFeatureSelectionResponse(s models.PlayerFeatureSelection) *PlayerFeatureSelectionResponse {
return &PlayerFeatureSelectionResponse{
ID: s.ID,
PlayerID: s.PlayerID,
FeatureTypeID: s.FeatureTypeID,
UserID: s.UserID,
Notes: s.Notes,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}
func toListShareResponse(s models.ListShare) *ListShareResponse {
return &ListShareResponse{
ID: s.ID,
ListID: s.ListID,
UserID: s.UserID,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}
package services
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type PlayerService struct {
db *gorm.DB
}
func NewPlayerService(db *gorm.DB) *PlayerService {
return &PlayerService{db: db}
}
// Nested response types to match NestJS StructuredPlayer
type PlayerAreaResponse struct {
ID uint `json:"id"`
WyID int `json:"wyId"`
Name string `json:"name"`
Alpha2Code *string `json:"alpha2code"`
Alpha3Code *string `json:"alpha3code"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
}
type PlayerRoleResponse struct {
Name string `json:"name"`
}
type PlayerPositionLocation struct {
X int `json:"x"`
Y int `json:"y"`
}
type PlayerPositionResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Code2 *string `json:"code2"`
Code3 *string `json:"code3"`
Order int `json:"order"`
Location *PlayerPositionLocation `json:"location"`
BGColor *string `json:"bgColor"`
TextColor *string `json:"textColor"`
Category string `json:"category"`
}
type PlayerTeamResponse struct {
ID *int `json:"id"`
WyID *int `json:"wyId"`
GsmID *int `json:"gsmId"`
Name *string `json:"name"`
OfficialName *string `json:"officialName"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
}
type PlayerReportResponse struct {
ID uint `json:"id"`
Grade *string `json:"grade"`
Rating *float64 `json:"rating"`
}
type PlayerResponse struct {
ID string `json:"id"`
WyID int `json:"wyId"`
TsID string `json:"tsId"`
GsmID *int `json:"gsmId"`
TeamWyID *int `json:"teamWyId"`
TeamTsID *string `json:"teamTsId"`
CountryTsID *string `json:"countryTsId"`
ShortName string `json:"shortName"`
FirstName string `json:"firstName"`
MiddleName string `json:"middleName"`
LastName string `json:"lastName"`
Height int `json:"height"`
Weight float64 `json:"weight"`
BirthDate string `json:"birthDate"`
BirthArea *PlayerAreaResponse `json:"birthArea"`
SecondBirthArea *PlayerAreaResponse `json:"secondBirthArea"`
PassportArea *PlayerAreaResponse `json:"passportArea"`
SecondPassportArea *PlayerAreaResponse `json:"secondPassportArea"`
Role PlayerRoleResponse `json:"role"`
Position *PlayerPositionResponse `json:"position"`
OtherPositions []PlayerPositionResponse `json:"otherPositions"`
Foot *string `json:"foot"`
CurrentTeam *PlayerTeamResponse `json:"currentTeam"`
CurrentNationalTeam *PlayerTeamResponse `json:"currentNationalTeam"`
Gender string `json:"gender"`
Status string `json:"status"`
JerseyNumber *int `json:"jerseyNumber"`
ImageDataURL *string `json:"imageDataURL"`
APILastSyncedAt *time.Time `json:"apiLastSyncedAt"`
APISyncStatus string `json:"apiSyncStatus"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
Reports []PlayerReportResponse `json:"reports"`
Email *string `json:"email"`
Phone *string `json:"phone"`
OnLoan bool `json:"onLoan"`
AgentID *int `json:"agentId"`
Agent *AgentResponse `json:"agent"`
Ranking *string `json:"ranking"`
ROI *string `json:"roi"`
MarketValue *int `json:"marketValue"`
MarketValueCurrency *string `json:"marketValueCurrency"`
ValueRange *string `json:"valueRange"`
TransferValue *float64 `json:"transferValue"`
Salary *float64 `json:"salary"`
ContractEndsAt *string `json:"contractEndsAt"`
Feasible bool `json:"feasible"`
Morphology *string `json:"morphology"`
AbilityJSON *string `json:"abilityJson"`
CharacteristicsJSON *string `json:"characteristicsJson"`
UID *string `json:"uid"`
Deathday *time.Time `json:"deathday"`
RetireTime *time.Time `json:"retireTime"`
}
type QueryPlayersRequest struct {
Name string `form:"name"`
Foot string `form:"foot"`
TeamWyID *int `form:"teamWyId"`
NationalityWyID *int `form:"nationalityWyId"`
NationalTeamID *int `form:"nationalTeamId"`
PassportAreaWyID *int `form:"passportAreaWyId"`
EUPassport *bool `form:"euPassport"`
Archived *bool `form:"archived"`
Positions string `form:"positions"`
ScoutID *int `form:"scoutId"`
BornOn string `form:"bornOn"`
MinAge *int `form:"minAge"`
MaxAge *int `form:"maxAge"`
MinHeight *int `form:"minHeight"`
MaxHeight *int `form:"maxHeight"`
MinWeight *int `form:"minWeight"`
MaxWeight *int `form:"maxWeight"`
Gender string `form:"gender"`
Status string `form:"status"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreatePlayerRequest struct {
WyID *int `json:"wyId"`
TeamWyID *int `json:"teamWyId"`
GsmID *int `json:"gsmId"`
FirstName string `json:"firstName" binding:"required"`
LastName string `json:"lastName"`
MiddleName *string `json:"middleName"`
ShortName *string `json:"shortName"`
CurrentTeamID *int `json:"currentTeamId"`
CurrentTeamName *string `json:"currentTeamName"`
CurrentTeamOfficialName *string `json:"currentTeamOfficialName"`
CurrentNationalTeamID *int `json:"currentNationalTeamId"`
CurrentNationalTeamName *string `json:"currentNationalTeamName"`
CurrentNationalTeamOfficialName *string `json:"currentNationalTeamOfficialName"`
CurrentTeam interface{} `json:"currentTeam"`
DateOfBirth *string `json:"dateOfBirth"`
BirthDate *string `json:"birthDate"`
HeightCm *int `json:"heightCm"`
Height *int `json:"height"`
WeightKg *float64 `json:"weightKg"`
Weight *float64 `json:"weight"`
Foot *string `json:"foot"`
Gender *string `json:"gender"`
PositionID *int `json:"positionId"`
OtherPositions []interface{} `json:"otherPositions"`
OtherPositionIds []int `json:"otherPositionIds"`
RoleName *string `json:"roleName"`
Role interface{} `json:"role"`
BirthAreaWyId *int `json:"birthAreaWyId"`
SecondBirthAreaWyId *int `json:"secondBirthAreaWyId"`
BirthArea interface{} `json:"birthArea"`
PassportAreaWyId *int `json:"passportAreaWyId"`
SecondPassportAreaWyId *int `json:"secondPassportAreaWyId"`
PassportArea interface{} `json:"passportArea"`
Status *string `json:"status"`
ImageDataUrl *string `json:"imageDataUrl"`
ImageDataURL *string `json:"imageDataURL"`
JerseyNumber *int `json:"jerseyNumber"`
APILastSyncedAt *string `json:"apiLastSyncedAt"`
APISyncStatus *string `json:"apiSyncStatus"`
IsActive *bool `json:"isActive"`
Email *string `json:"email"`
Phone *string `json:"phone"`
OnLoan *bool `json:"onLoan"`
Agent *string `json:"agent"`
Ranking *string `json:"ranking"`
ROI *string `json:"roi"`
MarketValue *float64 `json:"marketValue"`
ValueRange *string `json:"valueRange"`
TransferValue *float64 `json:"transferValue"`
Salary *float64 `json:"salary"`
ContractEndsAt *string `json:"contractEndsAt"`
Feasible *bool `json:"feasible"`
Morphology *string `json:"morphology"`
TsID *string `json:"tsId"`
TeamTsID *string `json:"teamTsId"`
CountryTsID *string `json:"countryTsId"`
MarketValueCurrency *string `json:"marketValueCurrency"`
ContractUntil *string `json:"contractUntil"`
}
type UpdatePlayerRequest struct {
WyID *int `json:"wyId"`
TeamWyID *int `json:"teamWyId"`
GsmID *int `json:"gsmId"`
FirstName *string `json:"firstName"`
LastName *string `json:"lastName"`
MiddleName *string `json:"middleName"`
ShortName *string `json:"shortName"`
CurrentTeamID *int `json:"currentTeamId"`
CurrentTeamName *string `json:"currentTeamName"`
CurrentTeamOfficialName *string `json:"currentTeamOfficialName"`
CurrentNationalTeamID *int `json:"currentNationalTeamId"`
CurrentNationalTeamName *string `json:"currentNationalTeamName"`
CurrentNationalTeamOfficialName *string `json:"currentNationalTeamOfficialName"`
CurrentTeam interface{} `json:"currentTeam"`
DateOfBirth *string `json:"dateOfBirth"`
BirthDate *string `json:"birthDate"`
HeightCm *int `json:"heightCm"`
Height *int `json:"height"`
WeightKg *float64 `json:"weightKg"`
Weight *float64 `json:"weight"`
Foot *string `json:"foot"`
Gender *string `json:"gender"`
PositionID *int `json:"positionId"`
OtherPositions []interface{} `json:"otherPositions"`
OtherPositionIds []int `json:"otherPositionIds"`
RoleName *string `json:"roleName"`
Role interface{} `json:"role"`
BirthAreaWyId *int `json:"birthAreaWyId"`
SecondBirthAreaWyId *int `json:"secondBirthAreaWyId"`
BirthArea interface{} `json:"birthArea"`
PassportAreaWyId *int `json:"passportAreaWyId"`
SecondPassportAreaWyId *int `json:"secondPassportAreaWyId"`
PassportArea interface{} `json:"passportArea"`
Status *string `json:"status"`
ImageDataUrl *string `json:"imageDataUrl"`
ImageDataURL *string `json:"imageDataURL"`
JerseyNumber *int `json:"jerseyNumber"`
APILastSyncedAt *string `json:"apiLastSyncedAt"`
APISyncStatus *string `json:"apiSyncStatus"`
IsActive *bool `json:"isActive"`
Email *string `json:"email"`
Phone *string `json:"phone"`
OnLoan *bool `json:"onLoan"`
Agent *string `json:"agent"`
Ranking *string `json:"ranking"`
ROI *string `json:"roi"`
MarketValue *float64 `json:"marketValue"`
ValueRange *string `json:"valueRange"`
TransferValue *float64 `json:"transferValue"`
Salary *float64 `json:"salary"`
ContractEndsAt *string `json:"contractEndsAt"`
Feasible *bool `json:"feasible"`
Morphology *string `json:"morphology"`
TsID *string `json:"tsId"`
TeamTsID *string `json:"teamTsId"`
CountryTsID *string `json:"countryTsId"`
MarketValueCurrency *string `json:"marketValueCurrency"`
ContractUntil *string `json:"contractUntil"`
}
func parseYYYYMMDD(s *string) (*time.Time, error) {
if s == nil || strings.TrimSpace(*s) == "" {
return nil, nil
}
t, err := time.Parse("2006-01-02", *s)
if err != nil {
return nil, err
}
return &t, nil
}
func parseTimeFlexible(s *string) (*time.Time, error) {
if s == nil || strings.TrimSpace(*s) == "" {
return nil, nil
}
if t, err := time.Parse(time.RFC3339, *s); err == nil {
return &t, nil
}
return parseYYYYMMDD(s)
}
func coalesceString(a *string, b *string) *string {
if a != nil && strings.TrimSpace(*a) != "" {
return a
}
if b != nil && strings.TrimSpace(*b) != "" {
return b
}
return nil
}
func coalesceInt(a *int, b *int) *int {
if a != nil {
return a
}
return b
}
func coalesceFloat(a *float64, b *float64) *float64 {
if a != nil {
return a
}
return b
}
type PaginatedPlayersResponse struct {
Data []PlayerResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
func (s *PlayerService) UpsertByWyID(req CreatePlayerRequest) (*PlayerResponse, error) {
if req.WyID != nil {
var existing models.Player
if err := s.db.Where("wy_id = ?", *req.WyID).First(&existing).Error; err == nil {
return s.updatePlayer(&existing, req)
}
}
dateStr := coalesceString(req.DateOfBirth, req.BirthDate)
dateOfBirth, err := parseTimeFlexible(dateStr)
if err != nil {
return nil, err
}
heightCm := coalesceInt(req.HeightCm, req.Height)
weightKg := coalesceFloat(req.WeightKg, req.Weight)
imageDataURL := coalesceString(req.ImageDataURL, req.ImageDataUrl)
apiLastSyncedAt, err := parseTimeFlexible(req.APILastSyncedAt)
if err != nil {
return nil, err
}
contractUntil, err := parseTimeFlexible(req.ContractUntil)
if err != nil {
return nil, err
}
contractEndsAt, err := parseTimeFlexible(req.ContractEndsAt)
if err != nil {
return nil, err
}
if contractUntil == nil {
contractUntil = contractEndsAt
}
var marketValue *int
if req.MarketValue != nil {
mv := int(*req.MarketValue)
marketValue = &mv
}
onLoan := false
if req.OnLoan != nil {
onLoan = *req.OnLoan
}
feasible := false
if req.Feasible != nil {
feasible = *req.Feasible
}
apiSyncStatus := "pending"
if req.APISyncStatus != nil && strings.TrimSpace(*req.APISyncStatus) != "" {
apiSyncStatus = *req.APISyncStatus
}
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
id := generatePlayerID()
var positionID *uint
if req.PositionID != nil {
v := uint(*req.PositionID)
positionID = &v
}
var otherPositionIDs datatypes.JSON
if req.OtherPositionIds != nil {
b, err := json.Marshal(req.OtherPositionIds)
if err != nil {
return nil, err
}
otherPositionIDs = datatypes.JSON(b)
}
player := models.Player{
ID: id,
WyID: req.WyID,
TsID: req.TsID,
TeamWyID: req.TeamWyID,
TeamTsID: req.TeamTsID,
CountryTsID: req.CountryTsID,
FirstName: req.FirstName,
LastName: req.LastName,
MiddleName: req.MiddleName,
ShortName: req.ShortName,
GSMID: req.GsmID,
CurrentTeamID: req.CurrentTeamID,
CurrentNationalTeamID: req.CurrentNationalTeamID,
DateOfBirth: dateOfBirth,
HeightCM: heightCm,
WeightKG: weightKg,
Foot: req.Foot,
Gender: req.Gender,
PositionID: positionID,
OtherPositionIDs: otherPositionIDs,
RoleName: req.RoleName,
BirthAreaWyID: req.BirthAreaWyId,
SecondBirthAreaWyID: req.SecondBirthAreaWyId,
PassportAreaWyID: req.PassportAreaWyId,
SecondPassportAreaWyID: req.SecondPassportAreaWyId,
MarketValue: marketValue,
MarketValueCurrency: req.MarketValueCurrency,
ContractUntil: contractUntil,
Email: req.Email,
Phone: req.Phone,
OnLoan: onLoan,
AgentName: req.Agent,
Ranking: req.Ranking,
ROI: req.ROI,
ValueRange: req.ValueRange,
TransferValue: req.TransferValue,
Salary: req.Salary,
Feasible: feasible,
Morphology: req.Morphology,
Status: derefStringPlayer(req.Status, "active"),
ImageDataURL: imageDataURL,
JerseyNumber: req.JerseyNumber,
APILastSyncedAt: apiLastSyncedAt,
APISyncStatus: apiSyncStatus,
IsActive: isActive,
}
if err := s.db.Create(&player).Error; err != nil {
return nil, err
}
return s.toPlayerResponse(player)
}
func (s *PlayerService) updatePlayer(player *models.Player, req CreatePlayerRequest) (*PlayerResponse, error) {
dateStr := coalesceString(req.DateOfBirth, req.BirthDate)
dateOfBirth, err := parseTimeFlexible(dateStr)
if err != nil {
return nil, err
}
heightCm := coalesceInt(req.HeightCm, req.Height)
weightKg := coalesceFloat(req.WeightKg, req.Weight)
imageDataURL := coalesceString(req.ImageDataURL, req.ImageDataUrl)
apiLastSyncedAt, err := parseTimeFlexible(req.APILastSyncedAt)
if err != nil {
return nil, err
}
contractUntil, err := parseTimeFlexible(req.ContractUntil)
if err != nil {
return nil, err
}
contractEndsAt, err := parseTimeFlexible(req.ContractEndsAt)
if err != nil {
return nil, err
}
if contractUntil == nil {
contractUntil = contractEndsAt
}
var marketValue *int
if req.MarketValue != nil {
mv := int(*req.MarketValue)
marketValue = &mv
}
onLoan := false
if req.OnLoan != nil {
onLoan = *req.OnLoan
}
feasible := false
if req.Feasible != nil {
feasible = *req.Feasible
}
apiSyncStatus := "pending"
if req.APISyncStatus != nil && strings.TrimSpace(*req.APISyncStatus) != "" {
apiSyncStatus = *req.APISyncStatus
}
updates := map[string]interface{}{
"ts_id": req.TsID,
"team_wy_id": req.TeamWyID,
"team_ts_id": req.TeamTsID,
"country_ts_id": req.CountryTsID,
"first_name": req.FirstName,
"last_name": req.LastName,
"middle_name": req.MiddleName,
"short_name": req.ShortName,
"gsm_id": req.GsmID,
"current_team_id": req.CurrentTeamID,
"current_national_team_id": req.CurrentNationalTeamID,
"date_of_birth": dateOfBirth,
"height_cm": heightCm,
"weight_kg": weightKg,
"foot": req.Foot,
"gender": req.Gender,
"position_id": req.PositionID,
"role_name": req.RoleName,
"birth_area_wy_id": req.BirthAreaWyId,
"second_birth_area_wy_id": req.SecondBirthAreaWyId,
"passport_area_wy_id": req.PassportAreaWyId,
"second_passport_area_wy_id": req.SecondPassportAreaWyId,
"market_value": marketValue,
"market_value_currency": req.MarketValueCurrency,
"contract_until": contractUntil,
"email": req.Email,
"phone": req.Phone,
"on_loan": onLoan,
"agent_name": req.Agent,
"ranking": req.Ranking,
"roi": req.ROI,
"value_range": req.ValueRange,
"transfer_value": req.TransferValue,
"salary": req.Salary,
"feasible": feasible,
"morphology": req.Morphology,
"image_data_url": imageDataURL,
"jersey_number": req.JerseyNumber,
"api_last_synced_at": apiLastSyncedAt,
"api_sync_status": apiSyncStatus,
}
if req.OtherPositionIds != nil {
b, err := json.Marshal(req.OtherPositionIds)
if err != nil {
return nil, err
}
updates["other_position_ids"] = datatypes.JSON(b)
}
if req.Status != nil {
updates["status"] = *req.Status
}
if err := s.db.Model(player).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(player, "id = ?", player.ID)
return s.toPlayerResponse(*player)
}
func (s *PlayerService) FindByID(id string) (*PlayerResponse, error) {
var player models.Player
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&player).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return s.toPlayerResponse(player)
}
func (s *PlayerService) FindByWyID(wyID int) (*PlayerResponse, error) {
var player models.Player
if err := s.db.Where("wy_id = ? AND deleted_at IS NULL", wyID).First(&player).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return s.toPlayerResponse(player)
}
func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersResponse, error) {
q := s.db.Model(&models.Player{}).Where("deleted_at IS NULL")
if query.Name != "" {
nameLower := "%" + strings.ToLower(query.Name) + "%"
q = q.Where("LOWER(first_name) LIKE ? OR LOWER(last_name) LIKE ? OR LOWER(middle_name) LIKE ? OR LOWER(short_name) LIKE ?",
nameLower, nameLower, nameLower, nameLower)
}
if query.Foot != "" {
q = q.Where("foot = ?", query.Foot)
}
if query.TeamWyID != nil {
q = q.Where("team_wy_id = ?", *query.TeamWyID)
}
if query.NationalityWyID != nil {
q = q.Where("birth_area_wy_id = ?", *query.NationalityWyID)
}
if query.NationalTeamID != nil {
q = q.Where("current_national_team_id = ?", *query.NationalTeamID)
}
if query.PassportAreaWyID != nil {
q = q.Where("passport_area_wy_id = ?", *query.PassportAreaWyID)
}
if query.Archived != nil {
if *query.Archived {
q = q.Where("status = ?", "inactive")
} else {
q = q.Where("status = ? OR status IS NULL", "active")
}
}
if query.Positions != "" {
positions := strings.Split(query.Positions, ",")
q = q.Where("role_name IN ?", positions)
}
if query.BornOn != "" {
if t, err := time.Parse("2006-01-02", query.BornOn); err == nil {
q = q.Where("DATE(date_of_birth) = ?", t.Format("2006-01-02"))
}
}
if query.MinAge != nil || query.MaxAge != nil {
now := time.Now()
if query.MinAge != nil {
maxBirthDate := now.AddDate(-*query.MinAge, 0, 0)
q = q.Where("date_of_birth <= ?", maxBirthDate)
}
if query.MaxAge != nil {
minBirthDate := now.AddDate(-*query.MaxAge-1, 0, 0)
q = q.Where("date_of_birth > ?", minBirthDate)
}
}
if query.MinHeight != nil {
q = q.Where("height_cm >= ?", *query.MinHeight)
}
if query.MaxHeight != nil {
q = q.Where("height_cm <= ?", *query.MaxHeight)
}
if query.MinWeight != nil {
q = q.Where("weight_kg >= ?", *query.MinWeight)
}
if query.MaxWeight != nil {
q = q.Where("weight_kg <= ?", *query.MaxWeight)
}
if query.Gender != "" {
q = q.Where("gender = ?", query.Gender)
}
if query.Status != "" {
q = q.Where("status = ?", query.Status)
}
var total int64
q.Count(&total)
sortBy := "last_name"
allowedSorts := map[string]string{
"firstName": "first_name",
"lastName": "last_name",
"shortName": "short_name",
"height": "height_cm",
"weight": "weight_kg",
"birthDate": "date_of_birth",
"position": "position",
"foot": "foot",
"gender": "gender",
"status": "status",
"createdAt": "created_at",
"updatedAt": "updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var players []models.Player
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&players).Error; err != nil {
return nil, err
}
data := make([]PlayerResponse, len(players))
for i, p := range players {
resp, err := s.toPlayerResponse(p)
if err != nil {
return nil, err
}
data[i] = *resp
}
return &PaginatedPlayersResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(players)) < total,
}, nil
}
func (s *PlayerService) UpdateByID(id string, req UpdatePlayerRequest) (*PlayerResponse, error) {
var player models.Player
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&player).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
updates := make(map[string]interface{})
if req.WyID != nil {
updates["wy_id"] = *req.WyID
}
if req.TsID != nil {
updates["ts_id"] = *req.TsID
}
if req.TeamWyID != nil {
updates["team_wy_id"] = *req.TeamWyID
}
if req.GsmID != nil {
updates["gsm_id"] = *req.GsmID
}
if req.TeamTsID != nil {
updates["team_ts_id"] = *req.TeamTsID
}
if req.CountryTsID != nil {
updates["country_ts_id"] = *req.CountryTsID
}
if req.FirstName != nil {
updates["first_name"] = *req.FirstName
}
if req.LastName != nil {
updates["last_name"] = *req.LastName
}
if req.MiddleName != nil {
updates["middle_name"] = *req.MiddleName
}
if req.ShortName != nil {
updates["short_name"] = *req.ShortName
}
if dateStr := coalesceString(req.DateOfBirth, req.BirthDate); dateStr != nil {
if dob, err := parseTimeFlexible(dateStr); err == nil && dob != nil {
updates["date_of_birth"] = *dob
}
}
if v := coalesceInt(req.HeightCm, req.Height); v != nil {
updates["height_cm"] = *v
}
if v := coalesceFloat(req.WeightKg, req.Weight); v != nil {
updates["weight_kg"] = *v
}
if req.Foot != nil {
updates["foot"] = *req.Foot
}
if req.Gender != nil {
updates["gender"] = *req.Gender
}
if req.PositionID != nil {
updates["position_id"] = *req.PositionID
}
if req.OtherPositionIds != nil {
b, err := json.Marshal(req.OtherPositionIds)
if err != nil {
return nil, err
}
updates["other_position_ids"] = datatypes.JSON(b)
}
if req.RoleName != nil {
updates["role_name"] = *req.RoleName
}
if req.BirthAreaWyId != nil {
updates["birth_area_wy_id"] = *req.BirthAreaWyId
}
if req.SecondBirthAreaWyId != nil {
updates["second_birth_area_wy_id"] = *req.SecondBirthAreaWyId
}
if req.PassportAreaWyId != nil {
updates["passport_area_wy_id"] = *req.PassportAreaWyId
}
if req.SecondPassportAreaWyId != nil {
updates["second_passport_area_wy_id"] = *req.SecondPassportAreaWyId
}
if req.CurrentTeamID != nil {
updates["current_team_id"] = *req.CurrentTeamID
}
if req.CurrentNationalTeamID != nil {
updates["current_national_team_id"] = *req.CurrentNationalTeamID
}
if req.MarketValue != nil {
updates["market_value"] = int(*req.MarketValue)
}
if req.MarketValueCurrency != nil {
updates["market_value_currency"] = *req.MarketValueCurrency
}
if contractUntil, err := parseTimeFlexible(req.ContractUntil); err == nil && contractUntil != nil {
updates["contract_until"] = *contractUntil
} else if contractEndsAt, err := parseTimeFlexible(req.ContractEndsAt); err == nil && contractEndsAt != nil {
updates["contract_until"] = *contractEndsAt
}
if req.Status != nil {
updates["status"] = *req.Status
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if img := coalesceString(req.ImageDataURL, req.ImageDataUrl); img != nil {
updates["image_data_url"] = *img
}
if req.JerseyNumber != nil {
updates["jersey_number"] = *req.JerseyNumber
}
if apiLast, err := parseTimeFlexible(req.APILastSyncedAt); err == nil && apiLast != nil {
updates["api_last_synced_at"] = *apiLast
}
if req.APISyncStatus != nil {
updates["api_sync_status"] = *req.APISyncStatus
}
if req.Email != nil {
updates["email"] = *req.Email
}
if req.Phone != nil {
updates["phone"] = *req.Phone
}
if req.OnLoan != nil {
updates["on_loan"] = *req.OnLoan
}
if req.Agent != nil {
updates["agent_name"] = *req.Agent
}
if req.Ranking != nil {
updates["ranking"] = *req.Ranking
}
if req.ROI != nil {
updates["roi"] = *req.ROI
}
if req.ValueRange != nil {
updates["value_range"] = *req.ValueRange
}
if req.TransferValue != nil {
updates["transfer_value"] = *req.TransferValue
}
if req.Salary != nil {
updates["salary"] = *req.Salary
}
if req.Feasible != nil {
updates["feasible"] = *req.Feasible
}
if req.Morphology != nil {
updates["morphology"] = *req.Morphology
}
if len(updates) > 0 {
if err := s.db.Model(&player).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&player, "id = ?", id)
}
return s.toPlayerResponse(player)
}
func (s *PlayerService) DeleteByID(id string) error {
var player models.Player
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&player).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("player not found")
}
return err
}
now := time.Now()
return s.db.Model(&player).Update("deleted_at", now).Error
}
func toPlayerResponse(p models.Player) *PlayerResponse {
// Helper functions
derefStr := func(s *string) string {
if s == nil {
return ""
}
return *s
}
derefInt := func(i *int) int {
if i == nil {
return 0
}
return *i
}
derefFloat := func(f *float64) float64 {
if f == nil {
return 0
}
return *f
}
// Format birthDate as YYYY-MM-DD
birthDate := ""
if p.DateOfBirth != nil {
birthDate = p.DateOfBirth.Format("2006-01-02")
}
// Format contractEndsAt as YYYY-MM-DD
var contractEndsAt *string
if p.ContractUntil != nil {
s := p.ContractUntil.Format("2006-01-02")
contractEndsAt = &s
}
// Build role response
role := PlayerRoleResponse{
Name: derefStr(p.RoleName),
}
// Build current team response if we have team info
var currentTeam *PlayerTeamResponse
if p.CurrentTeamID != nil {
currentTeam = &PlayerTeamResponse{
WyID: p.CurrentTeamID,
}
}
// Build current national team response
var currentNationalTeam *PlayerTeamResponse
if p.CurrentNationalTeamID != nil {
currentNationalTeam = &PlayerTeamResponse{
WyID: p.CurrentNationalTeamID,
}
}
return &PlayerResponse{
ID: p.ID,
WyID: derefInt(p.WyID),
TsID: derefStr(p.TsID),
GsmID: p.GSMID,
TeamWyID: p.TeamWyID,
TeamTsID: p.TeamTsID,
CountryTsID: p.CountryTsID,
ShortName: derefStr(p.ShortName),
FirstName: p.FirstName,
MiddleName: derefStr(p.MiddleName),
LastName: p.LastName,
Height: derefInt(p.HeightCM),
Weight: derefFloat(p.WeightKG),
BirthDate: birthDate,
BirthArea: nil,
SecondBirthArea: nil,
PassportArea: nil,
SecondPassportArea: nil,
Role: role,
Position: nil,
OtherPositions: nil,
Foot: p.Foot,
CurrentTeam: currentTeam,
CurrentNationalTeam: currentNationalTeam,
Gender: derefStr(p.Gender),
Status: p.Status,
JerseyNumber: p.JerseyNumber,
ImageDataURL: p.ImageDataURL,
APILastSyncedAt: p.APILastSyncedAt,
APISyncStatus: p.APISyncStatus,
IsActive: p.IsActive,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
DeletedAt: p.DeletedAt,
Reports: []PlayerReportResponse{},
Email: p.Email,
Phone: p.Phone,
OnLoan: p.OnLoan,
AgentID: nil,
Agent: nil,
Ranking: p.Ranking,
ROI: p.ROI,
MarketValue: p.MarketValue,
MarketValueCurrency: p.MarketValueCurrency,
ValueRange: p.ValueRange,
TransferValue: p.TransferValue,
Salary: p.Salary,
ContractEndsAt: contractEndsAt,
Feasible: p.Feasible,
Morphology: p.Morphology,
AbilityJSON: p.AbilityJSON,
CharacteristicsJSON: p.CharacteristicsJSON,
UID: p.UID,
Deathday: p.Deathday,
RetireTime: p.RetireTime,
}
}
func generatePlayerID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}
func derefStringPlayer(s *string, def string) string {
if s == nil {
return def
}
return *s
}
func (s *PlayerService) toPlayerResponse(p models.Player) (*PlayerResponse, error) {
resp := basePlayerResponse(p)
// Primary position
if p.PositionID != nil {
var pos models.Position
if err := s.db.Where("id = ? AND deleted_at IS NULL", *p.PositionID).First(&pos).Error; err == nil {
resp.Position = toPlayerPositionResponse(pos)
}
}
// Other positions
otherIDs, err := parseOtherPositionIDs(p.OtherPositionIDs)
if err != nil {
return nil, err
}
if len(otherIDs) > 0 {
var positions []models.Position
if err := s.db.Where("id IN ? AND deleted_at IS NULL", otherIDs).Find(&positions).Error; err != nil {
return nil, err
}
resp.OtherPositions = make([]PlayerPositionResponse, 0, len(positions))
for _, pos := range positions {
if r := toPlayerPositionResponse(pos); r != nil {
resp.OtherPositions = append(resp.OtherPositions, *r)
}
}
}
return resp, nil
}
func basePlayerResponse(p models.Player) *PlayerResponse {
// Helper functions
derefStr := func(s *string) string {
if s == nil {
return ""
}
return *s
}
derefInt := func(i *int) int {
if i == nil {
return 0
}
return *i
}
derefFloat := func(f *float64) float64 {
if f == nil {
return 0
}
return *f
}
// Format birthDate as YYYY-MM-DD
birthDate := ""
if p.DateOfBirth != nil {
birthDate = p.DateOfBirth.Format("2006-01-02")
}
// Format contractEndsAt as YYYY-MM-DD
var contractEndsAt *string
if p.ContractUntil != nil {
s := p.ContractUntil.Format("2006-01-02")
contractEndsAt = &s
}
role := PlayerRoleResponse{
Name: derefStr(p.RoleName),
}
var currentTeam *PlayerTeamResponse
if p.CurrentTeamID != nil {
currentTeam = &PlayerTeamResponse{WyID: p.CurrentTeamID}
}
var currentNationalTeam *PlayerTeamResponse
if p.CurrentNationalTeamID != nil {
currentNationalTeam = &PlayerTeamResponse{WyID: p.CurrentNationalTeamID}
}
return &PlayerResponse{
ID: p.ID,
WyID: derefInt(p.WyID),
TsID: derefStr(p.TsID),
GsmID: p.GSMID,
TeamWyID: p.TeamWyID,
TeamTsID: p.TeamTsID,
CountryTsID: p.CountryTsID,
ShortName: derefStr(p.ShortName),
FirstName: p.FirstName,
MiddleName: derefStr(p.MiddleName),
LastName: p.LastName,
Height: derefInt(p.HeightCM),
Weight: derefFloat(p.WeightKG),
BirthDate: birthDate,
BirthArea: nil,
SecondBirthArea: nil,
PassportArea: nil,
SecondPassportArea: nil,
Role: role,
Position: nil,
OtherPositions: nil,
Foot: p.Foot,
CurrentTeam: currentTeam,
CurrentNationalTeam: currentNationalTeam,
Gender: derefStr(p.Gender),
Status: p.Status,
JerseyNumber: p.JerseyNumber,
ImageDataURL: p.ImageDataURL,
APILastSyncedAt: p.APILastSyncedAt,
APISyncStatus: p.APISyncStatus,
IsActive: p.IsActive,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
DeletedAt: p.DeletedAt,
Reports: []PlayerReportResponse{},
Email: p.Email,
Phone: p.Phone,
OnLoan: p.OnLoan,
AgentID: nil,
Agent: nil,
Ranking: p.Ranking,
ROI: p.ROI,
MarketValue: p.MarketValue,
MarketValueCurrency: p.MarketValueCurrency,
ValueRange: p.ValueRange,
TransferValue: p.TransferValue,
Salary: p.Salary,
ContractEndsAt: contractEndsAt,
Feasible: p.Feasible,
Morphology: p.Morphology,
AbilityJSON: p.AbilityJSON,
CharacteristicsJSON: p.CharacteristicsJSON,
UID: p.UID,
Deathday: p.Deathday,
RetireTime: p.RetireTime,
}
}
func toPlayerPositionResponse(pos models.Position) *PlayerPositionResponse {
order := 0
if pos.Order != nil {
order = *pos.Order
}
var location *PlayerPositionLocation
if pos.LocationX != nil && pos.LocationY != nil {
location = &PlayerPositionLocation{X: *pos.LocationX, Y: *pos.LocationY}
}
return &PlayerPositionResponse{
ID: pos.ID,
Name: pos.Name,
Code2: pos.Code2,
Code3: pos.Code3,
Order: order,
Location: location,
BGColor: pos.BGColor,
TextColor: pos.TextColor,
Category: string(pos.Category),
}
}
func parseOtherPositionIDs(raw datatypes.JSON) ([]uint, error) {
if len(raw) == 0 {
return nil, nil
}
var ids []uint
if err := json.Unmarshal(raw, &ids); err == nil {
return ids, nil
}
var intIDs []int
if err := json.Unmarshal(raw, &intIDs); err != nil {
return nil, err
}
ids = make([]uint, 0, len(intIDs))
for _, v := range intIDs {
if v <= 0 {
continue
}
ids = append(ids, uint(v))
}
return ids, nil
}
package services
import (
"errors"
"fmt"
"strings"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type PositionService struct {
db *gorm.DB
}
func NewPositionService(db *gorm.DB) *PositionService {
return &PositionService{db: db}
}
type PositionResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Code2 *string `json:"code2"`
Code3 *string `json:"code3"`
Order *int `json:"order"`
LocationX *int `json:"locationX"`
LocationY *int `json:"locationY"`
BGColor *string `json:"bgColor"`
TextColor *string `json:"textColor"`
Category string `json:"category"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type QueryPositionsRequest struct {
Name string `form:"name"`
Category string `form:"category"`
IsActive *bool `form:"isActive"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreatePositionRequest struct {
Name string `json:"name" binding:"required"`
Code2 *string `json:"code2"`
Code3 *string `json:"code3"`
Order *int `json:"order"`
LocationX *int `json:"locationX"`
LocationY *int `json:"locationY"`
BGColor *string `json:"bgColor"`
TextColor *string `json:"textColor"`
Category string `json:"category" binding:"required"`
}
type UpdatePositionRequest struct {
Name *string `json:"name"`
Code2 *string `json:"code2"`
Code3 *string `json:"code3"`
Order *int `json:"order"`
LocationX *int `json:"locationX"`
LocationY *int `json:"locationY"`
BGColor *string `json:"bgColor"`
TextColor *string `json:"textColor"`
Category *string `json:"category"`
IsActive *bool `json:"isActive"`
}
type PaginatedPositionsResponse struct {
Data []PositionResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
func (s *PositionService) Create(req CreatePositionRequest) (*PositionResponse, error) {
isActive := true
category := models.PositionCategory(req.Category)
position := models.Position{
Name: req.Name,
Code2: req.Code2,
Code3: req.Code3,
Order: req.Order,
LocationX: req.LocationX,
LocationY: req.LocationY,
BGColor: req.BGColor,
TextColor: req.TextColor,
Category: category,
IsActive: &isActive,
}
if err := s.db.Create(&position).Error; err != nil {
return nil, err
}
return toPositionResponse(position), nil
}
func (s *PositionService) FindByID(id uint) (*PositionResponse, error) {
var position models.Position
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&position).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("position not found")
}
return nil, err
}
return toPositionResponse(position), nil
}
func (s *PositionService) FindAll(query QueryPositionsRequest) (*PaginatedPositionsResponse, error) {
q := s.db.Model(&models.Position{}).Where("deleted_at IS NULL")
if query.Name != "" {
q = q.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(query.Name)+"%")
}
if query.Category != "" {
q = q.Where("category = ?", query.Category)
}
if query.IsActive != nil {
q = q.Where("is_active = ?", *query.IsActive)
}
var total int64
q.Count(&total)
sortBy := "\"order\""
allowedSorts := map[string]string{
"name": "name",
"code2": "code2",
"code3": "code3",
"order": "\"order\"",
"category": "category",
"createdAt": "created_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var positions []models.Position
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&positions).Error; err != nil {
return nil, err
}
data := make([]PositionResponse, len(positions))
for i, p := range positions {
data[i] = *toPositionResponse(p)
}
return &PaginatedPositionsResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(positions)) < total,
}, nil
}
func (s *PositionService) Update(id uint, req UpdatePositionRequest) (*PositionResponse, error) {
var position models.Position
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&position).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("position not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Code2 != nil {
updates["code2"] = *req.Code2
}
if req.Code3 != nil {
updates["code3"] = *req.Code3
}
if req.Order != nil {
updates["order"] = *req.Order
}
if req.LocationX != nil {
updates["location_x"] = *req.LocationX
}
if req.LocationY != nil {
updates["location_y"] = *req.LocationY
}
if req.BGColor != nil {
updates["bg_color"] = *req.BGColor
}
if req.TextColor != nil {
updates["text_color"] = *req.TextColor
}
if req.Category != nil {
updates["category"] = *req.Category
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) > 0 {
if err := s.db.Model(&position).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&position, id)
}
return toPositionResponse(position), nil
}
func (s *PositionService) Delete(id uint) error {
var position models.Position
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&position).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("position not found")
}
return err
}
now := time.Now()
return s.db.Model(&position).Update("deleted_at", now).Error
}
func toPositionResponse(p models.Position) *PositionResponse {
isActive := p.IsActive != nil && *p.IsActive
return &PositionResponse{
ID: p.ID,
Name: p.Name,
Code2: p.Code2,
Code3: p.Code3,
Order: p.Order,
LocationX: p.LocationX,
LocationY: p.LocationY,
BGColor: p.BGColor,
TextColor: p.TextColor,
Category: string(p.Category),
IsActive: isActive,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
}
package services
import (
"errors"
"fmt"
"strconv"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type ProfileService struct {
db *gorm.DB
}
func NewProfileService(db *gorm.DB) *ProfileService {
return &ProfileService{db: db}
}
type CreateProfileDescriptionRequest struct {
PlayerID *int `json:"playerId"`
CoachID *int `json:"coachId"`
Description string `json:"description" binding:"required"`
}
type UpdateProfileDescriptionRequest struct {
PlayerID *int `json:"playerId"`
CoachID *int `json:"coachId"`
Description *string `json:"description"`
}
type CreateProfileLinkRequest struct {
PlayerID *int `json:"playerId"`
CoachID *int `json:"coachId"`
Title string `json:"title" binding:"required"`
URL string `json:"url" binding:"required"`
Icon *string `json:"icon"`
Order *int `json:"order"`
IsActive *bool `json:"isActive"`
}
type UpdateProfileLinkRequest struct {
PlayerID *int `json:"playerId"`
CoachID *int `json:"coachId"`
Title *string `json:"title"`
URL *string `json:"url"`
Icon *string `json:"icon"`
Order *int `json:"order"`
IsActive *bool `json:"isActive"`
UpdatedAt *time.Time `json:"updatedAt"`
}
func intToStringPtr(v *int) *string {
if v == nil {
return nil
}
s := strconv.Itoa(*v)
return &s
}
func (s *ProfileService) CreateDescription(req CreateProfileDescriptionRequest) (*models.ProfileDescription, error) {
d := models.ProfileDescription{
PlayerID: intToStringPtr(req.PlayerID),
CoachID: intToStringPtr(req.CoachID),
Description: req.Description,
}
if err := s.db.Create(&d).Error; err != nil {
return nil, err
}
return &d, nil
}
func (s *ProfileService) UpdateDescription(id uint, req UpdateProfileDescriptionRequest) (*models.ProfileDescription, error) {
var d models.ProfileDescription
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&d).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("description not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.PlayerID != nil {
updates["player_id"] = *intToStringPtr(req.PlayerID)
}
if req.CoachID != nil {
updates["coach_id"] = *intToStringPtr(req.CoachID)
}
if req.Description != nil {
updates["description"] = *req.Description
}
if len(updates) > 0 {
if err := s.db.Model(&d).Updates(updates).Error; err != nil {
return nil, err
}
_ = s.db.Where("id = ?", id).First(&d).Error
}
return &d, nil
}
func (s *ProfileService) GetDescriptionByID(id uint) (*models.ProfileDescription, error) {
var d models.ProfileDescription
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&d).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &d, nil
}
func (s *ProfileService) GetDescriptionByPlayerID(playerID int) (*models.ProfileDescription, error) {
pid := strconv.Itoa(playerID)
var d models.ProfileDescription
if err := s.db.Where("player_id = ? AND deleted_at IS NULL", pid).First(&d).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &d, nil
}
func (s *ProfileService) GetDescriptionByCoachID(coachID int) (*models.ProfileDescription, error) {
cid := strconv.Itoa(coachID)
var d models.ProfileDescription
if err := s.db.Where("coach_id = ? AND deleted_at IS NULL", cid).First(&d).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &d, nil
}
func (s *ProfileService) DeleteDescription(id uint) (bool, error) {
res := s.db.Where("id = ? AND deleted_at IS NULL", id).Delete(&models.ProfileDescription{})
if res.Error != nil {
return false, res.Error
}
if res.RowsAffected == 0 {
return false, nil
}
return true, nil
}
func (s *ProfileService) CreateLink(req CreateProfileLinkRequest) (*models.ProfileLink, error) {
isActive := true
link := models.ProfileLink{
PlayerID: intToStringPtr(req.PlayerID),
CoachID: intToStringPtr(req.CoachID),
Title: req.Title,
URL: req.URL,
Icon: req.Icon,
Order: req.Order,
IsActive: req.IsActive,
}
if link.IsActive == nil {
link.IsActive = &isActive
}
if err := s.db.Create(&link).Error; err != nil {
return nil, err
}
return &link, nil
}
func (s *ProfileService) UpdateLink(id uint, req UpdateProfileLinkRequest) (*models.ProfileLink, error) {
var link models.ProfileLink
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&link).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("link not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.PlayerID != nil {
updates["player_id"] = *intToStringPtr(req.PlayerID)
}
if req.CoachID != nil {
updates["coach_id"] = *intToStringPtr(req.CoachID)
}
if req.Title != nil {
updates["title"] = *req.Title
}
if req.URL != nil {
updates["url"] = *req.URL
}
if req.Icon != nil {
updates["icon"] = req.Icon
}
if req.Order != nil {
updates["order"] = req.Order
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) > 0 {
if err := s.db.Model(&link).Updates(updates).Error; err != nil {
return nil, err
}
_ = s.db.Where("id = ?", id).First(&link).Error
}
return &link, nil
}
func (s *ProfileService) GetLinkByID(id uint) (*models.ProfileLink, error) {
var link models.ProfileLink
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&link).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &link, nil
}
func (s *ProfileService) GetLinksByPlayerID(playerID int) ([]models.ProfileLink, error) {
pid := strconv.Itoa(playerID)
var links []models.ProfileLink
if err := s.db.Where("player_id = ? AND deleted_at IS NULL", pid).Order("\"order\" asc, id asc").Find(&links).Error; err != nil {
return nil, err
}
return links, nil
}
func (s *ProfileService) GetLinksByCoachID(coachID int) ([]models.ProfileLink, error) {
cid := strconv.Itoa(coachID)
var links []models.ProfileLink
if err := s.db.Where("coach_id = ? AND deleted_at IS NULL", cid).Order("\"order\" asc, id asc").Find(&links).Error; err != nil {
return nil, err
}
return links, nil
}
func (s *ProfileService) DeleteLink(id uint) (bool, error) {
res := s.db.Where("id = ? AND deleted_at IS NULL", id).Delete(&models.ProfileLink{})
if res.Error != nil {
return false, res.Error
}
if res.RowsAffected == 0 {
return false, nil
}
return true, nil
}
package services
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type ReportService struct {
db *gorm.DB
}
func NewReportService(db *gorm.DB) *ReportService {
return &ReportService{db: db}
}
type ReportResponse struct {
ID string `json:"id"`
Name string `json:"name"`
PlayerDataID *string `json:"playerDataId"`
CoachDataID *string `json:"coachDataId"`
MatchDataID *string `json:"matchDataId"`
PlayerWyID *int `json:"playerWyId"`
CoachWyID *int `json:"coachWyId"`
MatchWyID *int `json:"matchWyId"`
Type string `json:"type"`
Description interface{} `json:"description"`
Grade *string `json:"grade"`
Rating *float64 `json:"rating"`
Decision *string `json:"decision"`
Status *string `json:"status"`
UserID *uint `json:"userId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type QueryReportsRequest struct {
PlayerWyID *int `form:"playerWyId"`
CoachWyID *int `form:"coachWyId"`
MatchWyID *int `form:"matchWyId"`
Type string `form:"type"`
Status string `form:"status"`
UserID *uint `form:"userId"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreateReportRequest struct {
Name string `json:"name" binding:"required"`
PlayerDataID *string `json:"playerDataId"`
CoachDataID *string `json:"coachDataId"`
MatchDataID *string `json:"matchDataId"`
PlayerWyID *int `json:"playerWyId"`
CoachWyID *int `json:"coachWyId"`
MatchWyID *int `json:"matchWyId"`
Type string `json:"type" binding:"required"`
Description interface{} `json:"description"`
Grade *string `json:"grade"`
Rating *float64 `json:"rating"`
Decision *string `json:"decision"`
Status *string `json:"status"`
}
type UpdateReportRequest struct {
Name *string `json:"name"`
PlayerDataID *string `json:"playerDataId"`
CoachDataID *string `json:"coachDataId"`
MatchDataID *string `json:"matchDataId"`
PlayerWyID *int `json:"playerWyId"`
CoachWyID *int `json:"coachWyId"`
MatchWyID *int `json:"matchWyId"`
Type *string `json:"type"`
Description interface{} `json:"description"`
Grade *string `json:"grade"`
Rating *float64 `json:"rating"`
Decision *string `json:"decision"`
Status *string `json:"status"`
}
type PaginatedReportsResponse struct {
Data []ReportResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
func (s *ReportService) Create(userID uint, req CreateReportRequest) (*ReportResponse, error) {
var status *models.ReportStatus
if req.Status != nil {
st := models.ReportStatus(*req.Status)
status = &st
}
descJSON, err := json.Marshal(req.Description)
if err != nil {
return nil, err
}
report := models.Report{
Name: req.Name,
PlayerDataID: req.PlayerDataID,
CoachDataID: req.CoachDataID,
MatchDataID: req.MatchDataID,
PlayerWyID: req.PlayerWyID,
CoachWyID: req.CoachWyID,
MatchWyID: req.MatchWyID,
Type: req.Type,
Description: datatypes.JSON(descJSON),
Grade: req.Grade,
Rating: req.Rating,
Decision: req.Decision,
Status: status,
UserID: &userID,
}
if err := s.db.Create(&report).Error; err != nil {
return nil, err
}
return toReportResponse(report), nil
}
func (s *ReportService) FindByID(id string) (*ReportResponse, error) {
var report models.Report
if err := s.db.First(&report, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("report not found")
}
return nil, err
}
return toReportResponse(report), nil
}
func (s *ReportService) FindAll(query QueryReportsRequest) (*PaginatedReportsResponse, error) {
q := s.db.Model(&models.Report{})
if query.PlayerWyID != nil {
q = q.Where("player_wy_id = ?", *query.PlayerWyID)
}
if query.CoachWyID != nil {
q = q.Where("coach_wy_id = ?", *query.CoachWyID)
}
if query.MatchWyID != nil {
q = q.Where("match_wy_id = ?", *query.MatchWyID)
}
if query.Type != "" {
q = q.Where("type = ?", query.Type)
}
if query.Status != "" {
q = q.Where("status = ?", query.Status)
}
if query.UserID != nil {
q = q.Where("user_id = ?", *query.UserID)
}
var total int64
q.Count(&total)
sortBy := "created_at"
allowedSorts := map[string]string{
"name": "name",
"type": "type",
"status": "status",
"rating": "rating",
"createdAt": "created_at",
"updatedAt": "updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "DESC"
if strings.ToLower(query.SortOrder) == "asc" {
sortOrder = "ASC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var reports []models.Report
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&reports).Error; err != nil {
return nil, err
}
data := make([]ReportResponse, len(reports))
for i, r := range reports {
data[i] = *toReportResponse(r)
}
return &PaginatedReportsResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(reports)) < total,
}, nil
}
func (s *ReportService) Update(id string, req UpdateReportRequest) (*ReportResponse, error) {
var report models.Report
if err := s.db.First(&report, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("report not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.PlayerDataID != nil {
updates["player_data_id"] = *req.PlayerDataID
}
if req.CoachDataID != nil {
updates["coach_data_id"] = *req.CoachDataID
}
if req.MatchDataID != nil {
updates["match_data_id"] = *req.MatchDataID
}
if req.PlayerWyID != nil {
updates["player_wy_id"] = *req.PlayerWyID
}
if req.CoachWyID != nil {
updates["coach_wy_id"] = *req.CoachWyID
}
if req.MatchWyID != nil {
updates["match_wy_id"] = *req.MatchWyID
}
if req.Type != nil {
updates["type"] = *req.Type
}
if req.Description != nil {
descJSON, err := json.Marshal(req.Description)
if err != nil {
return nil, err
}
updates["description"] = datatypes.JSON(descJSON)
}
if req.Grade != nil {
updates["grade"] = *req.Grade
}
if req.Rating != nil {
updates["rating"] = *req.Rating
}
if req.Decision != nil {
updates["decision"] = *req.Decision
}
if req.Status != nil {
updates["status"] = *req.Status
}
if len(updates) > 0 {
if err := s.db.Model(&report).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&report, "id = ?", id)
}
return toReportResponse(report), nil
}
func (s *ReportService) Delete(id string) error {
var report models.Report
if err := s.db.First(&report, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("report not found")
}
return err
}
return s.db.Delete(&report).Error
}
func toReportResponse(r models.Report) *ReportResponse {
var status *string
if r.Status != nil {
st := string(*r.Status)
status = &st
}
var desc interface{}
if len(r.Description) > 0 {
_ = json.Unmarshal([]byte(r.Description), &desc)
}
return &ReportResponse{
ID: r.ID,
Name: r.Name,
PlayerDataID: r.PlayerDataID,
CoachDataID: r.CoachDataID,
MatchDataID: r.MatchDataID,
PlayerWyID: r.PlayerWyID,
CoachWyID: r.CoachWyID,
MatchWyID: r.MatchWyID,
Type: r.Type,
Description: desc,
Grade: r.Grade,
Rating: r.Rating,
Decision: r.Decision,
Status: status,
UserID: r.UserID,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
}
package services
import (
"encoding/json"
"errors"
"fmt"
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type SettingsService struct {
db *gorm.DB
}
func NewSettingsService(db *gorm.DB) *SettingsService {
return &SettingsService{db: db}
}
type GlobalSettingResponse struct {
ID uint `json:"id"`
Category string `json:"category"`
Key string `json:"key"`
Name string `json:"name"`
Description *string `json:"description"`
Value interface{} `json:"value"`
Color *string `json:"color"`
SortOrder *int `json:"sortOrder"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type UserSettingResponse struct {
ID uint `json:"id"`
UserID uint `json:"userId"`
Category string `json:"category"`
Key string `json:"key"`
Value interface{} `json:"value"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type CategoryResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
Type string `json:"type"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type CreateGlobalSettingRequest struct {
Category string `json:"category" binding:"required"`
Key string `json:"key" binding:"required"`
Name string `json:"name" binding:"required"`
Description *string `json:"description"`
Value interface{} `json:"value" binding:"required"`
Color *string `json:"color"`
SortOrder *int `json:"sortOrder"`
}
type UpdateGlobalSettingRequest struct {
Category *string `json:"category"`
Key *string `json:"key"`
Name *string `json:"name"`
Description *string `json:"description"`
Value interface{} `json:"value"`
Color *string `json:"color"`
SortOrder *int `json:"sortOrder"`
IsActive *bool `json:"isActive"`
}
type CreateUserSettingRequest struct {
Category string `json:"category" binding:"required"`
Key string `json:"key" binding:"required"`
Value interface{} `json:"value" binding:"required"`
}
type UpdateUserSettingRequest struct {
Category *string `json:"category"`
Key *string `json:"key"`
Value interface{} `json:"value"`
}
type BulkUpdateUserSettingsRequest struct {
Category string `json:"category" binding:"required"`
Settings map[string]interface{} `json:"settings" binding:"required"`
}
type CreateSettingsCategoryRequest struct {
Name string `json:"name" binding:"required"`
Description *string `json:"description"`
Type string `json:"type" binding:"required"`
}
func (s *SettingsService) CreateGlobalSetting(req CreateGlobalSettingRequest) (*GlobalSettingResponse, error) {
var existing models.GlobalSetting
if err := s.db.Where("category = ? AND key = ?", req.Category, req.Key).First(&existing).Error; err == nil {
return nil, fmt.Errorf("setting with same category and key already exists")
}
isActive := true
valueJSON, err := json.Marshal(req.Value)
if err != nil {
return nil, err
}
setting := models.GlobalSetting{
Category: req.Category,
Key: req.Key,
Name: req.Name,
Description: req.Description,
Value: datatypes.JSON(valueJSON),
Color: req.Color,
SortOrder: req.SortOrder,
IsActive: &isActive,
}
if err := s.db.Create(&setting).Error; err != nil {
return nil, err
}
return toGlobalSettingResponse(setting), nil
}
func (s *SettingsService) GetGlobalSettings(category string) ([]GlobalSettingResponse, error) {
var settings []models.GlobalSetting
q := s.db.Model(&models.GlobalSetting{})
if category != "" {
q = q.Where("category = ?", category)
}
if err := q.Order("category, sort_order, key").Find(&settings).Error; err != nil {
return nil, err
}
result := make([]GlobalSettingResponse, len(settings))
for i, setting := range settings {
result[i] = *toGlobalSettingResponse(setting)
}
return result, nil
}
func (s *SettingsService) GetGlobalSettingByID(id uint) (*GlobalSettingResponse, error) {
var setting models.GlobalSetting
if err := s.db.First(&setting, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("global setting not found")
}
return nil, err
}
return toGlobalSettingResponse(setting), nil
}
func (s *SettingsService) UpdateGlobalSetting(id uint, req UpdateGlobalSettingRequest) (*GlobalSettingResponse, error) {
var setting models.GlobalSetting
if err := s.db.First(&setting, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("global setting not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Category != nil {
updates["category"] = *req.Category
}
if req.Key != nil {
updates["key"] = *req.Key
}
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Value != nil {
valueJSON, err := json.Marshal(req.Value)
if err != nil {
return nil, err
}
updates["value"] = datatypes.JSON(valueJSON)
}
if req.Color != nil {
updates["color"] = *req.Color
}
if req.SortOrder != nil {
updates["sort_order"] = *req.SortOrder
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) > 0 {
if err := s.db.Model(&setting).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&setting, id)
}
return toGlobalSettingResponse(setting), nil
}
func (s *SettingsService) DeleteGlobalSetting(id uint) (*GlobalSettingResponse, error) {
var setting models.GlobalSetting
if err := s.db.First(&setting, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("global setting not found")
}
return nil, err
}
resp := toGlobalSettingResponse(setting)
if err := s.db.Delete(&setting).Error; err != nil {
return nil, err
}
return resp, nil
}
func (s *SettingsService) GetGlobalSettingsByCategory(category string) ([]GlobalSettingResponse, error) {
return s.GetGlobalSettings(category)
}
func (s *SettingsService) CreateUserSetting(userID uint, req CreateUserSettingRequest) (*UserSettingResponse, error) {
var existing models.UserSetting
if err := s.db.Where("user_id = ? AND category = ? AND key = ?", userID, req.Category, req.Key).First(&existing).Error; err == nil {
return nil, fmt.Errorf("setting with same category and key already exists for this user")
}
valueJSON, err := json.Marshal(req.Value)
if err != nil {
return nil, err
}
setting := models.UserSetting{
UserID: userID,
Category: req.Category,
Key: req.Key,
Value: datatypes.JSON(valueJSON),
}
if err := s.db.Create(&setting).Error; err != nil {
return nil, err
}
return toUserSettingResponse(setting), nil
}
func (s *SettingsService) GetUserSettings(userID uint, category string) ([]UserSettingResponse, error) {
var settings []models.UserSetting
q := s.db.Where("user_id = ?", userID)
if category != "" {
q = q.Where("category = ?", category)
}
if err := q.Order("category, key").Find(&settings).Error; err != nil {
return nil, err
}
result := make([]UserSettingResponse, len(settings))
for i, setting := range settings {
result[i] = *toUserSettingResponse(setting)
}
return result, nil
}
func (s *SettingsService) GetUserSettingByID(userID uint, id uint) (*UserSettingResponse, error) {
var setting models.UserSetting
if err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&setting).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("user setting not found")
}
return nil, err
}
return toUserSettingResponse(setting), nil
}
func (s *SettingsService) UpdateUserSetting(userID uint, id uint, req UpdateUserSettingRequest) (*UserSettingResponse, error) {
var setting models.UserSetting
if err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&setting).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("user setting not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Category != nil {
updates["category"] = *req.Category
}
if req.Key != nil {
updates["key"] = *req.Key
}
if req.Value != nil {
valueJSON, err := json.Marshal(req.Value)
if err != nil {
return nil, err
}
updates["value"] = datatypes.JSON(valueJSON)
}
if len(updates) > 0 {
if err := s.db.Model(&setting).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&setting, id)
}
return toUserSettingResponse(setting), nil
}
func (s *SettingsService) DeleteUserSetting(userID uint, id uint) (*UserSettingResponse, error) {
var setting models.UserSetting
if err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&setting).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("user setting not found")
}
return nil, err
}
resp := toUserSettingResponse(setting)
if err := s.db.Delete(&setting).Error; err != nil {
return nil, err
}
return resp, nil
}
func (s *SettingsService) BulkUpdateUserSettings(userID uint, req BulkUpdateUserSettingsRequest) ([]UserSettingResponse, error) {
for key, value := range req.Settings {
valueJSON, _ := json.Marshal(value)
var existing models.UserSetting
err := s.db.Where("user_id = ? AND category = ? AND key = ?", userID, req.Category, key).First(&existing).Error
if err == nil {
s.db.Model(&existing).Update("value", datatypes.JSON(valueJSON))
} else if errors.Is(err, gorm.ErrRecordNotFound) {
setting := models.UserSetting{
UserID: userID,
Category: req.Category,
Key: key,
Value: datatypes.JSON(valueJSON),
}
s.db.Create(&setting)
}
}
return s.GetUserSettings(userID, req.Category)
}
func (s *SettingsService) GetUserSettingsByCategory(userID uint, category string) ([]UserSettingResponse, error) {
return s.GetUserSettings(userID, category)
}
func (s *SettingsService) CreateCategory(req CreateSettingsCategoryRequest) (*CategoryResponse, error) {
var existing models.Category
if err := s.db.Where("name = ?", req.Name).First(&existing).Error; err == nil {
return nil, fmt.Errorf("category with same name already exists")
}
category := models.Category{
Name: req.Name,
Description: req.Description,
Type: req.Type,
}
if err := s.db.Create(&category).Error; err != nil {
return nil, err
}
return toCategoryResponse(category), nil
}
func (s *SettingsService) GetCategories(catType string) ([]CategoryResponse, error) {
var categories []models.Category
q := s.db.Model(&models.Category{})
if catType != "" {
q = q.Where("type = ?", catType)
}
if err := q.Order("name").Find(&categories).Error; err != nil {
return nil, err
}
result := make([]CategoryResponse, len(categories))
for i, c := range categories {
result[i] = *toCategoryResponse(c)
}
return result, nil
}
func (s *SettingsService) GetCategoryByID(id uint) (*CategoryResponse, error) {
var category models.Category
if err := s.db.First(&category, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("category not found")
}
return nil, err
}
return toCategoryResponse(category), nil
}
func (s *SettingsService) UpdateCategory(id uint, req CreateSettingsCategoryRequest) (*CategoryResponse, error) {
var category models.Category
if err := s.db.First(&category, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("category not found")
}
return nil, err
}
updates := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"type": req.Type,
}
if err := s.db.Model(&category).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&category, id)
return toCategoryResponse(category), nil
}
func (s *SettingsService) DeleteCategory(id uint) (*CategoryResponse, error) {
var category models.Category
if err := s.db.First(&category, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("category not found")
}
return nil, err
}
resp := toCategoryResponse(category)
if err := s.db.Delete(&category).Error; err != nil {
return nil, err
}
return resp, nil
}
func toGlobalSettingResponse(s models.GlobalSetting) *GlobalSettingResponse {
isActive := s.IsActive != nil && *s.IsActive
var val interface{}
if len(s.Value) > 0 {
_ = json.Unmarshal([]byte(s.Value), &val)
}
return &GlobalSettingResponse{
ID: s.ID,
Category: s.Category,
Key: s.Key,
Name: s.Name,
Description: s.Description,
Value: val,
Color: s.Color,
SortOrder: s.SortOrder,
IsActive: isActive,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}
func toUserSettingResponse(s models.UserSetting) *UserSettingResponse {
var val interface{}
if len(s.Value) > 0 {
_ = json.Unmarshal([]byte(s.Value), &val)
}
return &UserSettingResponse{
ID: s.ID,
UserID: s.UserID,
Category: s.Category,
Key: s.Key,
Value: val,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}
func toCategoryResponse(c models.Category) *CategoryResponse {
return &CategoryResponse{
ID: c.ID,
Name: c.Name,
Description: c.Description,
Type: c.Type,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
package services
import (
"errors"
"fmt"
"runtime"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/middleware"
"ScoutSystemElite/internal/models"
)
type SuperAdminService struct {
db *gorm.DB
userService *UserService
}
func NewSuperAdminService(db *gorm.DB) *SuperAdminService {
return &SuperAdminService{db: db, userService: NewUserService(db)}
}
type ServerStatusResponse struct {
Status string `json:"status"`
Time string `json:"time"`
GoVersion string `json:"goVersion"`
NumCPU int `json:"numCPU"`
}
type CreateClientModuleRequest struct {
ClientID string `json:"clientId" binding:"required"`
Scouting *bool `json:"scouting"`
DataAnalytics *bool `json:"dataAnalytics"`
Transfers *bool `json:"transfers"`
IsActive *bool `json:"isActive"`
}
type UpdateClientModuleRequest struct {
ClientID *string `json:"clientId"`
Scouting *bool `json:"scouting"`
DataAnalytics *bool `json:"dataAnalytics"`
Transfers *bool `json:"transfers"`
IsActive *bool `json:"isActive"`
}
type ModuleStatus struct {
Enabled bool `json:"enabled"`
AllowedRoles []string `json:"allowedRoles"`
}
type ModulesStatusResponse struct {
Scouting ModuleStatus `json:"scouting"`
DataAnalytics ModuleStatus `json:"dataAnalytics"`
Transfers ModuleStatus `json:"transfers"`
}
type ClientModulesStatusResponse struct {
ClientID string `json:"clientId"`
IsActive bool `json:"isActive"`
Modules ModulesStatusResponse `json:"modules"`
}
func (s *SuperAdminService) GetServerStatus() (*ServerStatusResponse, error) {
return &ServerStatusResponse{
Status: "ok",
Time: time.Now().UTC().Format(time.RFC3339),
GoVersion: runtime.Version(),
NumCPU: runtime.NumCPU(),
}, nil
}
// Users
func (s *SuperAdminService) CreateUser(req CreateUserRequest) (*UserResponse, error) {
return s.userService.Create(req)
}
func (s *SuperAdminService) GetAllUsers() ([]UserResponse, error) {
return s.userService.FindAll(QueryUsersRequest{})
}
func (s *SuperAdminService) GetUserByID(id uint) (*UserResponse, error) {
return s.userService.FindOne(id)
}
func (s *SuperAdminService) UpdateUser(id uint, req UpdateUserRequest) (*UserResponse, error) {
return s.userService.Update(id, req)
}
func (s *SuperAdminService) DeleteUser(id uint) error {
return s.userService.Remove(id)
}
func (s *SuperAdminService) ActivateUser(id uint) (*UserResponse, error) {
return s.userService.Activate(id)
}
func (s *SuperAdminService) DeactivateUser(id uint) (*UserResponse, error) {
return s.userService.Deactivate(id)
}
func (s *SuperAdminService) UnlockAccount(id uint) error {
var user models.User
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("user not found")
}
return err
}
zero := 0
updates := map[string]interface{}{
"failed_login_attempts": &zero,
"locked_until": nil,
}
return s.db.Model(&user).Updates(updates).Error
}
// Client Modules
func (s *SuperAdminService) CreateClientModule(req CreateClientModuleRequest) (*models.ClientModule, error) {
var existing models.ClientModule
if err := s.db.Where("client_id = ?", req.ClientID).First(&existing).Error; err == nil {
return nil, fmt.Errorf("client module configuration already exists")
}
isActive := true
cm := models.ClientModule{
ClientID: req.ClientID,
Scouting: req.Scouting,
DataAnalytics: req.DataAnalytics,
Transfers: req.Transfers,
IsActive: req.IsActive,
}
if cm.IsActive == nil {
cm.IsActive = &isActive
}
if err := s.db.Create(&cm).Error; err != nil {
return nil, err
}
return &cm, nil
}
func (s *SuperAdminService) GetClientModules() ([]models.ClientModule, error) {
var rows []models.ClientModule
if err := s.db.Order("id asc").Find(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (s *SuperAdminService) GetClientModuleByID(id uint) (*models.ClientModule, error) {
var row models.ClientModule
if err := s.db.First(&row, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("client module configuration not found")
}
return nil, err
}
return &row, nil
}
func (s *SuperAdminService) GetClientModuleByClientID(clientID string) (*models.ClientModule, error) {
var row models.ClientModule
if err := s.db.Where("client_id = ?", clientID).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("client module configuration not found")
}
return nil, err
}
return &row, nil
}
func (s *SuperAdminService) UpdateClientModule(id uint, req UpdateClientModuleRequest) (*models.ClientModule, error) {
var row models.ClientModule
if err := s.db.First(&row, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("client module configuration not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.ClientID != nil {
updates["client_id"] = *req.ClientID
}
if req.Scouting != nil {
updates["scouting"] = *req.Scouting
}
if req.DataAnalytics != nil {
updates["data_analytics"] = *req.DataAnalytics
}
if req.Transfers != nil {
updates["transfers"] = *req.Transfers
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) > 0 {
if err := s.db.Model(&row).Updates(updates).Error; err != nil {
return nil, err
}
_ = s.db.First(&row, id).Error
}
return &row, nil
}
func (s *SuperAdminService) DeleteClientModule(id uint) (*models.ClientModule, error) {
var row models.ClientModule
if err := s.db.First(&row, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("client module configuration not found")
}
return nil, err
}
if err := s.db.Delete(&row).Error; err != nil {
return nil, err
}
return &row, nil
}
func allowedRolesForModule(module string) []string {
base := []string{"superadmin", "admin", "groupmanager", "president", "sportsdirector"}
specific := []string{}
switch module {
case "scouting":
specific = []string{"chiefscout", "staffscout"}
case "dataAnalytics":
specific = []string{"chiefdataanalyst", "staffdataanalyst"}
case "transfers":
specific = []string{"chieftransfermarket", "stafftransfermarket"}
default:
specific = middleware.AllRoles
}
outSet := make(map[string]bool)
out := make([]string, 0, len(base)+len(specific))
for _, r := range append(base, specific...) {
if !outSet[r] {
outSet[r] = true
out = append(out, r)
}
}
return out
}
func (s *SuperAdminService) GetModuleStatus() (*ModulesStatusResponse, error) {
return &ModulesStatusResponse{
Scouting: ModuleStatus{Enabled: true, AllowedRoles: allowedRolesForModule("scouting")},
DataAnalytics: ModuleStatus{Enabled: true, AllowedRoles: allowedRolesForModule("dataAnalytics")},
Transfers: ModuleStatus{Enabled: true, AllowedRoles: allowedRolesForModule("transfers")},
}, nil
}
func (s *SuperAdminService) GetClientModuleStatus(clientID string) (*ClientModulesStatusResponse, error) {
row, err := s.GetClientModuleByClientID(clientID)
if err != nil {
return nil, err
}
isActive := row.IsActive != nil && *row.IsActive
scoutingEnabled := row.Scouting != nil && *row.Scouting
dataEnabled := row.DataAnalytics != nil && *row.DataAnalytics
transfersEnabled := row.Transfers != nil && *row.Transfers
return &ClientModulesStatusResponse{
ClientID: clientID,
IsActive: isActive,
Modules: ModulesStatusResponse{
Scouting: ModuleStatus{Enabled: scoutingEnabled, AllowedRoles: allowedRolesForModule("scouting")},
DataAnalytics: ModuleStatus{Enabled: dataEnabled, AllowedRoles: allowedRolesForModule("dataAnalytics")},
Transfers: ModuleStatus{Enabled: transfersEnabled, AllowedRoles: allowedRolesForModule("transfers")},
},
}, nil
}
package services
import (
"errors"
"fmt"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type UserService struct {
db *gorm.DB
}
func NewUserService(db *gorm.DB) *UserService {
return &UserService{db: db}
}
type UserResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
IsActive bool `json:"isActive"`
EmailVerifiedAt *time.Time `json:"emailVerifiedAt,omitempty"`
LastLoginAt *time.Time `json:"lastLoginAt,omitempty"`
TwoFactorEnabled bool `json:"twoFactorEnabled"`
TwoFactorSecret *string `json:"twoFactorSecret,omitempty"`
FailedLoginAttempts int `json:"failedLoginAttempts"`
LockedUntil *time.Time `json:"lockedUntil,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type QueryUsersRequest struct {
Role string `form:"role"`
Name string `form:"name"`
Email string `form:"email"`
IsActive *bool `form:"isActive"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type UpdateUserRequest struct {
Name *string `json:"name"`
Email *string `json:"email"`
Role *string `json:"role"`
IsActive *bool `json:"isActive"`
}
type CreateUserRequest struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Role string `json:"role" binding:"required"`
}
func (s *UserService) FindAll(query QueryUsersRequest) ([]UserResponse, error) {
var users []models.User
q := s.db.Where("deleted_at IS NULL")
if query.Role != "" {
q = q.Where("role = ?", query.Role)
}
if query.Name != "" {
q = q.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(query.Name)+"%")
}
if query.Email != "" {
q = q.Where("LOWER(email) LIKE ?", "%"+strings.ToLower(query.Email)+"%")
}
if query.IsActive != nil {
q = q.Where("is_active = ?", *query.IsActive)
}
sortBy := "name"
if query.SortBy != "" {
allowedSorts := map[string]bool{
"name": true, "email": true, "role": true,
"isActive": true, "createdAt": true, "updatedAt": true, "lastLoginAt": true,
}
if allowedSorts[query.SortBy] {
sortBy = toSnakeCase(query.SortBy)
}
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
if err := q.Order(sortBy + " " + sortOrder).Find(&users).Error; err != nil {
return nil, err
}
result := make([]UserResponse, len(users))
for i, u := range users {
result[i] = toUserResponse(u)
}
return result, nil
}
func (s *UserService) FindOne(id uint) (*UserResponse, error) {
var user models.User
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("user not found")
}
return nil, err
}
resp := toUserResponse(user)
return &resp, nil
}
func (s *UserService) FindByEmail(email string) (*models.User, error) {
var user models.User
if err := s.db.Where("email = ? AND deleted_at IS NULL", email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (s *UserService) Create(req CreateUserRequest) (*UserResponse, error) {
existing, _ := s.FindByEmail(req.Email)
if existing != nil {
return nil, fmt.Errorf("email already exists")
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
isActive := true
user := models.User{
Name: req.Name,
Email: req.Email,
PasswordHash: string(hash),
Role: req.Role,
IsActive: &isActive,
}
if err := s.db.Create(&user).Error; err != nil {
return nil, err
}
resp := toUserResponse(user)
return &resp, nil
}
func (s *UserService) Update(id uint, req UpdateUserRequest) (*UserResponse, error) {
var user models.User
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("user not found")
}
return nil, err
}
if req.Email != nil && *req.Email != user.Email {
existing, _ := s.FindByEmail(*req.Email)
if existing != nil {
return nil, fmt.Errorf("email already exists")
}
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Email != nil {
updates["email"] = *req.Email
}
if req.Role != nil {
updates["role"] = *req.Role
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) > 0 {
if err := s.db.Model(&user).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&user, id)
}
resp := toUserResponse(user)
return &resp, nil
}
func (s *UserService) Remove(id uint) error {
var user models.User
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("user not found")
}
return err
}
return s.db.Delete(&user).Error
}
func (s *UserService) Activate(id uint) (*UserResponse, error) {
isActive := true
return s.Update(id, UpdateUserRequest{IsActive: &isActive})
}
func (s *UserService) Deactivate(id uint) (*UserResponse, error) {
isActive := false
return s.Update(id, UpdateUserRequest{IsActive: &isActive})
}
func toUserResponse(u models.User) UserResponse {
isActive := u.IsActive != nil && *u.IsActive
twoFactorEnabled := u.TwoFactorEnabled != nil && *u.TwoFactorEnabled
failedAttempts := 0
if u.FailedLoginAttempts != nil {
failedAttempts = *u.FailedLoginAttempts
}
return UserResponse{
ID: u.ID,
Name: u.Name,
Email: u.Email,
Role: u.Role,
IsActive: isActive,
EmailVerifiedAt: u.EmailVerifiedAt,
LastLoginAt: u.LastLoginAt,
TwoFactorEnabled: twoFactorEnabled,
TwoFactorSecret: u.TwoFactorSecret,
FailedLoginAttempts: failedAttempts,
LockedUntil: u.LockedUntil,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
func toSnakeCase(s string) string {
switch s {
case "isActive":
return "is_active"
case "createdAt":
return "created_at"
case "updatedAt":
return "updated_at"
case "lastLoginAt":
return "last_login_at"
default:
return s
}
}
import {
pgTable,
text,
integer,
boolean,
timestamp,
serial,
json,
uniqueIndex,
index,
varchar,
decimal,
date,
pgEnum,
} from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
// ============================================================================
// ENUMS
// ============================================================================
export const footEnum = pgEnum('foot', ['left', 'right', 'both']);
export const genderEnum = pgEnum('gender', ['male', 'female', 'other']);
export const statusEnum = pgEnum('status', ['active', 'inactive']);
export const coachPositionEnum = pgEnum('coach_position', [
'head_coach',
'assistant_coach',
'analyst',
]);
export const apiSyncStatusEnum = pgEnum('api_sync_status', [
'pending',
'synced',
'error',
]);
export const matchTypeEnum = pgEnum('match_type', [
'regular',
'friendly',
'playoff',
'cup',
]);
export const matchStatusEnum = pgEnum('match_status', [
'scheduled',
'in_progress',
'finished',
'postponed',
'cancelled',
]);
export const reportStatusEnum = pgEnum('report_status', ['saved', 'finished']);
export const calendarEventTypeEnum = pgEnum('calendar_event_type', [
'match',
'travel',
'player_observation',
'meeting',
'training',
'other',
]);
export const listTypeEnum = pgEnum('list_type', [
'shortlist',
'shadow_team',
'target_list',
]);
export const positionCategoryEnum = pgEnum('position_category', [
'Forward',
'Goalkeeper',
'Defender',
'Midfield',
]);
export const auditLogActionEnum = pgEnum('audit_log_action', [
'create',
'update',
'delete',
'soft_delete',
'restore',
]);
// ============================================================================
// USERS & AUTHENTICATION
// ============================================================================
export const users = pgTable(
'users',
{
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
passwordHash: text('password_hash').notNull(),
role: text('role')
.notNull()
.default('viewer')
.$type<
| 'superadmin'
| 'admin'
| 'groupmanager'
| 'president'
| 'sportsdirector'
| 'chiefscout'
| 'chieftransfermarket'
| 'chiefdataanalyst'
| 'stafftransfermarket'
| 'staffscout'
| 'staffdataanalyst'
| 'viewer'
>(),
isActive: boolean('is_active').default(true),
emailVerifiedAt: timestamp('email_verified_at'),
lastLoginAt: timestamp('last_login_at'),
twoFactorEnabled: boolean('two_factor_enabled').default(false),
twoFactorSecret: text('two_factor_secret'),
failedLoginAttempts: integer('failed_login_attempts').default(0),
lockedUntil: timestamp('locked_until'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
deletedAt: timestamp('deleted_at'),
},
(table) => ({
emailIdx: uniqueIndex('users_email_idx').on(table.email),
roleIdx: index('users_role_idx').on(table.role),
isActiveIdx: index('users_is_active_idx').on(table.isActive),
twoFactorEnabledIdx: index('users_two_factor_enabled_idx').on(
table.twoFactorEnabled,
),
lockedUntilIdx: index('users_locked_until_idx').on(table.lockedUntil),
}),
);
export const userSessions = pgTable(
'user_sessions',
{
id: serial('id').primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
token: text('token').notNull().unique(),
deviceId: text('device_id').notNull(),
deviceInfo: json('device_info').$type<{
userAgent?: string;
ipAddress?: string;
platform?: string;
}>(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
},
(table) => ({
tokenIdx: uniqueIndex('user_sessions_token_idx').on(table.token),
userIdIdx: index('user_sessions_user_id_idx').on(table.userId),
deviceIdIdx: index('user_sessions_device_id_idx').on(table.deviceId),
expiresAtIdx: index('user_sessions_expires_at_idx').on(table.expiresAt),
}),
);
// ============================================================================
// Clones
// ============================================================================
export const players = pgTable(
'players',
{
id: serial('id').primaryKey(),
wyId: integer('wy_id').unique(), // Wyscout external reference - OPTIONAL for non-Wyscout players
// CHANGED: Now using wy_id for foreign key
teamWyId: integer('team_wy_id'),
// Basic player info
firstName: varchar('first_name', { length: 100 }).notNull(),
lastName: varchar('last_name', { length: 100 }).notNull(),
middleName: varchar('middle_name', { length: 100 }),
shortName: varchar('short_name', { length: 100 }),
// API identifiers
gsmId: integer('gsm_id'), // Global Sports Media ID
currentTeamId: integer('current_team_id'), // Current team ID from API
currentNationalTeamId: integer('current_national_team_id'), // National team ID
currentTeamName: varchar('current_team_name', { length: 255 }), // Current team name from API
currentTeamOfficialName: varchar('current_team_official_name', {
length: 255,
}),
currentNationalTeamName: varchar('current_national_team_name', {
length: 255,
}),
currentNationalTeamOfficialName: varchar(
'current_national_team_official_name',
{
length: 255,
},
),
// Physical attributes
dateOfBirth: date('date_of_birth'),
heightCm: integer('height_cm'),
weightKg: decimal('weight_kg', { precision: 5, scale: 2 }),
foot: footEnum('foot'),
gender: genderEnum('gender'),
// Position and role
positionId: integer('position_id').references(() => positions.id, {
onDelete: 'set null',
}),
otherPositionIds: integer('other_position_ids').array(), // Array of position IDs
roleCode2: varchar('role_code2', { length: 10 }), // e.g., 'FW'
roleCode3: varchar('role_code3', { length: 10 }), // e.g., 'FWD'
roleName: varchar('role_name', { length: 50 }), // e.g., 'Forward'
// Foreign keys (NORMALIZED) - CHANGED: Now using wy_id
birthAreaWyId: integer('birth_area_wy_id'),
secondBirthAreaWyId: integer('second_birth_area_wy_id'),
passportAreaWyId: integer('passport_area_wy_id'),
secondPassportAreaWyId: integer('second_passport_area_wy_id'),
// Status and metadata
status: statusEnum('status').default('active'),
imageDataUrl: text('image_data_url'),
jerseyNumber: integer('jersey_number'),
// Contact information
email: varchar('email', { length: 255 }),
phone: varchar('phone', { length: 50 }),
// Transfer and financial information
onLoan: boolean('on_loan').default(false),
ranking: varchar('ranking', { length: 255 }),
roi: varchar('roi', { length: 255 }), // Return on Investment
marketValue: decimal('market_value', { precision: 15, scale: 2 }), // Monetary value
valueRange: varchar('value_range', { length: 100 }), // e.g., "100000-200000"
transferValue: decimal('transfer_value', { precision: 15, scale: 2 }), // Monetary value
salary: decimal('salary', { precision: 15, scale: 2 }), // Monetary value
contractEndsAt: date('contract_ends_at'),
feasible: boolean('feasible').default(false),
// Physical characteristics
morphology: varchar('morphology', { length: 100 }),
// API sync metadata
apiLastSyncedAt: timestamp('api_last_synced_at', { withTimezone: true }),
apiSyncStatus: apiSyncStatusEnum('api_sync_status').default('pending'),
// Standard fields
isActive: boolean('is_active').default(true),
euPassword: boolean('eu_password').default(false),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
teamWyIdIdx: index('idx_players_team_wy_id').on(table.teamWyId),
positionIdIdx: index('idx_players_position_id').on(table.positionId),
deletedAtIdx: index('idx_players_deleted_at').on(table.deletedAt),
isActiveIdx: index('idx_players_is_active').on(table.isActive),
wyIdIdx: index('idx_players_wy_id').on(table.wyId),
gsmIdIdx: index('idx_players_gsm_id').on(table.gsmId),
currentTeamIdIdx: index('idx_players_current_team_id').on(
table.currentTeamId,
),
currentNationalTeamIdIdx: index('idx_players_current_national_team_id').on(
table.currentNationalTeamId,
),
lastNameIdx: index('idx_players_last_name').on(table.lastName),
firstNameIdx: index('idx_players_first_name').on(table.firstName),
birthAreaWyIdIdx: index('idx_players_birth_area_wy_id').on(
table.birthAreaWyId,
),
passportAreaWyIdIdx: index('idx_players_passport_area_wy_id').on(
table.passportAreaWyId,
),
statusIdx: index('idx_players_status').on(table.status),
genderIdx: index('idx_players_gender').on(table.gender),
footIdx: index('idx_players_foot').on(table.foot),
apiSyncStatusIdx: index('idx_players_api_sync_status').on(
table.apiSyncStatus,
),
apiLastSyncedAtIdx: index('idx_players_api_last_synced_at').on(
table.apiLastSyncedAt,
),
}),
);
export const coaches = pgTable(
'coaches',
{
id: serial('id').primaryKey(),
wyId: integer('wy_id').unique().notNull(), // Wyscout external reference - REQUIRED
gsmId: integer('gsm_id'),
firstName: varchar('first_name', { length: 100 }).notNull(),
lastName: varchar('last_name', { length: 100 }).notNull(),
middleName: varchar('middle_name', { length: 100 }),
shortName: varchar('short_name', { length: 100 }),
// Personal details - CHANGED: Now using wy_id
dateOfBirth: date('date_of_birth'),
nationalityWyId: integer('nationality_wy_id'),
// Current position - CHANGED: Now using wy_id
currentTeamWyId: integer('current_team_wy_id'),
position: coachPositionEnum('position').default('head_coach'),
// Career details
coachingLicense: varchar('coaching_license', { length: 100 }),
yearsExperience: integer('years_experience'),
previousTeams: text('previous_teams').array(), // Array of previous team names
// Status and metadata
status: statusEnum('status').default('active'),
imageDataUrl: text('image_data_url'),
// API sync metadata
apiLastSyncedAt: timestamp('api_last_synced_at', { withTimezone: true }),
apiSyncStatus: apiSyncStatusEnum('api_sync_status').default('pending'),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
wyIdIdx: index('idx_coaches_wy_id').on(table.wyId),
gsmIdIdx: index('idx_coaches_gsm_id').on(table.gsmId),
currentTeamWyIdIdx: index('idx_coaches_current_team_wy_id').on(
table.currentTeamWyId,
),
nationalityWyIdIdx: index('idx_coaches_nationality_wy_id').on(
table.nationalityWyId,
),
positionIdx: index('idx_coaches_position').on(table.position),
statusIdx: index('idx_coaches_status').on(table.status),
lastNameIdx: index('idx_coaches_last_name').on(table.lastName),
firstNameIdx: index('idx_coaches_first_name').on(table.firstName),
deletedAtIdx: index('idx_coaches_deleted_at').on(table.deletedAt),
isActiveIdx: index('idx_coaches_is_active').on(table.isActive),
apiSyncStatusIdx: index('idx_coaches_api_sync_status').on(
table.apiSyncStatus,
),
}),
);
export const matches = pgTable(
'matches',
{
id: serial('id').primaryKey(),
wyId: integer('wy_id').unique().notNull(), // Wyscout external reference - REQUIRED
// CHANGED: All foreign keys now use wy_id
homeTeamWyId: integer('home_team_wy_id'),
awayTeamWyId: integer('away_team_wy_id'),
competitionWyId: integer('competition_wy_id'),
seasonWyId: integer('season_wy_id'),
roundWyId: integer('round_wy_id'),
// Match details
matchDate: timestamp('match_date', { withTimezone: true }).notNull(),
venue: varchar('venue', { length: 255 }),
venueCity: varchar('venue_city', { length: 100 }),
venueCountry: varchar('venue_country', { length: 100 }),
// Match type and status
matchType: matchTypeEnum('match_type').default('regular'),
status: matchStatusEnum('status').default('scheduled'),
// Scores
homeScore: integer('home_score').default(0),
awayScore: integer('away_score').default(0),
homeScorePenalties: integer('home_score_penalties').default(0),
awayScorePenalties: integer('away_score_penalties').default(0),
// Match metadata - Individual Referee Info (not linked to referees table)
mainRefereeId: integer('main_referee_id'),
mainRefereeName: text('main_referee_name'),
assistantReferee1Id: integer('assistant_referee_1_id'),
assistantReferee1Name: text('assistant_referee_1_name'),
assistantReferee2Id: integer('assistant_referee_2_id'),
assistantReferee2Name: text('assistant_referee_2_name'),
fourthRefereeId: integer('fourth_referee_id'),
fourthRefereeName: text('fourth_referee_name'),
varRefereeId: integer('var_referee_id'),
varRefereeName: text('var_referee_name'),
weather: varchar('weather', { length: 50 }),
temperature: decimal('temperature', { precision: 5, scale: 2 }),
// API sync metadata
apiLastSyncedAt: timestamp('api_last_synced_at', { withTimezone: true }),
apiSyncStatus: apiSyncStatusEnum('api_sync_status').default('pending'),
notes: text('notes'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
wyIdIdx: index('idx_matches_wy_id').on(table.wyId),
homeTeamWyIdIdx: index('idx_matches_home_team_wy_id').on(
table.homeTeamWyId,
),
awayTeamWyIdIdx: index('idx_matches_away_team_wy_id').on(
table.awayTeamWyId,
),
competitionWyIdIdx: index('idx_matches_competition_wy_id').on(
table.competitionWyId,
),
seasonWyIdIdx: index('idx_matches_season_wy_id').on(table.seasonWyId),
roundWyIdIdx: index('idx_matches_round_wy_id').on(table.roundWyId),
matchDateIdx: index('idx_matches_match_date').on(table.matchDate),
statusIdx: index('idx_matches_status').on(table.status),
matchTypeIdx: index('idx_matches_match_type').on(table.matchType),
mainRefereeIdIdx: index('idx_matches_main_referee_id').on(
table.mainRefereeId,
),
assistantReferee1IdIdx: index('idx_matches_assistant_referee_1_id').on(
table.assistantReferee1Id,
),
assistantReferee2IdIdx: index('idx_matches_assistant_referee_2_id').on(
table.assistantReferee2Id,
),
fourthRefereeIdIdx: index('idx_matches_fourth_referee_id').on(
table.fourthRefereeId,
),
varRefereeIdIdx: index('idx_matches_var_referee_id').on(table.varRefereeId),
deletedAtIdx: index('idx_matches_deleted_at').on(table.deletedAt),
apiSyncStatusIdx: index('idx_matches_api_sync_status').on(
table.apiSyncStatus,
),
}),
);
// ============================================================================
// Agents
// ============================================================================
export const agents = pgTable('agents', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
description: text('description'),
type: text('type').notNull(),
email: text('email').notNull(),
phone: text('phone').notNull(),
status: text('status').notNull(),
address: text('address').notNull(),
countryWyId: integer('country_wy_id')
.notNull()
.references(() => areas.wyId, { onDelete: 'restrict' }),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
});
export const playerAgents = pgTable(
'player_agents',
{
id: serial('id').primaryKey(),
playerId: integer('player_id')
.notNull()
.references(() => players.id, { onDelete: 'cascade' }),
agentId: integer('agent_id')
.notNull()
.references(() => agents.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
playerAgentIdx: uniqueIndex('player_agents_player_id_agent_id_idx').on(
table.playerId,
table.agentId,
),
}),
);
export const coachAgents = pgTable(
'coach_agents',
{
id: serial('id').primaryKey(),
coachId: integer('coach_id')
.notNull()
.references(() => coaches.id, { onDelete: 'cascade' }),
agentId: integer('agent_id')
.notNull()
.references(() => agents.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
coachAgentIdx: uniqueIndex('coach_agents_coach_id_agent_id_idx').on(
table.coachId,
table.agentId,
),
}),
);
// ============================================================================
// Reports
// ============================================================================
export const reports = pgTable('reports', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
// Any of these may be set; all nullable to allow for different report types:
// Using wyId instead of database ID to match the rest of the app
playerWyId: integer('player_wy_id'),
coachWyId: integer('coach_wy_id'),
matchWyId: integer('match_wy_id'),
// Store report details as JSON for flexibility (form, content, structure)
type: text('type').notNull(),
description: json('description'),
grade: text('grade'),
rating: decimal('rating', { precision: 5, scale: 2 }),
decision: text('decision'),
status: reportStatusEnum('status').default('saved'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
userId: integer('user_id').references(() => users.id, {
onDelete: 'cascade',
}),
// App logic should enforce at least one of playerWyId, coachWyId, or matchWyId is set,
// or use additional table(s) for complex relations if needed.
});
// ============================================================================
// Calendar Events
// ============================================================================
export const calendarEvents = pgTable(
'calendar_events',
{
id: serial('id').primaryKey(),
// User relation - each event belongs to a user
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
// Event details
title: text('title').notNull(),
description: text('description'),
eventType: calendarEventTypeEnum('event_type').notNull(),
// Date and time
startDate: timestamp('start_date', { withTimezone: true }).notNull(),
endDate: timestamp('end_date', { withTimezone: true }),
// For match events - store match information
matchWyId: integer('match_wy_id'),
// For player observation events
playerWyId: integer('player_wy_id'),
// Additional metadata stored as JSON for flexibility
metadata: json('metadata').$type<{
location?: string;
venue?: string;
notes?: string;
[key: string]: any;
}>(),
// Standard fields
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
userIdIdx: index('idx_calendar_events_user_id').on(table.userId),
eventTypeIdx: index('idx_calendar_events_event_type').on(table.eventType),
startDateIdx: index('idx_calendar_events_start_date').on(table.startDate),
matchWyIdIdx: index('idx_calendar_events_match_wy_id').on(table.matchWyId),
playerWyIdIdx: index('idx_calendar_events_player_wy_id').on(
table.playerWyId,
),
isActiveIdx: index('idx_calendar_events_is_active').on(table.isActive),
deletedAtIdx: index('idx_calendar_events_deleted_at').on(table.deletedAt),
userIdStartDateIdx: index('idx_calendar_events_user_start_date').on(
table.userId,
table.startDate,
),
}),
);
// ============================================================================
// Lists
// ============================================================================
export const lists = pgTable(
'lists',
{
id: serial('id').primaryKey(),
// User relation - each list belongs to a user (the creator/owner)
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
// List details
name: text('name').notNull(),
season: varchar('season', { length: 50 }).notNull(),
type: listTypeEnum('type').notNull(),
// Players organized by position - stored as JSON
// Structure: { "GK": [playerWyId1, playerWyId2], "DF": [playerWyId3], ... }
playersByPosition: json('players_by_position').$type<{
[position: string]: number[]; // Array of player wyIds
}>(),
// Standard fields
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
userIdIdx: index('idx_lists_user_id').on(table.userId),
typeIdx: index('idx_lists_type').on(table.type),
seasonIdx: index('idx_lists_season').on(table.season),
isActiveIdx: index('idx_lists_is_active').on(table.isActive),
deletedAtIdx: index('idx_lists_deleted_at').on(table.deletedAt),
userIdTypeIdx: index('idx_lists_user_type').on(table.userId, table.type),
}),
);
// ============================================================================
// List Shares - Many-to-many relationship for sharing lists with users
// ============================================================================
export const listShares = pgTable(
'list_shares',
{
id: serial('id').primaryKey(),
listId: integer('list_id')
.notNull()
.references(() => lists.id, { onDelete: 'cascade' }),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
// Standard fields
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
},
(table) => ({
listIdIdx: index('idx_list_shares_list_id').on(table.listId),
userIdIdx: index('idx_list_shares_user_id').on(table.userId),
listUserIdIdx: uniqueIndex('idx_list_shares_list_user').on(
table.listId,
table.userId,
), // Prevent duplicate shares
}),
);
// ============================================================================
// Files & Images
// ============================================================================
export const files = pgTable(
'files',
{
id: serial('id').primaryKey(),
// File metadata
fileName: text('file_name').notNull(),
originalFileName: text('original_file_name').notNull(),
filePath: text('file_path').notNull(), // Storage path/URL
mimeType: text('mime_type').notNull(),
fileSize: integer('file_size').notNull(), // Size in bytes
// Polymorphic relationship - can be related to any entity
entityType: text('entity_type').notNull(), // 'player', 'coach', 'match', 'report', 'user', etc.
entityId: integer('entity_id'), // Can be database ID or wyId depending on entity type
// Optional: For entities that use wyId, we can also store it explicitly
entityWyId: integer('entity_wy_id'), // For player, coach, match that use wyId
// File categorization
category: text('category'), // e.g., 'profile_image', 'document', 'attachment', 'evidence'
description: text('description'),
// Metadata
uploadedBy: integer('uploaded_by').references(() => users.id, {
onDelete: 'set null',
}),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
entityTypeIdx: index('idx_files_entity_type').on(table.entityType),
entityIdIdx: index('idx_files_entity_id').on(table.entityId),
entityWyIdIdx: index('idx_files_entity_wy_id').on(table.entityWyId),
entityTypeIdIdx: index('idx_files_entity_type_id').on(
table.entityType,
table.entityId,
),
uploadedByIdx: index('idx_files_uploaded_by').on(table.uploadedBy),
categoryIdx: index('idx_files_category').on(table.category),
isActiveIdx: index('idx_files_is_active').on(table.isActive),
deletedAtIdx: index('idx_files_deleted_at').on(table.deletedAt),
}),
);
// ============================================================================
// AREAS
// ============================================================================
export const areas = pgTable(
'areas',
{
id: serial('id').primaryKey(),
wyId: integer('wy_id').unique().notNull(), // Wyscout external reference - REQUIRED
name: varchar('name', { length: 255 }).notNull(),
alpha2code: varchar('alpha2code', { length: 2 }),
alpha3code: varchar('alpha3code', { length: 3 }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
wyIdIdx: index('idx_areas_wy_id').on(table.wyId),
alpha2codeIdx: index('idx_areas_alpha2code').on(table.alpha2code),
alpha3codeIdx: index('idx_areas_alpha3code').on(table.alpha3code),
deletedAtIdx: index('idx_areas_deleted_at').on(table.deletedAt),
}),
);
// ============================================================================
// POSITIONS
// ============================================================================
export const positions = pgTable(
'positions',
{
id: serial('id').primaryKey(),
// Position identifiers
name: varchar('name', { length: 100 }).notNull(), // "Goalkeeper"
code2: varchar('code2', { length: 10 }), // "GK"
code3: varchar('code3', { length: 10 }), // "GKP"
// Position metadata
order: integer('order').default(0),
locationX: integer('location_x'),
locationY: integer('location_y'),
bgColor: varchar('bg_color', { length: 7 }), // "#C14B50"
textColor: varchar('text_color', { length: 7 }), // "#000000"
// Category: One of the 4 main position categories
// - Forward: Forward positions (e.g., "Center Forward", "Right Winger", "Left Winger")
// - Goalkeeper: Goalkeeper positions (e.g., "Goalkeeper")
// - Defender: Defender positions (e.g., "Center Defender", "Right Defender", "Left Defender")
// - Midfield: Midfield positions (e.g., "Center Midfielder", "Right Midfielder", "Left Midfielder")
category: positionCategoryEnum('category').notNull(),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
// Unique constraints on codes for fast lookups
code2Idx: uniqueIndex('positions_code2_unique').on(table.code2),
code3Idx: uniqueIndex('positions_code3_unique').on(table.code3),
// Indexes for common queries
nameIdx: index('positions_name_idx').on(table.name),
categoryIdx: index('positions_category_idx').on(table.category),
isActiveIdx: index('positions_is_active_idx').on(table.isActive),
// Composite index for active positions by category
categoryActiveIdx: index('positions_category_active_idx').on(
table.category,
table.isActive,
),
deletedAtIdx: index('positions_deleted_at_idx').on(table.deletedAt),
}),
);
// ============================================================================
// SETTINGS
// ============================================================================
export const categories = pgTable(
'categories',
{
id: serial('id').primaryKey(),
name: text('name').notNull().unique(),
description: text('description'),
type: text('type').notNull().$type<'global' | 'user'>(),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
nameIdx: uniqueIndex('categories_name_idx').on(table.name),
typeIdx: index('categories_type_idx').on(table.type),
isActiveIdx: index('categories_is_active_idx').on(table.isActive),
}),
);
export const globalSettings = pgTable(
'global_settings',
{
id: serial('id').primaryKey(),
category: text('category').notNull(), // 'grades', 'formations', 'positions', 'seasons', 'labels'
key: text('key').notNull(),
name: text('name').notNull(),
description: text('description'),
value: json('value').notNull(),
color: text('color'), // For grades and other color-coded settings
isActive: boolean('is_active').default(true),
sortOrder: integer('sort_order').default(0),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
categoryIdx: index('global_settings_category_idx').on(table.category),
keyIdx: index('global_settings_key_idx').on(table.key),
isActiveIdx: index('global_settings_is_active_idx').on(table.isActive),
sortOrderIdx: index('global_settings_sort_order_idx').on(table.sortOrder),
}),
);
export const userSettings = pgTable(
'user_settings',
{
id: serial('id').primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
category: text('category').notNull(), // 'dashboard', 'player_page', 'reports', etc.
key: text('key').notNull(),
value: json('value').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
userIdIdx: index('user_settings_user_id_idx').on(table.userId),
categoryIdx: index('user_settings_category_idx').on(table.category),
keyIdx: index('user_settings_key_idx').on(table.key),
userCategoryKeyIdx: uniqueIndex('user_settings_user_category_key_idx').on(
table.userId,
table.category,
table.key,
),
}),
);
// ============================================================================
// FEATURE FLAGS & CLIENT SUBSCRIPTIONS
// ============================================================================
export const clientSubscriptions = pgTable(
'client_subscriptions',
{
id: serial('id').primaryKey(),
clientId: text('client_id').notNull().unique(),
features: json('features').notNull().$type<{
data: boolean;
scouting: boolean;
transfer: boolean;
auth: boolean;
}>(),
expiresAt: timestamp('expires_at'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
clientIdIdx: uniqueIndex('client_subscriptions_client_id_idx').on(
table.clientId,
),
expiresAtIdx: index('client_subscriptions_expires_at_idx').on(
table.expiresAt,
),
}),
);
export const clientModules = pgTable(
'client_modules',
{
id: serial('id').primaryKey(),
clientId: text('client_id').notNull().unique().default('default-client'),
scouting: boolean('scouting').default(false),
dataAnalytics: boolean('data_analytics').default(false),
transfers: boolean('transfers').default(false),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
clientIdIdx: uniqueIndex('client_modules_client_id_idx').on(table.clientId),
isActiveIdx: index('client_modules_is_active_idx').on(table.isActive),
}),
);
// ============================================================================
// PLAYER FEATURES
// ============================================================================
export const playerFeatureCategories = pgTable(
'player_feature_categories',
{
id: serial('id').primaryKey(),
name: varchar('name', { length: 100 }).notNull().unique(), // e.g., "Físicas", "Técnicas", "Tácticas", "Mentales"
description: text('description'),
order: integer('order').default(0),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
nameIdx: uniqueIndex('player_feature_categories_name_idx').on(table.name),
orderIdx: index('player_feature_categories_order_idx').on(table.order),
isActiveIdx: index('player_feature_categories_is_active_idx').on(
table.isActive,
),
deletedAtIdx: index('player_feature_categories_deleted_at_idx').on(
table.deletedAt,
),
}),
);
export const playerFeatureTypes = pgTable(
'player_feature_types',
{
id: serial('id').primaryKey(),
categoryId: integer('category_id')
.notNull()
.references(() => playerFeatureCategories.id, { onDelete: 'cascade' }),
name: varchar('name', { length: 100 }).notNull(), // e.g., "Agilidad", "Agresividad", "Cambio de Ritmo"
description: text('description'),
order: integer('order').default(0),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
categoryIdIdx: index('player_feature_types_category_id_idx').on(
table.categoryId,
),
nameIdx: index('player_feature_types_name_idx').on(table.name),
orderIdx: index('player_feature_types_order_idx').on(table.order),
isActiveIdx: index('player_feature_types_is_active_idx').on(table.isActive),
deletedAtIdx: index('player_feature_types_deleted_at_idx').on(
table.deletedAt,
),
categoryOrderIdx: index('player_feature_types_category_order_idx').on(
table.categoryId,
table.order,
),
}),
);
export const playerFeatureRatings = pgTable(
'player_feature_ratings',
{
id: serial('id').primaryKey(),
playerId: integer('player_id')
.notNull()
.references(() => players.id, { onDelete: 'cascade' }),
featureTypeId: integer('feature_type_id')
.notNull()
.references(() => playerFeatureTypes.id, { onDelete: 'cascade' }),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
rating: integer('rating').$type<1 | 2 | 3 | 4 | 5 | null>(), // Rating scale 1-5, optional
notes: text('notes'), // Optional scout notes
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
playerIdIdx: index('player_feature_ratings_player_id_idx').on(
table.playerId,
),
featureTypeIdIdx: index('player_feature_ratings_feature_type_id_idx').on(
table.featureTypeId,
),
userIdIdx: index('player_feature_ratings_user_id_idx').on(table.userId),
ratingIdx: index('player_feature_ratings_rating_idx').on(table.rating),
deletedAtIdx: index('player_feature_ratings_deleted_at_idx').on(
table.deletedAt,
),
// Unique constraint: one rating per scout per feature per player
playerFeatureUserIdx: uniqueIndex(
'player_feature_ratings_player_feature_user_idx',
).on(table.playerId, table.featureTypeId, table.userId),
// Composite indexes for common queries
playerFeatureIdx: index('player_feature_ratings_player_feature_idx').on(
table.playerId,
table.featureTypeId,
),
playerUserIdx: index('player_feature_ratings_player_user_idx').on(
table.playerId,
table.userId,
),
}),
);
export const playerFeatureSelections = pgTable(
'player_feature_selections',
{
id: serial('id').primaryKey(),
playerId: integer('player_id')
.notNull()
.references(() => players.id, { onDelete: 'cascade' }),
featureTypeId: integer('feature_type_id')
.notNull()
.references(() => playerFeatureTypes.id, { onDelete: 'cascade' }),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
notes: text('notes'), // Optional scout notes
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
playerIdIdx: index('player_feature_selections_player_id_idx').on(
table.playerId,
),
featureTypeIdIdx: index('player_feature_selections_feature_type_id_idx').on(
table.featureTypeId,
),
userIdIdx: index('player_feature_selections_user_id_idx').on(table.userId),
deletedAtIdx: index('player_feature_selections_deleted_at_idx').on(
table.deletedAt,
),
// Unique constraint: one selection per scout per feature per player
playerFeatureUserIdx: uniqueIndex(
'player_feature_selections_player_feature_user_idx',
).on(table.playerId, table.featureTypeId, table.userId),
// Composite indexes for common queries
playerFeatureIdx: index('player_feature_selections_player_feature_idx').on(
table.playerId,
table.featureTypeId,
),
playerUserIdx: index('player_feature_selections_player_user_idx').on(
table.playerId,
table.userId,
),
}),
);
// ============================================================================
// PROFILE DESCRIPTIONS & LINKS
// ============================================================================
export const profileDescriptions = pgTable(
'profile_descriptions',
{
id: serial('id').primaryKey(),
playerId: integer('player_id').references(() => players.id, {
onDelete: 'cascade',
}),
coachId: integer('coach_id').references(() => coaches.id, {
onDelete: 'cascade',
}),
description: text('description').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
playerIdIdx: index('profile_descriptions_player_id_idx').on(table.playerId),
coachIdIdx: index('profile_descriptions_coach_id_idx').on(table.coachId),
deletedAtIdx: index('profile_descriptions_deleted_at_idx').on(
table.deletedAt,
),
}),
);
export const profileLinks = pgTable(
'profile_links',
{
id: serial('id').primaryKey(),
playerId: integer('player_id').references(() => players.id, {
onDelete: 'cascade',
}),
coachId: integer('coach_id').references(() => coaches.id, {
onDelete: 'cascade',
}),
title: varchar('title', { length: 255 }).notNull(), // e.g., "YouTube", "Instagram", "Portfolio"
url: text('url').notNull(),
icon: varchar('icon', { length: 255 }),
order: integer('order').default(0),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
playerIdIdx: index('profile_links_player_id_idx').on(table.playerId),
coachIdIdx: index('profile_links_coach_id_idx').on(table.coachId),
isActiveIdx: index('profile_links_is_active_idx').on(table.isActive),
deletedAtIdx: index('profile_links_deleted_at_idx').on(table.deletedAt),
}),
);
// ============================================================================
// AUDIT LOGS
// ============================================================================
export const auditLogs = pgTable(
'audit_logs',
{
id: serial('id').primaryKey(),
// User who made the change
userId: integer('user_id').references(() => users.id, {
onDelete: 'set null',
}),
// Action type
action: auditLogActionEnum('action').notNull(),
// Entity information
entityType: text('entity_type').notNull(), // e.g., 'players', 'coaches', 'matches', 'reports', etc.
entityId: integer('entity_id'), // The ID of the changed record
entityWyId: integer('entity_wy_id'), // For entities that use wyId (players, coaches, matches, areas)
// Change data
oldValues: json('old_values').$type<Record<string, any>>(), // Previous state of the record
newValues: json('new_values').$type<Record<string, any>>(), // New state of the record
changes: json('changes').$type<Record<string, any>>(), // Only the fields that changed (diff)
// Request metadata
ipAddress: varchar('ip_address', { length: 45 }), // IPv4 or IPv6
userAgent: text('user_agent'),
// Additional metadata
metadata: json('metadata').$type<{
endpoint?: string;
method?: string;
requestId?: string;
[key: string]: any;
}>(),
// Timestamp
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => ({
userIdIdx: index('idx_audit_logs_user_id').on(table.userId),
actionIdx: index('idx_audit_logs_action').on(table.action),
entityTypeIdx: index('idx_audit_logs_entity_type').on(table.entityType),
entityIdIdx: index('idx_audit_logs_entity_id').on(table.entityId),
entityWyIdIdx: index('idx_audit_logs_entity_wy_id').on(table.entityWyId),
createdAtIdx: index('idx_audit_logs_created_at').on(table.createdAt),
// Composite indexes for common queries
entityTypeIdIdx: index('idx_audit_logs_entity_type_id').on(
table.entityType,
table.entityId,
),
entityTypeWyIdIdx: index('idx_audit_logs_entity_type_wy_id').on(
table.entityType,
table.entityWyId,
),
userIdCreatedAtIdx: index('idx_audit_logs_user_created_at').on(
table.userId,
table.createdAt,
),
}),
);
// ============================================================================
// ZOD SCHEMAS FOR VALIDATION
// ============================================================================
// Users
export const insertUserSchema = createInsertSchema(users);
export const selectUserSchema = createSelectSchema(users);
export const insertUserSessionSchema = createInsertSchema(userSessions);
export const selectUserSessionSchema = createSelectSchema(userSessions);
// Categories
export const insertCategorySchema = createInsertSchema(categories);
export const selectCategorySchema = createSelectSchema(categories);
// Settings
export const insertGlobalSettingSchema = createInsertSchema(globalSettings);
export const selectGlobalSettingSchema = createSelectSchema(globalSettings);
export const insertUserSettingSchema = createInsertSchema(userSettings);
export const selectUserSettingSchema = createSelectSchema(userSettings);
// Feature Flags
export const insertClientSubscriptionSchema =
createInsertSchema(clientSubscriptions);
export const selectClientSubscriptionSchema =
createSelectSchema(clientSubscriptions);
// Client Modules
export const insertClientModuleSchema = createInsertSchema(clientModules);
export const selectClientModuleSchema = createSelectSchema(clientModules);
// Reports
export const insertReportSchema = createInsertSchema(reports);
export const selectReportSchema = createSelectSchema(reports);
// Files
export const insertFileSchema = createInsertSchema(files);
export const selectFileSchema = createSelectSchema(files);
// Calendar Events
export const insertCalendarEventSchema = createInsertSchema(calendarEvents);
export const selectCalendarEventSchema = createSelectSchema(calendarEvents);
// Lists
export const insertListSchema = createInsertSchema(lists);
export const selectListSchema = createSelectSchema(lists);
export const insertListShareSchema = createInsertSchema(listShares);
export const selectListShareSchema = createSelectSchema(listShares);
// Areas
export const insertAreaSchema = createInsertSchema(areas);
export const selectAreaSchema = createSelectSchema(areas);
// Positions
export const insertPositionSchema = createInsertSchema(positions);
export const selectPositionSchema = createSelectSchema(positions);
// Player Features
export const insertPlayerFeatureCategorySchema = createInsertSchema(
playerFeatureCategories,
);
export const selectPlayerFeatureCategorySchema = createSelectSchema(
playerFeatureCategories,
);
export const insertPlayerFeatureTypeSchema =
createInsertSchema(playerFeatureTypes);
export const selectPlayerFeatureTypeSchema =
createSelectSchema(playerFeatureTypes);
export const insertPlayerFeatureRatingSchema =
createInsertSchema(playerFeatureRatings);
export const selectPlayerFeatureRatingSchema =
createSelectSchema(playerFeatureRatings);
export const insertPlayerFeatureSelectionSchema = createInsertSchema(
playerFeatureSelections,
);
export const selectPlayerFeatureSelectionSchema = createSelectSchema(
playerFeatureSelections,
);
// Profile Descriptions & Links
export const insertProfileDescriptionSchema =
createInsertSchema(profileDescriptions);
export const selectProfileDescriptionSchema =
createSelectSchema(profileDescriptions);
export const insertProfileLinkSchema = createInsertSchema(profileLinks);
export const selectProfileLinkSchema = createSelectSchema(profileLinks);
// Audit Logs
export const insertAuditLogSchema = createInsertSchema(auditLogs);
export const selectAuditLogSchema = createSelectSchema(auditLogs);
// ============================================================================
// TYPE EXPORTS
// ============================================================================
// Users
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type UserSession = typeof userSessions.$inferSelect;
export type NewUserSession = typeof userSessions.$inferInsert;
// Clones
export type Player = typeof players.$inferSelect;
export type NewPlayer = typeof players.$inferInsert;
export type Coach = typeof coaches.$inferSelect;
export type NewCoach = typeof coaches.$inferInsert;
export type Match = typeof matches.$inferSelect;
export type NewMatch = typeof matches.$inferInsert;
export type Agent = typeof agents.$inferSelect;
export type NewAgent = typeof agents.$inferInsert;
export type PlayerAgent = typeof playerAgents.$inferSelect;
export type NewPlayerAgent = typeof playerAgents.$inferInsert;
export type CoachAgent = typeof coachAgents.$inferSelect;
export type NewCoachAgent = typeof coachAgents.$inferInsert;
// Categories
export type Category = typeof categories.$inferSelect;
export type NewCategory = typeof categories.$inferInsert;
// Settings
export type GlobalSetting = typeof globalSettings.$inferSelect;
export type NewGlobalSetting = typeof globalSettings.$inferInsert;
export type UserSetting = typeof userSettings.$inferSelect;
export type NewUserSetting = typeof userSettings.$inferInsert;
// Feature Flags
export type ClientSubscriptionRecord = typeof clientSubscriptions.$inferSelect;
export type NewClientSubscriptionRecord =
typeof clientSubscriptions.$inferInsert;
// Client Modules
export type ClientModule = typeof clientModules.$inferSelect;
export type NewClientModule = typeof clientModules.$inferInsert;
// Reports
export type Report = typeof reports.$inferSelect;
export type NewReport = typeof reports.$inferInsert;
// Files
export type File = typeof files.$inferSelect;
export type NewFile = typeof files.$inferInsert;
// Calendar Events
export type CalendarEvent = typeof calendarEvents.$inferSelect;
export type NewCalendarEvent = typeof calendarEvents.$inferInsert;
// Lists
export type List = typeof lists.$inferSelect;
export type NewList = typeof lists.$inferInsert;
export type ListShare = typeof listShares.$inferSelect;
export type NewListShare = typeof listShares.$inferInsert;
// Areas
export type Area = typeof areas.$inferSelect;
export type NewArea = typeof areas.$inferInsert;
// Positions
export type Position = typeof positions.$inferSelect;
export type NewPosition = typeof positions.$inferInsert;
// Player Features
export type PlayerFeatureCategory = typeof playerFeatureCategories.$inferSelect;
export type NewPlayerFeatureCategory =
typeof playerFeatureCategories.$inferInsert;
export type PlayerFeatureType = typeof playerFeatureTypes.$inferSelect;
export type NewPlayerFeatureType = typeof playerFeatureTypes.$inferInsert;
export type PlayerFeatureRating = typeof playerFeatureRatings.$inferSelect;
export type NewPlayerFeatureRating = typeof playerFeatureRatings.$inferInsert;
export type PlayerFeatureSelection =
typeof playerFeatureSelections.$inferSelect;
export type NewPlayerFeatureSelection =
typeof playerFeatureSelections.$inferInsert;
// Profile Descriptions & Links
export type ProfileDescription = typeof profileDescriptions.$inferSelect;
export type NewProfileDescription = typeof profileDescriptions.$inferInsert;
export type ProfileLink = typeof profileLinks.$inferSelect;
export type NewProfileLink = typeof profileLinks.$inferInsert;
// Audit Logs
export type AuditLog = typeof auditLogs.$inferSelect;
export type NewAuditLog = typeof auditLogs.$inferInsert;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment