commit eacfacb13b62d7f232a5aa551fced5565b02a5fe Author: MEHMETKECECI Date: Wed Feb 11 17:46:22 2026 +0300 ilk diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..98cd9e7 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +backend \ No newline at end of file diff --git a/.idea/backend.iml b/.idea/backend.iml new file mode 100644 index 0000000..7ee078d --- /dev/null +++ b/.idea/backend.iml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/bssapp-backend.iml b/.idea/bssapp-backend.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/bssapp-backend.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml new file mode 100644 index 0000000..d7202f0 --- /dev/null +++ b/.idea/go.imports.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7675e2a --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e92123c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/svc/.env b/svc/.env new file mode 100644 index 0000000..d29fb64 --- /dev/null +++ b/svc/.env @@ -0,0 +1,8 @@ +JWT_SECRET=bssapp_super_secret_key_1234567890 +PASSWORD_RESET_SECRET=1dc7d6d52fd0459a8b1f288a6590428e760f54339f8e47beb20db36b6df6070b +APP_FRONTEND_URL=http://localhost:9000 +API_URL=http://localhost:8080 + + + + diff --git a/svc/auth/claims.go b/svc/auth/claims.go new file mode 100644 index 0000000..12859e5 --- /dev/null +++ b/svc/auth/claims.go @@ -0,0 +1,47 @@ +package auth + +import ( + "strings" + + "github.com/golang-jwt/jwt/v5" +) + +type Claims struct { + // ================================================== + // 🔑 IDENTITY + // ================================================== + ID int64 `json:"id"` + Username string `json:"username"` + RoleID int64 `json:"role_id"` + + RoleCode string `json:"role_code"` + DepartmentCodes []string `json:"department_codes"` + + // ================================================== + // 🧾 NEBIM (frontend filtre & backend guard için) + // ================================================== + V3Username string `json:"v3_username"` + V3UserGroup string `json:"v3_usergroup"` + + // ================================================== + // 🔐 SESSION + // ================================================== + SessionID string `json:"session_id"` + + // ================================================== + // ⚠️ SECURITY + // ================================================== + ForcePasswordChange bool `json:"force_password_change"` + + jwt.RegisteredClaims +} + +func (c *Claims) IsAdmin() bool { + if c == nil { + return false + } + + role := strings.ToLower(strings.TrimSpace(c.RoleCode)) + + return role == "admin" +} diff --git a/svc/auth/claims_mapper.go b/svc/auth/claims_mapper.go new file mode 100644 index 0000000..fdb1d83 --- /dev/null +++ b/svc/auth/claims_mapper.go @@ -0,0 +1,36 @@ +package auth + +import ( + "time" + + "bssapp-backend/models" + + "github.com/golang-jwt/jwt/v5" +) + +func BuildClaimsFromUser(u *models.MkUser, ttl time.Duration) Claims { + now := time.Now() + + return Claims{ + // 🔴 mk_dfusr.id + ID: u.ID, + + Username: u.Username, + RoleCode: u.RoleCode, + RoleID: u.RoleID, + // ✅ BURASI + DepartmentCodes: u.DepartmentCodes, + + SessionID: u.SessionID, + + ForcePasswordChange: u.ForcePasswordChange, + + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: "bssapp", + Subject: u.Username, + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(ttl)), + }, + } +} diff --git a/svc/auth/context.go b/svc/auth/context.go new file mode 100644 index 0000000..d407d33 --- /dev/null +++ b/svc/auth/context.go @@ -0,0 +1,15 @@ +package auth + +import ( + "bssapp-backend/ctxkeys" + "context" +) + +func WithClaims(ctx context.Context, claims *Claims) context.Context { + return context.WithValue(ctx, ctxkeys.UserContextKey, claims) +} + +func GetClaimsFromContext(ctx context.Context) (*Claims, bool) { + claims, ok := ctx.Value(ctxkeys.UserContextKey).(*Claims) + return claims, ok +} diff --git a/svc/auth/jwt.go b/svc/auth/jwt.go new file mode 100644 index 0000000..afaa5aa --- /dev/null +++ b/svc/auth/jwt.go @@ -0,0 +1,53 @@ +package auth + +import ( + "errors" + "os" + + "github.com/golang-jwt/jwt/v5" +) + +// package auth + +func jwtSecret() ([]byte, error) { + sec := os.Getenv("JWT_SECRET") + if len(sec) < 10 { + return nil, errors.New("JWT_SECRET environment boş veya çok kısa") + } + return []byte(sec), nil +} + +// ✅ TEK VE DOĞRU TOKEN ÜRETİCİ +func GenerateToken(claims Claims, username string, change bool) (string, error) { + secret, err := jwtSecret() + if err != nil { + return "", err + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(secret) +} + +func ValidateToken(tokenStr string) (*Claims, error) { + secret, err := jwtSecret() + if err != nil { + return nil, err + } + + token, err := jwt.ParseWithClaims( + tokenStr, + &Claims{}, + func(token *jwt.Token) (interface{}, error) { + return secret, nil + }, + ) + if err != nil { + return nil, err + } + + claims, ok := token.Claims.(*Claims) + if !ok || !token.Valid { + return nil, errors.New("token geçersiz") + } + return claims, nil +} diff --git a/svc/auth/logout.go b/svc/auth/logout.go new file mode 100644 index 0000000..c37d427 --- /dev/null +++ b/svc/auth/logout.go @@ -0,0 +1,44 @@ +package auth + +import ( + "bssapp-backend/internal/auditlog" + "bssapp-backend/repository" + "database/sql" + "encoding/json" + "net/http" + "time" +) + +func LogoutAllHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + claims, ok := GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + userID := claims.ID + + _ = repository.NewRefreshTokenRepository(db). + RevokeAllForUser(userID) + + http.SetCookie(w, &http.Cookie{ + Name: "mk_refresh", + Value: "", + Path: "/", + Expires: time.Unix(0, 0), + HttpOnly: true, + }) + + auditlog.Write(auditlog.ActivityLog{ + UserID: auditlog.IntUserIDToUUID(int(userID)), + ActionType: "logout_all", + ActionCategory: "auth", + Description: "user logged out from all devices", + IsSuccess: true, + }) + + _ = json.NewEncoder(w).Encode(map[string]bool{"success": true}) + } +} diff --git a/svc/config/config.go b/svc/config/config.go new file mode 100644 index 0000000..d912156 --- /dev/null +++ b/svc/config/config.go @@ -0,0 +1 @@ +package config diff --git a/svc/ctxkeys/keys.go b/svc/ctxkeys/keys.go new file mode 100644 index 0000000..2ebfdd1 --- /dev/null +++ b/svc/ctxkeys/keys.go @@ -0,0 +1,5 @@ +package ctxkeys + +type ContextKey string + +const UserContextKey ContextKey = "jwt_claims" diff --git a/svc/db/mssql.go b/svc/db/mssql.go new file mode 100644 index 0000000..1917147 --- /dev/null +++ b/svc/db/mssql.go @@ -0,0 +1,31 @@ +package db + +import ( + "database/sql" + "fmt" + "log" + + _ "github.com/microsoft/go-mssqldb" +) + +var MssqlDB *sql.DB + +func ConnectMSSQL() { + connString := "sqlserver://sa:Gil_0150@10.0.0.9:1433?databaseName=BAGGI_V3" + + var err error + MssqlDB, err = sql.Open("sqlserver", connString) + if err != nil { + log.Fatal("MSSQL bağlantı hatası:", err) + } + + err = MssqlDB.Ping() + if err != nil { + log.Fatal("MSSQL erişilemiyor:", err) + } + + fmt.Println("✅ MSSQL bağlantısı başarılı!") +} +func GetDB() *sql.DB { + return MssqlDB +} diff --git a/svc/db/postgres.go b/svc/db/postgres.go new file mode 100644 index 0000000..b24cfa8 --- /dev/null +++ b/svc/db/postgres.go @@ -0,0 +1,68 @@ +package db + +import ( + "database/sql" + "fmt" + "log" + "os" + "time" + + _ "github.com/lib/pq" +) + +var PgDB *sql.DB + +// ConnectPostgres → PostgreSQL veritabanına bağlanır +func ConnectPostgres() (*sql.DB, error) { + // Bağlantı stringi (istersen .env’den oku) + connStr := os.Getenv("POSTGRES_CONN") + if connStr == "" { + // fallback → sabit tanımlı bağlantı + connStr = "host=172.16.0.3 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable" + } + + db, err := sql.Open("postgres", connStr) + if err != nil { + return nil, fmt.Errorf("PostgreSQL bağlantı hatası: %w", err) + } + + // ======================================================= + // 🔹 BAĞLANTI HAVUZU (AUDIT LOG UYUMLU) + // ======================================================= + db.SetMaxOpenConns(30) // audit + api paralel çalışsın + db.SetMaxIdleConns(10) + db.SetConnMaxLifetime(30 * time.Minute) + db.SetConnMaxIdleTime(5 * time.Minute) // 🔥 uzun idle audit bağlantılarını kapat + + // 🔹 Test et + if err = db.Ping(); err != nil { + return nil, fmt.Errorf("PostgreSQL erişilemiyor: %w", err) + } + + log.Println("✅ PostgreSQL bağlantısı başarılı!") + PgDB = db + return db, nil + +} + +// GetPostgresUsers → test amaçlı ilk 5 kullanıcıyı listeler +func GetPostgresUsers(db *sql.DB) error { + query := `SELECT id, code, email FROM mk_dfusr ORDER BY id LIMIT 5` + rows, err := db.Query(query) + if err != nil { + return fmt.Errorf("PostgreSQL sorgu hatası: %w", err) + } + defer rows.Close() + + fmt.Println("📋 İlk 5 PostgreSQL kullanıcısı:") + for rows.Next() { + var id int + var code, email string + if err := rows.Scan(&id, &code, &email); err != nil { + return err + } + fmt.Printf(" ➜ ID: %-4d | USER: %-20s | EMAIL: %s\n", id, code, email) + } + + return rows.Err() +} diff --git a/svc/deneme b/svc/deneme new file mode 100644 index 0000000..e69de29 diff --git a/svc/fonts/DejaVuSans-Bold.ttf b/svc/fonts/DejaVuSans-Bold.ttf new file mode 100644 index 0000000..6d65fa7 Binary files /dev/null and b/svc/fonts/DejaVuSans-Bold.ttf differ diff --git a/svc/fonts/DejaVuSans.ttf b/svc/fonts/DejaVuSans.ttf new file mode 100644 index 0000000..e5f7eec Binary files /dev/null and b/svc/fonts/DejaVuSans.ttf differ diff --git a/svc/fonts/FreeSans.ttf b/svc/fonts/FreeSans.ttf new file mode 100644 index 0000000..e69de29 diff --git a/svc/fonts/FreeSansBold.ttf b/svc/fonts/FreeSansBold.ttf new file mode 100644 index 0000000..e69de29 diff --git a/svc/go.mod b/svc/go.mod new file mode 100644 index 0000000..8273984 --- /dev/null +++ b/svc/go.mod @@ -0,0 +1,30 @@ +module bssapp-backend + +go 1.24.0 // senin Go versiyonuna göre değişir + +toolchain go1.24.5 + +require ( + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 + github.com/joho/godotenv v1.5.1 + github.com/jung-kurt/gofpdf v1.16.2 + github.com/lib/pq v1.10.9 + github.com/microsoft/go-mssqldb v1.9.3 + github.com/xuri/excelize/v2 v2.10.0 + golang.org/x/crypto v0.43.0 + golang.org/x/oauth2 v0.34.0 +) + +require ( + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/tiendc/go-deepcopy v1.7.1 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/text v0.30.0 // indirect +) diff --git a/svc/go.sum b/svc/go.sum new file mode 100644 index 0000000..903d9dd --- /dev/null +++ b/svc/go.sum @@ -0,0 +1,76 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +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/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= +github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs= +github.com/microsoft/go-mssqldb v1.9.3/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA= +github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4= +github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4= +github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/svc/internal/auditlog/events.go b/svc/internal/auditlog/events.go new file mode 100644 index 0000000..068ed48 --- /dev/null +++ b/svc/internal/auditlog/events.go @@ -0,0 +1,35 @@ +package auditlog + +import ( + "context" +) + +func ForcePasswordChangeStarted( + ctx context.Context, + targetUserID int64, + reason string, // admin_reset | login_enforced +) { + Write(ActivityLog{ + UserID: IntUserIDToUUID(int(targetUserID)), + ActionType: "force_password_change_started", + ActionCategory: "auth", + Description: "kullanıcı için zorunlu parola değişimi başlatıldı", + IsSuccess: true, + ErrorMessage: reason, + }) +} + +func ForcePasswordChangeCompleted( + ctx context.Context, + userID int64, + source string, // reset_link | self_change | admin_reset +) { + Write(ActivityLog{ + UserID: IntUserIDToUUID(int(userID)), + ActionType: "force_password_change_completed", + ActionCategory: "auth", + Description: "kullanıcı parolasını başarıyla güncelledi", + IsSuccess: true, + ErrorMessage: source, + }) +} diff --git a/svc/internal/auditlog/helpers.go b/svc/internal/auditlog/helpers.go new file mode 100644 index 0000000..ea023f5 --- /dev/null +++ b/svc/internal/auditlog/helpers.go @@ -0,0 +1,73 @@ +package auditlog + +import ( + "crypto/md5" + "fmt" + "time" +) + +// +// ======================================================= +// 🕵️ AUDIT LOG — HELPER FUNCTIONS +// ======================================================= +// Bu dosya: +// - UUID bekleyen kolonlar için int → uuid dönüşümü +// - NULL-safe insert yardımcıları +// içerir +// + +// ------------------------------------------------------- +// 🔹 IntUserIDToUUID +// ------------------------------------------------------- +// int user_id → deterministic UUID +// PostgreSQL uuid kolonu ile %100 uyumlu +// +// Aynı userID → her zaman aynı UUID +func IntUserIDToUUID(userID int) string { + if userID <= 0 { + return "" + } + + sum := md5.Sum([]byte(fmt.Sprintf("bssapp-user-%d", userID))) + + return fmt.Sprintf( + "%x-%x-%x-%x-%x", + sum[0:4], + sum[4:6], + sum[6:8], + sum[8:10], + sum[10:16], + ) +} + +// ------------------------------------------------------- +// 🔹 nullIfZeroTime +// ------------------------------------------------------- +// Zero time → NULL (SQL uyumlu) +func nullIfZeroTime(t time.Time) interface{} { + if t.IsZero() { + return nil + } + return t +} + +// ------------------------------------------------------- +// 🔹 nullIfZeroInt +// ------------------------------------------------------- +// 0 → NULL (SQL uyumlu) +func nullIfZeroInt(v int) interface{} { + if v == 0 { + return nil + } + return v +} + +func Int64UserIDToUUID(id int64) string { + return IntUserIDToUUID(int(id)) +} +func nullIfEmpty(s string) any { + if s == "" { + return nil + } + return s +} diff --git a/svc/internal/auditlog/init.go b/svc/internal/auditlog/init.go new file mode 100644 index 0000000..c3c3ee4 --- /dev/null +++ b/svc/internal/auditlog/init.go @@ -0,0 +1,30 @@ +package auditlog + +import ( + "database/sql" + "log" + "sync" +) + +var ( + logQueue chan ActivityLog + dbConn *sql.DB + once sync.Once +) + +// Init → main.go içinden çağrılacak (tek sefer) +func Init(db *sql.DB, bufferSize int) { + log.Println("🟢 auditlog Init called, buffer:", bufferSize) + + dbConn = db + logQueue = make(chan ActivityLog, bufferSize) + + go logWorker() +} + +// Optional: app kapanırken flush/stop istersen +func Close() { + if logQueue != nil { + close(logQueue) + } +} diff --git a/svc/internal/auditlog/model.go b/svc/internal/auditlog/model.go new file mode 100644 index 0000000..e9aaa47 --- /dev/null +++ b/svc/internal/auditlog/model.go @@ -0,0 +1,37 @@ +package auditlog + +import "time" + +type ActivityLog struct { + // identity + UserID string // UUID (auth) + DfUsrID int64 // DF user id (mk_dfusr.id) + + Username string + RoleCode string + + // action + ActionType string + ActionCategory string + ActionTarget string + Description string + + // tech + IpAddress string + UserAgent string + SessionID string + + // timing + RequestStartedAt time.Time + RequestFinishedAt time.Time + DurationMs int + HttpStatus int + + // result + IsSuccess bool + ErrorMessage string + TargetDfUsrID int64 + TargetUsername string + ChangeBefore any // map[string]any + ChangeAfter any +} diff --git a/svc/internal/auditlog/worker.go b/svc/internal/auditlog/worker.go new file mode 100644 index 0000000..354e02e --- /dev/null +++ b/svc/internal/auditlog/worker.go @@ -0,0 +1,141 @@ +package auditlog + +import ( + "encoding/json" + "log" +) + +func toJSONB(v any) any { + if v == nil { + return nil + } + b, err := json.Marshal(v) + if err != nil { + // JSON marshal hata olursa log’u bozmayalım + log.Println("⚠️ auditlog json marshal error:", err) + return nil + } + return b // pq jsonb için []byte kabul eder +} + +func logWorker() { + log.Println("🟢 auditlog worker STARTED") + + for entry := range logQueue { + + // ---------- DFUSR_ID ---------- + var dfusrID any + if entry.DfUsrID > 0 { + dfusrID = entry.DfUsrID + } else { + dfusrID = nil + } + + // ---------- USERNAME ---------- + var username any + if entry.Username != "" { + username = entry.Username + } else { + username = nil + } + + // ---------- ROLE CODE (SNAPSHOT) ---------- + roleCode := entry.RoleCode + if roleCode == "" { + roleCode = "public" + } + + // ---------- TARGET ---------- + var targetDfUsrID any + if entry.TargetDfUsrID > 0 { + targetDfUsrID = entry.TargetDfUsrID + } else { + targetDfUsrID = nil + } + targetUsername := nullIfEmpty(entry.TargetUsername) + + log.Printf( + "🧾 auditlog INSERT | actor_dfusr=%v actor_user=%v role=%s %s %s target=%v", + dfusrID, + username, + roleCode, + entry.ActionCategory, + entry.ActionTarget, + targetDfUsrID, + ) + + _, err := dbConn.Exec(` + INSERT INTO mk_user_activity_log ( + log_id, + dfusr_id, + username, + role_code, + + action_type, + action_category, + action_target, + description, + + ip_address, + user_agent, + session_id, + + request_started_at, + request_finished_at, + duration_ms, + http_status, + + is_success, + error_message, + + -- ✅ NEW + target_dfusr_id, + target_username, + change_before, + change_after, + + created_at + ) VALUES ( + gen_random_uuid(), + $1,$2,$3, + $4,$5,$6,$7, + $8,$9,$10, + $11,$12,$13,$14, + $15,$16, + $17,$18,$19,$20, + now() + ) + `, + dfusrID, + username, + roleCode, + + entry.ActionType, + entry.ActionCategory, + entry.ActionTarget, + entry.Description, + + entry.IpAddress, + entry.UserAgent, + entry.SessionID, + + nullIfZeroTime(entry.RequestStartedAt), + nullIfZeroTime(entry.RequestFinishedAt), + nullIfZeroInt(entry.DurationMs), + nullIfZeroInt(entry.HttpStatus), + + entry.IsSuccess, + entry.ErrorMessage, + + // ✅ NEW + targetDfUsrID, + targetUsername, + toJSONB(entry.ChangeBefore), + toJSONB(entry.ChangeAfter), + ) + + if err != nil { + log.Println("❌ auditlog insert error:", err) + } + } +} diff --git a/svc/internal/auditlog/writer.go b/svc/internal/auditlog/writer.go new file mode 100644 index 0000000..5969e4f --- /dev/null +++ b/svc/internal/auditlog/writer.go @@ -0,0 +1,25 @@ +package auditlog + +import "context" + +func Write(log ActivityLog) { + if logQueue == nil { + return // sistem henüz init edilmediyse sessizce çık + } + + select { + case logQueue <- log: + // kuyruğa alındı + default: + // kuyruk dolu → drop edilir, ana akış bozulmaz + } +} +func Enqueue(ctx context.Context, al ActivityLog) { + + select { + case logQueue <- al: + // ok + default: + // queue dolu → drop + } +} diff --git a/svc/internal/authz/context.go b/svc/internal/authz/context.go new file mode 100644 index 0000000..ea81a32 --- /dev/null +++ b/svc/internal/authz/context.go @@ -0,0 +1,36 @@ +package authz + +import "context" + +type scopeKey string + +const ( + CtxDeptCodesKey scopeKey = "authz.dept_codes" + CtxPiyasaCodesKey scopeKey = "authz.piyasa_codes" +) + +func WithDeptCodes(ctx context.Context, codes []string) context.Context { + return context.WithValue(ctx, CtxDeptCodesKey, codes) +} + +func WithPiyasaCodes(ctx context.Context, codes []string) context.Context { + return context.WithValue(ctx, CtxPiyasaCodesKey, codes) +} + +func GetDeptCodesFromCtx(ctx context.Context) []string { + if v := ctx.Value(CtxDeptCodesKey); v != nil { + if codes, ok := v.([]string); ok { + return codes + } + } + return nil +} + +func GetPiyasaCodesFromCtx(ctx context.Context) []string { + if v := ctx.Value(CtxPiyasaCodesKey); v != nil { + if codes, ok := v.([]string); ok { + return codes + } + } + return nil +} diff --git a/svc/internal/authz/mssql.go b/svc/internal/authz/mssql.go new file mode 100644 index 0000000..a8c0fb2 --- /dev/null +++ b/svc/internal/authz/mssql.go @@ -0,0 +1,32 @@ +package authz + +import ( + "context" + "fmt" + "strings" +) + +func BuildMSSQLPiyasaFilter( + ctx context.Context, + column string, +) string { + + codes := GetPiyasaCodesFromCtx(ctx) + + if len(codes) == 0 { + return "1=1" + + } + + var quoted []string + + for _, c := range codes { + quoted = append(quoted, "'"+c+"'") + } + + return fmt.Sprintf( + "%s IN (%s)", + column, + strings.Join(quoted, ","), + ) +} diff --git a/svc/internal/authz/mssql_helpers.go b/svc/internal/authz/mssql_helpers.go new file mode 100644 index 0000000..cb20bdf --- /dev/null +++ b/svc/internal/authz/mssql_helpers.go @@ -0,0 +1,24 @@ +package authz + +import ( + "fmt" + "strings" +) + +func BuildINClause(column string, codes []string) string { + if len(codes) == 0 { + return "1=0" + } + var quoted []string + for _, c := range codes { + c = strings.TrimSpace(strings.ToUpper(c)) + if c == "" { + continue + } + quoted = append(quoted, "'"+c+"'") + } + if len(quoted) == 0 { + return "1=0" + } + return fmt.Sprintf("%s IN (%s)", column, strings.Join(quoted, ",")) +} diff --git a/svc/internal/authz/piyasa_repo.go b/svc/internal/authz/piyasa_repo.go new file mode 100644 index 0000000..7784328 --- /dev/null +++ b/svc/internal/authz/piyasa_repo.go @@ -0,0 +1,74 @@ +package authz + +import ( + "database/sql" + "fmt" + "sync" +) + +// ===================================================== +// 🧠 PIYASA CACHE (USER → CODES) +// ===================================================== + +var ( + piyasaCache = make(map[int][]string) + piyasaMu sync.RWMutex +) + +// ===================================================== +// 📌 GET USER PIYASA CODES (CACHED) +// ===================================================== + +func GetUserPiyasaCodes(pg *sql.DB, userID int) ([]string, error) { + + // ----------------------------- + // CACHE READ + // ----------------------------- + piyasaMu.RLock() + if it, ok := piyasaCache[userID]; ok { + piyasaMu.RUnlock() + return it, nil + } + piyasaMu.RUnlock() + + // ----------------------------- + // DB QUERY + // ----------------------------- + rows, err := pg.Query(` + SELECT piyasa_code + FROM dfusr_piyasa + WHERE dfusr_id = $1 + AND is_allowed = true + `, userID) + if err != nil { + return nil, fmt.Errorf("pg piyasa query error: %w", err) + } + defer rows.Close() + + var out []string + for rows.Next() { + var code string + if err := rows.Scan(&code); err == nil { + out = append(out, code) + } + } + + // ----------------------------- + // CACHE WRITE + // ----------------------------- + piyasaMu.Lock() + piyasaCache[userID] = out + piyasaMu.Unlock() + + return out, nil +} + +// ===================================================== +// 🧹 CLEAR USER PIYASA CACHE +// ===================================================== + +func ClearPiyasaCache(userID int) { + piyasaMu.Lock() + defer piyasaMu.Unlock() + delete(piyasaCache, userID) +} diff --git a/svc/internal/mailer/config.go b/svc/internal/mailer/config.go new file mode 100644 index 0000000..d8b48f3 --- /dev/null +++ b/svc/internal/mailer/config.go @@ -0,0 +1,50 @@ +package mailer + +import ( + "os" + "strconv" + "strings" +) + +type Config struct { + Host string + Port int + Username string + Password string + From string + StartTLS bool +} + +func ConfigFromEnv() (Config, error) { + var cfg Config + + cfg.Host = strings.TrimSpace(os.Getenv("SMTP_HOST")) + cfg.Username = strings.TrimSpace(os.Getenv("SMTP_USERNAME")) + cfg.Password = os.Getenv("SMTP_PASSWORD") + cfg.From = strings.TrimSpace(os.Getenv("SMTP_FROM")) + + portStr := strings.TrimSpace(os.Getenv("SMTP_PORT")) + if portStr == "" { + cfg.Port = 587 + } else { + p, err := strconv.Atoi(portStr) + if err != nil { + return Config{}, err + } + cfg.Port = p + } + + startTLS := strings.TrimSpace(os.Getenv("SMTP_STARTTLS")) + if startTLS == "" { + cfg.StartTLS = true + } else { + cfg.StartTLS = strings.EqualFold(startTLS, "true") || startTLS == "1" + } + + // minimal validation + if cfg.Host == "" || cfg.Username == "" || cfg.Password == "" || cfg.From == "" || cfg.Port <= 0 { + return Config{}, ErrInvalidConfig + } + + return cfg, nil +} diff --git a/svc/internal/mailer/graph_mailer.go b/svc/internal/mailer/graph_mailer.go new file mode 100644 index 0000000..0d1c9da --- /dev/null +++ b/svc/internal/mailer/graph_mailer.go @@ -0,0 +1,270 @@ +package mailer + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "strconv" + "strings" + "time" + + "golang.org/x/oauth2/clientcredentials" +) + +type GraphMailer struct { + httpClient *http.Client + from string // noreply@baggi.com.tr + replyTo string // opsiyonel +} + +func NewGraphMailer() (*GraphMailer, error) { + tenantID := strings.TrimSpace(os.Getenv("AZURE_TENANT_ID")) + clientID := strings.TrimSpace(os.Getenv("AZURE_CLIENT_ID")) + clientSecret := strings.TrimSpace(os.Getenv("AZURE_CLIENT_SECRET")) + from := strings.TrimSpace(os.Getenv("MAIL_FROM")) + replyTo := strings.TrimSpace(os.Getenv("MAIL_REPLY_TO")) // opsiyonel + + if tenantID == "" || clientID == "" || clientSecret == "" || from == "" { + return nil, fmt.Errorf("azure graph mailer env missing (AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET/MAIL_FROM)") + } + + conf := clientcredentials.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID), + Scopes: []string{"https://graph.microsoft.com/.default"}, + } + + httpClient := conf.Client(context.Background()) + httpClient.Timeout = 25 * time.Second + + log.Printf("✉️ Graph Mailer hazır (App-only token) | from=%s", from) + + return &GraphMailer{ + httpClient: httpClient, + from: from, + replyTo: replyTo, + }, nil +} + +func (g *GraphMailer) Send(ctx context.Context, msg Message) error { + start := time.Now() + + // ---------- validate ---------- + cleanTo := make([]string, 0, len(msg.To)) + for _, t := range msg.To { + t = strings.TrimSpace(t) + if t != "" { + cleanTo = append(cleanTo, t) + } + } + if len(cleanTo) == 0 { + return fmt.Errorf("recipient missing") + } + subject := strings.TrimSpace(msg.Subject) + if subject == "" { + return fmt.Errorf("subject missing") + } + + // internal-safe adjustments + isInternal := allRecipientsInDomain(cleanTo, "baggi.com.tr") + if isInternal && !strings.HasPrefix(subject, "[BSSApp]") { + subject = "[BSSApp] " + subject + } + + html := strings.TrimSpace(msg.BodyHTML) + text := strings.TrimSpace(msg.Body) + + // Eğer sadece HTML geldiyse text üret (spam skorunu düşürür) + if text == "" && html != "" { + text = stripHTMLVerySimple(html) + } + // Eğer sadece text geldiyse basit HTML üret + if html == "" && text != "" { + html = "
" +
+			htmlEscape(text) + "
" + } + if html == "" { + return fmt.Errorf("body missing (Body or BodyHTML)") + } + + log.Printf("✉️ [MAIL] SEND START | from=%s | to=%v | internal=%v | subject=%s", g.from, cleanTo, isInternal, subject) + + // ---------- build recipients ---------- + type recipient struct { + EmailAddress struct { + Address string `json:"address"` + } `json:"emailAddress"` + } + toRecipients := make([]recipient, 0, len(cleanTo)) + for _, m := range cleanTo { + r := recipient{} + r.EmailAddress.Address = m + toRecipients = append(toRecipients, r) + } + + // ---------- headers to reduce auto-phish/spam signals ---------- + headers := []map[string]string{ + { + "name": "X-Mailer", + "value": "BSSApp Graph Mailer", + }, + { + "name": "X-BSSApp-Internal", + "value": strconv.FormatBool(isInternal), + }, + } + + // replyTo (opsiyonel) + var replyToRecipients []recipient + if strings.TrimSpace(g.replyTo) != "" { + rt := recipient{} + rt.EmailAddress.Address = strings.TrimSpace(g.replyTo) + replyToRecipients = []recipient{rt} + } + + // ---------- Graph payload ---------- + message := map[string]any{ + "subject": subject, + "body": map[string]string{ + "contentType": "HTML", + "content": buildHTML(html, text, isInternal), + }, + "toRecipients": toRecipients, + "internetMessageHeaders": headers, + "importance": "normal", + } + + // replyTo SADECE doluysa ekle + if len(replyToRecipients) > 0 { + message["replyTo"] = replyToRecipients + } + + payload := map[string]any{ + "message": message, + "saveToSentItems": true, + } + + b, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("json marshal: %w", err) + } + + url := fmt.Sprintf( + "https://graph.microsoft.com/v1.0/users/%s/sendMail", + g.from, + ) + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + url, + bytes.NewBuffer(b), + ) + if err != nil { + return fmt.Errorf("new request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + res, err := g.httpClient.Do(req) + if err != nil { + return fmt.Errorf("graph request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode >= 300 { + bodyBytes, _ := io.ReadAll(res.Body) + log.Printf( + "❌ [MAIL] SEND FAILED | status=%s | body=%s", + res.Status, + string(bodyBytes), + ) + return fmt.Errorf("graph send mail failed: %s", res.Status) + } + + log.Printf( + "✅ [MAIL] SEND OK | to=%v | duration=%s", + cleanTo, + time.Since(start), + ) + + return nil +} + +// ---------- helpers ---------- + +func allRecipientsInDomain(to []string, domain string) bool { + domain = strings.ToLower(strings.TrimSpace(domain)) + for _, addr := range to { + addr = strings.ToLower(strings.TrimSpace(addr)) + if !strings.HasSuffix(addr, "@"+domain) { + return false + } + } + return true +} + +func buildHTML(htmlBody, textBody string, internal bool) string { + // Internal ise daha sade, daha az “marketing-like” + if internal { + return ` +
+ ` + htmlBody + ` +
+
+ Bu e-posta BSSApp sistemi tarafından otomatik oluşturulmuştur. +
+
` + } + + // External + return ` +
+ ` + htmlBody + ` +
` +} + +// Çok basit “html -> text” (tam değil ama yeterli) +func stripHTMLVerySimple(s string) string { + s = strings.ReplaceAll(s, "
", "\n") + s = strings.ReplaceAll(s, "
", "\n") + s = strings.ReplaceAll(s, "
", "\n") + s = strings.ReplaceAll(s, "

", "\n\n") + // kaba temizlik + for { + i := strings.Index(s, "<") + j := strings.Index(s, ">") + if i >= 0 && j > i { + s = s[:i] + s[j+1:] + continue + } + break + } + return strings.TrimSpace(s) +} + +func htmlEscape(s string) string { + replacer := strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + "\"", """, + "'", "'", + ) + return replacer.Replace(s) +} +func (g *GraphMailer) SendMail(to string, subject string, html string) error { + msg := Message{ + To: []string{to}, + Subject: subject, + BodyHTML: html, + } + + // context burada internal olarak veriliyor + return g.Send(context.Background(), msg) +} diff --git a/svc/internal/mailer/mailer.go b/svc/internal/mailer/mailer.go new file mode 100644 index 0000000..c7f32b5 --- /dev/null +++ b/svc/internal/mailer/mailer.go @@ -0,0 +1,148 @@ +package mailer + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "net/smtp" + "strings" + "time" +) + +var ErrInvalidConfig = errors.New("invalid smtp config") + +type Mailer struct { + cfg Config +} + +type Message struct { + To []string + Subject string + Body string + BodyHTML string +} + +func New(cfg Config) *Mailer { + return &Mailer{cfg: cfg} +} + +func (m *Mailer) Send(ctx context.Context, msg Message) error { + if len(msg.To) == 0 { + return errors.New("recipient missing") + } + if strings.TrimSpace(msg.Subject) == "" { + return errors.New("subject missing") + } + + // timeout kontrolü (ctx) + deadline, ok := ctx.Deadline() + if !ok { + // default timeout + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 20*time.Second) + defer cancel() + deadline, _ = ctx.Deadline() + } + timeout := time.Until(deadline) + if timeout <= 0 { + return context.DeadlineExceeded + } + + addr := fmt.Sprintf("%s:%d", m.cfg.Host, m.cfg.Port) + + dialer := net.Dialer{Timeout: timeout} + conn, err := dialer.DialContext(ctx, "tcp", addr) + if err != nil { + return fmt.Errorf("smtp dial: %w", err) + } + defer conn.Close() + + c, err := smtp.NewClient(conn, m.cfg.Host) + if err != nil { + return fmt.Errorf("smtp client: %w", err) + } + defer c.Close() + + // STARTTLS + if m.cfg.StartTLS { + tlsCfg := &tls.Config{ + ServerName: m.cfg.Host, + MinVersion: tls.VersionTLS12, + } + if err := c.StartTLS(tlsCfg); err != nil { + return fmt.Errorf("starttls: %w", err) + } + } + + // AUTH + auth := smtp.PlainAuth("", m.cfg.Username, m.cfg.Password, m.cfg.Host) + if err := c.Auth(auth); err != nil { + return fmt.Errorf("smtp auth: %w", err) + } + + // MAIL FROM + if err := c.Mail(m.cfg.From); err != nil { + return fmt.Errorf("mail from: %w", err) + } + + // RCPT TO + for _, rcpt := range msg.To { + rcpt = strings.TrimSpace(rcpt) + if rcpt == "" { + continue + } + if err := c.Rcpt(rcpt); err != nil { + return fmt.Errorf("rcpt %s: %w", rcpt, err) + } + } + + w, err := c.Data() + if err != nil { + return fmt.Errorf("data: %w", err) + } + + // basit mime + contentType := "text/plain; charset=UTF-8" + body := msg.Body + if strings.TrimSpace(msg.BodyHTML) != "" { + contentType = "text/html; charset=UTF-8" + body = msg.BodyHTML + } + + raw := buildMIME(m.cfg.From, msg.To, msg.Subject, contentType, body) + + _, writeErr := w.Write([]byte(raw)) + closeErr := w.Close() + + if writeErr != nil { + return fmt.Errorf("write body: %w", writeErr) + } + if closeErr != nil { + return fmt.Errorf("close body: %w", closeErr) + } + + if err := c.Quit(); err != nil { + return fmt.Errorf("quit: %w", err) + } + + return nil +} + +func buildMIME(from string, to []string, subject, contentType, body string) string { + // Subject UTF-8 basit hali (gerekirse sonra MIME encoded-word ekleriz) + headers := []string{ + "From: " + from, + "To: " + strings.Join(to, ", "), + "Subject: " + subject, + "MIME-Version: 1.0", + "Content-Type: " + contentType, + "", + } + return strings.Join(headers, "\r\n") + "\r\n" + body + "\r\n" +} + +type MailerInterface interface { + Send(ctx context.Context, msg Message) error +} diff --git a/svc/internal/mailer/password_reset.go b/svc/internal/mailer/password_reset.go new file mode 100644 index 0000000..a8e6a94 --- /dev/null +++ b/svc/internal/mailer/password_reset.go @@ -0,0 +1,18 @@ +package mailer + +import "fmt" + +func (m *GraphMailer) SendPasswordResetMail(toEmail string, resetURL string) error { + subject := "Parola Sıfırlama" + + html := fmt.Sprintf(` +

Merhaba,

+

Parolanızı sıfırlamak için aşağıdaki bağlantıya tıklayın:

+

+ %s +

+

Bu bağlantı 30 dakika geçerlidir ve tek kullanımlıktır.

+ `, resetURL, resetURL) + + return m.SendMail(toEmail, subject, html) +} diff --git a/svc/internal/security/errors.go b/svc/internal/security/errors.go new file mode 100644 index 0000000..ea8b872 --- /dev/null +++ b/svc/internal/security/errors.go @@ -0,0 +1,11 @@ +package security + +import "errors" + +var ( + ErrPasswordTooShort = errors.New("password must be at least 8 characters") + ErrPasswordUpper = errors.New("password must contain an uppercase letter") + ErrPasswordLower = errors.New("password must contain a lowercase letter") + ErrPasswordDigit = errors.New("password must contain a digit") + ErrPasswordSpecial = errors.New("password must contain a special character") +) diff --git a/svc/internal/security/password_policy.go b/svc/internal/security/password_policy.go new file mode 100644 index 0000000..4b9855c --- /dev/null +++ b/svc/internal/security/password_policy.go @@ -0,0 +1,35 @@ +package security + +import ( + "errors" + "regexp" + "strings" +) + +var ( + reUpper = regexp.MustCompile(`[A-Z]`) + reLower = regexp.MustCompile(`[a-z]`) + reDigit = regexp.MustCompile(`[0-9]`) + reSpecial = regexp.MustCompile(`[^A-Za-z0-9]`) +) + +func ValidatePassword(pw string) error { + pw = strings.TrimSpace(pw) + + if len(pw) < 8 { + return errors.New("Parola en az 8 karakter olmalı") + } + if !reUpper.MatchString(pw) { + return errors.New("Parola en az 1 büyük harf içermeli") + } + if !reLower.MatchString(pw) { + return errors.New("Parola en az 1 küçük harf içermeli") + } + if !reDigit.MatchString(pw) { + return errors.New("Parola en az 1 rakam içermeli") + } + if !reSpecial.MatchString(pw) { + return errors.New("Parola en az 1 özel karakter içermeli") + } + return nil +} diff --git a/svc/internal/security/password_reset.go b/svc/internal/security/password_reset.go new file mode 100644 index 0000000..d364717 --- /dev/null +++ b/svc/internal/security/password_reset.go @@ -0,0 +1,13 @@ +package security + +import ( + "os" +) + +func BuildResetURL(token string) string { + base := os.Getenv("FRONTEND_URL") + if base == "" { + base = "http://localhost:9000" + } + return base + "/password-reset/" + token +} diff --git a/svc/internal/security/refresh_token.go b/svc/internal/security/refresh_token.go new file mode 100644 index 0000000..c59b14b --- /dev/null +++ b/svc/internal/security/refresh_token.go @@ -0,0 +1,23 @@ +package security + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" +) + +func GenerateRefreshToken() (plain string, hash string, err error) { + b := make([]byte, 32) // 256 bit + if _, err = rand.Read(b); err != nil { + return + } + plain = hex.EncodeToString(b) + sum := sha256.Sum256([]byte(plain)) + hash = hex.EncodeToString(sum[:]) + return +} + +func HashRefreshToken(plain string) string { + sum := sha256.Sum256([]byte(plain)) + return hex.EncodeToString(sum[:]) +} diff --git a/svc/internal/security/reset_token.go b/svc/internal/security/reset_token.go new file mode 100644 index 0000000..b1acf47 --- /dev/null +++ b/svc/internal/security/reset_token.go @@ -0,0 +1,26 @@ +package security + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" +) + +func GenerateResetToken() (plain string, hash string, err error) { + b := make([]byte, 32) // 256 bit + if _, err = rand.Read(b); err != nil { + return + } + + plain = hex.EncodeToString(b) + + sum := sha256.Sum256([]byte(plain)) + hash = hex.EncodeToString(sum[:]) + + return +} + +func HashToken(plain string) string { + sum := sha256.Sum256([]byte(plain)) + return hex.EncodeToString(sum[:]) +} diff --git a/svc/mail.env b/svc/mail.env new file mode 100644 index 0000000..0e9f91e --- /dev/null +++ b/svc/mail.env @@ -0,0 +1,4 @@ +AZURE_TENANT_ID=c8e0675d-1f6e-40f3-ba5f-3d1985b92317 +AZURE_CLIENT_ID=94a134b7-757f-4bcc-9e4b-d577b631a9a3 +AZURE_CLIENT_SECRET=PaW8Q~9NzYXHrESZcKoP6.hRxS.CyQshvJ0Y0czx +MAIL_FROM=baggiss@baggi.com.tr diff --git a/svc/main.go b/svc/main.go new file mode 100644 index 0000000..ca41e3d --- /dev/null +++ b/svc/main.go @@ -0,0 +1,587 @@ +package main + +import ( + "bssapp-backend/db" + "bssapp-backend/internal/auditlog" + "bssapp-backend/internal/mailer" + "bssapp-backend/middlewares" + "bssapp-backend/permissions" + "bssapp-backend/repository" + "bssapp-backend/routes" + "database/sql" + "log" + "net/http" + "os" + "runtime/debug" + + "github.com/gorilla/mux" + "github.com/joho/godotenv" +) + +/* +=========================================================== +✅ CORS +=========================================================== +*/ +func enableCORS(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:9000") + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + h.ServeHTTP(w, r) + }) +} + +/* +=========================================================== +✅ V3 — Method-aware Route Auto Register +- mk_sys_routes: (path, method, module_code, action) +- unique: (path, method) +- admin auto-allow: mk_sys_role_permissions (role_id=3) +=========================================================== +*/ +func autoRegisterRouteV3( + pg *sql.DB, + path string, + method string, + module string, + action string, +) { + tx, err := pg.Begin() + if err != nil { + log.Println("❌ TX begin error:", err) + return + } + defer tx.Rollback() + + // 1) ROUTE REGISTER (path+method) + _, err = tx.Exec(` + INSERT INTO mk_sys_routes + (path, method, module_code, action) + VALUES + ($1, $2, $3, $4) + ON CONFLICT (path, method) DO UPDATE + SET + module_code = EXCLUDED.module_code, + action = EXCLUDED.action + `, + path, + method, + module, + action, + ) + if err != nil { + log.Printf("❌ Route register error (%s %s): %v", method, path, err) + return + } + + // 2) ADMIN AUTO PERMISSION (module+action bazlı) + _, err = tx.Exec(` + INSERT INTO mk_sys_role_permissions + (role_id, module_code, action, allowed) + SELECT + id, + $1, + $2, + true + FROM dfrole + WHERE id = 3 -- ADMIN + ON CONFLICT DO NOTHING + `, + module, + action, + ) + if err != nil { + log.Printf("❌ Admin perm seed error (%s %s): %v", method, path, err) + return + } + + if err := tx.Commit(); err != nil { + log.Println("❌ TX commit error:", err) + return + } + + log.Printf("✅ Route+Perm registered → %s %s [%s:%s]", + method, path, module, action, + ) +} + +/* +=========================================================== +✅ V3 Route Bind Helper +- tek satırda: autoRegister + wrap + Handle +=========================================================== +*/ +func bindV3( + r *mux.Router, + pg *sql.DB, + path string, + method string, + module string, + action string, + h http.Handler, +) { + // main + autoRegisterRouteV3(pg, path, method, module, action) + + r.Handle(path, h).Methods(method, "OPTIONS") +} + +/* +=========================================================== +InitRoutes — FULL V3 (Method-aware) PERMISSION EDITION +=========================================================== +*/ +func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router { + + r := mux.NewRouter() + + /* + =========================================================== + ✅ wrapV3 (method-aware): + - AuthMiddleware (JWT) + - panic recover + - AuthzGuardByRoute(pgDB) => route(path+method) lookup + =========================================================== + */ + wrapV3 := func(h http.Handler) http.Handler { + return middlewares.AuthMiddleware( + pgDB, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + log.Printf("🔥 PANIC %s %s\n%v", r.Method, r.URL.Path, rec) + debug.PrintStack() + http.Error(w, "internal server error", 500) + } + }() + + // ✅ method-aware route guard + middlewares.AuthzGuardByRoute(pgDB)(h).ServeHTTP(w, r) + }), + ) + } + + // ============================================================ + // PUBLIC (NO AUTHZ) + // ============================================================ + bindV3(r, pgDB, + "/api/auth/login", "POST", + "auth", "login", + http.HandlerFunc(routes.LoginHandler(pgDB)), + ) + + bindV3(r, pgDB, + "/api/auth/refresh", "POST", + "auth", "refresh", + routes.AuthRefreshHandler(pgDB), + ) + + // ============================================================ + // SYSTEM + // ============================================================ + bindV3(r, pgDB, + "/api/password/change", "POST", + "system", "update", + wrapV3(http.HandlerFunc(routes.FirstPasswordChangeHandler(pgDB))), + ) + + bindV3(r, pgDB, + "/api/activity-logs", "GET", + "user", "view", + wrapV3(routes.AdminActivityLogsHandler(pgDB)), + ) + + bindV3(r, pgDB, + "/api/test-mail", "POST", + "user", "insert", + wrapV3(routes.TestMailHandler(ml)), + ) + + // ============================================================ + // PERMISSIONS + // ============================================================ + rolePerm := "/api/roles/{id}/permissions" + + bindV3(r, pgDB, + rolePerm, "GET", + "user", "update", + wrapV3(routes.GetRolePermissionMatrix(pgDB)), + ) + bindV3(r, pgDB, + rolePerm, "POST", + "user", "update", + wrapV3(routes.SaveRolePermissionMatrix(pgDB)), + ) + + userPerm := "/api/users/{id}/permissions" + + bindV3(r, pgDB, + userPerm, "GET", + "user", "update", + wrapV3(routes.GetUserPermissionsHandler(pgDB)), + ) + bindV3(r, pgDB, + userPerm, "POST", + "user", "update", + wrapV3(routes.SaveUserPermissionsHandler(pgDB)), + ) + + // ✅ permissions/routes (system:view) + bindV3(r, pgDB, + "/api/permissions/routes", "GET", + "system", "view", + wrapV3(routes.GetUserRoutePermissionsHandler(pgDB)), + ) + + // ✅ permissions/effective (system:view) + bindV3(r, pgDB, + "/api/permissions/effective", "GET", + "system", "view", + wrapV3(routes.GetMyEffectivePermissions(pgDB)), + ) + + // ✅ my permission matrix (system:view) (Senin tabloda user:view görünüyor; düzeltiyoruz) + bindV3(r, pgDB, + "/api/permissions/matrix", "GET", + "system", "view", + wrapV3(routes.GetMyPermissionMatrix(pgDB)), + ) + + // ============================================================ + // ROLE + DEPARTMENT PERMISSIONS + // ============================================================ + rdPerm := "/api/roles/{roleId}/departments/{deptCode}/permissions" + rdHandler := routes.NewRoleDepartmentPermissionHandler(pgDB) + + bindV3(r, pgDB, + rdPerm, "GET", + "user", "update", + wrapV3(http.HandlerFunc(rdHandler.Get)), + ) + bindV3(r, pgDB, + rdPerm, "POST", + "user", "update", + wrapV3(http.HandlerFunc(rdHandler.Save)), + ) + + // ============================================================ + // USERS + // ============================================================ + bindV3(r, pgDB, + "/api/users/list", "GET", + "user", "view", + wrapV3(routes.UserListRoute(pgDB)), + ) + + bindV3(r, pgDB, + "/api/users", "POST", + "user", "insert", + wrapV3(routes.UserCreateRoute(pgDB)), + ) + + bindV3(r, pgDB, + "/api/users/{id}", "GET", + "user", "update", + wrapV3(routes.UserDetailRoute(pgDB)), + ) + bindV3(r, pgDB, + "/api/users/{id}", "PUT", + "user", "update", + wrapV3(routes.UserDetailRoute(pgDB)), + ) + + bindV3(r, pgDB, + "/api/users/{id}/admin-reset-password", "POST", + "user", "update", + wrapV3(routes.AdminResetPasswordHandler(pgDB)), + ) + + bindV3(r, pgDB, + "/api/users/{id}/send-password-mail", "POST", + "user", "update", + wrapV3(routes.SendPasswordResetMailHandler(pgDB, ml)), + ) + + // ✅ eski kısayol create endpoint (senin eski main.go’da vardı) + bindV3(r, pgDB, + "/api/users/create", "POST", + "user", "insert", + wrapV3(routes.UserCreateRoute(pgDB)), + ) + + // ============================================================ + // LOOKUPS + // ============================================================ + lookups := map[string]http.Handler{ + "/api/lookups/roles": routes.GetRoleLookupRoute(pgDB), + "/api/lookups/departments": routes.GetDepartmentLookupRoute(pgDB), + "/api/lookups/nebim-users": routes.GetNebimUserLookupRoute(pgDB), + "/api/lookups/piyasalar": routes.GetPiyasaLookupRoute(pgDB), + "/api/lookups/users-perm": routes.GetUsersForPermissionSelectRoute(pgDB), + "/api/lookups/roles-perm": routes.GetRolesForPermissionSelectRoute(pgDB), + "/api/lookups/departments-perm": routes.GetDepartmentsForPermissionSelectRoute(pgDB), + "/api/lookups/modules": routes.GetModuleLookupRoute(pgDB), + } + + for path, handler := range lookups { + bindV3(r, pgDB, + path, "GET", + "user", "view", + wrapV3(handler), + ) + } + + // ============================================================ + // CUSTOMER + // ============================================================ + bindV3(r, pgDB, + "/api/accounts", "GET", + "customer", "view", + wrapV3(http.HandlerFunc(routes.GetAccountsHandler)), + ) + + bindV3(r, pgDB, + "/api/customer-list", "GET", + "customer", "view", + wrapV3(http.HandlerFunc(routes.GetCustomerListHandler)), + ) + + // ============================================================ + // FINANCE + // ============================================================ + bindV3(r, pgDB, + "/api/today-currency", "GET", + "finance", "view", + wrapV3(routes.GetTodayCurrencyV3Handler(mssql)), + ) + + bindV3(r, pgDB, + "/api/export-pdf", "GET", + "finance", "export", + wrapV3(routes.ExportPDFHandler(mssql)), + ) + + bindV3(r, pgDB, + "/api/exportstamentheaderreport-pdf", "GET", + "finance", "export", + wrapV3(routes.ExportStatementHeaderReportPDFHandler(mssql)), + ) + + // ============================================================ + // REPORT (STATEMENTS) + // ============================================================ + bindV3(r, pgDB, + "/api/statements", "GET", + "finance", "view", + wrapV3(http.HandlerFunc(routes.GetStatementHeadersHandler)), + ) + + // ⚠️ Senin handler: GetStatementDetailsHandler vars["accountCode"] bekliyor, + // route’da {id} var. Burada, DB route’unu ve path’i bozmayalım diye {id}’yi koruyorum, + // ama handler içinde accountCode := mux.Vars(r)["id"] yapman daha doğru. + bindV3(r, pgDB, + "/api/statements/{id}/details", "GET", + "finance", "view", + wrapV3(http.HandlerFunc(routes.GetStatementDetailsHandler)), + ) + + // ============================================================ + // ORDER + // ============================================================ + orderRoutes := []struct { + Path string + Method string + Action string + Handle http.Handler + }{ + {"/api/order/create", "POST", "insert", routes.CreateOrderHandler(pgDB, mssql)}, + {"/api/order/update", "POST", "update", http.HandlerFunc(routes.UpdateOrderHandler)}, + {"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)}, + {"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)}, + {"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)}, + {"/api/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)}, + {"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)}, + {"/api/order/pdf/{id}", "GET", "export", routes.OrderPDFHandler(mssql)}, + {"/api/order-inventory", "GET", "view", http.HandlerFunc(routes.GetOrderInventoryHandler)}, + {"/api/orderpricelistb2b", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)}, + {"/api/min-price", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)}, + } + + for _, rt := range orderRoutes { + bindV3(r, pgDB, + rt.Path, rt.Method, + "order", rt.Action, + wrapV3(rt.Handle), + ) + } + + // ============================================================ + // PRODUCTS (✅ handler mapping fix) + // ============================================================ + bindV3(r, pgDB, + "/api/products", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductListHandler)), + ) + + bindV3(r, pgDB, + "/api/product-detail", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductDetailHandler)), + ) + + bindV3(r, pgDB, + "/api/product-colors", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductColorsHandler)), + ) + + bindV3(r, pgDB, + "/api/product-colorsize", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductColorSizesHandler)), + ) + + bindV3(r, pgDB, + "/api/product-secondcolor", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductSecondColorsHandler)), + ) + + // ============================================================ + // ROLE MANAGEMENT + // ============================================================ + bindV3(r, pgDB, + "/api/roles", "GET", + "user", "view", + wrapV3(routes.GetRolesHandler(pgDB)), + ) + + bindV3(r, pgDB, + "/api/departments", "GET", + "user", "view", + wrapV3(routes.GetDepartmentsHandler(pgDB)), + ) + + bindV3(r, pgDB, + "/api/piyasalar", "GET", + "user", "view", + wrapV3(routes.GetPiyasalarHandler(pgDB)), + ) + + // ============================================================ + // ROLE RELATIONS + // ============================================================ + bindV3(r, pgDB, + "/api/roles/{id}/departments", "POST", + "user", "update", + wrapV3(routes.UpdateRoleDepartmentsHandler(pgDB)), + ) + + bindV3(r, pgDB, + "/api/roles/{id}/piyasalar", "POST", + "user", "update", + wrapV3(routes.UpdateRolePiyasalarHandler(pgDB)), + ) + + // ============================================================ + // USER ↔ ROLE + // ============================================================ + bindV3(r, pgDB, + "/api/users/{id}/roles", "POST", + "user", "update", + wrapV3(routes.UpdateUserRolesHandler(pgDB)), + ) + + // ============================================================ + // ADMIN EXTRA (eski main.go’da vardı, yeni sisteme alındı) + // ============================================================ + bindV3(r, pgDB, + "/api/admin/users/{id}/piyasa-sync", "POST", + "admin", "user.update", + wrapV3(http.HandlerFunc(routes.AdminSyncUserPiyasaHandler)), + ) + + return r +} + +func main() { + log.Println("🔥🔥🔥 BSSAPP BACKEND STARTED — LOGIN ROUTE SHOULD EXIST 🔥🔥🔥") + + // ------------------------------------------------------- + // 🔑 ENV + // ------------------------------------------------------- + if err := godotenv.Load(".env", "mail.env"); err != nil { + log.Println("⚠️ .env / mail.env bulunamadı") + } + + jwtSecret := os.Getenv("JWT_SECRET") + if len(jwtSecret) < 10 { + log.Fatal("❌ JWT_SECRET tanımlı değil veya çok kısa (min 10 karakter)") + } + log.Println("🔐 JWT_SECRET yüklendi") + + // ------------------------------------------------------- + // 🔗 DATABASE + // ------------------------------------------------------- + db.ConnectMSSQL() + + pgDB, err := db.ConnectPostgres() + if err != nil { + log.Fatalf("❌ PostgreSQL bağlantı hatası: %v", err) + } + defer pgDB.Close() + + // ------------------------------------------------------- + // 🔐 ADMIN ROLE + DEPARTMENT AUTO SEED + // ------------------------------------------------------- + if err := permissions.SeedAdminRoleDepartments(pgDB); err != nil { + log.Println("❌ Admin dept seed failed:", err) + } else { + log.Println("✅ Admin dept permissions seeded") + } + + // ------------------------------------------------------- + // 🕵️ AUDIT LOG INIT + // ------------------------------------------------------- + auditlog.Init(pgDB, 1000) + log.Println("🕵️ AuditLog sistemi başlatıldı (buffer=1000)") + + // ------------------------------------------------------- + // ✉️ MAILER INIT + // ------------------------------------------------------- + graphMailer, err := mailer.NewGraphMailer() + if err != nil { + log.Fatalf("❌ Graph Mailer init error: %v", err) + } + log.Println("✉️ Graph Mailer hazır") + + // ------------------------------------------------------- + // 👤 DEBUG + // ------------------------------------------------------- + repository.NewUserRepository(pgDB).DebugListUsers() + + // ------------------------------------------------------- + // 🌍 SERVER + // ------------------------------------------------------- + router := InitRoutes(pgDB, db.MssqlDB, graphMailer) + + handler := enableCORS( + middlewares.GlobalAuthMiddleware( + pgDB, + middlewares.RequestLogger(router), + ), + ) + + log.Println("✅ Server çalışıyor: http://localhost:8080") + log.Fatal(http.ListenAndServe(":8080", handler)) +} diff --git a/svc/middlewares/auditlog_middleware.go b/svc/middlewares/auditlog_middleware.go new file mode 100644 index 0000000..d0cbd70 --- /dev/null +++ b/svc/middlewares/auditlog_middleware.go @@ -0,0 +1,71 @@ +package middlewares + +import ( + "bssapp-backend/auth" + "net/http" + "time" + + "bssapp-backend/internal/auditlog" +) + +type ResponseWriter struct { + http.ResponseWriter + status int +} + +func NewResponseWriter(w http.ResponseWriter) *ResponseWriter { + return &ResponseWriter{ + ResponseWriter: w, + status: http.StatusOK, + } +} + +func (rw *ResponseWriter) WriteHeader(code int) { + rw.status = code + rw.ResponseWriter.WriteHeader(code) +} + +func (rw *ResponseWriter) Status() int { return rw.status } + +func Audit(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + start := time.Now() + rw := NewResponseWriter(w) + + next.ServeHTTP(rw, r) + + // ✅ AuthMiddleware sonrası burada claims VAR + var dfusrID int64 + var username, roleCode string + + if claims, ok := auth.GetClaimsFromContext(r.Context()); ok && claims != nil { + dfusrID = int64(claims.ID) + username = claims.Username + roleCode = claims.RoleCode // token’da varsa + } + + entry := auditlog.ActivityLog{ + DfUsrID: dfusrID, + Username: username, + RoleCode: roleCode, + + ActionType: "route_access", + ActionCategory: "nav", + ActionTarget: r.URL.Path, + Description: r.Method + " " + r.URL.Path, + + IpAddress: r.RemoteAddr, + UserAgent: r.UserAgent(), + SessionID: "", + + RequestStartedAt: start, + RequestFinishedAt: time.Now(), + DurationMs: int(time.Since(start).Milliseconds()), + HttpStatus: rw.Status(), + IsSuccess: rw.Status() < 400, + } + + auditlog.Write(entry) + }) +} diff --git a/svc/middlewares/auth_middleware.go b/svc/middlewares/auth_middleware.go new file mode 100644 index 0000000..ff37dfa --- /dev/null +++ b/svc/middlewares/auth_middleware.go @@ -0,0 +1,39 @@ +package middlewares + +import ( + "bssapp-backend/auth" + "database/sql" + "log" + "net/http" + "strings" +) + +func AuthMiddleware(db *sql.DB, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + claims, err := auth.ValidateToken(parts[1]) + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // 🔥 BU SATIR ŞART + ctx := auth.WithClaims(r.Context(), claims) + + log.Printf("🔐 AUTH CTX SET user=%d role=%s", claims.ID, claims.RoleCode) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/svc/middlewares/authz_v2.go b/svc/middlewares/authz_v2.go new file mode 100644 index 0000000..4d347dd --- /dev/null +++ b/svc/middlewares/authz_v2.go @@ -0,0 +1,961 @@ +package middlewares + +import ( + "bssapp-backend/internal/authz" + "bssapp-backend/permissions" + "bytes" + "database/sql" + "encoding/json" + "io" + "log" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "bssapp-backend/auth" + + "github.com/gorilla/mux" +) + +/* +AuthzGuardV2 +- module+action role permission check (mk_sys_role_permissions) +- optional scope checks (department / piyasa) via intersection: + user_allowed ∩ role_allowed +- cache with TTL + +Expected: +- AuthMiddleware runs before this and sets JWT claims in context. +- claims should contain RoleID and UserID. +*/ + +// ===================================================== +// 🔧 CONFIG / CONSTANTS +// ===================================================== + +const ( + defaultPermTTL = 60 * time.Second + defaultScopeTTL = 30 * time.Second + + maxBodyRead = 1 << 20 // 1MB +) + +// ===================================================== +// 🧠 CACHE +// ===================================================== + +type cacheItem struct { + val any + expires time.Time +} + +type ttlCache struct { + mu sync.RWMutex + ttl time.Duration + m map[string]cacheItem +} + +// ===================================================== +// 🌍 GLOBAL SCOPE CACHE (for invalidation) +// ===================================================== + +var globalScopeCache *ttlCache + +func newTTLCache(ttl time.Duration) *ttlCache { + return &ttlCache{ + ttl: ttl, + m: make(map[string]cacheItem), + } +} + +func (c *ttlCache) get(key string) (any, bool) { + now := time.Now() + c.mu.RLock() + item, ok := c.m[key] + c.mu.RUnlock() + if !ok { + return nil, false + } + if now.After(item.expires) { + // lazy delete + c.mu.Lock() + delete(c.m, key) + c.mu.Unlock() + return nil, false + } + return item.val, true +} + +func (c *ttlCache) set(key string, val any) { + c.mu.Lock() + c.m[key] = cacheItem{val: val, expires: time.Now().Add(c.ttl)} + c.mu.Unlock() +} + +// ===================================================== +// ✅ MAIN MIDDLEWARE +// ===================================================== + +type AuthzV2Options struct { + // If true, scope checks are attempted when scope can be extracted. + EnableScopeChecks bool + + // If true, when scope is required but cannot be extracted, deny. + // If false, when scope cannot be extracted, scope check is skipped. + StrictScope bool + + // Override TTLs (optional) + PermTTL time.Duration + ScopeTTL time.Duration + + // Custom extractors (optional). If nil, built-in extractors are used. + ExtractDepartmentCodes func(r *http.Request) []string + ExtractPiyasaCodes func(r *http.Request) []string + + // Decide whether this request should be treated as scope-sensitive. + // If nil, built-in heuristic is used. + IsScopeSensitive func(module string, r *http.Request) bool +} + +func AuthzGuardV2(pg *sql.DB, module string, action string) func(http.Handler) http.Handler { + return AuthzGuardV2WithOptions(pg, module, action, AuthzV2Options{ + EnableScopeChecks: true, + StrictScope: false, + }) +} + +func AuthzGuardV2WithOptions(pg *sql.DB, module string, action string, opt AuthzV2Options) func(http.Handler) http.Handler { + + permTTL := opt.PermTTL + if permTTL <= 0 { + permTTL = defaultPermTTL + } + scopeTTL := opt.ScopeTTL + if scopeTTL <= 0 { + scopeTTL = defaultScopeTTL + } + + permCache := newTTLCache(permTTL) + if globalScopeCache == nil { + globalScopeCache = newTTLCache(scopeTTL) + } + scopeCache := globalScopeCache + + // default extractors + extractDept := opt.ExtractDepartmentCodes + if extractDept == nil { + extractDept = defaultExtractDepartmentCodes + } + extractPiy := opt.ExtractPiyasaCodes + if extractPiy == nil { + extractPiy = defaultExtractPiyasaCodes + } + + isScopeSensitive := opt.IsScopeSensitive + if isScopeSensitive == nil { + isScopeSensitive = defaultIsScopeSensitive + } + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // OPTIONS passthrough + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", 401) + return + } + + userID := claims.ID + roleCode := claims.RoleCode + + // ADMIN BYPASS + if claims.IsAdmin() { + next.ServeHTTP(w, r) + return + } + + // resolve role_id from role_code + roleID, err := cachedRoleID(pg, permCache, roleCode) + if err != nil { + log.Println("❌ role resolve error:", err) + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + // -------------------------------------------------- + // 🔐 PERMISSION RESOLUTION (USER > ROLE > DENY) + // -------------------------------------------------- + + permRepo := permissions.NewPermissionRepository(pg) + + allowed := false + resolved := false // karar verildi mi? + + // -------------------------------------------------- + // 1️⃣ USER OVERRIDE (ÖNCELİK) + // -------------------------------------------------- + + overrides, err := permRepo.GetUserOverrides(userID) + if err != nil { + log.Println("❌ override load error:", err) + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + for _, o := range overrides { + + if o.Module == module && + o.Action == action { + + log.Printf( + "🔁 USER OVERRIDE → %s:%s = %v", + module, + action, + o.Allowed, + ) + + allowed = o.Allowed + resolved = true + break + } + } + + // -------------------------------------------------- + // 2️⃣ ROLE + DEPARTMENT (NEW SYSTEM) + // -------------------------------------------------- + + if !resolved { + + deptCodes := claims.DepartmentCodes + + roleDeptAllowed, err := permRepo.ResolvePermissionChain( + userID, + roleID, + deptCodes, + module, + action, + ) + + if err != nil { + log.Println("❌ perm resolve error:", err) + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + if roleDeptAllowed { + + log.Printf("🆕 ROLE+DEPT → %s:%s = true", module, action) + + allowed = true + resolved = true + + } else { + + log.Printf("🆕 ROLE+DEPT → %s:%s = false (try legacy)", module, action) + } + } + // -------------------------------------------------- + // 3️⃣ ROLE ONLY (LEGACY FALLBACK) + // -------------------------------------------------- + + if !resolved { + + legacyAllowed, err := cachedRolePermission( + pg, + permCache, + roleID, + module, + action, + ) + + if err != nil { + log.Println("❌ legacy perm error:", err) + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + log.Printf("🕰️ LEGACY ROLE → %s:%s = %v", module, action, legacyAllowed) + + allowed = legacyAllowed + resolved = true + } + + // -------------------------------------------------- + // 3️⃣ FINAL DECISION + // -------------------------------------------------- + + if !allowed { + + log.Printf( + "⛔ ACCESS DENIED user=%d %s:%s path=%s", + claims.ID, + module, + action, + r.URL.Path, + ) + + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + log.Printf( + "✅ ACCESS OK user=%d %s:%s %s", + claims.ID, + module, + action, + r.URL.Path, + ) + + // -------------------------------------------------- + // 4️⃣ OPTIONAL SCOPE CHECKS (FINAL - SECURE) + // -------------------------------------------------- + + if opt.EnableScopeChecks && isScopeSensitive(module, r) { + + // 🔹 Request’ten gelenler + reqDepts := normalizeCodes(extractDept(r)) + reqPiy := normalizeCodes(extractPiy(r)) + + ctx := r.Context() + + // 🔹 USER PIYASA (DB’den) + userPiy, err := authz.GetUserPiyasaCodes(pg, int(userID)) + if err != nil { + log.Println("❌ piyasa load error:", err) + http.Error(w, "forbidden", 403) + return + } + + userPiy = normalizeCodes(userPiy) + + // ------------------------------------------------ + // ✅ PIYASA INTERSECTION + // ------------------------------------------------ + + var effectivePiy []string + + switch { + case len(reqPiy) > 0 && len(userPiy) > 0: + effectivePiy = intersect(reqPiy, userPiy) + + case len(reqPiy) > 0 && len(userPiy) == 0: + // user piyasa tanımlı değilse request'e güvenme → boş kalsın (StrictScope varsa deny) + effectivePiy = nil + + case len(reqPiy) == 0 && len(userPiy) > 0: + effectivePiy = userPiy + } + if len(reqPiy) > 0 && len(effectivePiy) == 0 { + // request piyasa istiyor ama user scope karşılamıyor + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + // ------------------------------------------------ + // ✅ CONTEXT’E YAZ + // ------------------------------------------------ + + if len(reqDepts) > 0 { + ctx = authz.WithDeptCodes(ctx, reqDepts) + } + + if len(effectivePiy) > 0 { + ctx = authz.WithPiyasaCodes(ctx, effectivePiy) + } + + r = r.WithContext(ctx) + + // ------------------------------------------------ + // ❗ STRICT MODE + // ------------------------------------------------ + + if len(reqDepts) == 0 && len(effectivePiy) == 0 { + + if opt.StrictScope { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + return + } + + // ------------------------------------------------ + // 🔍 DEPARTMENT CHECK + // ------------------------------------------------ + + if len(reqDepts) > 0 { + + okDept, err := cachedDeptIntersectionAny( + pg, + scopeCache, + userID, + roleID, + reqDepts, + ) + + if err != nil { + log.Println("❌ dept scope error:", err) + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + if !okDept { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + } + + // ------------------------------------------------ + // 🔍 PIYASA CHECK + // ------------------------------------------------ + + if len(effectivePiy) > 0 { + + okPiy, err := cachedPiyasaIntersectionAny( + pg, + scopeCache, + userID, + roleID, + effectivePiy, + ) + + if err != nil { + log.Println("❌ piyasa scope error:", err) + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + if !okPiy { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + } + } + + // -------------------------------------------------- + // ✅ ALLOW + // -------------------------------------------------- + + next.ServeHTTP(w, r) + }) + } +} + +// ===================================================== +// 🔐 PERMISSION CHECK (mk_sys_role_permissions) +// ===================================================== + +func cachedRolePermission(pg *sql.DB, c *ttlCache, roleID int64, module, action string) (bool, error) { + + key := "perm|" + itoa(roleID) + "|" + module + "|" + action + if v, ok := c.get(key); ok { + return v.(bool), nil + } + + var allowed bool + err := pg.QueryRow(` + SELECT allowed + FROM mk_sys_role_permissions + WHERE role_id = $1 AND module_code = $2 AND action = $3 + `, roleID, module, action).Scan(&allowed) + + if err == sql.ErrNoRows { + c.set(key, false) + return false, nil + } + if err != nil { + return false, err + } + + c.set(key, allowed) + return allowed, nil +} + +// ===================================================== +// 🧩 SCOPE INTERSECTION +// user scope ∩ role scope +// ===================================================== + +func cachedDeptIntersectionAny(pg *sql.DB, c *ttlCache, userID, roleID int64, deptCodes []string) (bool, error) { + // cache by exact request list (sorted would be ideal; normalizeCodes already stabilizes somewhat) + key := "deptAny|" + itoa(userID) + "|" + itoa(roleID) + "|" + strings.Join(deptCodes, ",") + if v, ok := c.get(key); ok { + return v.(bool), nil + } + + // ANY match: if request wants multiple codes, allow if at least one is in intersection + // Intersection query: + // user departments: dfusr_dprt -> mk_dprt(code) + // role allowed: dfrole_dprt -> mk_dprt(id) OR directly by id + okAny := false + + // We do it in a single query with ANY($3) + var dummy int + err := pg.QueryRow(` + SELECT 1 + FROM dfusr_dprt ud + JOIN mk_dprt d ON d.id = ud.dprt_id + JOIN dfrole_dprt rd ON rd.dprt_id = ud.dprt_id + WHERE ud.dfusr_id = $1 + AND rd.dfrole_id = $2 + AND ud.is_active = true + AND rd.is_allowed = true + AND d.code = ANY($3) + LIMIT 1 + `, userID, roleID, pqArray(deptCodes)).Scan(&dummy) + + if err == sql.ErrNoRows { + c.set(key, false) + return false, nil + } + if err != nil { + return false, err + } + + okAny = true + c.set(key, okAny) + return okAny, nil +} + +func cachedPiyasaIntersectionAny(pg *sql.DB, c *ttlCache, userID, roleID int64, piyasaCodes []string) (bool, error) { + + key := "piyAny|" + itoa(userID) + "|" + itoa(roleID) + "|" + strings.Join(piyasaCodes, ",") + if v, ok := c.get(key); ok { + return v.(bool), nil + } + + var dummy int + err := pg.QueryRow(` + SELECT 1 + FROM dfusr_piyasa up + WHERE up.dfusr_id = $1 + AND up.is_allowed = true + AND up.piyasa_code = ANY($2) + LIMIT 1 + `, userID, pqArray(piyasaCodes)).Scan(&dummy) + + if err == sql.ErrNoRows { + c.set(key, false) + return false, nil + } + if err != nil { + return false, err + } + + c.set(key, true) + return true, nil +} + +// ===================================================== +// 🧲 DEFAULT SCOPE DETECTION +// ===================================================== + +// defaultIsScopeSensitive decides whether this module likely needs dept/piyasa checks. +// You can tighten/extend later. +func defaultIsScopeSensitive(module string, r *http.Request) bool { + switch module { + case "order", "customer", "report", "finance": + return true + default: + return false + } +} + +// ===================================================== +// 🔎 DEFAULT EXTRACTORS +// ===================================================== + +// We try to extract scope from: +// - query params: dprt, dprt_code, department, department_code, piyasa, piyasa_code, market +// - headers: X-Department, X-Piyasa +// - json body fields: department / department_code / dprt_code, piyasa / piyasa_code / market_code +// (body read is safe: we re-inject the body) +func defaultExtractDepartmentCodes(r *http.Request) []string { + var out []string + + // query params + for _, k := range []string{"dprt", "dprt_code", "department", "department_code"} { + out = append(out, splitCSV(r.URL.Query().Get(k))...) + } + + // headers + out = append(out, splitCSV(r.Header.Get("X-Department"))...) + + // JSON body (if any) + out = append(out, extractFromJSONBody(r, []string{ + "department", "department_code", "dprt", "dprt_code", + })...) + + return out +} + +func defaultExtractPiyasaCodes(r *http.Request) []string { + var out []string + + for _, k := range []string{"piyasa", "piyasa_code", "market", "market_code"} { + out = append(out, splitCSV(r.URL.Query().Get(k))...) + } + + out = append(out, splitCSV(r.Header.Get("X-Piyasa"))...) + + out = append(out, extractFromJSONBody(r, []string{ + "piyasa", "piyasa_code", "market", "market_code", "customer_attribute", + })...) + + return out +} + +func extractFromJSONBody(r *http.Request, keys []string) []string { + // Only for methods that might have body + switch r.Method { + case http.MethodPost, http.MethodPut, http.MethodPatch: + default: + return nil + } + + // read body (and restore) + raw, err := readBodyAndRestore(r, maxBodyRead) + if err != nil || len(raw) == 0 { + return nil + } + + // try parse object + var obj map[string]any + if err := json.Unmarshal(raw, &obj); err != nil { + return nil + } + + var out []string + for _, k := range keys { + if v, ok := obj[k]; ok { + switch t := v.(type) { + case string: + out = append(out, splitCSV(t)...) + case []any: + for _, it := range t { + if s, ok := it.(string); ok { + out = append(out, splitCSV(s)...) + } + } + } + } + } + + return out +} + +func readBodyAndRestore(r *http.Request, limit int64) ([]byte, error) { + if r.Body == nil { + return nil, nil + } + + // Read with limit + raw, err := io.ReadAll(io.LimitReader(r.Body, limit)) + if err != nil { + return nil, err + } + + // restore + r.Body = io.NopCloser(bytes.NewBuffer(raw)) + return raw, nil +} + +// ===================================================== +// 🧼 HELPERS +// ===================================================== + +func splitCSV(s string) []string { + s = strings.TrimSpace(s) + if s == "" { + return nil + } + parts := strings.Split(s, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + out = append(out, p) + } + } + return out +} + +func normalizeCodes(in []string) []string { + if len(in) == 0 { + return nil + } + seen := map[string]struct{}{} + out := make([]string, 0, len(in)) + for _, s := range in { + s = strings.ToUpper(strings.TrimSpace(s)) + if s == "" { + continue + } + if _, ok := seen[s]; ok { + continue + } + seen[s] = struct{}{} + out = append(out, s) + } + return out +} + +// pqArray: minimal adapter to pass []string as Postgres array. +// If you already use lib/pq, replace this with pq.Array. +// Here we use a simple JSON array -> Postgres can cast text[] from ARRAY[]? Not directly. +// So: YOU SHOULD USE lib/pq in your project. If it's already there, change pqArray() to pq.Array(slice). +// +// For now, we implement as a driver.Value using "{A,B}" format (Postgres text[] literal). +type pgTextArray string + +func (a pgTextArray) Value() (any, error) { return string(a), nil } + +func pqArray(ss []string) any { + // produce "{A,B,C}" as text[] literal + // escape quotes minimally + if len(ss) == 0 { + return pgTextArray("{}") + } + var b strings.Builder + b.WriteString("{") + for i, s := range ss { + if i > 0 { + b.WriteString(",") + } + s = strings.ReplaceAll(s, `"`, `\"`) + b.WriteString(`"`) + b.WriteString(s) + b.WriteString(`"`) + } + b.WriteString("}") + return pgTextArray(b.String()) +} + +func itoa(n int64) string { + return strconv.FormatInt(n, 10) +} + +// isolate strconv usage without importing it globally in this snippet +func strconvFormatInt(n int64) string { + // local minimal + // NOTE: in real code just import strconv and use strconv.FormatInt(n, 10) + if n == 0 { + return "0" + } + neg := n < 0 + if neg { + n = -n + } + var buf [32]byte + i := len(buf) + for n > 0 { + i-- + buf[i] = byte('0' + (n % 10)) + n /= 10 + } + if neg { + i-- + buf[i] = '-' + } + return string(buf[i:]) +} + +// optional: allow passing scope explicitly from handlers via context (advanced use) +type scopeKey string + +// ===================================================== +// 🔍 ROLE RESOLVER (code -> id) WITH CACHE +// ===================================================== + +func cachedRoleID(pg *sql.DB, c *ttlCache, roleCode string) (int64, error) { + + key := "role|" + strings.ToLower(roleCode) + + if v, ok := c.get(key); ok { + return v.(int64), nil + } + + var id int64 + + err := pg.QueryRow(` + SELECT id + FROM dfrole + WHERE LOWER(code) = LOWER($1) + `, roleCode).Scan(&id) + + if err != nil { + return 0, err + } + + c.set(key, id) + return id, nil +} + +// ===================================================== +// 🧹 CACHE INVALIDATION (ADMIN) +// ===================================================== + +func ClearAuthzScopeCacheForUser(userID int64) { + + // NOTE: this clears ALL scope cache. + // Simple & safe. Optimize later if needed. + + if globalScopeCache != nil { + globalScopeCache.mu.Lock() + defer globalScopeCache.mu.Unlock() + + for k := range globalScopeCache.m { + if strings.Contains(k, "|"+itoa(userID)+"|") { + delete(globalScopeCache.m, k) + } + } + } +} + +// intersect: A ∩ B +func intersect(a, b []string) []string { + + set := make(map[string]struct{}, len(a)) + + for _, v := range a { + set[v] = struct{}{} + } + + var out []string + + for _, v := range b { + if _, ok := set[v]; ok { + out = append(out, v) + } + } + + return out +} +func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler { + + return func(next http.Handler) http.Handler { + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // ===================================================== + // 0️⃣ OPTIONS → PASS (CORS preflight) + // ===================================================== + if r.Method == http.MethodOptions { + next.ServeHTTP(w, r) + return + } + + // ===================================================== + // 1️⃣ AUTH + // ===================================================== + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + // ===================================================== + // 2️⃣ REAL ROUTE TEMPLATE ( /api/users/{id} ) + // ===================================================== + route := mux.CurrentRoute(r) + + if route == nil { + log.Printf("❌ AUTHZ: route not resolved: %s %s", + r.Method, r.URL.Path, + ) + + http.Error(w, "route not resolved", 403) + return + } + + pathTemplate, err := route.GetPathTemplate() + if err != nil { + log.Printf("❌ AUTHZ: path template error: %v", err) + + http.Error(w, "route template error", 403) + return + } + + // ===================================================== + // 3️⃣ ROUTE LOOKUP (path + method) + // ===================================================== + var module, action string + + err = pg.QueryRow(` + SELECT module_code, action + FROM mk_sys_routes + WHERE path = $1 + AND method = $2 + `, + pathTemplate, + r.Method, + ).Scan(&module, &action) + + if err != nil { + + log.Printf( + "❌ AUTHZ: route not registered: %s %s", + r.Method, + pathTemplate, + ) + + http.Error(w, "route permission not found", 403) + return + } + + // ===================================================== + // 4️⃣ PERMISSION RESOLVE + // ===================================================== + repo := permissions.NewPermissionRepository(pg) + + allowed, err := repo.ResolvePermissionChain( + int64(claims.ID), + int64(claims.RoleID), + claims.DepartmentCodes, + module, + action, + ) + + if err != nil { + + log.Printf( + "❌ AUTHZ: resolve error user=%d %s:%s err=%v", + claims.ID, + module, + action, + err, + ) + + http.Error(w, "forbidden", 403) + return + } + + if !allowed { + + log.Printf( + "⛔ AUTHZ: denied user=%d %s:%s", + claims.ID, + module, + action, + ) + + http.Error(w, "forbidden", 403) + return + } + + // ===================================================== + // 5️⃣ PASS + // ===================================================== + next.ServeHTTP(w, r) + }) + } +} diff --git a/svc/middlewares/current_user.go b/svc/middlewares/current_user.go new file mode 100644 index 0000000..825350f --- /dev/null +++ b/svc/middlewares/current_user.go @@ -0,0 +1,24 @@ +package middlewares + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/utils" + "net/http" +) + +func CurrentUser(r *http.Request) (*models.User, bool) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + + if !ok || claims == nil { + return nil, false + } + + user := utils.UserFromClaims(claims) + if user == nil { + return nil, false + } + + return user, true +} diff --git a/svc/middlewares/force_password_change.go b/svc/middlewares/force_password_change.go new file mode 100644 index 0000000..91354df --- /dev/null +++ b/svc/middlewares/force_password_change.go @@ -0,0 +1,66 @@ +package middlewares + +import ( + "bssapp-backend/auth" + "database/sql" + "log" + "net/http" + "strings" +) + +// 🔓 force_password_change=true iken izinli endpoint prefixleri +var passwordChangeAllowlist = []string{ + "/api/password/change", + "/api/password/reset", + "/api/password/reset/validate", + "/api/auth/refresh", +} + +func ForcePasswordChangeGuard(db *sql.DB) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + log.Println("❌ FPC GUARD: claims NOT FOUND") + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + log.Printf( + "🛡️ FPC GUARD user=%s force=%v path=%s", + claims.Username, + claims.ForcePasswordChange, + r.URL.Path, + ) + + // 🔓 Şifre değişimi zorunlu DEĞİL → serbest + if !claims.ForcePasswordChange { + next.ServeHTTP(w, r) + return + } + + // 🔐 Şifre değişimi ZORUNLU → allowlist kontrolü + for _, allowed := range passwordChangeAllowlist { + if strings.HasPrefix(r.URL.Path, allowed) { + log.Printf( + "✅ FPC GUARD PASS user=%s path=%s", + claims.Username, + r.URL.Path, + ) + next.ServeHTTP(w, r) + return + } + } + + // ⛔ Zorunlu ama yanlış endpoint + log.Printf( + "⛔ FPC GUARD BLOCK user=%s path=%s", + claims.Username, + r.URL.Path, + ) + + http.Error(w, "password change required", http.StatusUnauthorized) + }) + } +} diff --git a/svc/middlewares/global_auth.go b/svc/middlewares/global_auth.go new file mode 100644 index 0000000..b962993 --- /dev/null +++ b/svc/middlewares/global_auth.go @@ -0,0 +1,58 @@ +package middlewares + +import ( + "bssapp-backend/auth" + "log" + "net/http" + "strings" +) + +var publicPaths = []string{ + "/api/auth/login", + "/api/auth/refresh", + "/api/password/forgot", + "/api/password/reset", +} + +func GlobalAuthMiddleware(db any, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + path := r.URL.Path + + // PUBLIC ROUTES + for _, p := range publicPaths { + if strings.HasPrefix(path, p) { + next.ServeHTTP(w, r) + return + } + } + + // JWT + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + claims, err := auth.ValidateToken(parts[1]) + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + ctx := auth.WithClaims(r.Context(), claims) + + log.Printf("🔐 GLOBAL AUTH user=%d role=%s", + claims.ID, + claims.RoleCode, + ) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/svc/middlewares/helpers.go b/svc/middlewares/helpers.go new file mode 100644 index 0000000..1de99d9 --- /dev/null +++ b/svc/middlewares/helpers.go @@ -0,0 +1,56 @@ +package middlewares + +import ( + "bssapp-backend/auth" + "bssapp-backend/ctxkeys" + "context" +) + +func getClaims(ctx context.Context) *auth.Claims { + if v := ctx.Value(ctxkeys.UserContextKey); v != nil { + if c, ok := v.(*auth.Claims); ok { + return c + } + } + return nil +} + +// -------------------------------------------------- +// 🔐 SESSION +// -------------------------------------------------- +func GetSessionID(ctx context.Context) string { + if c := getClaims(ctx); c != nil { + return c.SessionID + } + return "" +} + +// -------------------------------------------------- +// 🔑 USER ID (mk_dfusr.id) +// -------------------------------------------------- +func GetUserID(ctx context.Context) int64 { + if c := getClaims(ctx); c != nil { + return c.ID + } + return 0 +} + +// -------------------------------------------------- +// 👤 USERNAME +// -------------------------------------------------- +func GetUsername(ctx context.Context) string { + if c := getClaims(ctx); c != nil { + return c.Username + } + return "" +} + +// -------------------------------------------------- +// 🧩 ROLE +// -------------------------------------------------- +func GetRoleCode(ctx context.Context) string { + if c := getClaims(ctx); c != nil && c.RoleCode != "" { + return c.RoleCode + } + return "public" +} diff --git a/svc/middlewares/rate_limit.go b/svc/middlewares/rate_limit.go new file mode 100644 index 0000000..be0a884 --- /dev/null +++ b/svc/middlewares/rate_limit.go @@ -0,0 +1,59 @@ +package middlewares + +import ( + "net" + "net/http" + "sync" + "time" +) + +type rateEntry struct { + Count int + ExpiresAt time.Time +} + +var ( + rateMu sync.Mutex + rateDB = make(map[string]*rateEntry) +) + +func RateLimit(keyFn func(*http.Request) string, limit int, window time.Duration) func(http.Handler) http.Handler { + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + key := keyFn(r) + now := time.Now() + + rateMu.Lock() + e, ok := rateDB[key] + if !ok || now.After(e.ExpiresAt) { + e = &rateEntry{ + Count: 0, + ExpiresAt: now.Add(window), + } + rateDB[key] = e + } + + e.Count++ + if e.Count > limit { + rateMu.Unlock() + http.Error(w, "Too many requests", http.StatusTooManyRequests) + return + } + rateMu.Unlock() + + next.ServeHTTP(w, r) + }) + } +} + +// helpers +func RateByIP(r *http.Request) string { + ip, _, _ := net.SplitHostPort(r.RemoteAddr) + return "ip:" + ip +} + +func RateByUser(r *http.Request) string { + return "user:" + r.URL.Path // id path’ten okunabilir +} diff --git a/svc/middlewares/request_logger.go b/svc/middlewares/request_logger.go new file mode 100644 index 0000000..bd8e4f6 --- /dev/null +++ b/svc/middlewares/request_logger.go @@ -0,0 +1,108 @@ +package middlewares + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/auditlog" + "log" + "net" + "net/http" + "time" +) + +type statusWriter struct { + http.ResponseWriter + status int +} + +func (w *statusWriter) WriteHeader(code int) { + w.status = code + w.ResponseWriter.WriteHeader(code) +} + +func RequestLogger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + start := time.Now() + + sw := &statusWriter{ + ResponseWriter: w, + status: 200, + } + + // ---------- CLAIMS ---------- + claims, _ := auth.GetClaimsFromContext(r.Context()) + + // ---------- IP ---------- + ip := r.RemoteAddr + if host, _, err := net.SplitHostPort(ip); err == nil { + ip = host + } + + // ---------- UA ---------- + ua := r.UserAgent() + + // ---------- SESSION ---------- + sessionID := "" + if claims != nil { + sessionID = claims.SessionID + } + + hasAuth := r.Header.Get("Authorization") != "" + + log.Printf("➡️ %s %s | auth=%v", r.Method, r.URL.Path, hasAuth) + + // ---------- RUN ---------- + next.ServeHTTP(sw, r) + + finish := time.Now() + dur := int(finish.Sub(start).Milliseconds()) + + log.Printf("⬅️ %s %s | status=%d | %s", r.Method, r.URL.Path, sw.status, time.Since(start)) + + // ---------- AUDIT (route_access) ---------- + al := auditlog.ActivityLog{ + ActionType: "route_access", + ActionCategory: "nav", + ActionTarget: r.URL.Path, + Description: r.Method + " " + r.URL.Path, + + IpAddress: ip, + UserAgent: ua, + SessionID: sessionID, + + RequestStartedAt: start, + RequestFinishedAt: finish, + DurationMs: dur, + HttpStatus: sw.status, + + IsSuccess: sw.status < 400, + } + + // ---------- CLAIMS → LOG ---------- + if claims != nil { + al.Username = claims.Username + al.RoleCode = claims.RoleCode + al.DfUsrID = int64(claims.ID) + + // Eğer claims içinde UUID varsa ekle (sende varsa aç) + // al.UserID = claims.UserUUID + } else { + al.RoleCode = "public" + } + + // ---------- ERROR ---------- + if sw.status >= 400 { + al.ErrorMessage = http.StatusText(sw.status) + } + + // ✅ ESKİ: auditlog.Write(al) + // ✅ YENİ: + auditlog.Enqueue(r.Context(), al) + + if claims == nil { + log.Println("⚠️ LOGGER: claims is NIL") + } else { + log.Printf("✅ LOGGER CLAIMS user=%s role=%s id=%d", claims.Username, claims.RoleCode, claims.ID) + } + }) +} diff --git a/svc/models/NewOrderNumberResponse.go b/svc/models/NewOrderNumberResponse.go new file mode 100644 index 0000000..6d5a908 --- /dev/null +++ b/svc/models/NewOrderNumberResponse.go @@ -0,0 +1,6 @@ +package models + +type NewOrderNumberResponse struct { + OrderHeaderID string `json:"OrderHeaderID"` + OrderNumber string `json:"OrderNumber"` +} diff --git a/svc/models/account.go b/svc/models/account.go new file mode 100644 index 0000000..f1d5edb --- /dev/null +++ b/svc/models/account.go @@ -0,0 +1,8 @@ +package models + +// Cari hesap modeli +type Account struct { + DisplayCode string `json:"display_code"` // "ZLA 0127" + AccountCode string `json:"account_code"` // "ZLA0127" + AccountName string `json:"account_name"` // "EKO TEXTIL COM SRL" ya da "" +} diff --git a/svc/models/custom_time.go b/svc/models/custom_time.go new file mode 100644 index 0000000..50ee9fb --- /dev/null +++ b/svc/models/custom_time.go @@ -0,0 +1,45 @@ +package models + +import ( + "database/sql" + "strings" + "time" +) + +type CustomTime struct { + sql.NullTime +} + +func (ct *CustomTime) UnmarshalJSON(b []byte) error { + s := strings.Trim(string(b), `"`) + if s == "" || s == "null" { + ct.Valid = false + return nil + } + + // DESTEKLENEN TÜM FORMATLAR + layouts := []string{ + time.RFC3339, // 2025-11-21T00:10:27Z + "2006-01-02T15:04:05", // 2025-11-21T00:10:27 + "2006-01-02 15:04:05", // 2025-11-21 00:10:27 ← FRONTEND FORMATIN! + } + + for _, layout := range layouts { + if t, err := time.Parse(layout, s); err == nil { + ct.Time = t + ct.Valid = true + return nil + } + } + + // Hâlâ parse edemediyse → invalid kabul et + ct.Valid = false + return nil +} + +func (ct CustomTime) MarshalJSON() ([]byte, error) { + if ct.Valid { + return []byte(`"` + ct.Time.Format("2006-01-02 15:04:05") + `"`), nil + } + return []byte(`null`), nil +} diff --git a/svc/models/customerlist.go b/svc/models/customerlist.go new file mode 100644 index 0000000..07eda1e --- /dev/null +++ b/svc/models/customerlist.go @@ -0,0 +1,13 @@ +// models/customerlist.go +package models + +type CustomerList struct { + CurrAccTypeCode int `json:"CurrAccTypeCode"` // 🆕 FK için lazım + Cari_Kod string `json:"Cari_Kod"` + Cari_Ad string `json:"Cari_Ad"` + Musteri_Ana_Grubu string `json:"Musteri_Ana_Grubu"` + Piyasa string `json:"Piyasa"` + Musteri_Temsilcisi string `json:"Musteri_Temsilcisi"` + Ulke string `json:"Ulke"` + Doviz_cinsi string `json:"Doviz_Cinsi"` +} diff --git a/svc/models/invalid_variant.go b/svc/models/invalid_variant.go new file mode 100644 index 0000000..c6b9a42 --- /dev/null +++ b/svc/models/invalid_variant.go @@ -0,0 +1,13 @@ +package models + +type InvalidVariant struct { + Index int `json:"index"` + ClientKey string `json:"clientKey"` + ItemCode string `json:"itemCode"` + ColorCode string `json:"colorCode"` + Dim1 string `json:"dim1"` + Dim2 string `json:"dim2"` + Qty1 float64 `json:"qty1"` + ComboKey string `json:"comboKey,omitempty"` + Reason string `json:"reason,omitempty"` +} diff --git a/svc/models/mk_user.go b/svc/models/mk_user.go new file mode 100644 index 0000000..da7a4bf --- /dev/null +++ b/svc/models/mk_user.go @@ -0,0 +1,57 @@ +package models + +import "time" + +type MkUser struct { + // ================================================== + // 🔑 PRIMARY ID (mk_dfusr.id) + // ================================================== + ID int64 `json:"id"` // mk_dfusr.id + RoleID int64 `json:"role_id"` + + // ================================================== + // 🔁 LEGACY DFUSR (opsiyonel – migrate/debug) + // ================================================== + LegacyDfUsrID *int64 `json:"legacy_dfusr_id,omitempty"` + + // ================================================== + // 👤 CORE + // ================================================== + Username string `json:"username"` + FullName string `json:"full_name"` + Email string `json:"email"` + Mobile string `json:"mobile"` + IsActive bool `json:"is_active"` + + // ================================================== + // 🔐 AUTH + // ================================================== + PasswordHash string `json:"-"` + ForcePasswordChange bool `json:"force_password_change"` + PasswordUpdatedAt *time.Time `json:"password_updated_at,omitempty"` + SessionID string `json:"session_id"` + + // ================================================== + // 🧩 ROLE + // ================================================== + RoleCode string `json:"role_code"` + + // ================================================== + // 🧾 NEBIM + // ================================================== + V3Username string `json:"v3_username"` + V3UserGroup string `json:"v3_usergroup"` + + // ================================================== + // 🏭 RUNTIME SCOPES + // ================================================== + DepartmentCodes []string `json:"departments"` + PiyasaCodes []string `json:"piyasalar"` + + // ================================================== + // ⏱ AUDIT + // ================================================== + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastLoginAt *time.Time `json:"last_login_at,omitempty"` +} diff --git a/svc/models/modelcombo_error.go b/svc/models/modelcombo_error.go new file mode 100644 index 0000000..9a97f3e --- /dev/null +++ b/svc/models/modelcombo_error.go @@ -0,0 +1,16 @@ +// models/errors.go +package models + +type ValidationError struct { + Code string `json:"code"` + Message string `json:"message"` + ClientKey string `json:"clientKey,omitempty"` + ItemCode string `json:"itemCode,omitempty"` + ColorCode string `json:"colorCode,omitempty"` + Dim1 string `json:"dim1,omitempty"` + Dim2 string `json:"dim2,omitempty"` +} + +func (e *ValidationError) Error() string { + return e.Message +} diff --git a/svc/models/null_uuid.go b/svc/models/null_uuid.go new file mode 100644 index 0000000..f380e3c --- /dev/null +++ b/svc/models/null_uuid.go @@ -0,0 +1,120 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "strings" +) + +// ============================================================ +// 🔹 NullUUID — MSSQL UNIQUEIDENTIFIER için nullable tip +// ============================================================ +type NullUUID struct { + UUID string `json:"UUID"` + Valid bool `json:"Valid"` +} + +// ------------------------------------------------------------ +// +// SQL Valuer (INSERT/UPDATE parametresi) +// +// ------------------------------------------------------------ +func (n NullUUID) Value() (driver.Value, error) { + if !n.Valid || strings.TrimSpace(n.UUID) == "" { + return nil, nil + } + return n.UUID, nil +} + +// ------------------------------------------------------------ +// +// SQL Scanner (SELECT sonucu) +// +// ------------------------------------------------------------ +func (n *NullUUID) Scan(value interface{}) error { + if value == nil { + n.UUID = "" + n.Valid = false + return nil + } + + switch v := value.(type) { + case string: + n.UUID = v + n.Valid = true + case []byte: + n.UUID = string(v) + n.Valid = true + default: + n.UUID = "" + n.Valid = false + } + return nil +} + +// ------------------------------------------------------------ +// +// JSON Marshal — Go → JSON +// Null ise: null +// Değer varsa: "guid-string" +// +// ------------------------------------------------------------ +func (n NullUUID) MarshalJSON() ([]byte, error) { + if !n.Valid || strings.TrimSpace(n.UUID) == "" { + return []byte("null"), nil + } + return json.Marshal(n.UUID) +} + +// ------------------------------------------------------------ +// +// JSON Unmarshal — JSON → Go +// Kabul ettiği formatlar: +// • null +// • "guid-string" +// • {"UUID":"...","Valid":true} (ileride istersen) +// +// ------------------------------------------------------------ +func (n *NullUUID) UnmarshalJSON(data []byte) error { + s := strings.TrimSpace(string(data)) + + // null veya boş + if s == "null" || s == "" { + n.UUID = "" + n.Valid = false + return nil + } + + // Önce string gibi dene: "guid" + var str string + if err := json.Unmarshal(data, &str); err == nil { + str = strings.TrimSpace(str) + if str == "" { + n.UUID = "" + n.Valid = false + } else { + n.UUID = str + n.Valid = true + } + return nil + } + + // Objeyse: { "UUID":"...", "Valid":true } + var aux struct { + UUID string `json:"UUID"` + Valid bool `json:"Valid"` + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + aux.UUID = strings.TrimSpace(aux.UUID) + if aux.UUID == "" || !aux.Valid { + n.UUID = "" + n.Valid = false + } else { + n.UUID = aux.UUID + n.Valid = true + } + return nil +} diff --git a/svc/models/order_pdf.go b/svc/models/order_pdf.go new file mode 100644 index 0000000..189a724 --- /dev/null +++ b/svc/models/order_pdf.go @@ -0,0 +1,54 @@ +package models + +// =============================== +// 🔹 ORDER HEADER (PDF için) +// =============================== +type OrderHeaderPDF struct { + OrderHeaderID string `json:"OrderHeaderID"` + OrderNumber string `json:"OrderNumber"` + CurrAccCode string `json:"CurrAccCode"` + CariAdi string `json:"CariAdi"` + OrderDate string `json:"OrderDate"` // yyyy-MM-dd + AverageDueDate string `json:"AverageDueDate"` // Termin + Description string `json:"Description"` +} + +// =============================== +// 🔹 ORDER LINE (PDF için) +// =============================== +type OrderLinePDF struct { + OrderLineID string `json:"OrderLineID"` + OrderHeaderID string `json:"OrderHeaderID"` + + Model string `json:"Model"` + Renk string `json:"Renk"` + Renk2 string `json:"Renk2"` + AnaGrup string `json:"AnaGrup"` + AltGrup string `json:"AltGrup"` + Aciklama string `json:"Aciklama"` + + Fiyat float64 `json:"Fiyat"` + Adet float64 `json:"Adet"` + Tutar float64 `json:"Tutar"` + PB string `json:"PB"` + + TerminTarihi string `json:"TerminTarihi"` + + // 16 beden alanı + Size1 float64 `json:"Size1"` + Size2 float64 `json:"Size2"` + Size3 float64 `json:"Size3"` + Size4 float64 `json:"Size4"` + Size5 float64 `json:"Size5"` + Size6 float64 `json:"Size6"` + Size7 float64 `json:"Size7"` + Size8 float64 `json:"Size8"` + Size9 float64 `json:"Size9"` + Size10 float64 `json:"Size10"` + Size11 float64 `json:"Size11"` + Size12 float64 `json:"Size12"` + Size13 float64 `json:"Size13"` + Size14 float64 `json:"Size14"` + Size15 float64 `json:"Size15"` + Size16 float64 `json:"Size16"` +} diff --git a/svc/models/orderdetail.go b/svc/models/orderdetail.go new file mode 100644 index 0000000..f8aee81 --- /dev/null +++ b/svc/models/orderdetail.go @@ -0,0 +1,98 @@ +package models + +// ============================================================ +// 📦 ORDER DETAIL (trOrderLine) +// ============================================================ +type OrderDetail struct { + OrderLineID string `json:"OrderLineID"` + + // 🔑 Frontend’den gelen benzersiz anahtar (sadece JSON, DB kolonu değil) + ClientKey NullString `json:"clientKey"` + ComboKey NullString `json:"ComboKey"` + SortOrder NullInt32 `json:"SortOrder"` + ItemTypeCode NullInt16 `json:"ItemTypeCode"` + ItemCode NullString `json:"ItemCode"` + ColorCode NullString `json:"ColorCode"` + ItemDim1Code NullString `json:"ItemDim1Code"` + ItemDim2Code NullString `json:"ItemDim2Code"` + ItemDim3Code NullString `json:"ItemDim3Code"` + + Qty1 NullFloat64 `json:"Qty1"` + Qty2 NullFloat64 `json:"Qty2"` + + CancelQty1 NullFloat64 `json:"CancelQty1"` + CancelQty2 NullFloat64 `json:"CancelQty2"` + CancelDate NullTime `json:"CancelDate"` + + OrderCancelReasonCode NullString `json:"OrderCancelReasonCode"` + ClosedDate NullTime `json:"ClosedDate"` + IsClosed NullBool `json:"IsClosed"` + + SalespersonCode NullString `json:"SalespersonCode"` + PaymentPlanCode NullString `json:"PaymentPlanCode"` + PurchasePlanCode NullString `json:"PurchasePlanCode"` + + // MSSQL smalldatetime + DeliveryDate NullTime `json:"DeliveryDate"` + + // MSSQL date (YYYY-MM-DD) + PlannedDateOfLading NullString `json:"PlannedDateOfLading"` + + LineDescription NullString `json:"LineDescription"` + UsedBarcode NullString `json:"UsedBarcode"` + CostCenterCode NullString `json:"CostCenterCode"` + + VatCode NullString `json:"VatCode"` + VatRate NullFloat64 `json:"VatRate"` + + PCTCode NullString `json:"PCTCode"` + PCTRate NullFloat64 `json:"PCTRate"` + + LDisRate1 NullFloat64 `json:"LDisRate1"` + LDisRate2 NullFloat64 `json:"LDisRate2"` + LDisRate3 NullFloat64 `json:"LDisRate3"` + LDisRate4 NullFloat64 `json:"LDisRate4"` + LDisRate5 NullFloat64 `json:"LDisRate5"` + + DocCurrencyCode NullString `json:"DocCurrencyCode"` + PriceCurrencyCode NullString `json:"PriceCurrencyCode"` + PriceExchangeRate NullFloat64 `json:"PriceExchangeRate"` + + Price NullFloat64 `json:"Price"` + + PriceListLineID NullString `json:"PriceListLineID"` + + BaseProcessCode NullString `json:"BaseProcessCode"` + BaseOrderNumber NullString `json:"BaseOrderNumber"` + BaseCustomerTypeCode NullInt32 `json:"BaseCustomerTypeCode"` + BaseCustomerCode NullString `json:"BaseCustomerCode"` + BaseSubCurrAccID NullString `json:"BaseSubCurrAccID"` + BaseStoreCode NullString `json:"BaseStoreCode"` + + SupportRequestHeaderID NullString `json:"SupportRequestHeaderID"` + + OrderHeaderID string `json:"OrderHeaderID"` + + OrderLineSumID NullInt32 `json:"OrderLineSumID"` + OrderLineBOMID NullInt32 `json:"OrderLineBOMID"` + + CreatedUserName NullString `json:"CreatedUserName"` + CreatedDate NullString `json:"CreatedDate"` + LastUpdatedUserName NullString `json:"LastUpdatedUserName"` + LastUpdatedDate NullString `json:"LastUpdatedDate"` + + SurplusOrderQtyToleranceRate NullFloat64 `json:"SurplusOrderQtyToleranceRate"` + + PurchaseRequisitionLineID NullString `json:"PurchaseRequisitionLineID"` + WithHoldingTaxTypeCode NullString `json:"WithHoldingTaxTypeCode"` + + DOVCode NullString `json:"DOVCode"` + OrderLineLinkedProductID NullString `json:"OrderLineLinkedProductID"` + + // JOIN attr + UrunIlkGrubu NullString `json:"UrunIlkGrubu"` + UrunAnaGrubu NullString `json:"UrunAnaGrubu"` + UrunAltGrubu NullString `json:"UrunAltGrubu"` + Fit1 NullString `json:"Fit1"` + Fit2 NullString `json:"Fit2"` +} diff --git a/svc/models/orderheader.go b/svc/models/orderheader.go new file mode 100644 index 0000000..9b61c0f --- /dev/null +++ b/svc/models/orderheader.go @@ -0,0 +1,109 @@ +package models + +// ============================================================ +// 🧾 ORDER HEADER +// ============================================================ +type OrderHeader struct { + OrderHeaderID string `json:"OrderHeaderID"` + + OrderTypeCode NullInt16 `json:"OrderTypeCode"` + ProcessCode NullString `json:"ProcessCode"` + OrderNumber NullString `json:"OrderNumber"` + IsCancelOrder NullBool `json:"IsCancelOrder"` + + // 🔥 Date & Time string olarak geliyor (frontend) + OrderDate NullString `json:"OrderDate"` // "YYYY-MM-DD" + OrderTime NullString `json:"OrderTime"` // "HH:mm:ss" + + DocumentNumber NullString `json:"DocumentNumber"` + + PaymentTerm NullInt16 `json:"PaymentTerm"` + AverageDueDate NullString `json:"AverageDueDate"` // "YYYY-MM-DD" + + Description NullString `json:"Description"` + InternalDescription NullString `json:"InternalDescription"` + + CurrAccTypeCode NullInt16 `json:"CurrAccTypeCode"` + CurrAccCode NullString `json:"CurrAccCode"` + + CurrAccDescription NullString `json:"CurrAccDescription"` + + // 🔥 GUID alanları NullUUID yapıldı + SubCurrAccID NullUUID `json:"SubCurrAccID"` + ContactID NullUUID `json:"ContactID"` + ShipmentMethodCode NullString `json:"ShipmentMethodCode"` + ShippingPostalAddressID NullUUID `json:"ShippingPostalAddressID"` + BillingPostalAddressID NullUUID `json:"BillingPostalAddressID"` + GuarantorContactID NullUUID `json:"GuarantorContactID"` + GuarantorContactID2 NullUUID `json:"GuarantorContactID2"` + + RoundsmanCode NullString `json:"RoundsmanCode"` + DeliveryCompanyCode NullString `json:"DeliveryCompanyCode"` + + TaxTypeCode NullInt16 `json:"TaxTypeCode"` + WithHoldingTaxTypeCode NullString `json:"WithHoldingTaxTypeCode"` + DOVCode NullString `json:"DOVCode"` + TaxExemptionCode NullInt16 `json:"TaxExemptionCode"` + + CompanyCode NullInt32 `json:"CompanyCode"` + OfficeCode NullString `json:"OfficeCode"` + StoreTypeCode NullInt16 `json:"StoreTypeCode"` + StoreCode NullString `json:"StoreCode"` + + POSTerminalID NullInt16 `json:"POSTerminalID"` + WarehouseCode NullString `json:"WarehouseCode"` + ToWarehouseCode NullString `json:"ToWarehouseCode"` + + OrdererCompanyCode NullInt32 `json:"OrdererCompanyCode"` + OrdererOfficeCode NullString `json:"OrdererOfficeCode"` + OrdererStoreCode NullString `json:"OrdererStoreCode"` + + GLTypeCode NullString `json:"GLTypeCode"` + DocCurrencyCode NullString `json:"DocCurrencyCode"` + LocalCurrencyCode NullString `json:"LocalCurrencyCode"` + ExchangeRate NullFloat64 `json:"ExchangeRate"` + + TDisRate1 NullFloat64 `json:"TDisRate1"` + TDisRate2 NullFloat64 `json:"TDisRate2"` + TDisRate3 NullFloat64 `json:"TDisRate3"` + TDisRate4 NullFloat64 `json:"TDisRate4"` + TDisRate5 NullFloat64 `json:"TDisRate5"` + + DiscountReasonCode NullInt16 `json:"DiscountReasonCode"` + SurplusOrderQtyToleranceRate NullFloat64 `json:"SurplusOrderQtyToleranceRate"` + + ImportFileNumber NullString `json:"ImportFileNumber"` + ExportFileNumber NullString `json:"ExportFileNumber"` + IncotermCode1 NullString `json:"IncotermCode1"` + IncotermCode2 NullString `json:"IncotermCode2"` + LettersOfCreditNumber NullString `json:"LettersOfCreditNumber"` + PaymentMethodCode NullString `json:"PaymentMethodCode"` + + IsInclutedVat NullBool `json:"IsInclutedVat"` + IsCreditSale NullBool `json:"IsCreditSale"` + IsCreditableConfirmed NullBool `json:"IsCreditableConfirmed"` + + CreditableConfirmedUser NullString `json:"CreditableConfirmedUser"` + // MSSQL datetime + CreditableConfirmedDate CustomTime `json:"CreditableConfirmedDate"` + + IsSalesViaInternet NullBool `json:"IsSalesViaInternet"` + IsSuspended NullBool `json:"IsSuspended"` + IsCompleted NullBool `json:"IsCompleted"` + IsPrinted NullBool `json:"IsPrinted"` + IsLocked NullBool `json:"IsLocked"` + UserLocked NullBool `json:"UserLocked"` + IsClosed NullBool `json:"IsClosed"` + + ApplicationCode NullString `json:"ApplicationCode"` + ApplicationID NullUUID `json:"ApplicationID"` + + CreatedUserName NullString `json:"CreatedUserName"` + // Frontend "YYYY-MM-DD HH:mm:ss" gönderiyor + CreatedDate NullString `json:"CreatedDate"` + + LastUpdatedUserName NullString `json:"LastUpdatedUserName"` + LastUpdatedDate NullString `json:"LastUpdatedDate"` + + IsProposalBased NullBool `json:"IsProposalBased"` +} diff --git a/svc/models/orderinventory.go b/svc/models/orderinventory.go new file mode 100644 index 0000000..bdb3b2b --- /dev/null +++ b/svc/models/orderinventory.go @@ -0,0 +1,10 @@ +package models + +type OrderInventory struct { + UrunKodu string `json:"urun_kodu"` + RenkKodu string `json:"renk_kodu"` + RenkAciklamasi string `json:"renk_aciklamasi"` + Beden string `json:"beden"` + Yaka string `json:"yaka"` + KullanilabilirEnvanter float64 `json:"kullanilabilir_envanter"` +} diff --git a/svc/models/orderlist.go b/svc/models/orderlist.go new file mode 100644 index 0000000..a91ceff --- /dev/null +++ b/svc/models/orderlist.go @@ -0,0 +1,35 @@ +package models + +// ======================================================== +// 📌 OrderList — Sipariş Listeleme Modeli (FINAL & UI SAFE) +// ======================================================== +type OrderList struct { + + // 🆔 Sipariş Bilgileri + OrderHeaderID string `json:"OrderHeaderID"` + OrderNumber string `json:"OrderNumber"` + OrderDate string `json:"OrderDate"` + + // 🧾 Cari Bilgileri + CurrAccCode string `json:"CurrAccCode"` + CurrAccDescription string `json:"CurrAccDescription"` + + // 👤 Müşteri Tanımları + MusteriTemsilcisi string `json:"MusteriTemsilcisi"` + Piyasa string `json:"Piyasa"` + + // ℹ️ Sipariş Durumu + CreditableConfirmedDate string `json:"CreditableConfirmedDate"` + IsCreditableConfirmed bool `json:"IsCreditableConfirmed"` + + // 💱 Para Birimi + DocCurrencyCode string `json:"DocCurrencyCode"` + + // 💰 Tutarlar + TotalAmount float64 `json:"TotalAmount"` + TotalAmountUSD float64 `json:"TotalAmountUSD"` + + // 📝 Açıklama + Description string `json:"Description"` + ExchangeRateUSD float64 `json:"ExchangeRateUSD"` +} diff --git a/svc/models/ordernull_types.go b/svc/models/ordernull_types.go new file mode 100644 index 0000000..11e29f8 --- /dev/null +++ b/svc/models/ordernull_types.go @@ -0,0 +1,179 @@ +package models + +import ( + "database/sql" + "encoding/json" + "time" +) + +/* ============================================================== + 🔹 NULL TYPE WRAPPERS + MSSQL sql.Null* tiplerini JSON'a dönüştürürken: + - null yerine "", 0 veya false döndürür + - frontend'de (Vue/Quasar) doğrudan okunabilir hale getirir +============================================================== */ + +// 🟦 STRING +type NullString struct { + sql.NullString +} + +func (ns *NullString) MarshalJSON() ([]byte, error) { + if ns.Valid { + return json.Marshal(ns.String) + } + return json.Marshal("") +} + +func (ns *NullString) UnmarshalJSON(b []byte) error { + var s *string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + if s != nil { + ns.Valid = true + ns.String = *s + } else { + ns.Valid = false + ns.String = "" + } + return nil +} + +// 🟩 INT32 +type NullInt32 struct { + sql.NullInt32 +} + +func (ni *NullInt32) MarshalJSON() ([]byte, error) { + if ni.Valid { + return json.Marshal(ni.Int32) + } + return json.Marshal(0) +} + +func (ni *NullInt32) UnmarshalJSON(b []byte) error { + var n *int32 + if err := json.Unmarshal(b, &n); err != nil { + return err + } + if n != nil { + ni.Valid = true + ni.Int32 = *n + } else { + ni.Valid = false + ni.Int32 = 0 + } + return nil +} + +// 🟧 INT16 +type NullInt16 struct { + sql.NullInt16 +} + +func (ni *NullInt16) MarshalJSON() ([]byte, error) { + if ni.Valid { + return json.Marshal(ni.Int16) + } + return json.Marshal(0) +} + +func (ni *NullInt16) UnmarshalJSON(b []byte) error { + var n *int16 + if err := json.Unmarshal(b, &n); err != nil { + return err + } + if n != nil { + ni.Valid = true + ni.Int16 = *n + } else { + ni.Valid = false + ni.Int16 = 0 + } + return nil +} + +// 🟨 FLOAT64 +type NullFloat64 struct { + sql.NullFloat64 +} + +func (nf *NullFloat64) MarshalJSON() ([]byte, error) { + if nf.Valid { + return json.Marshal(nf.Float64) + } + return json.Marshal(0.0) +} + +func (nf *NullFloat64) UnmarshalJSON(b []byte) error { + var f *float64 + if err := json.Unmarshal(b, &f); err != nil { + return err + } + if f != nil { + nf.Valid = true + nf.Float64 = *f + } else { + nf.Valid = false + nf.Float64 = 0 + } + return nil +} + +// 🟥 BOOL +type NullBool struct { + sql.NullBool +} + +func (nb *NullBool) MarshalJSON() ([]byte, error) { + if nb.Valid { + return json.Marshal(nb.Bool) + } + return json.Marshal(false) +} + +func (nb *NullBool) UnmarshalJSON(b []byte) error { + var v *bool + if err := json.Unmarshal(b, &v); err != nil { + return err + } + if v != nil { + nb.Valid = true + nb.Bool = *v + } else { + nb.Valid = false + nb.Bool = false + } + return nil +} + +// 🟪 TIME +type NullTime struct { + sql.NullTime +} + +func (nt *NullTime) MarshalJSON() ([]byte, error) { + if nt.Valid { + return json.Marshal(nt.Time.Format("2006-01-02 15:04:05")) + } + return json.Marshal(nil) +} + +func (nt *NullTime) UnmarshalJSON(b []byte) error { + var s *string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + if s != nil && *s != "" { + t, err := time.Parse("2006-01-02 15:04:05", *s) + if err != nil { + return err + } + nt.Valid = true + nt.Time = t + } else { + nt.Valid = false + } + return nil +} diff --git a/svc/models/orderpricelistb2b.go b/svc/models/orderpricelistb2b.go new file mode 100644 index 0000000..3a94e7a --- /dev/null +++ b/svc/models/orderpricelistb2b.go @@ -0,0 +1,13 @@ +package models + +// OrderPriceListB2B B2B ürün fiyat listesini temsil eder +type OrderPriceListB2B struct { + ModelCode string `json:"modelCode"` + CurrencyCode string `json:"currencyCode"` + Price float64 `json:"price"` + PriceGroupID int `json:"priceGroupId"` + LastUpdate string `json:"lastUpdate"` + RateToTRY float64 `json:"rateToTRY"` + PriceTRY float64 `json:"priceTRY"` + BaseCurrency string `json:"baseCurrency"` +} diff --git a/svc/models/product.go b/svc/models/product.go new file mode 100644 index 0000000..eefe700 --- /dev/null +++ b/svc/models/product.go @@ -0,0 +1,5 @@ +package models + +type Product struct { + ProductCode string `json:"ProductCode"` // ✅ büyük P harf ile +} diff --git a/svc/models/productcolor.go b/svc/models/productcolor.go new file mode 100644 index 0000000..850e607 --- /dev/null +++ b/svc/models/productcolor.go @@ -0,0 +1,7 @@ +package models + +type ProductColor struct { + ProductCode string `json:"product_code"` + ColorCode string `json:"color_code"` + ColorDescription string `json:"color_description"` +} diff --git a/svc/models/productcolorsize.go b/svc/models/productcolorsize.go new file mode 100644 index 0000000..ace2b4f --- /dev/null +++ b/svc/models/productcolorsize.go @@ -0,0 +1,9 @@ +package models + +// 🔹 Ürün renk + beden varyasyon listesi (açıklamalar çıkarıldı) +type ProductColorSize struct { + ProductCode string `json:"product_code"` + ColorCode string `json:"color_code"` + ItemDim1Code string `json:"item_dim1_code"` + ItemDim2Code string `json:"item_dim2_code"` +} diff --git a/svc/models/productdetail.go b/svc/models/productdetail.go new file mode 100644 index 0000000..fd8644d --- /dev/null +++ b/svc/models/productdetail.go @@ -0,0 +1,14 @@ +package models + +type ProductDetail struct { + ProductCode string `json:"ProductCode"` + UrunIlkGrubu string `json:"UrunIlkGrubu"` + UrunAnaGrubu string `json:"UrunAnaGrubu"` + UrunAltGrubu string `json:"UrunAltGrubu"` + UrunIcerik string `json:"UrunIcerik"` + Drop string `json:"Drop"` + Kategori string `json:"Kategori"` + AskiliYan string `json:"AskiliYan"` + Fit1 string `json:"Fit1"` + Fit2 string `json:"Fit2"` +} diff --git a/svc/models/productsecondcolor.go b/svc/models/productsecondcolor.go new file mode 100644 index 0000000..3977480 --- /dev/null +++ b/svc/models/productsecondcolor.go @@ -0,0 +1,7 @@ +package models + +type ProductSecondColor struct { + ProductCode string `json:"product_code"` + ColorCode string `json:"color_code"` + ItemDim2Code string `json:"item_dim2_code"` +} diff --git a/svc/models/statement_detail.go b/svc/models/statement_detail.go new file mode 100644 index 0000000..f7a6786 --- /dev/null +++ b/svc/models/statement_detail.go @@ -0,0 +1,17 @@ +package models + +// Detay tablo (alt satır) +type StatementDetail struct { + BelgeTarihi string `json:"belge_tarihi"` // Belge tarihi + BelgeRefNumarasi string `json:"belge_ref_numarasi"` // Belge referans numarası + UrunAnaGrubu string `json:"urun_ana_grubu"` // Ürün ana grubu + UrunAltGrubu string `json:"urun_alt_grubu"` // Ürün alt grubu + YetiskinGarson string `json:"yetiskin_garson"` // Yetişkin/Çocuk (garson) + Fit string `json:"fit"` // Fit bilgisi + Icerik string `json:"icerik"` // İçerik + UrunKodu string `json:"urun_kodu"` // Ürün kodu + UrunRengi string `json:"urun_rengi"` // Ürün rengi + ToplamAdet float64 `json:"toplam_adet"` // Toplam adet + ToplamFiyat float64 `json:"toplam_fiyat"` // Birim fiyat + ToplamTutar float64 `json:"toplam_tutar"` // Toplam tutar +} diff --git a/svc/models/statement_header.go b/svc/models/statement_header.go new file mode 100644 index 0000000..c7144cf --- /dev/null +++ b/svc/models/statement_header.go @@ -0,0 +1,41 @@ +package models + +import ( + "database/sql" + "encoding/json" +) + +type StatementHeader struct { + CariKod string `json:"cari_kod"` + CariIsim string `json:"cari_isim"` + BelgeTarihi string `json:"belge_tarihi"` + VadeTarihi string `json:"vade_tarihi"` + BelgeNo string `json:"belge_no"` + IslemTipi string `json:"islem_tipi"` + Aciklama string `json:"aciklama"` + ParaBirimi string `json:"para_birimi"` + Borc float64 `json:"borc"` + Alacak float64 `json:"alacak"` + Bakiye float64 `json:"bakiye"` + Parislemler sql.NullString `json:"parislemler"` + + // 🔹 PDF için detaylar + Details []StatementDetail `json:"details,omitempty"` +} + +// JSON dönüşümünde NULL değerleri "" yap +func (s StatementHeader) MarshalJSON() ([]byte, error) { + type Alias StatementHeader + return json.Marshal(&struct { + Parislemler string `json:"parislemler"` + *Alias + }{ + Parislemler: func() string { + if s.Parislemler.Valid { + return s.Parislemler.String + } + return "" + }(), + Alias: (*Alias)(&s), + }) +} diff --git a/svc/models/statements_params.go b/svc/models/statements_params.go new file mode 100644 index 0000000..595363d --- /dev/null +++ b/svc/models/statements_params.go @@ -0,0 +1,10 @@ +package models + +type StatementParams struct { + CariKod string `json:"cari_kod"` + StartDate string `json:"startdate"` + EndDate string `json:"enddate"` + AccountCode string `json:"accountcode"` + LangCode string `json:"langcode"` + Parislemler []string `json:"parislemler"` // ✅ slice olmalı +} diff --git a/svc/models/todaycurrencyv3.go b/svc/models/todaycurrencyv3.go new file mode 100644 index 0000000..e4eab48 --- /dev/null +++ b/svc/models/todaycurrencyv3.go @@ -0,0 +1,10 @@ +package models + +// TodayCurrencyV3 sistemdeki döviz kurlarını temsil eder +type TodayCurrencyV3 struct { + CurrencyCode string `json:"currencyCode"` + RelationCurrencyCode string `json:"relationCurrencyCode"` + ExchangeTypeCode int `json:"exchangeTypeCode"` + Rate float64 `json:"rate"` + Date string `json:"date"` +} diff --git a/svc/models/user_detail.go b/svc/models/user_detail.go new file mode 100644 index 0000000..8e83a67 --- /dev/null +++ b/svc/models/user_detail.go @@ -0,0 +1,88 @@ +package models + +import "encoding/json" + +// ====================================================== +// 👤 USER DETAIL — GET RESPONSE +// UserDetail.vue formuna birebir +// ====================================================== +type UserDetail struct { + ID int64 `json:"id"` + Code string `json:"code"` + IsActive bool `json:"is_active"` + FullName string `json:"full_name"` + Email string `json:"email"` + Mobile string `json:"mobile"` + Address string `json:"address"` + + HasPassword bool `json:"has_password"` // 🔐 SADECE DURUM + + // ===== İLİŞKİLER ===== + Roles []string `json:"roles"` + Departments []DeptOption `json:"departments"` + Piyasalar []DeptOption `json:"piyasalar"` + NebimUsers []NebimOption `json:"nebim_users"` +} + +// ====================================================== +// ✍️ USER WRITE — PUT PAYLOAD +// ====================================================== +type UserWrite struct { + Code string `json:"code"` + IsActive bool `json:"is_active"` + FullName string `json:"full_name"` + Email string `json:"email"` + Mobile string `json:"mobile"` + Address string `json:"address"` + + Roles []string `json:"roles"` + Departments []DeptOption `json:"departments"` + Piyasalar []DeptOption `json:"piyasalar"` + NebimUsers []NebimOption `json:"nebim_users"` +} + +// ====================================================== +// 🔹 COMMON OPTION MODELS +// ====================================================== +type DeptOption struct { + Code string `json:"code"` + Title string `json:"title,omitempty"` +} + +// Flexible JSON decode +func (d *DeptOption) UnmarshalJSON(data []byte) error { + + var raw struct { + Code any `json:"code"` + Title string `json:"title"` + } + + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + d.Title = raw.Title + + switch v := raw.Code.(type) { + + case string: + d.Code = v + + case []any: + if len(v) > 0 { + if s, ok := v[0].(string); ok { + d.Code = s + } + } + + default: + d.Code = "" + } + + return nil +} + +type NebimOption struct { + Username string `json:"username"` + UserGroupCode string `json:"user_group_code"` +} diff --git a/svc/models/user_list.go b/svc/models/user_list.go new file mode 100644 index 0000000..87a316e --- /dev/null +++ b/svc/models/user_list.go @@ -0,0 +1,18 @@ +package models + +// UserListRow — UserList.vue satır modeli (FINAL) +type UserListRow struct { + ID int64 `json:"id"` + Code string `json:"code"` + IsActive bool `json:"is_active"` + + // Nebim eşleşmesi (ID yok) + NebimUsername *string `json:"nebim_username,omitempty"` + UserGroupCode *string `json:"user_group_code,omitempty"` + + // UI’da gösterilecek toplu alanlar + RoleNames string `json:"role_names"` // "ADMIN, USER" + DepartmentNames string `json:"department_names"` // "UST YONETIM, ..." + PiyasaNames string `json:"piyasa_names"` // "AVRUPA, LALELI" + +} diff --git a/svc/models/users.go b/svc/models/users.go new file mode 100644 index 0000000..6d068e4 --- /dev/null +++ b/svc/models/users.go @@ -0,0 +1,20 @@ +package models + +type User struct { + ID int `json:"id"` + Username string `json:"username"` + + IsActive bool `json:"is_active"` + Email string `json:"email"` + + RoleID int `json:"role_id"` + RoleCode string `json:"role_code"` + FullName string + Mobile string + Address string + V3Username string `json:"v3_username"` + V3UserGroup int `json:"v3_usergroup"` + ForcePasswordChange bool `json:"force_password_change"` + + Upass string // 🔐 dfusr.upass (TEK KAYNAK) +} diff --git a/svc/permissions/models.go b/svc/permissions/models.go new file mode 100644 index 0000000..9e1e520 --- /dev/null +++ b/svc/permissions/models.go @@ -0,0 +1,83 @@ +package permissions + +/* ===================================================== + ROLE PERMISSION MATRIX (READ) + - Role bazlı matrix ekranı için: role_id + role_code + module/action/allowed + - UI isterse "source" alanı ile satırın nereden geldiğini gösterebilir. +===================================================== */ + +type PermissionMatrixRow struct { + // Role tarafı + RoleID int `json:"role_id,omitempty"` + RoleCode string `json:"role_code,omitempty"` + + // User override tarafı + UserID int64 `json:"user_id,omitempty"` + + // Ortak alanlar + Module string `json:"module"` + ModuleCode string `json:"module_code"` + Action string `json:"action"` + Allowed bool `json:"allowed"` + + // role | user + Source string `json:"source"` +} + +/* ===================================================== + ROLE PERMISSION UPDATE (WRITE) + - /api/permissions/matrix POST gibi update endpoint'lerinde kullanılır +===================================================== */ + +type PermissionUpdateRequest struct { + RoleID int `json:"role_id"` + Module string `json:"module"` + Action string `json:"action"` + Allowed bool `json:"allowed"` +} + +/* ===================================================== + USER OVERRIDE MODELS + - mk_sys_user_permissions tablosu için +===================================================== */ + +// DB’den okurken döndüğümüz satır (GET /api/users/{id}/permissions) +type UserOverrideRow struct { + Module string `json:"module"` + Action string `json:"action"` + Allowed bool `json:"allowed"` +} + +// Middleware içinde hızlı okuma için de kullanılabilir (GetUserOverrides) +type UserPermissionOverride struct { + Module string `json:"module"` + Action string `json:"action"` + Allowed bool `json:"allowed"` +} + +// Repo “upsert/insert” gibi operasyonlarda kullanılacak internal model +type UserPermission struct { + UserID int64 `json:"user_id"` + Module string `json:"module"` + Action string `json:"action"` + Allowed bool `json:"allowed"` +} + +// POST payload için tip (SaveUserOverrides input) +type UserPermissionRequest struct { + Module string `json:"module"` + Action string `json:"action"` + Allowed bool `json:"allowed"` +} + +// Role + Department Permission Row +type RoleDepartmentPermission struct { + RoleID int `json:"role_id"` + DepartmentCode string `json:"department_code"` + + Module string `json:"module"` + Action string `json:"action"` + Allowed bool `json:"allowed"` + + Source string `json:"source"` // "role_dept" +} diff --git a/svc/permissions/repository.go b/svc/permissions/repository.go new file mode 100644 index 0000000..aa40648 --- /dev/null +++ b/svc/permissions/repository.go @@ -0,0 +1,601 @@ +package permissions + +import ( + "database/sql" + "log" + "strings" + + "github.com/lib/pq" +) + +type PermissionRepository struct { + DB *sql.DB +} + +func NewPermissionRepository(db *sql.DB) *PermissionRepository { + return &PermissionRepository{DB: db} +} + +/* ===================================================== + MATRIX READ (V2) - ROLE BASED +===================================================== */ + +func (r *PermissionRepository) GetPermissionMatrixForRoles( + roleIDs []int, +) ([]PermissionMatrixRow, error) { + + if len(roleIDs) == 0 { + return []PermissionMatrixRow{}, nil + } + + query := ` + SELECT + rp.role_id, + rol.code, + rp.module_code, + rp.action, + rp.allowed + FROM mk_sys_role_permissions rp + JOIN dfrole rol ON rol.id = rp.role_id + WHERE rp.role_id = ANY($1) + ORDER BY rol.id, rp.module_code, rp.action + ` + + rows, err := r.DB.Query(query, pq.Array(roleIDs)) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]PermissionMatrixRow, 0) + + for rows.Next() { + var row PermissionMatrixRow + if err := rows.Scan( + &row.RoleID, + &row.RoleCode, + &row.Module, + &row.Action, + &row.Allowed, + ); err != nil { + return nil, err + } + + row.Source = "role" + list = append(list, row) + } + + return list, nil +} + +/* ===================================================== + MATRIX UPDATE (V2) - ROLE BASED +===================================================== */ + +func (r *PermissionRepository) UpdatePermissions( + list []PermissionUpdateRequest, +) error { + + tx, err := r.DB.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + stmt, err := tx.Prepare(` + INSERT INTO mk_sys_role_permissions + (role_id, module_code, action, allowed) + VALUES ($1,$2,$3,$4) + ON CONFLICT (role_id, module_code, action) + DO UPDATE SET allowed = EXCLUDED.allowed + `) + if err != nil { + return err + } + defer stmt.Close() + + for _, p := range list { + if _, err := stmt.Exec( + p.RoleID, + p.Module, + p.Action, + p.Allowed, + ); err != nil { + return err + } + } + + return tx.Commit() +} + +/* ===================================================== + USER OVERRIDES - READ + GET /api/users/{id}/permissions +===================================================== */ + +// Tek tip: PermissionMatrixRow döndürüyoruz (source=user) +func (r *PermissionRepository) GetUserOverridesByUserID( + userID int64, +) ([]PermissionMatrixRow, error) { + + rows, err := r.DB.Query(` + SELECT + user_id, + module_code, + action, + allowed + FROM mk_sys_user_permissions + WHERE user_id = $1 + ORDER BY module_code, action + `, userID) + + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]PermissionMatrixRow, 0) + + for rows.Next() { + var row PermissionMatrixRow + if err := rows.Scan( + &row.UserID, + &row.Module, + &row.Action, + &row.Allowed, + ); err != nil { + return nil, err + } + + row.Source = "user" + list = append(list, row) + } + + return list, nil +} + +/* ===================================================== + USER OVERRIDES - UPSERT SAVE (typed) + POST /api/users/{id}/permissions +===================================================== */ + +func (r *PermissionRepository) SaveUserOverrides( + userID int64, + list []UserPermissionRequest, +) error { + + log.Println("➡️ REPO SaveUserOverrides START") + log.Println("USER:", userID) + log.Println("ROWS:", len(list)) + + tx, err := r.DB.Begin() + if err != nil { + log.Println("❌ TX BEGIN ERROR:", err) + return err + } + defer tx.Rollback() + + // önce sil + _, err = tx.Exec(` + DELETE FROM mk_sys_user_permissions + WHERE user_id = $1 + `, userID) + + if err != nil { + log.Println("❌ DELETE ERROR:", err) + return err + } + + stmt, err := tx.Prepare(` + INSERT INTO mk_sys_user_permissions + (user_id, module_code, action, allowed) + VALUES ($1,$2,$3,$4) + `) + + if err != nil { + log.Println("❌ PREPARE ERROR:", err) + return err + } + defer stmt.Close() + + for _, p := range list { + if strings.TrimSpace(p.Module) == "" { + log.Printf("⚠️ SKIP EMPTY MODULE user=%d action=%s", + userID, + p.Action, + ) + continue + } + + _, err := stmt.Exec( + userID, + p.Module, + p.Action, + p.Allowed, + ) + if err != nil { + log.Println("❌ INSERT ERROR:", err) + return err + } + } + + if err := tx.Commit(); err != nil { + log.Println("❌ COMMIT ERROR:", err) + return err + } + + log.Println("✅ REPO SaveUserOverrides DONE") + return nil + +} + +/* ===================================================== + RESOLUTION HELPERS (middleware için) +===================================================== */ + +// user override var mı? varsa *bool döner, yoksa nil +func (r *PermissionRepository) HasUserOverride( + userID int64, + module string, + action string, +) (*bool, error) { + + var allowed bool + + err := r.DB.QueryRow(` + SELECT allowed + FROM mk_sys_user_permissions + WHERE user_id=$1 + AND module_code=$2 + AND action=$3 + `, + userID, + module, + action, + ).Scan(&allowed) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + + return &allowed, nil +} + +// roleIDs içinden OR logic: herhangi biri allowed=true ise true +func (r *PermissionRepository) HasRoleAccess( + roleIDs []int, + module string, + action string, +) (bool, error) { + + if len(roleIDs) == 0 { + return false, nil + } + + var count int + + err := r.DB.QueryRow(` + SELECT COUNT(*) + FROM mk_sys_role_permissions + WHERE role_id = ANY($1) + AND module_code=$2 + AND action=$3 + AND allowed=true + `, + pq.Array(roleIDs), + module, + action, + ).Scan(&count) + + if err != nil { + return false, err + } + + return count > 0, nil +} + +// Final decision: user override varsa onu uygula, yoksa role bazlı +func (r *PermissionRepository) ResolvePermission( + userID int64, + roleIDs []int, + module string, + action string, +) (bool, error) { + + override, err := r.HasUserOverride(userID, module, action) + if err != nil { + return false, err + } + + if override != nil { + return *override, nil + } + + return r.HasRoleAccess(roleIDs, module, action) +} +func (r *PermissionRepository) GetUserOverrides( + userID int64, +) ([]UserPermissionOverride, error) { + + rows, err := r.DB.Query(` + SELECT module_code, action, allowed + FROM mk_sys_user_permissions + WHERE user_id = $1 + `, userID) + + if err != nil { + return nil, err + } + defer rows.Close() + + var list []UserPermissionOverride + + for rows.Next() { + + var o UserPermissionOverride + + if err := rows.Scan( + &o.Module, + &o.Action, + &o.Allowed, + ); err != nil { + return nil, err + } + + list = append(list, o) + } + + return list, nil +} +func (r *PermissionRepository) UpdateUserOverrides( + list []UserPermission, +) error { + + tx, err := r.DB.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + stmt, err := tx.Prepare(` + INSERT INTO mk_sys_user_permissions + (user_id, module_code, action, allowed) + + VALUES ($1,$2,$3,$4) + + ON CONFLICT (user_id, module_code, action) + DO UPDATE SET allowed = EXCLUDED.allowed + `) + if err != nil { + return err + } + defer stmt.Close() + + for _, p := range list { + + _, err := stmt.Exec( + p.UserID, + p.Module, + p.Action, + p.Allowed, + ) + + if err != nil { + return err + } + } + + return tx.Commit() +} +func (r *PermissionRepository) ResolveEffectivePermission( + userID int64, + roleID int64, + deptCode string, + module string, + action string, +) (bool, error) { + + // 1️⃣ USER OVERRIDE + var allowed bool + + err := r.DB.QueryRow(` + SELECT allowed + FROM mk_sys_user_permissions + WHERE user_id = $1 + AND module_code = $2 + AND action = $3 + `, + userID, module, action, + ).Scan(&allowed) + + if err == nil { + return allowed, nil + } + if err != sql.ErrNoRows { + return false, err + } + + // ================================================== + // 2️⃣ ROLE + DEPARTMENT + // ================================================== + + if len(deptCode) > 0 { + + var allowed bool + + err = r.DB.QueryRow(` -- 🔥 := DEĞİL = + SELECT allowed + FROM vw_role_dept_permissions + WHERE role_id = $1 + AND department_code = ANY($2) + AND module_code = $3 + AND action = $4 + ORDER BY allowed DESC + LIMIT 1 + `, + roleID, + pq.Array([]string{deptCode}), + module, + action, + ).Scan(&allowed) + + if err == nil { + + log.Printf( + " ↳ ROLE+DEPT OVERRIDE = %v", + allowed, + ) + + return allowed, nil + } + + if err != sql.ErrNoRows { + log.Println("❌ ROLE+DEPT ERR:", err) + return false, err + } + } + + // 3️⃣ ROLE DEFAULT + err = r.DB.QueryRow(` + SELECT allowed + FROM mk_sys_role_permissions + WHERE role_id = $1 + AND module_code = $2 + AND action = $3 + `, + roleID, module, action, + ).Scan(&allowed) + + if err == nil { + return allowed, nil + } + if err != sql.ErrNoRows { + return false, err + } + + // 4️⃣ DENY + return false, nil +} +func (r *PermissionRepository) ResolvePermissionChain( + userID int64, + roleID int64, + deptCodes []string, + module string, + action string, +) (bool, error) { + + log.Printf( + "🔐 PERM CHECK user=%d role=%d dept=%v %s:%s", + userID, + roleID, + deptCodes, + module, + action, + ) + + // ================================================== + // 1️⃣ USER OVERRIDE + // ================================================== + + override, err := r.HasUserOverride(userID, module, action) + if err != nil { + log.Println("❌ USER OVERRIDE ERR:", err) + return false, err + } + + if override != nil { + + log.Printf( + " ↳ USER OVERRIDE = %v", + *override, + ) + + return *override, nil + } + + // ================================================== + // 2️⃣ ROLE + DEPARTMENT + // ================================================== + + if len(deptCodes) > 0 { + + var allowed bool + + err := r.DB.QueryRow(` + SELECT allowed + FROM vw_role_dept_permissions + WHERE role_id = $1 + AND department_code IN ( + SELECT UNNEST($2::text[]) + ) + AND module_code = $3 + AND action = $4 + ORDER BY allowed DESC + LIMIT 1 +`, + roleID, + pq.Array(deptCodes), + module, + action, + ).Scan(&allowed) + + if err == nil { + + log.Printf( + " ↳ ROLE+DEPT OVERRIDE = %v", + allowed, + ) + + return allowed, nil + } + + if err != sql.ErrNoRows { + log.Println("❌ ROLE+DEPT ERR:", err) + return false, err + } + } + + // ================================================== + // 3️⃣ ROLE DEFAULT + // ================================================== + + var roleAllowed bool + + err = r.DB.QueryRow(` + SELECT allowed + FROM mk_sys_role_permissions + WHERE role_id = $1 + AND module_code = $2 + AND action = $3 + `, + roleID, + module, + action, + ).Scan(&roleAllowed) + + if err == nil { + + log.Printf( + " ↳ ROLE DEFAULT = %v", + roleAllowed, + ) + + return roleAllowed, nil + } + + if err != sql.ErrNoRows { + log.Println("❌ ROLE DEFAULT ERR:", err) + return false, err + } + + // ================================================== + // 4️⃣ DENY + // ================================================== + + log.Println(" ↳ NO RULE → DENY") + + return false, nil +} diff --git a/svc/permissions/role_department_repo.go b/svc/permissions/role_department_repo.go new file mode 100644 index 0000000..c73040c --- /dev/null +++ b/svc/permissions/role_department_repo.go @@ -0,0 +1,123 @@ +package permissions + +import ( + "bssapp-backend/queries" + "database/sql" + "log" +) + +type RoleDepartmentPermissionRepo struct { + db *sql.DB +} + +func NewRoleDepartmentPermissionRepo(db *sql.DB) *RoleDepartmentPermissionRepo { + return &RoleDepartmentPermissionRepo{db: db} +} + +/* ====================================================== + GET +====================================================== */ + +func (r *RoleDepartmentPermissionRepo) Get( + roleID int, + deptCode string, +) ([]RoleDepartmentPermission, error) { + + rows, err := r.db.Query( + queries.GetRoleDepartmentPermissions, + roleID, + deptCode, + ) + + if err != nil { + return nil, err + } + defer rows.Close() + + var list []RoleDepartmentPermission + + for rows.Next() { + + var p RoleDepartmentPermission + + if err := rows.Scan( + &p.Module, + &p.Action, + &p.Allowed, + ); err != nil { + return nil, err + } + + p.RoleID = roleID + p.DepartmentCode = deptCode + p.Source = "role_department" + + list = append(list, p) + } + + return list, nil +} + +/* ====================================================== + SAVE +====================================================== */ + +func (r *RoleDepartmentPermissionRepo) Save( + roleID int, + deptCode string, + list []RoleDepartmentPermission, +) error { + + tx, err := r.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + stmt, err := tx.Prepare( + queries.UpsertRoleDepartmentPermission, + ) + if err != nil { + return err + } + defer stmt.Close() + + for _, p := range list { + + if p.Module == "" { + log.Printf( + "⚠️ SKIP EMPTY MODULE role=%v dept=%v action=%v", + roleID, + deptCode, + p.Action, + ) + continue + } + + _, err := stmt.Exec( + roleID, + deptCode, + p.Module, + p.Action, + p.Allowed, + ) + + // 🔴🔴🔴 KRİTİK DEBUG LOG + if err != nil { + + log.Printf( + "❌ UPSERT FAIL role=%v dept=%v module=%v action=%v allowed=%v err=%v", + roleID, + deptCode, + p.Module, + p.Action, + p.Allowed, + err, + ) + + return err + } + } + + return tx.Commit() +} diff --git a/svc/permissions/role_policy.go b/svc/permissions/role_policy.go new file mode 100644 index 0000000..6f14666 --- /dev/null +++ b/svc/permissions/role_policy.go @@ -0,0 +1,33 @@ +package permissions + +// 🔥 SYSTEM / ADMIN ROLE IDS +var AdminRoleIDs = map[int]struct{}{ + 1: {}, + 3: {}, +} + +// ------------------------------------------------------- +// 🧠 ROLE POLICY +// ------------------------------------------------------- +func ResolveEffectiveRoles( + roleIDs []int, +) (effectiveRoleIDs []int, isAdmin bool) { + + roleMap := make(map[int]struct{}) + + for _, roleID := range roleIDs { + + // 🔥 SYSTEM / ADMIN OVERRIDE (1,3,…) + if _, ok := AdminRoleIDs[roleID]; ok { + return []int{roleID}, true + } + + roleMap[roleID] = struct{}{} + } + + for id := range roleMap { + effectiveRoleIDs = append(effectiveRoleIDs, id) + } + + return effectiveRoleIDs, false +} diff --git a/svc/permissions/seed.go b/svc/permissions/seed.go new file mode 100644 index 0000000..5216e0e --- /dev/null +++ b/svc/permissions/seed.go @@ -0,0 +1,40 @@ +package permissions + +import "database/sql" + +func SeedAdminRoleDepartments(db *sql.DB) error { + + var adminID int + var err error + + // Admin role id al + err = db.QueryRow(` + SELECT id + FROM dfrole + WHERE code = 'admin' + `).Scan(&adminID) + + if err != nil { + return err + } + + // Seed + _, err = db.Exec(` +INSERT INTO mk_sys_role_department_permissions + (role_id, department_code, module_code, action, allowed) + +SELECT + $1, + d.code, + r.module_code, + r.action, + true +FROM mk_dprt d +CROSS JOIN mk_sys_routes r + +ON CONFLICT (role_id, department_code, module_code, action) +DO NOTHING +`, adminID) + + return err +} diff --git a/svc/public/Baggi-Tekstil-A.s-Logolu.jpeg b/svc/public/Baggi-Tekstil-A.s-Logolu.jpeg new file mode 100644 index 0000000..129519a Binary files /dev/null and b/svc/public/Baggi-Tekstil-A.s-Logolu.jpeg differ diff --git a/svc/queries/account.go b/svc/queries/account.go new file mode 100644 index 0000000..a0ac4d8 --- /dev/null +++ b/svc/queries/account.go @@ -0,0 +1,74 @@ +package queries + +import ( + "context" + "fmt" + "log" + "strings" + + "bssapp-backend/db" + "bssapp-backend/internal/authz" + "bssapp-backend/models" +) + +func GetAccounts(ctx context.Context) ([]models.Account, error) { + + piyasaFilter := authz.BuildMSSQLPiyasaFilter(ctx, "f2.CustomerAtt01") + + if strings.TrimSpace(piyasaFilter) == "" { + piyasaFilter = "1=1" + } + + query := fmt.Sprintf(` + SELECT + x.AccountCode, + MAX(x.AccountName) AS AccountName + FROM ( + SELECT + LEFT(b.CurrAccCode, 8) AS AccountCode, + COALESCE(d.CurrAccDescription, '') AS AccountName + FROM trCurrAccBook b + LEFT JOIN cdCurrAccDesc d + ON d.CurrAccCode = b.CurrAccCode + JOIN CustomerAttributesFilter f2 + ON f2.CurrAccCode = b.CurrAccCode + WHERE %s + ) x + GROUP BY x.AccountCode + ORDER BY x.AccountCode +`, piyasaFilter) + + log.Println("🔎 ACCOUNT PIYASA FILTER =", piyasaFilter) + log.Println("🔎 ACCOUNT QUERY =", query) + + rows, err := db.MssqlDB.Query(query) + if err != nil { + return nil, fmt.Errorf("MSSQL query error: %w", err) + } + defer rows.Close() + + var accounts []models.Account + + for rows.Next() { + + var acc models.Account + + if err := rows.Scan( + &acc.AccountCode, + &acc.AccountName, + ); err != nil { + return nil, err + } + + if len(acc.AccountCode) >= 4 { + acc.DisplayCode = + strings.TrimSpace(acc.AccountCode[:3] + " " + acc.AccountCode[3:]) + } else { + acc.DisplayCode = acc.AccountCode + } + + accounts = append(accounts, acc) + } + + return accounts, rows.Err() +} diff --git a/svc/queries/currency_cache.go b/svc/queries/currency_cache.go new file mode 100644 index 0000000..3859591 --- /dev/null +++ b/svc/queries/currency_cache.go @@ -0,0 +1,64 @@ +package queries + +import ( + "bssapp-backend/models" + "database/sql" + "sync" + "time" +) + +/* =============================== + CACHE STRUCT +================================ */ + +type currencyCacheItem struct { + data *models.TodayCurrencyV3 + expiresAt time.Time +} + +var ( + currencyCache = make(map[string]currencyCacheItem) + cacheMutex sync.RWMutex + cacheTTL = 5 * time.Minute +) + +/* =============================== + MAIN CACHE FUNC +================================ */ + +func GetCachedCurrencyV3(db *sql.DB, code string) (*models.TodayCurrencyV3, error) { + + now := time.Now() + + /* ---------- READ CACHE ---------- */ + cacheMutex.RLock() + + item, ok := currencyCache[code] + + if ok && now.Before(item.expiresAt) { + cacheMutex.RUnlock() + return item.data, nil + } + + cacheMutex.RUnlock() + + /* ---------- FETCH DB ---------- */ + + data, err := GetTodayCurrencyV3(db, code) + if err != nil { + return nil, err + } + + /* ---------- WRITE CACHE ---------- */ + + cacheMutex.Lock() + + currencyCache[code] = currencyCacheItem{ + data: data, + expiresAt: now.Add(cacheTTL), + } + + cacheMutex.Unlock() + + return data, nil +} diff --git a/svc/queries/customerlist.go b/svc/queries/customerlist.go new file mode 100644 index 0000000..6daeb6a --- /dev/null +++ b/svc/queries/customerlist.go @@ -0,0 +1,114 @@ +package queries + +import ( + "context" + "fmt" + + "bssapp-backend/db" + "bssapp-backend/internal/authz" + "bssapp-backend/models" +) + +func GetCustomerList(ctx context.Context) ([]models.CustomerList, error) { + + piyasaFilter := authz.BuildMSSQLPiyasaFilter( + ctx, + "f.CustomerAtt01", + ) + + query := fmt.Sprintf(` + SELECT + c.CurrAccTypeCode, + c.CurrAccCode, + ISNULL(d.CurrAccDescription, ''), + + dbo.HG_Temizlik( + ISNULL(( + SELECT AttributeDescription + FROM cdCurrAccAttributeDesc WITH(NOLOCK) + WHERE CurrAccTypeCode = 3 + AND AttributeTypeCode = 8 + AND AttributeCode = f.CustomerAtt08 + AND LangCode = 'TR' + ), SPACE(0)) + ), + + dbo.HG_Temizlik( + ISNULL(( + SELECT AttributeDescription + FROM cdCurrAccAttributeDesc WITH(NOLOCK) + WHERE CurrAccTypeCode = 3 + AND AttributeTypeCode = 1 + AND AttributeCode = f.CustomerAtt01 + AND LangCode = 'TR' + ), SPACE(0)) + ), + + dbo.HG_Temizlik( + ISNULL(( + SELECT AttributeDescription + FROM cdCurrAccAttributeDesc WITH(NOLOCK) + WHERE CurrAccTypeCode = 3 + AND AttributeTypeCode = 2 + AND AttributeCode = f.CustomerAtt02 + AND LangCode = 'TR' + ), SPACE(0)) + ), + + dbo.HG_Temizlik( + ISNULL(( + SELECT AttributeDescription + FROM cdCurrAccAttributeDesc WITH(NOLOCK) + WHERE CurrAccTypeCode = 3 + AND AttributeTypeCode = 5 + AND AttributeCode = f.CustomerAtt05 + AND LangCode = 'TR' + ), SPACE(0)) + ), + + ISNULL(c.CurrencyCode, '') + + FROM cdCurrAcc c + LEFT JOIN cdCurrAccDesc d + ON c.CurrAccCode = d.CurrAccCode + LEFT JOIN CustomerAttributesFilter f + ON c.CurrAccCode = f.CurrAccCode + + WHERE + c.CompanyCode = 1 + AND c.CurrAccTypeCode = 3 + AND c.IsBlocked = 0 + AND %s + + ORDER BY d.CurrAccDescription + `, piyasaFilter) + + rows, err := db.MssqlDB.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var list []models.CustomerList + + for rows.Next() { + var c models.CustomerList + + if err := rows.Scan( + &c.CurrAccTypeCode, + &c.Cari_Kod, + &c.Cari_Ad, + &c.Musteri_Ana_Grubu, + &c.Piyasa, + &c.Musteri_Temsilcisi, + &c.Ulke, + &c.Doviz_cinsi, + ); err != nil { + return nil, err + } + + list = append(list, c) + } + + return list, rows.Err() +} diff --git a/svc/queries/get_order_list_excel.go b/svc/queries/get_order_list_excel.go new file mode 100644 index 0000000..81c2c3c --- /dev/null +++ b/svc/queries/get_order_list_excel.go @@ -0,0 +1,52 @@ +package queries + +import ( + "database/sql" + "fmt" +) + +func GetOrderListExcel( + db *sql.DB, + search string, + currAcc string, + orderDate string, +) (*sql.Rows, error) { + + q := OrderListBaseQuery + " AND 1=1 " + args := []interface{}{} + + // SEARCH + if search != "" { + q += ` +AND ( + LOWER(h.OrderNumber) LIKE LOWER(@p1) OR + LOWER(h.CurrAccCode) LIKE LOWER(@p1) OR + LOWER(ca.CurrAccDescription) LIKE LOWER(@p1) OR + LOWER(h.Description) LIKE LOWER(@p1) OR + LOWER(mt.AttributeDescription) LIKE LOWER(@p1) OR + LOWER(py.AttributeDescription) LIKE LOWER(@p1) +) +` + args = append(args, "%"+search+"%") + } + + // CURRACC + if currAcc != "" { + q += fmt.Sprintf(" AND h.CurrAccCode = @p%d ", len(args)+1) + args = append(args, currAcc) + } + + // DATE + if orderDate != "" { + q += fmt.Sprintf( + " AND CONVERT(varchar, h.OrderDate, 23) = @p%d ", + len(args)+1, + ) + args = append(args, orderDate) + } + + // ORDER BY SONDA + q += " ORDER BY h.CreatedDate DESC " + + return db.Query(q, args...) +} diff --git a/svc/queries/helpers.go b/svc/queries/helpers.go new file mode 100644 index 0000000..5dac5b1 --- /dev/null +++ b/svc/queries/helpers.go @@ -0,0 +1,174 @@ +package queries + +import ( + "bssapp-backend/models" + "database/sql" + "fmt" + "github.com/google/uuid" + "log" + "strings" + "time" +) + +// ============================================================ +// 🔥 UNIVERSAL DATETIME PARSER +// ============================================================ +func parseDateTime(str string) (time.Time, error) { + layouts := []string{ + "2006-01-02 15:04:05", // "2025-11-21 12:37:21" + time.RFC3339, // "2025-11-21T12:37:21Z" + "2006-01-02", // "2025-11-21" + } + for _, layout := range layouts { + if t, err := time.Parse(layout, str); err == nil { + return t, nil + } + } + return time.Time{}, fmt.Errorf("datetime parse edilemedi: %s", str) +} + +// ============================================================ +// 📌 DATE (YYYY-MM-DD) → sql.NullTime +// ============================================================ +func nullableDateString(ns models.NullString) sql.NullTime { + if ns.Valid && strings.TrimSpace(ns.String) != "" { + t, err := time.Parse("2006-01-02", ns.String) + if err == nil { + return sql.NullTime{Valid: true, Time: t} + } + } + return sql.NullTime{} +} + +// ============================================================ +// 📌 TIME (HH:mm:ss) → sql.NullString +// ============================================================ +func nullableTimeString(ns models.NullString) sql.NullString { + if ns.Valid && strings.TrimSpace(ns.String) != "" { + return sql.NullString{String: ns.String, Valid: true} + } + return sql.NullString{} +} + +// ============================================================ +// 📌 DATETIME (CustomTime → sql.NullTime) +// ============================================================ +func nullableDateTime(ct models.CustomTime, fallback time.Time) sql.NullTime { + if ct.Valid && !ct.Time.IsZero() { + return sql.NullTime{Valid: true, Time: ct.Time} + } + return sql.NullTime{Valid: true, Time: fallback} +} + +// ============================================================ +// 📌 DATETIME (NullTime → sql.NullTime) +// ============================================================ +func nullableTime(nt models.NullTime, fallback time.Time) sql.NullTime { + if nt.Valid && !nt.Time.IsZero() { + return sql.NullTime{Valid: true, Time: nt.Time} + } + return sql.NullTime{Valid: true, Time: fallback} +} + +// ============================================================ +// 📌 DATETIME (NullString → sql.NullTime) +// ============================================================ +func nullableDateTimeString(ns models.NullString, fallback time.Time) sql.NullTime { + if !ns.Valid || strings.TrimSpace(ns.String) == "" { + return sql.NullTime{Time: fallback, Valid: true} + } + t, err := parseDateTime(ns.String) + if err != nil { + return sql.NullTime{Time: fallback, Valid: true} + } + return sql.NullTime{Time: t, Valid: true} +} + +// ============================================================ +// 📌 STRING → sql.NullString +// ============================================================ +func nullableString(val models.NullString, fallback string) sql.NullString { + if val.Valid && strings.TrimSpace(val.String) != "" { + return sql.NullString{String: val.String, Valid: true} + } + return sql.NullString{String: fallback, Valid: true} +} + +// ============================================================ +// 📌 GUID (UNIQUEIDENTIFIER) → interface{} +// ============================================================ +func nullableUUID(v interface{}) interface{} { + switch x := v.(type) { + + // 1️⃣ models.NullUUID → direkt geri dön + case models.NullUUID: + if !x.Valid { + return nil + } + return x.UUID + + // 2️⃣ models.NullString → GUID parse et + case models.NullString: + if !x.Valid || strings.TrimSpace(x.String) == "" { + return nil + } + id, err := uuid.Parse(x.String) + if err != nil { + return nil + } + return id + + // 3️⃣ string → GUID parse et + case string: + id, err := uuid.Parse(x) + if err != nil { + return nil + } + return id + } + + return nil +} + +// ============================================================ +// 📌 NUMERIC → sql.NullX +// ============================================================ +func nullableFloat64(val models.NullFloat64, fallback float64) sql.NullFloat64 { + if val.Valid { + return sql.NullFloat64{Float64: val.Float64, Valid: true} + } + return sql.NullFloat64{Float64: fallback, Valid: true} +} + +func nullableInt16(val models.NullInt16, fallback int16) sql.NullInt16 { + if val.Valid { + return sql.NullInt16{Int16: val.Int16, Valid: true} + } + return sql.NullInt16{Int16: fallback, Valid: true} +} + +func nullableInt32(val models.NullInt32, fallback int32) sql.NullInt32 { + if val.Valid { + return sql.NullInt32{Int32: val.Int32, Valid: true} + } + return sql.NullInt32{Int32: fallback, Valid: true} +} + +func nullableInt32ToInt16(val models.NullInt32, fallback int16) sql.NullInt16 { + if val.Valid { + return sql.NullInt16{Int16: int16(val.Int32), Valid: true} + } + return sql.NullInt16{Int16: fallback, Valid: true} +} + +// ============================================================ +// 📌 BOOL → sql.NullBool +// ============================================================ +func nullableBool(val models.NullBool, fallback bool) sql.NullBool { + if val.Valid { + return sql.NullBool{Bool: val.Bool, Valid: true} + } + return sql.NullBool{Bool: fallback, Valid: true} +} + +var logger = log.Default() diff --git a/svc/queries/inventoryproduct.go b/svc/queries/inventoryproduct.go new file mode 100644 index 0000000..ac88ec5 --- /dev/null +++ b/svc/queries/inventoryproduct.go @@ -0,0 +1,93 @@ +package queries + +// 🔹 Ürüne göre stok detay sorgusu +const GetInventoryProduct = ` +-- 🔹 Ürüne göre stok detay sorgusu (Nebim V3 uyumlu, parametre güvenli) +SELECT + bsItemTypeDesc.ItemTypeDescription AS InventoryType, + inv.WarehouseCode AS Depo_Kodu, + wh.WarehouseDescription AS Depo_Adi, + inv.ItemCode AS Urun_Kodu, + ISNULL(descItem.ItemDescription, '') AS Madde_Aciklamasi, + inv.ColorCode AS Renk_Kodu, + ISNULL(descColor.ColorDescription, '') AS Renk_Aciklamasi, + inv.ItemDim1Code AS Beden, + attr.ProductAtt01 AS Urun_Grubu, + attr.ProductAtt02 AS Urun_Alt_Grubu, + attr.ProductAtt41 AS Kisa_Karisim, + attr.ProductAtt42 AS SERI, + attr.ProductAtt43 AS FASON_ISCLIK, + attr.ProductAtt45 AS ASKILI_YAN, + inv.ItemDim2Code AS YAKA, + attr.ProductAtt44 AS GARSON_YETISKIN, + attr.ProductAtt10 AS MarkaKodu, + ROUND( + SUM(inv.InventoryQty1) + - (SUM(inv.ReserveQty1) + SUM(inv.DispOrderQty1) + SUM(inv.PickingQty1)), + cdUnitOfMeasure.RoundDigit + ) AS Kullanilabilir_Envanter +FROM cdItem WITH (NOLOCK) +JOIN cdUnitOfMeasure WITH (NOLOCK) + ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode +JOIN ( + SELECT + CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, + SUM(CASE WHEN SourceTable = 'PickingStates' THEN Qty1 ELSE 0 END) AS PickingQty1, + SUM(CASE WHEN SourceTable = 'ReserveStates' THEN Qty1 ELSE 0 END) AS ReserveQty1, + SUM(CASE WHEN SourceTable = 'DispOrderStates' THEN Qty1 ELSE 0 END) AS DispOrderQty1, + SUM(CASE WHEN SourceTable = 'trStock' THEN (In_Qty1 - Out_Qty1) ELSE 0 END) AS InventoryQty1 + FROM ( + SELECT 'PickingStates' AS SourceTable, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0 AS In_Qty1, 0 AS Out_Qty1 + FROM PickingStates WITH (NOLOCK) + UNION ALL + SELECT 'ReserveStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0, 0 + FROM ReserveStates WITH (NOLOCK) + UNION ALL + SELECT 'DispOrderStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0, 0 + FROM DispOrderStates WITH (NOLOCK) + UNION ALL + SELECT 'trStock', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, 0, SUM(In_Qty1), SUM(Out_Qty1) + FROM trStock WITH (NOLOCK) + GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code + ) AS src + GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code +) AS inv + ON cdItem.ItemTypeCode = inv.ItemTypeCode + AND cdItem.ItemCode = inv.ItemCode +LEFT JOIN ProductAttributesFilter AS attr WITH (NOLOCK) + ON attr.ItemCode = inv.ItemCode +LEFT JOIN bsItemTypeDesc WITH (NOLOCK) + ON bsItemTypeDesc.ItemTypeCode = inv.ItemTypeCode + AND bsItemTypeDesc.LangCode = 'TR' +LEFT JOIN cdWarehouseDesc AS wh WITH (NOLOCK) + ON wh.WarehouseCode = inv.WarehouseCode +LEFT JOIN cdItemDesc AS descItem WITH (NOLOCK) + ON descItem.ItemTypeCode = inv.ItemTypeCode + AND descItem.ItemCode = inv.ItemCode + AND descItem.LangCode = 'TR' +LEFT JOIN cdColorDesc AS descColor WITH (NOLOCK) + ON descColor.ColorCode = inv.ColorCode + AND descColor.LangCode = 'TR' +WHERE + inv.ItemTypeCode IN (1) + AND inv.WarehouseCode IN ('1-0-12','1-0-21','1-0-10','1-0-2','1-1-3','1-2-4','1-2-5','') + AND inv.InventoryQty1 >= 0 + AND cdItem.IsBlocked = 0 + AND inv.ItemCode = @p1 -- ✅ doğrudan parametre, DECLARE yok +GROUP BY + inv.CompanyCode, inv.OfficeCode, inv.StoreTypeCode, inv.StoreCode, + inv.WarehouseCode, inv.ItemTypeCode, inv.ItemCode, inv.ColorCode, + inv.ItemDim1Code, inv.ItemDim2Code, inv.ItemDim3Code, + cdUnitOfMeasure.RoundDigit, attr.ProductAtt01, attr.ProductAtt02, + attr.ProductAtt41, attr.ProductAtt42, attr.ProductAtt43, attr.ProductAtt44, + attr.ProductAtt45, attr.ProductAtt10, bsItemTypeDesc.ItemTypeDescription, + wh.WarehouseDescription, descItem.ItemDescription, descColor.ColorDescription; + +` diff --git a/svc/queries/order_get.go b/svc/queries/order_get.go new file mode 100644 index 0000000..b7fb55a --- /dev/null +++ b/svc/queries/order_get.go @@ -0,0 +1,280 @@ +package queries + +import ( + "bssapp-backend/db" + "bssapp-backend/models" + "database/sql" + "errors" + "fmt" +) + +// GetOrderByID — Sipariş başlığı (header) ve satırlarını (lines) getirir. +func GetOrderByID(orderID string) (*models.OrderHeader, []models.OrderDetail, error) { + conn := db.GetDB() + + logger.Printf("🧾 [GetOrderByID] begin • id=%s", orderID) + + // ===================================================== + // HEADER (Cari adı join'li) + // ===================================================== + var header models.OrderHeader + qHeader := ` + SELECT + CAST(h.OrderHeaderID AS varchar(36)) AS OrderHeaderID, + h.OrderTypeCode, + h.ProcessCode, + h.OrderNumber, + h.IsCancelOrder, + h.OrderDate, + h.OrderTime, + h.DocumentNumber, + h.PaymentTerm, + h.AverageDueDate, + h.Description, + h.InternalDescription, + h.CurrAccTypeCode, + h.CurrAccCode, + d.CurrAccDescription, + h.SubCurrAccID, + h.ContactID, + h.ShipmentMethodCode, + h.ShippingPostalAddressID, + h.BillingPostalAddressID, + h.GuarantorContactID, + h.GuarantorContactID2, + h.RoundsmanCode, + h.DeliveryCompanyCode, + h.TaxTypeCode, + h.WithHoldingTaxTypeCode, + h.DOVCode, + h.TaxExemptionCode, + h.CompanyCode, + h.OfficeCode, + h.StoreTypeCode, + h.StoreCode, + h.POSTerminalID, + h.WarehouseCode, + h.ToWarehouseCode, + h.OrdererCompanyCode, + h.OrdererOfficeCode, + h.OrdererStoreCode, + h.GLTypeCode, + h.DocCurrencyCode, + h.LocalCurrencyCode, + h.ExchangeRate, + h.TDisRate1, + h.TDisRate2, + h.TDisRate3, + h.TDisRate4, + h.TDisRate5, + h.DiscountReasonCode, + h.SurplusOrderQtyToleranceRate, + h.ImportFileNumber, + h.ExportFileNumber, + h.IncotermCode1, + h.IncotermCode2, + h.LettersOfCreditNumber, + h.PaymentMethodCode, + h.IsInclutedVat, + h.IsCreditSale, + h.IsCreditableConfirmed, + h.CreditableConfirmedUser, + h.CreditableConfirmedDate, + h.IsSalesViaInternet, + h.IsSuspended, + h.IsCompleted, + h.IsPrinted, + h.IsLocked, + h.UserLocked, + h.IsClosed, + h.ApplicationCode, + h.ApplicationID, + h.CreatedUserName, + h.CreatedDate, + h.LastUpdatedUserName, + h.LastUpdatedDate, + h.IsProposalBased + FROM BAGGI_V3.dbo.trOrderHeader AS h + LEFT JOIN BAGGI_V3.dbo.cdCurrAccDesc AS d + ON h.CurrAccCode = d.CurrAccCode + WHERE h.OrderHeaderID = @p1; + ` + + err := conn.QueryRow(qHeader, orderID).Scan( + &header.OrderHeaderID, + &header.OrderTypeCode, + &header.ProcessCode, + &header.OrderNumber, + &header.IsCancelOrder, + &header.OrderDate, + &header.OrderTime, + &header.DocumentNumber, + &header.PaymentTerm, + &header.AverageDueDate, + &header.Description, + &header.InternalDescription, + &header.CurrAccTypeCode, + &header.CurrAccCode, + &header.CurrAccDescription, + &header.SubCurrAccID, + &header.ContactID, + &header.ShipmentMethodCode, + &header.ShippingPostalAddressID, + &header.BillingPostalAddressID, + &header.GuarantorContactID, + &header.GuarantorContactID2, + &header.RoundsmanCode, + &header.DeliveryCompanyCode, + &header.TaxTypeCode, + &header.WithHoldingTaxTypeCode, + &header.DOVCode, + &header.TaxExemptionCode, + &header.CompanyCode, + &header.OfficeCode, + &header.StoreTypeCode, + &header.StoreCode, + &header.POSTerminalID, + &header.WarehouseCode, + &header.ToWarehouseCode, + &header.OrdererCompanyCode, + &header.OrdererOfficeCode, + &header.OrdererStoreCode, + &header.GLTypeCode, + &header.DocCurrencyCode, + &header.LocalCurrencyCode, + &header.ExchangeRate, + &header.TDisRate1, + &header.TDisRate2, + &header.TDisRate3, + &header.TDisRate4, + &header.TDisRate5, + &header.DiscountReasonCode, + &header.SurplusOrderQtyToleranceRate, + &header.ImportFileNumber, + &header.ExportFileNumber, + &header.IncotermCode1, + &header.IncotermCode2, + &header.LettersOfCreditNumber, + &header.PaymentMethodCode, + &header.IsInclutedVat, + &header.IsCreditSale, + &header.IsCreditableConfirmed, + &header.CreditableConfirmedUser, + &header.CreditableConfirmedDate, + &header.IsSalesViaInternet, + &header.IsSuspended, + &header.IsCompleted, + &header.IsPrinted, + &header.IsLocked, + &header.UserLocked, + &header.IsClosed, + &header.ApplicationCode, + &header.ApplicationID, + &header.CreatedUserName, + &header.CreatedDate, + &header.LastUpdatedUserName, + &header.LastUpdatedDate, + &header.IsProposalBased, + ) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + logger.Printf("⚠️ [GetOrderByID] sipariş bulunamadı: %s", orderID) + return nil, nil, sql.ErrNoRows + } + logger.Printf("❌ [GetOrderByID] header sorgu hatası: %v", err) + return nil, nil, err + } + + logger.Printf("✅ [GetOrderByID] header loaded • orderNo=%v currAcc=%v", + header.OrderNumber, header.CurrAccCode.String) + + // ===================================================== + // LINES + // ===================================================== + qLines := ` + SELECT + CAST(L.OrderLineID AS varchar(36)) AS OrderLineID, + L.SortOrder, + L.ItemTypeCode, + L.ItemCode, + L.ColorCode, + L.ItemDim1Code, + L.ItemDim2Code, + L.ItemDim3Code, + L.Qty1, + L.Qty2, + L.Price, + L.VatRate, + L.PCTRate, + L.DocCurrencyCode, + L.DeliveryDate, + L.PlannedDateOfLading, + L.LineDescription, + L.IsClosed, + L.CreatedUserName, + L.CreatedDate, + L.LastUpdatedUserName, + L.LastUpdatedDate, + P.ProductAtt42Desc AS UrunIlkGrubu, + P.ProductAtt01Desc AS UrunAnaGrubu, + P.ProductAtt02Desc AS UrunAltGrubu, + P.ProductAtt38Desc AS Fit1, + P.ProductAtt39Desc AS Fit2 + FROM BAGGI_V3.dbo.trOrderLine AS L + LEFT JOIN ProductFilterWithDescription('TR') AS P + ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode)) + WHERE L.OrderHeaderID = @p1 + ORDER BY L.SortOrder ASC; + ` + + rows, err := conn.Query(qLines, orderID) + if err != nil { + logger.Printf("❌ [GetOrderByID] line sorgu hatası: %v", err) + return &header, nil, err + } + defer rows.Close() + + lines := make([]models.OrderDetail, 0, 32) + for rows.Next() { + var ln models.OrderDetail + if err := rows.Scan( + &ln.OrderLineID, + &ln.SortOrder, + &ln.ItemTypeCode, + &ln.ItemCode, + &ln.ColorCode, + &ln.ItemDim1Code, + &ln.ItemDim2Code, + &ln.ItemDim3Code, + &ln.Qty1, + &ln.Qty2, + &ln.Price, + &ln.VatRate, + &ln.PCTRate, + &ln.DocCurrencyCode, + &ln.DeliveryDate, + &ln.PlannedDateOfLading, + &ln.LineDescription, + &ln.IsClosed, + &ln.CreatedUserName, + &ln.CreatedDate, + &ln.LastUpdatedUserName, + &ln.LastUpdatedDate, + &ln.UrunIlkGrubu, + &ln.UrunAnaGrubu, + &ln.UrunAltGrubu, + &ln.Fit1, + &ln.Fit2, + ); err != nil { + return &header, nil, fmt.Errorf("line scan hatası: %w", err) + } + lines = append(lines, ln) + } + if err := rows.Err(); err != nil { + return &header, nil, fmt.Errorf("line rows hatası: %w", err) + } + + logger.Printf("📦 [GetOrderByID] lines loaded • count=%d", len(lines)) + return &header, lines, nil +} diff --git a/svc/queries/order_pdf.go b/svc/queries/order_pdf.go new file mode 100644 index 0000000..6b200c9 --- /dev/null +++ b/svc/queries/order_pdf.go @@ -0,0 +1,161 @@ +package queries + +import ( + "context" + "database/sql" +) + +/* +============================================================ + + HEADER GETIRME — OrderHeader struct’ına uygun + ============================================================ +*/ +type OrderHeaderDB struct { + OrderHeaderID string + OrderNumber string + CurrAccCode string + CurrAccName string + DocCurrency string + OrderDate sql.NullTime + Description sql.NullString + InternalDesc sql.NullString + OfficeCode sql.NullString + CreatedUser sql.NullString +} + +func GetOrderHeaderDB(ctx context.Context, db *sql.DB, id string) (*OrderHeaderDB, error) { + q := ` +SELECT + CAST(h.OrderHeaderID AS varchar(36)), + h.OrderNumber, + h.CurrAccCode, + d.CurrAccDescription, + h.DocCurrencyCode, + h.OrderDate, + h.Description, + h.InternalDescription, + h.OfficeCode, + h.CreatedUserName +FROM BAGGI_V3.dbo.trOrderHeader AS h +LEFT JOIN BAGGI_V3.dbo.cdCurrAccDesc AS d + ON h.CurrAccCode = d.CurrAccCode +WHERE h.OrderHeaderID = @p1 +` + row := db.QueryRowContext(ctx, q, id) + + var h OrderHeaderDB + err := row.Scan( + &h.OrderHeaderID, + &h.OrderNumber, + &h.CurrAccCode, + &h.CurrAccName, + &h.DocCurrency, + &h.OrderDate, + &h.Description, + &h.InternalDesc, + &h.OfficeCode, + &h.CreatedUser, + ) + if err != nil { + return nil, err + } + return &h, nil +} + +/* +============================================================ + + SATIRLARI GETIRME — OrderLineRaw struct’ına uygun + ============================================================ +*/ +type OrderLineRawDB struct { + OrderLineID sql.NullString + ItemCode string + ColorCode string + ItemDim1Code sql.NullString + ItemDim2Code sql.NullString + Qty1 sql.NullFloat64 + Price sql.NullFloat64 + DocCurrencyCode sql.NullString + DeliveryDate sql.NullTime + LineDescription sql.NullString + UrunAnaGrubu sql.NullString + UrunAltGrubu sql.NullString + IsClosed sql.NullBool + WithHoldingTaxType sql.NullString + DOVCode sql.NullString + PlannedDateOfLading sql.NullTime + CostCenterCode sql.NullString + VatCode sql.NullString + VatRate sql.NullFloat64 +} + +func GetOrderLinesDB(ctx context.Context, db *sql.DB, id string) ([]OrderLineRawDB, error) { + + q := ` +SELECT + CAST(L.OrderLineID AS varchar(36)), + L.ItemCode, + L.ColorCode, + L.ItemDim1Code, + L.ItemDim2Code, + L.Qty1, + L.Price, + L.DocCurrencyCode, + L.DeliveryDate, + L.LineDescription, + P.ProductAtt01Desc, + P.ProductAtt02Desc, + L.IsClosed, + L.WithHoldingTaxTypeCode, + L.DOVCode, + L.PlannedDateOfLading, + L.CostCenterCode, + L.VatCode, + L.VatRate +FROM BAGGI_V3.dbo.trOrderLine AS L +LEFT JOIN ProductFilterWithDescription('TR') AS P + ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode)) +WHERE L.OrderHeaderID = @p1 +ORDER BY L.SortOrder, L.OrderLineID +` + + rows, err := db.QueryContext(ctx, q, id) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []OrderLineRawDB + + for rows.Next() { + var r OrderLineRawDB + err = rows.Scan( + &r.OrderLineID, + &r.ItemCode, + &r.ColorCode, + &r.ItemDim1Code, + &r.ItemDim2Code, + &r.Qty1, + &r.Price, + &r.DocCurrencyCode, + &r.DeliveryDate, + &r.LineDescription, + &r.UrunAnaGrubu, + &r.UrunAltGrubu, + &r.IsClosed, + &r.WithHoldingTaxType, + &r.DOVCode, + &r.PlannedDateOfLading, + &r.CostCenterCode, + &r.VatCode, + &r.VatRate, + ) + if err != nil { + return nil, err + } + out = append(out, r) + } + return out, nil +} diff --git a/svc/queries/order_write.go b/svc/queries/order_write.go new file mode 100644 index 0000000..4e3f41d --- /dev/null +++ b/svc/queries/order_write.go @@ -0,0 +1,1079 @@ +// ===================== PART 1 (Satır 1-300) ===================== +// ======================================================= +// order_write.go — v4.3 FINAL (Insert + Update + Delete) +// - PCTCode her zaman "%0" olarak yazılır (cdPCT FK uyumlu) +// - INSERT/UPDATE öncesi ItemVariant Guard + Duplicate Guard (payload içi) +// - UpdateOrder: DELETE öncesi child tablolar (trOrderLineCurrency) temizlenir +// ======================================================= + +package queries + +import ( + "bssapp-backend/db" + "bssapp-backend/models" + "database/sql" + "fmt" + "github.com/google/uuid" + "strings" + "time" +) + +func nf0(v models.NullFloat64) float64 { + if !v.Valid { + return 0 + } + return v.Float64 +} + +// ======================================================= +// COMBO KEY & STRING HELPERS +// ======================================================= +func normalizeComboKey(s string) string { + return strings.ToUpper(strings.TrimSpace(s)) +} + +// makeComboKey: frontend tarafında NullString olmayan struct’lar için +func makeComboKey(ln models.OrderDetail) string { + model := safeNS(ln.ItemCode) + renk := safeNS(ln.ColorCode) + renk2 := safeNS(ln.ItemDim2Code) + beden := safeNS(ln.ItemDim1Code) + + return normalizeComboKey( + fmt.Sprintf("%s||%s||%s||%s", model, renk, beden, renk2), + ) +} + +// qtyValue → NullFloat64 güvenli float64 +func qtyValue(q models.NullFloat64) float64 { + if !q.Valid { + return 0 + } + return q.Float64 +} + +// VatCode: NullString → string +// - NULL → "" +// - "0" → "" (FK patlamasın, sadece anlamlı kodlar gönderiyoruz) +// - "%0", "%10" vb → trimlenmiş hali +func normalizeVatCode(ns models.NullString) string { + if !ns.Valid { + return "" + } + s := strings.TrimSpace(ns.String) + if s == "0" { + return "" + } + return s +} + +// PCTCode: NullString → string +// - Artık her durumda "%0" döndürür (tek PCT tipi kullanımı) +// NOT: SQL tarafında da "%0" cdPCT içinde tanımlı olmalı +func normalizePCTCode(ns models.NullString) string { + return "%0" +} + +// models.NullString → trimlenmiş string (NULL ise "") +func safeNS(ns models.NullString) string { + if !ns.Valid { + return "" + } + return strings.TrimSpace(ns.String) +} + +// ======================================================= +// COMBO KEY HELPERS (DB alanlarından) +// ======================================================= + +// makeComboKeyParts: düz string alanlardan comboKey üret +// comboKey = model || renk || beden || renk2 +func makeComboKeyParts(item, color, dim1, dim2 string) string { + item = strings.TrimSpace(item) + color = strings.TrimSpace(color) + dim1 = strings.TrimSpace(dim1) + dim2 = strings.TrimSpace(dim2) + + if item == "" && color == "" && dim1 == "" && dim2 == "" { + return "" + } + + return normalizeComboKey(item + "||" + color + "||" + dim1 + "||" + dim2) +} + +// comboFromNulls: NullString alanlardan comboKey üret +func comboFromNulls(item, color, dim1, dim2 models.NullString) string { + return makeComboKeyParts( + safeNS(item), + safeNS(color), + safeNS(dim1), + safeNS(dim2), + ) +} + +// ======================================================= +// ✅ ItemVariant Guard — INSERT / UPDATE öncesi +// ======================================================= + +// normalizeKeyPart: NullString → trim + UPPER +func normalizeKeyPart(ns models.NullString) string { + s := strings.TrimSpace(safeNS(ns)) + return strings.ToUpper(s) +} + +// ======================================================= +// AKSBIR DETECTION +// ======================================================= + +// ======================================================= +// COMBO KEY BUILDER (AKSBIR AWARE) +// ======================================================= + +// Variant check: ItemCode + ColorCode + Dim1 + Dim2 +func ValidateItemVariant(tx *sql.Tx, ln models.OrderDetail) error { + fmt.Printf( + "🧪 VARIANT GUARD INPUT | ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q Qty1=%v\n", + safeNS(ln.ClientKey), + safeNS(ln.ItemCode), + safeNS(ln.ColorCode), + safeNS(ln.ItemDim1Code), + safeNS(ln.ItemDim2Code), + safeNS(ln.ItemDim3Code), + nf0(ln.Qty1), + ) + + item := normalizeKeyPart(ln.ItemCode) + color := normalizeKeyPart(ln.ColorCode) + dim1 := normalizeKeyPart(ln.ItemDim1Code) + dim2 := normalizeKeyPart(ln.ItemDim2Code) + + // ✅ Placeholder/boş standardizasyon (SENDE "_" geliyor) + normalizeEmpty := func(s string) string { + s = strings.TrimSpace(strings.ToUpper(s)) + if s == "_" || s == "-" { + return "" + } + return s + } + + item = normalizeEmpty(item) + color = normalizeEmpty(color) + dim1 = normalizeEmpty(dim1) + dim2 = normalizeEmpty(dim2) + + if item == "" { + return fmt.Errorf( + "ItemCode boş olamaz (ClientKey=%s)", + safeNS(ln.ClientKey), + ) + fmt.Printf( + "🧪 VARIANT NORMALIZED | Item=%q Color=%q Dim1=%q Dim2=%q\n", + item, color, dim1, dim2, + ) + + } + + // İstersen debug: + // fmt.Printf("🧪 VARIANT CHECK item=%q color=%q dim1=%q dim2=%q clientKey=%s\n", item, color, dim1, dim2, safeNS(ln.ClientKey)) + + var exists int + err := tx.QueryRow(` + SELECT CASE WHEN EXISTS ( + SELECT 1 + FROM BAGGI_V3.dbo.prItemVariant V WITH (NOLOCK) + WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1 + AND ISNULL(LTRIM(RTRIM(V.ColorCode)),'') = @p2 + AND ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') = @p3 + AND ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p4 + ) THEN 1 ELSE 0 END + `, item, color, dim1, dim2).Scan(&exists) + + if err != nil { + return fmt.Errorf("ItemVariant kontrol query hatası: %w", err) + } + + if exists != 1 { + return &models.ValidationError{ + Code: "INVALID_ITEM_VARIANT", + Message: "Tanımsız ürün kombinasyonu", + ClientKey: safeNS(ln.ClientKey), + ItemCode: item, + ColorCode: color, + Dim1: dim1, + Dim2: dim2, + } + } + + return nil +} + +// ValidateOrderVariants: save/update öncesi payload satırlarını prItemVariant'a göre doğrular. +// invalid döner; error sadece DB/prepare/query hatalarında döner. +func ValidateOrderVariants(db *sql.DB, lines []models.OrderDetail) ([]models.InvalidVariant, error) { + + normalizeEmpty := func(s string) string { + s = strings.TrimSpace(strings.ToUpper(s)) + if s == "_" || s == "-" { + return "" + } + return s + } + + stmt, err := db.Prepare(` + SELECT CASE WHEN EXISTS ( + SELECT 1 + FROM BAGGI_V3.dbo.prItemVariant V WITH (NOLOCK) + WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1 + AND ISNULL(LTRIM(RTRIM(V.ColorCode)),'') = @p2 + AND ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') = @p3 + AND ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p4 + ) THEN 1 ELSE 0 END + `) + if err != nil { + return nil, fmt.Errorf("validate prepare hatası: %w", err) + } + defer stmt.Close() + + invalid := make([]models.InvalidVariant, 0) + + for i, ln := range lines { + qty := qtyValue(ln.Qty1) + if qty <= 0 { + continue + } + + item := normalizeEmpty(normalizeKeyPart(ln.ItemCode)) + color := normalizeEmpty(normalizeKeyPart(ln.ColorCode)) + dim1 := normalizeEmpty(normalizeKeyPart(ln.ItemDim1Code)) + dim2 := normalizeEmpty(normalizeKeyPart(ln.ItemDim2Code)) + + // ItemCode boş ise invalid + if strings.TrimSpace(item) == "" { + invalid = append(invalid, models.InvalidVariant{ + Index: i, + ClientKey: safeNS(ln.ClientKey), + ItemCode: item, + ColorCode: color, + Dim1: dim1, + Dim2: dim2, + Qty1: qty, + ComboKey: safeNS(ln.ComboKey), + Reason: "ItemCode boş", + }) + continue + } + + var exists int + if err := stmt.QueryRow(item, color, dim1, dim2).Scan(&exists); err != nil { + return nil, fmt.Errorf("validate query hatası (i=%d): %w", i, err) + } + + if exists != 1 { + invalid = append(invalid, models.InvalidVariant{ + Index: i, + ClientKey: safeNS(ln.ClientKey), + ItemCode: item, + ColorCode: color, + Dim1: dim1, + Dim2: dim2, + Qty1: qty, + ComboKey: safeNS(ln.ComboKey), + Reason: "prItemVariant’ta yok", + }) + } + } + + return invalid, nil +} + +// ======================================================= +// LineResult → frontend senkronu için +// ======================================================= + +type OrderLineResult struct { + ClientKey string `json:"clientKey"` + OrderLineID string `json:"orderLineID"` +} + +// ======================================================= +// PART 1 — InsertOrder (header + lines insert) — FINAL v5.1 +// ✔ OrderHeaderID backend üretir +// ✔ LOCAL-... numara gelirse gerçek WS numarası üretir +// ✔ Full debug +// ✔ Tüm satırlar INSERT edilir +// ✔ INSERT öncesi ItemVariant Guard +// ✔ Payload içi Duplicate Guard (comboKey) +// ======================================================= + +func InsertOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) (string, []OrderLineResult, error) { + conn := db.GetDB() + + fmt.Println("🟦 InsertOrder() BAŞLADI -----------------------------") + + tx, err := conn.Begin() + if err != nil { + return "", nil, fmt.Errorf("tx baslatilamadi: %w", err) + } + defer tx.Rollback() + + now := time.Now() + v3User := fmt.Sprintf("V3U%d-%s", user.V3UserGroup, user.V3Username) + + // ======================================================= + // 1) BACKEND — OrderHeaderID üretimi (HER ZAMAN) + // ======================================================= + realHeaderID := uuid.New().String() + + fmt.Println("🟩 Backend yeni OrderHeaderID üretti:", realHeaderID) + + header.OrderHeaderID = realHeaderID + + // ======================================================= + // 2) OrderNumber üretimi (LOCAL-* ise gerçek WS numarası) + // ======================================================= + if !header.OrderNumber.Valid || + strings.HasPrefix(header.OrderNumber.String, "LOCAL-") || + len(strings.TrimSpace(header.OrderNumber.String)) == 0 { + + fmt.Println("🟨 LOCAL numara geldi → gerçek WS numarası üretilecek...") + + var realNumber string + err := tx.QueryRow(` + SELECT CONCAT( + '1-WS-3-', + RIGHT('00000' + CAST(NEXT VALUE FOR BAGGI_V3.dbo.Seq_OrderNumber_WS AS VARCHAR(10)), 5) + ) + `).Scan(&realNumber) + + if err != nil { + return "", nil, fmt.Errorf("Gerçek sipariş numarası üretilemedi: %w", err) + } + + fmt.Println("🟩 Üretilen gerçek WS numarası:", realNumber) + + header.OrderNumber.String = realNumber + header.OrderNumber.Valid = true + } + + newID := realHeaderID // artık DB’ye bu yazılacak + + // ======================================================= + // 3) Döviz kuru çözümü + // ======================================================= + exRate := 1.0 + if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" { + if c, err := GetTodayCurrencyV3(conn, header.DocCurrencyCode.String); err == nil { + if c.Rate > 0 { + exRate = c.Rate + } + } + } + + // ======================================================= + // 4) HEADER INSERT + // ======================================================= + + queryHeader := ` +INSERT INTO BAGGI_V3.dbo.trOrderHeader ( + OrderHeaderID, OrderTypeCode, ProcessCode, OrderNumber, IsCancelOrder, + OrderDate, OrderTime, DocumentNumber, PaymentTerm, + AverageDueDate, Description, InternalDescription, + CurrAccTypeCode, CurrAccCode, SubCurrAccID, ContactID, + ShipmentMethodCode, ShippingPostalAddressID, BillingPostalAddressID, + GuarantorContactID, GuarantorContactID2, RoundsmanCode, + DeliveryCompanyCode, TaxTypeCode, WithHoldingTaxTypeCode, DOVCode, + TaxExemptionCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, + POSTerminalID, WarehouseCode, ToWarehouseCode, + OrdererCompanyCode, OrdererOfficeCode, OrdererStoreCode, + GLTypeCode, DocCurrencyCode, LocalCurrencyCode, ExchangeRate, + TDisRate1, TDisRate2, TDisRate3, TDisRate4, TDisRate5, + DiscountReasonCode, SurplusOrderQtyToleranceRate, + ImportFileNumber, ExportFileNumber, + IncotermCode1, IncotermCode2, LettersOfCreditNumber, + PaymentMethodCode, IsInclutedVat, IsCreditSale, IsCreditableConfirmed, + CreditableConfirmedUser, CreditableConfirmedDate, + IsSalesViaInternet, IsSuspended, IsCompleted, IsPrinted, + IsLocked, UserLocked, IsClosed, + ApplicationCode, ApplicationID, + CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate, + IsProposalBased +) +VALUES ( + @p1,@p2,@p3,@p4, + @p5,@p6,@p7,@p8, + @p9,@p10,@p11, + @p12,@p13,@p14,@p15, + @p16,@p17,@p18, + @p19,@p20,@p21, + @p22,@p23,@p24,@p25, + @p26,@p27,@p28,@p29,@p30, + @p31,@p32,@p33, + @p34,@p35,@p36, + @p37,@p38,@p39,@p40, + @p41,@p42,@p43,@p44,@p45, + @p46,@p47, + @p48,@p49, + @p50,@p51,@p52, + @p53,@p54,@p55,@p56,@p57, + @p58,@p59,@p60,@p61,@p62, + @p63,@p64,@p65, + @p66,@p67,@p68,@p69,@p70,@p71,@p72,@p73 +); +` + + fmt.Println("🟪 HEADER INSERT ÇALIŞIYOR...") + + headerParams := []any{ + header.OrderHeaderID, + nullableInt16(header.OrderTypeCode, 1), + nullableString(header.ProcessCode, "WS"), + nullableString(header.OrderNumber, ""), + nullableBool(header.IsCancelOrder, false), + + nullableDateString(header.OrderDate), + nullableTimeString(header.OrderTime), + nullableString(header.DocumentNumber, ""), + nullableInt16(header.PaymentTerm, 0), + + nullableDateString(header.AverageDueDate), + nullableString(header.Description, ""), + nullableString(header.InternalDescription, ""), + + nullableInt16(header.CurrAccTypeCode, 0), + nullableString(header.CurrAccCode, ""), + nullableUUID(header.SubCurrAccID), + nullableUUID(header.ContactID), + + nullableString(header.ShipmentMethodCode, ""), + nullableUUID(header.ShippingPostalAddressID), + nullableUUID(header.BillingPostalAddressID), + + nullableUUID(header.GuarantorContactID), + nullableUUID(header.GuarantorContactID2), + nullableString(header.RoundsmanCode, ""), + + nullableString(header.DeliveryCompanyCode, ""), + nullableInt16(header.TaxTypeCode, 0), + nullableString(header.WithHoldingTaxTypeCode, ""), + nullableString(header.DOVCode, ""), + + nullableInt16(header.TaxExemptionCode, 0), + nullableInt32ToInt16(header.CompanyCode, 1), + nullableString(header.OfficeCode, "101"), + nullableInt16(header.StoreTypeCode, 5), + nullableString(header.StoreCode, ""), + + nullableInt16(header.POSTerminalID, 0), + nullableString(header.WarehouseCode, "1-0-12"), + nullableString(header.ToWarehouseCode, ""), + + nullableInt32ToInt16(header.OrdererCompanyCode, 1), + nullableString(header.OrdererOfficeCode, "101"), + nullableString(header.OrdererStoreCode, ""), + + nullableString(header.GLTypeCode, ""), + nullableString(header.DocCurrencyCode, "TRY"), + nullableString(header.LocalCurrencyCode, "TRY"), + nullableFloat64(header.ExchangeRate, exRate), + + nullableFloat64(header.TDisRate1, 0), + nullableFloat64(header.TDisRate2, 0), + nullableFloat64(header.TDisRate3, 0), + nullableFloat64(header.TDisRate4, 0), + nullableFloat64(header.TDisRate5, 0), + + nullableInt16(header.DiscountReasonCode, 0), + nullableFloat64(header.SurplusOrderQtyToleranceRate, 0), + + nullableString(header.ImportFileNumber, ""), + nullableString(header.ExportFileNumber, ""), + + nullableString(header.IncotermCode1, ""), + nullableString(header.IncotermCode2, ""), + nullableString(header.LettersOfCreditNumber, ""), + + nullableString(header.PaymentMethodCode, ""), + nullableBool(header.IsInclutedVat, false), + nullableBool(header.IsCreditSale, true), + nullableBool(header.IsCreditableConfirmed, true), + + nullableString(header.CreditableConfirmedUser, v3User), + nullableDateTime(header.CreditableConfirmedDate, now), + + nullableBool(header.IsSalesViaInternet, false), + nullableBool(header.IsSuspended, false), + nullableBool(header.IsCompleted, false), + nullableBool(header.IsPrinted, false), + + nullableBool(header.IsLocked, false), + nullableBool(header.UserLocked, false), + nullableBool(header.IsClosed, false), + + nullableString(header.ApplicationCode, "Order"), + nullableUUID(header.ApplicationID), + + nullableString(header.CreatedUserName, v3User), + nullableDateTimeString(header.CreatedDate, now), + nullableString(header.LastUpdatedUserName, v3User), + nullableDateTimeString(header.LastUpdatedDate, now), + + nullableBool(header.IsProposalBased, false), + } + + if _, err := tx.Exec(queryHeader, headerParams...); err != nil { + fmt.Println("❌ HEADER INSERT ERROR:", err) + return "", nil, fmt.Errorf("header insert hatasi: %w", err) + } + + fmt.Println("🟩 HEADER INSERT OK — ID:", newID) + + // ======================================================= + // 5) LINE INSERT + // ======================================================= + + insStmt, err := tx.Prepare(` +INSERT INTO BAGGI_V3.dbo.trOrderLine ( + OrderLineID, + SortOrder, ItemTypeCode, ItemCode, ColorCode, + ItemDim1Code, ItemDim2Code, ItemDim3Code, + Qty1, Qty2, + CancelQty1, CancelQty2, + OrderCancelReasonCode, + DeliveryDate, PlannedDateOfLading, + LineDescription, + UsedBarcode, CostCenterCode, + VatCode, VatRate, PCTCode, PCTRate, + LDisRate1, LDisRate2, LDisRate3, LDisRate4, LDisRate5, + DocCurrencyCode, PriceCurrencyCode, PriceExchangeRate, + Price, + BaseProcessCode, BaseOrderNumber, + BaseCustomerTypeCode, BaseCustomerCode, + BaseSubCurrAccID, BaseStoreCode, + OrderHeaderID, + CreatedUserName, CreatedDate, + LastUpdatedUserName, LastUpdatedDate, + SurplusOrderQtyToleranceRate, + WithHoldingTaxTypeCode, + DOVCode +) +VALUES ( + @p1,@p2,@p3,@p4, + @p5,@p6,@p7,@p8, + @p9,@p10, + @p11,@p12, + @p13, + @p14,@p15, + @p16, + @p17,@p18, + @p19,@p20,@p21,@p22, + @p23,@p24,@p25,@p26,@p27, + @p28,@p29,@p30, + @p31, + @p32,@p33, + @p34,@p35, + @p36,@p37, + @p38, + @p39,@p40, + @p41,@p42, + @p43, + @p44, + @p45 +)`) + if err != nil { + return "", nil, fmt.Errorf("line insert stmt hazirlanamadi: %w", err) + } + defer insStmt.Close() + + lineResults := make([]OrderLineResult, 0, len(lines)) + + // ✅ Duplicate Guard (payload içi) + seenCombo := make(map[string]bool) + + for i, ln := range lines { + // ===================== PART 2 (Satır 301-600) ===================== + fmt.Println("────────────────────────────────────") + fmt.Printf("🟨 [INSERT] LINE %d — gelen OrderLineID=%s\n", i+1, ln.OrderLineID) + + // Her satır için yeni GUID + if ln.OrderLineID == "" { + newLineID := uuid.New().String() + fmt.Println("🆕 Yeni LineID üretildi:", newLineID) + ln.OrderLineID = newLineID + } + + // ✅ Duplicate Guard (comboKey) + comboKey := normalizeComboKey(safeNS(ln.ComboKey)) + if comboKey == "" { + comboKey = makeComboKey(ln) + } + + if qtyValue(ln.Qty1) > 0 && comboKey != "" { + if seenCombo[comboKey] { + return "", nil, fmt.Errorf( + "Duplicate satır (comboKey=%s, ClientKey=%s)", + comboKey, + safeNS(ln.ClientKey), + ) + } + seenCombo[comboKey] = true + } + + // V2 Logic → %0 PCT + vatCode := normalizeVatCode(ln.VatCode) + pctCode := normalizePCTCode(ln.PCTCode) + + var pctParam any + if pctCode == "" { + pctParam = nil + } else { + pctParam = pctCode + } + + planned := nullableDateString(ln.PlannedDateOfLading) + // ✅ INSERT ÖNCESİ ItemVariant GUARD + if qtyValue(ln.Qty1) > 0 { + if err := ValidateItemVariant(tx, ln); err != nil { + fmt.Println("❌ VARIANT GUARD (INSERT):", err) + return "", nil, err + } + } + fmt.Printf( + "🚨 INSERT LINE[%d] | LineID=%s ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q Qty1=%v\n", + i+1, + ln.OrderLineID, + safeNS(ln.ClientKey), + safeNS(ln.ItemCode), + safeNS(ln.ColorCode), + safeNS(ln.ItemDim1Code), + safeNS(ln.ItemDim2Code), + safeNS(ln.ItemDim3Code), + nf0(ln.Qty1), + ) + + _, err := insStmt.Exec( + ln.OrderLineID, + ln.SortOrder, + ln.ItemTypeCode, + nullableString(ln.ItemCode, ""), + safeNS(ln.ColorCode), + safeNS(ln.ItemDim1Code), + safeNS(ln.ItemDim2Code), + nullableString(ln.ItemDim3Code, ""), + ln.Qty1, ln.Qty2, + ln.CancelQty1, ln.CancelQty2, + nullableString(ln.OrderCancelReasonCode, ""), + nullableTime(ln.DeliveryDate, now), + planned, + nullableString(ln.LineDescription, ""), + nullableString(ln.UsedBarcode, ""), + nullableString(ln.CostCenterCode, ""), + vatCode, nf0(ln.VatRate), + pctParam, nf0(ln.PCTRate), + nf0(ln.LDisRate1), + nf0(ln.LDisRate2), + nf0(ln.LDisRate3), + nf0(ln.LDisRate4), + nf0(ln.LDisRate5), + nullableString(ln.DocCurrencyCode, "TRY"), + nullableString(ln.PriceCurrencyCode, "TRY"), + nf0(ln.PriceExchangeRate), + nf0(ln.Price), + nullableString(ln.BaseProcessCode, ""), + nullableString(ln.BaseOrderNumber, ""), + ln.BaseCustomerTypeCode, + nullableString(ln.BaseCustomerCode, ""), + nullableUUID(ln.BaseSubCurrAccID), + nullableString(ln.BaseStoreCode, ""), + header.OrderHeaderID, + v3User, now, + v3User, now, + nf0(ln.SurplusOrderQtyToleranceRate), + nullableString(ln.WithHoldingTaxTypeCode, ""), + nullableString(ln.DOVCode, ""), + ) + + if err != nil { + fmt.Println("❌ INSERT LINE ERROR") + fmt.Printf( + "💥 FAILED LINE | LineID=%s ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q\n", + ln.OrderLineID, + safeNS(ln.ClientKey), + safeNS(ln.ItemCode), + safeNS(ln.ColorCode), + safeNS(ln.ItemDim1Code), + safeNS(ln.ItemDim2Code), + safeNS(ln.ItemDim3Code), + ) + fmt.Println("SQL ERROR:", err) + return "", nil, fmt.Errorf("line insert hatasi: %w", err) + } + + if ln.ClientKey.Valid && ln.ClientKey.String != "" { + lineResults = append(lineResults, OrderLineResult{ + ClientKey: ln.ClientKey.String, + OrderLineID: ln.OrderLineID, + }) + } + } + + // ======================================================= + // 6) COMMIT + // ======================================================= + fmt.Println("🟨 COMMIT EDİLİYOR...") + + if err := tx.Commit(); err != nil { + fmt.Println("❌ COMMIT ERROR:", err) + return "", nil, err + } + + fmt.Println("✅ COMMIT OK — INSERT ORDER BİTTİ") + fmt.Println("────────────────────────────────────") + + return newID, lineResults, nil +} + +// ======================================================= +// PART 2 — UpdateOrder FULL DEBUG (v4.3) +// ✔ ComboKey ile açık satır eşleştirme +// ✔ Kapalı satırları korur +// ✔ Payload içi Duplicate Guard +// ✔ INSERT/UPDATE öncesi ItemVariant Guard (tek noktada) +// ✔ Grid’de olmayan açık satırları siler (önce child) +// ======================================================= + +func UpdateOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) ([]OrderLineResult, error) { + conn := db.GetDB() + + // ====================================================== + // 🔍 SCAN DEBUG — HEADER bilgisi + // ====================================================== + fmt.Println("══════════════════════════════════════") + fmt.Println("🔍 [DEBUG] UpdateOrder çağrıldı") + fmt.Printf("🔍 HeaderID: %v\n", header.OrderHeaderID) + fmt.Printf("🔍 Line sayısı: %v\n", len(lines)) + fmt.Printf("🔍 User: %v (V3: %s/%d)\n", + user.Username, user.V3Username, user.V3UserGroup) + fmt.Println("══════════════════════════════════════") + + tx, err := conn.Begin() + if err != nil { + return nil, fmt.Errorf("tx baslatilamadi: %w", err) + } + defer tx.Rollback() + + now := time.Now() + v3User := fmt.Sprintf("V3U%d-%s", user.V3UserGroup, user.V3Username) + + // Döviz kuru + exRate := 1.0 + if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" { + if c, err := GetTodayCurrencyV3(conn, header.DocCurrencyCode.String); err == nil && c.Rate > 0 { + exRate = c.Rate + } + } + + // ======================================================= + // 0) Mevcut satırları oku (GUID STRING olarak!) + // ======================================================= + + existingOpen := make(map[string]bool) + existingClosed := make(map[string]bool) + existingOpenCombo := make(map[string]string) + existingClosedCombo := make(map[string]string) + + rows, err := tx.Query(` +SELECT + CONVERT(varchar(36), OrderLineID), + ISNULL(IsClosed, 0), + ISNULL(ItemCode,''), + ISNULL(ColorCode,''), + ISNULL(ItemDim1Code,''), + ISNULL(ItemDim2Code,'') +FROM BAGGI_V3.dbo.trOrderLine +WHERE OrderHeaderID=@p1 +`, header.OrderHeaderID) + if err != nil { + return nil, fmt.Errorf("mevcut satirlar okunamadi: %w", err) + } + defer rows.Close() + + for rows.Next() { + var id, item, color, dim1, dim2 string + var closed bool + + if err := rows.Scan(&id, &closed, &item, &color, &dim1, &dim2); err != nil { + return nil, err + } + + combo := makeComboKeyParts(item, color, dim1, dim2) + + if closed { + existingClosed[id] = true + if combo != "" { + existingClosedCombo[combo] = id + } + } else { + existingOpen[id] = true + if combo != "" { + existingOpenCombo[combo] = id + } + } + } + + // ====================================================== + // HEADER UPDATE + // ====================================================== + + _, err = tx.Exec(` +UPDATE BAGGI_V3.dbo.trOrderHeader SET + OrderDate=@p1, + OrderTime=@p2, + AverageDueDate=@p3, + Description=@p4, + InternalDescription=@p5, + DocCurrencyCode=@p6, + LocalCurrencyCode=@p7, + ExchangeRate=@p8, + LastUpdatedUserName=@p9, + LastUpdatedDate=@p10 +WHERE OrderHeaderID=@p11 +`, + nullableDateString(header.OrderDate), + nullableTimeString(header.OrderTime), + nullableDateString(header.AverageDueDate), + nullableString(header.Description, ""), + nullableString(header.InternalDescription, ""), + nullableString(header.DocCurrencyCode, "TRY"), + nullableString(header.LocalCurrencyCode, "TRY"), + nullableFloat64(header.ExchangeRate, exRate), + v3User, + now, + header.OrderHeaderID, + ) + if err != nil { + return nil, err + } + // ====================================================== + // PREPARE STATEMENTS + // ====================================================== + + insStmt, err := tx.Prepare(`INSERT INTO BAGGI_V3.dbo.trOrderLine ( +OrderLineID, SortOrder, ItemTypeCode, ItemCode, ColorCode, +ItemDim1Code, ItemDim2Code, ItemDim3Code, +Qty1, Qty2, CancelQty1, CancelQty2, OrderCancelReasonCode, +DeliveryDate, PlannedDateOfLading, LineDescription, +UsedBarcode, CostCenterCode, +VatCode, VatRate, PCTCode, PCTRate, +LDisRate1, LDisRate2, LDisRate3, LDisRate4, LDisRate5, +DocCurrencyCode, PriceCurrencyCode, PriceExchangeRate, +Price, BaseProcessCode, BaseOrderNumber, +BaseCustomerTypeCode, BaseCustomerCode, +BaseSubCurrAccID, BaseStoreCode, +OrderHeaderID, CreatedUserName, CreatedDate, +LastUpdatedUserName, LastUpdatedDate, +SurplusOrderQtyToleranceRate, +WithHoldingTaxTypeCode, DOVCode) +VALUES ( +@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p10, +@p11,@p12,@p13,@p14,@p15,@p16,@p17,@p18, +@p19,@p20,@p21,@p22,@p23,@p24,@p25,@p26,@p27, +@p28,@p29,@p30,@p31,@p32,@p33,@p34,@p35, +@p36,@p37,@p38,@p39,@p40,@p41,@p42,@p43, +@p44,@p45)`) + + if err != nil { + return nil, err + } + defer insStmt.Close() + + updStmt, err := tx.Prepare(`UPDATE BAGGI_V3.dbo.trOrderLine SET +SortOrder=@p1, ItemTypeCode=@p2, ItemCode=@p3, ColorCode=@p4, +ItemDim1Code=@p5, ItemDim2Code=@p6, ItemDim3Code=@p7, +Qty1=@p8, Qty2=@p9, CancelQty1=@p10, CancelQty2=@p11, +OrderCancelReasonCode=@p12, +DeliveryDate=@p13, PlannedDateOfLading=@p14, +LineDescription=@p15, UsedBarcode=@p16, CostCenterCode=@p17, +VatCode=@p18, VatRate=@p19, PCTCode=@p20, PCTRate=@p21, +LDisRate1=@p22, LDisRate2=@p23, LDisRate3=@p24, +LDisRate4=@p25, LDisRate5=@p26, +DocCurrencyCode=@p27, PriceCurrencyCode=@p28, +PriceExchangeRate=@p29, Price=@p30, +BaseProcessCode=@p31, BaseOrderNumber=@p32, +BaseCustomerTypeCode=@p33, BaseCustomerCode=@p34, +BaseSubCurrAccID=@p35, BaseStoreCode=@p36, +LastUpdatedUserName=@p37, LastUpdatedDate=@p38, +SurplusOrderQtyToleranceRate=@p39, +WithHoldingTaxTypeCode=@p40, DOVCode=@p41 +WHERE OrderLineID=@p42 AND ISNULL(IsClosed,0)=0`) + + if err != nil { + return nil, err + } + defer updStmt.Close() + + lineResults := make([]OrderLineResult, 0) + seenCombo := make(map[string]bool) + + for _, ln := range lines { + + comboKey := normalizeComboKey(safeNS(ln.ComboKey)) + if comboKey == "" { + comboKey = makeComboKey(ln) + } + + // Duplicate guard (SADECE aktif) + if qtyValue(ln.Qty1) > 0 && comboKey != "" { + if seenCombo[comboKey] { + return nil, fmt.Errorf("Duplicate satır: %s", comboKey) + } + seenCombo[comboKey] = true + } + + // Kapalı satır + if ln.OrderLineID != "" && existingClosed[ln.OrderLineID] { + continue + } + if comboKey != "" { + if _, ok := existingClosedCombo[comboKey]; ok { + continue + } + } + + // DELETE SIGNAL + if ln.OrderLineID != "" && qtyValue(ln.Qty1) <= 0 { + _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, ln.OrderLineID) + if err != nil { + return nil, err + } + _, err = tx.Exec(` +DELETE FROM BAGGI_V3.dbo.trOrderLine +WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0 +`, header.OrderHeaderID, ln.OrderLineID) + if err != nil { + return nil, err + } + delete(existingOpen, ln.OrderLineID) + delete(existingOpenCombo, comboKey) + continue + } + isNew := false + + if ln.OrderLineID == "" { + if dbID, ok := existingOpenCombo[comboKey]; ok { + ln.OrderLineID = dbID + } else { + ln.OrderLineID = uuid.New().String() + isNew = true + } + } + + if qtyValue(ln.Qty1) > 0 { + if err := ValidateItemVariant(tx, ln); err != nil { + return nil, err + } + } + + if isNew { + _, err := insStmt.Exec( + ln.OrderLineID, ln.SortOrder, ln.ItemTypeCode, + nullableString(ln.ItemCode, ""), safeNS(ln.ColorCode), + safeNS(ln.ItemDim1Code), safeNS(ln.ItemDim2Code), + nullableString(ln.ItemDim3Code, ""), + ln.Qty1, ln.Qty2, ln.CancelQty1, ln.CancelQty2, + nullableString(ln.OrderCancelReasonCode, ""), + nullableTime(ln.DeliveryDate, now), + nullableDateString(ln.PlannedDateOfLading), + nullableString(ln.LineDescription, ""), + nullableString(ln.UsedBarcode, ""), + nullableString(ln.CostCenterCode, ""), + normalizeVatCode(ln.VatCode), nf0(ln.VatRate), + normalizePCTCode(ln.PCTCode), nf0(ln.PCTRate), + nf0(ln.LDisRate1), nf0(ln.LDisRate2), + nf0(ln.LDisRate3), nf0(ln.LDisRate4), nf0(ln.LDisRate5), + nullableString(ln.DocCurrencyCode, "TRY"), + nullableString(ln.PriceCurrencyCode, "TRY"), + nf0(ln.PriceExchangeRate), nf0(ln.Price), + nullableString(ln.BaseProcessCode, ""), + nullableString(ln.BaseOrderNumber, ""), + ln.BaseCustomerTypeCode, + nullableString(ln.BaseCustomerCode, ""), + nullableUUID(ln.BaseSubCurrAccID), + nullableString(ln.BaseStoreCode, ""), + header.OrderHeaderID, + v3User, now, v3User, now, + nf0(ln.SurplusOrderQtyToleranceRate), + nullableString(ln.WithHoldingTaxTypeCode, ""), + nullableString(ln.DOVCode, ""), + ) + if err != nil { + return nil, err + } + } else { + _, err := updStmt.Exec( + ln.SortOrder, ln.ItemTypeCode, + nullableString(ln.ItemCode, ""), + safeNS(ln.ColorCode), + safeNS(ln.ItemDim1Code), + safeNS(ln.ItemDim2Code), + nullableString(ln.ItemDim3Code, ""), + nf0(ln.Qty1), nf0(ln.Qty2), + nf0(ln.CancelQty1), nf0(ln.CancelQty2), + nullableString(ln.OrderCancelReasonCode, ""), + nullableTime(ln.DeliveryDate, now), + nullableDateString(ln.PlannedDateOfLading), + nullableString(ln.LineDescription, ""), + nullableString(ln.UsedBarcode, ""), + nullableString(ln.CostCenterCode, ""), + normalizeVatCode(ln.VatCode), nf0(ln.VatRate), + normalizePCTCode(ln.PCTCode), nf0(ln.PCTRate), + nf0(ln.LDisRate1), nf0(ln.LDisRate2), + nf0(ln.LDisRate3), nf0(ln.LDisRate4), nf0(ln.LDisRate5), + nullableString(ln.DocCurrencyCode, "TRY"), + nullableString(ln.PriceCurrencyCode, "TRY"), + nf0(ln.PriceExchangeRate), nf0(ln.Price), + nullableString(ln.BaseProcessCode, ""), + nullableString(ln.BaseOrderNumber, ""), + ln.BaseCustomerTypeCode, + nullableString(ln.BaseCustomerCode, ""), + nullableUUID(ln.BaseSubCurrAccID), + nullableString(ln.BaseStoreCode, ""), + v3User, now, + nf0(ln.SurplusOrderQtyToleranceRate), + nullableString(ln.WithHoldingTaxTypeCode, ""), + nullableString(ln.DOVCode, ""), + ln.OrderLineID, + ) + if err != nil { + return nil, err + } + } + + delete(existingOpen, ln.OrderLineID) + delete(existingOpenCombo, comboKey) + + if ln.ClientKey.Valid { + lineResults = append(lineResults, OrderLineResult{ + ClientKey: ln.ClientKey.String, + OrderLineID: ln.OrderLineID, + }) + } + } + + // Grid dışı kalan açık satırlar + for id := range existingOpen { + _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, id) + if err != nil { + return nil, err + } + _, err = tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLine WHERE OrderLineID=@p1 AND ISNULL(IsClosed,0)=0`, id) + if err != nil { + return nil, err + } + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return lineResults, nil +} diff --git a/svc/queries/orderinventory.go b/svc/queries/orderinventory.go new file mode 100644 index 0000000..a7c20b1 --- /dev/null +++ b/svc/queries/orderinventory.go @@ -0,0 +1,109 @@ +package queries + +// 🔹 Sipariş ekranı stok kontrolü (ürün + renk + 2. renk + beden) +// 🔧 Renk1 boşsa (aksesuar vb.) renksiz stokları da çeker +const GetOrderInventory = ` +DECLARE @ProductCode NVARCHAR(30) = @p1; +DECLARE @ColorCode NVARCHAR(30) = @p2; +DECLARE @ColorCode2 NVARCHAR(30) = @p3; + +-- 🔧 Normalize giriş parametreleri +SET @ColorCode = LTRIM(RTRIM(ISNULL(@ColorCode, ''))); +SET @ColorCode2 = LTRIM(RTRIM(ISNULL(@ColorCode2, ''))); + +IF @ColorCode = '' +BEGIN + SET @ColorCode = ''; +END + +IF @ColorCode2 = '' +BEGIN + SET @ColorCode2 = NULL; +END + +------------------------------------------------------------ +-- 🔹 Ana sorgu +------------------------------------------------------------ +SELECT + Inventory.ItemCode AS Urun_Kodu, + Inventory.ColorCode AS Renk_Kodu, + ISNULL(( + SELECT TOP 1 ColorDescription + FROM cdColorDesc WITH(NOLOCK) + WHERE cdColorDesc.ColorCode = Inventory.ColorCode + AND cdColorDesc.LangCode = N'TR' + ), '') AS Renk_Aciklamasi, + + -- ✅ NULL bedenleri boş string olarak getir + ISNULL(Inventory.ItemDim1Code, '') AS Beden, + ISNULL(Inventory.ItemDim2Code, '') AS Yaka, + + ROUND( + SUM(Inventory.InventoryQty1) - + (SUM(Inventory.ReserveQty1) + SUM(Inventory.DispOrderQty1) + SUM(Inventory.PickingQty1)), + cdUnitOfMeasure.RoundDigit + ) AS Kullanilabilir_Envanter + +FROM cdItem WITH (NOLOCK) +JOIN cdUnitOfMeasure WITH (NOLOCK) + ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode +JOIN ( + SELECT + CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, + SUM(CASE WHEN SourceTable = 'PickingStates' THEN Qty1 ELSE 0 END) AS PickingQty1, + SUM(CASE WHEN SourceTable = 'ReserveStates' THEN Qty1 ELSE 0 END) AS ReserveQty1, + SUM(CASE WHEN SourceTable = 'DispOrderStates' THEN Qty1 ELSE 0 END) AS DispOrderQty1, + SUM(CASE WHEN SourceTable = 'trStock' THEN (In_Qty1 - Out_Qty1) ELSE 0 END) AS InventoryQty1 + FROM ( + SELECT 'PickingStates' AS SourceTable, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, + Qty1, 0 AS In_Qty1, 0 AS Out_Qty1 + FROM PickingStates + UNION ALL + SELECT 'ReserveStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, + Qty1, 0, 0 + FROM ReserveStates + UNION ALL + SELECT 'DispOrderStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, + Qty1, 0, 0 + FROM DispOrderStates + UNION ALL + SELECT 'trStock', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, + 0, SUM(In_Qty1), SUM(Out_Qty1) + FROM trStock WITH (NOLOCK) + GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code + ) AS SourceData + GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, + ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code +) AS Inventory + ON cdItem.ItemTypeCode = Inventory.ItemTypeCode + AND cdItem.ItemCode = Inventory.ItemCode +LEFT JOIN ProductAttributesFilter + ON ProductAttributesFilter.ItemCode = Inventory.ItemCode +WHERE + Inventory.ItemTypeCode IN (1) + AND Inventory.WarehouseCode IN ('1-0-12','1-0-21','1-0-10','1-0-2','1-1-3','1-2-4','1-2-5') + AND cdItem.IsBlocked = 0 + AND Inventory.ItemCode = @ProductCode + AND ( + -- 🔹 Eğer renk girilmişse o renk; boşsa renksiz stokları getir + (LTRIM(RTRIM(@ColorCode)) <> '' AND Inventory.ColorCode = @ColorCode) + OR (LTRIM(RTRIM(@ColorCode)) = '' AND (Inventory.ColorCode IS NULL OR LTRIM(RTRIM(Inventory.ColorCode)) = '')) + ) + AND ( + -- 🔹 2. renk sadece doluysa filtrelensin + @ColorCode2 IS NULL + OR Inventory.ItemDim2Code = @ColorCode2 + ) +GROUP BY + Inventory.ItemCode, + Inventory.ColorCode, + ISNULL(Inventory.ItemDim1Code, ''), -- ✅ NULL bedenleri boş string olarak gruplar + ISNULL(Inventory.ItemDim2Code, ''), -- ✅ NULL yakaları boş string olarak gruplar + cdUnitOfMeasure.RoundDigit; +` diff --git a/svc/queries/orderlist.go b/svc/queries/orderlist.go new file mode 100644 index 0000000..78a7107 --- /dev/null +++ b/svc/queries/orderlist.go @@ -0,0 +1,213 @@ +package queries + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/authz" + "context" + "database/sql" + "fmt" +) + +// ======================================================== +// 📌 GetOrderList — FINAL + CURRENCY SAFE + PIYASA AUTHZ +// ======================================================== +func GetOrderList( + ctx context.Context, + mssql *sql.DB, + pg *sql.DB, + search string, +) (*sql.Rows, error) { + + claims, ok := auth.GetClaimsFromContext(ctx) + if !ok || claims == nil { + return nil, fmt.Errorf("unauthorized: claims not found") + } + + // ---------------------------------------------------- + // 🔐 PIYASA FILTER (ADMIN BYPASS) + // ---------------------------------------------------- + piyasaWhere := "1=1" + + if !claims.IsAdmin() { + + codes, err := authz.GetUserPiyasaCodes(pg, int(claims.ID)) + if err != nil { + return nil, fmt.Errorf("piyasa codes load error: %w", err) + } + + if len(codes) == 0 { + // hiç yetkisi yok → hiç kayıt dönmesin + piyasaWhere = "1=0" + } else { + // ⚠️ EXISTS içinde kullanılacak + piyasaWhere = authz.BuildINClause( + "UPPER(f2.CustomerAtt01)", + codes, + ) + } + } + + // ---------------------------------------------------- + // 📄 BASE QUERY + // ---------------------------------------------------- + baseQuery := fmt.Sprintf(` +SELECT + CAST(h.OrderHeaderID AS NVARCHAR(50)) AS OrderHeaderID, + ISNULL(h.OrderNumber, '') AS OrderNumber, + CONVERT(varchar, h.OrderDate, 23) AS OrderDate, + + ISNULL(h.CurrAccCode, '') AS CurrAccCode, + ISNULL(ca.CurrAccDescription, '') AS CurrAccDescription, + + ISNULL(mt.AttributeDescription, '') AS MusteriTemsilcisi, + ISNULL(py.AttributeDescription, '') AS Piyasa, + + CONVERT(varchar, h.CreditableConfirmedDate,23) AS CreditableConfirmedDate, + ISNULL(h.DocCurrencyCode,'TRY') AS DocCurrencyCode, + + ISNULL(l.TotalAmount,0) AS TotalAmount, + + ---------------------------------------------------------------- + -- USD HESABI (TRY / EUR / GBP / USD DESTEKLİ) + ---------------------------------------------------------------- + CASE + WHEN h.DocCurrencyCode = 'USD' + THEN ISNULL(l.TotalAmount,0) + + WHEN h.DocCurrencyCode = 'TRY' + AND usd.Rate > 0 + THEN ISNULL(l.TotalAmount,0) / usd.Rate + + WHEN h.DocCurrencyCode IN ('EUR','GBP') + AND cur.Rate > 0 + AND usd.Rate > 0 + THEN (ISNULL(l.TotalAmount,0) * cur.Rate) / usd.Rate + + ELSE 0 + END AS TotalAmountUSD, + + ISNULL(h.IsCreditableConfirmed,0) AS IsCreditableConfirmed, + ISNULL(h.Description,'') AS Description, + + usd.Rate AS ExchangeRateUSD + +FROM dbo.trOrderHeader h + +JOIN ( + SELECT + OrderHeaderID, + SUM(Qty1 * Price) AS TotalAmount + FROM dbo.trOrderLine + GROUP BY OrderHeaderID +) l + ON l.OrderHeaderID = h.OrderHeaderID + +LEFT JOIN dbo.cdCurrAccDesc ca + ON ca.CurrAccCode = h.CurrAccCode + AND ca.LangCode = 'TR' + +-- müşteri temsilcisi + piyasa açıklamaları +LEFT JOIN dbo.CustomerAttributesFilter f + ON f.CurrAccCode = h.CurrAccCode + +LEFT JOIN dbo.cdCurrAccAttributeDesc mt + ON mt.CurrAccTypeCode = 3 + AND mt.AttributeTypeCode = 2 + AND mt.AttributeCode = f.CustomerAtt02 + AND mt.LangCode = 'TR' + +LEFT JOIN dbo.cdCurrAccAttributeDesc py + ON py.CurrAccTypeCode = 3 + AND py.AttributeTypeCode = 1 + AND py.AttributeCode = f.CustomerAtt01 + AND py.LangCode = 'TR' + +---------------------------------------------------------------- +-- USD → TRY +---------------------------------------------------------------- +OUTER APPLY ( + SELECT TOP 1 Rate + FROM dbo.AllExchangeRates + WHERE CurrencyCode = 'USD' + AND RelationCurrencyCode = 'TRY' + AND ExchangeTypeCode = 6 + AND Rate > 0 + AND Date <= CAST(GETDATE() AS date) + ORDER BY Date DESC +) usd + +---------------------------------------------------------------- +-- ORDER PB → TRY +---------------------------------------------------------------- +OUTER APPLY ( + SELECT TOP 1 Rate + FROM dbo.AllExchangeRates + WHERE CurrencyCode = h.DocCurrencyCode + AND RelationCurrencyCode = 'TRY' + AND ExchangeTypeCode = 6 + AND Rate > 0 + AND Date <= CAST(GETDATE() AS date) + ORDER BY Date DESC +) cur + +WHERE + ISNULL(h.IsCancelOrder,0) = 0 + AND h.OrderTypeCode = 1 + AND h.ProcessCode = 'WS' + AND h.IsClosed = 0 + + -- 🔐 PIYASA AUTHZ (EXISTS — SAĞLAM YOL) + AND EXISTS ( + SELECT 1 + FROM dbo.CustomerAttributesFilter f2 + WHERE f2.CurrAccCode = h.CurrAccCode + AND %s + ) +`, piyasaWhere) + + // ---------------------------------------------------- + // 🔍 SEARCH FILTER (CASE + TR SAFE) + // ---------------------------------------------------- + if search != "" { + baseQuery += ` +AND EXISTS ( + SELECT 1 + FROM dbo.trOrderHeader h2 + LEFT JOIN dbo.cdCurrAccDesc ca2 + ON ca2.CurrAccCode = h2.CurrAccCode + AND ca2.LangCode = 'TR' + WHERE h2.OrderHeaderID = h.OrderHeaderID + AND ( + LOWER(REPLACE(REPLACE(h2.OrderNumber,'İ','I'),'ı','i')) + COLLATE Latin1_General_CI_AI LIKE LOWER(@p1) + + OR LOWER(REPLACE(REPLACE(h2.CurrAccCode,'İ','I'),'ı','i')) + COLLATE Latin1_General_CI_AI LIKE LOWER(@p1) + + OR LOWER(REPLACE(REPLACE(ca2.CurrAccDescription,'İ','I'),'ı','i')) + COLLATE Latin1_General_CI_AI LIKE LOWER(@p1) + + OR LOWER(REPLACE(REPLACE(h2.Description,'İ','I'),'ı','i')) + COLLATE Latin1_General_CI_AI LIKE LOWER(@p1) + ) +) +` + } + + // ---------------------------------------------------- + // 📌 ORDER + // ---------------------------------------------------- + baseQuery += ` +ORDER BY h.CreatedDate DESC +` + + // ---------------------------------------------------- + // ▶ EXECUTE + // ---------------------------------------------------- + if search != "" { + searchLike := fmt.Sprintf("%%%s%%", search) + return mssql.Query(baseQuery, searchLike) + } + + return mssql.Query(baseQuery) +} diff --git a/svc/queries/orderlist_base.go b/svc/queries/orderlist_base.go new file mode 100644 index 0000000..02d5afd --- /dev/null +++ b/svc/queries/orderlist_base.go @@ -0,0 +1,75 @@ +package queries + +const OrderListBaseQuery = ` +SELECT + CAST(h.OrderHeaderID AS NVARCHAR(50)) AS OrderHeaderID, + ISNULL(h.OrderNumber, '') AS OrderNumber, + CONVERT(varchar, h.OrderDate, 23) AS OrderDate, + + ISNULL(h.CurrAccCode, '') AS CurrAccCode, + ISNULL(ca.CurrAccDescription, '') AS CurrAccDescription, + + ISNULL(mt.AttributeDescription, '') AS MusteriTemsilcisi, + ISNULL(py.AttributeDescription, '') AS Piyasa, + + CONVERT(varchar, h.CreditableConfirmedDate, 23) AS CreditableConfirmedDate, + ISNULL(h.DocCurrencyCode, 'TRY') AS DocCurrencyCode, + + ISNULL(l.TotalAmount, 0) AS TotalAmount, + + CASE + WHEN h.DocCurrencyCode = 'USD' + THEN ISNULL(l.TotalAmount, 0) + WHEN h.DocCurrencyCode = 'TRY' + THEN ISNULL(l.TotalAmount, 0) / NULLIF(er.Rate, 1) + ELSE 0 + END AS TotalAmountUSD, + + ISNULL(h.IsCreditableConfirmed, 0) AS IsCreditableConfirmed, + ISNULL(h.Description, '') AS Description, + + ISNULL(er.Rate, 1) AS ExchangeRateUSD + +FROM dbo.trOrderHeader h + +JOIN ( + SELECT OrderHeaderID, SUM(Qty1 * Price) AS TotalAmount + FROM dbo.trOrderLine + GROUP BY OrderHeaderID +) l ON l.OrderHeaderID = h.OrderHeaderID + +LEFT JOIN dbo.cdCurrAccDesc ca + ON ca.CurrAccCode = h.CurrAccCode AND ca.LangCode = 'TR' + +LEFT JOIN dbo.CustomerAttributes f + ON f.CurrAccTypeCode = h.CurrAccTypeCode + AND f.CurrAccCode = h.CurrAccCode + +LEFT JOIN dbo.cdCurrAccAttributeDesc mt + ON mt.CurrAccTypeCode = f.CurrAccTypeCode + AND mt.AttributeTypeCode = 2 + AND mt.AttributeCode = f.CustomerAtt02 + AND mt.LangCode = 'TR' + +LEFT JOIN dbo.cdCurrAccAttributeDesc py + ON py.CurrAccTypeCode = f.CurrAccTypeCode + AND py.AttributeTypeCode = 3 + AND py.AttributeCode = f.CustomerAtt03 + AND py.LangCode = 'TR' + +OUTER APPLY ( + SELECT TOP 1 Rate + FROM dbo.AllExchangeRates er + WHERE er.CurrencyCode = 'USD' + AND er.RelationCurrencyCode = 'TRY' + AND er.ExchangeTypeCode = 6 + AND er.Rate > 0 + ORDER BY ABS(DATEDIFF(DAY, er.Date, GETDATE())) +) er + +WHERE + ISNULL(h.IsCancelOrder, 0) = 0 + AND h.OrderTypeCode = 1 + AND h.ProcessCode = 'WS' + AND h.IsClosed = 0 +` diff --git a/svc/queries/orderpricelistb2b.go b/svc/queries/orderpricelistb2b.go new file mode 100644 index 0000000..de26d9e --- /dev/null +++ b/svc/queries/orderpricelistb2b.go @@ -0,0 +1,36 @@ +package queries + +import ( + "bssapp-backend/models" + "database/sql" + "fmt" +) + +// GetOrderPriceListB2B → model + currency bazlı ürün fiyatını döndürür (PostgreSQL sürümü) +func GetOrderPriceListB2B(db *sql.DB, modelCode string, currency string) (*models.OrderPriceListB2B, error) { + query := ` + SELECT + mmitem.code AS ModelCode, + sdprc.crn AS CurrencyCode, + sdprc.prc AS Price, + sdprc.sdprcgrp_id AS PriceGroupID, + TO_CHAR(sdprc.zlins_dttm, 'YYYY-MM-DD') AS LastUpdate + FROM sdprc + LEFT JOIN mmitem ON sdprc.mmitem_id = mmitem.id + WHERE mmitem.code = $1 + AND sdprc.prc IS NOT NULL + AND sdprc.prc > 0 + AND sdprc.crn = $2 + AND sdprc.sdprcgrp_id = 1 + ORDER BY sdprc.zlins_dttm DESC + LIMIT 1; + ` + + row := db.QueryRow(query, modelCode, currency) + var p models.OrderPriceListB2B + err := row.Scan(&p.ModelCode, &p.CurrencyCode, &p.Price, &p.PriceGroupID, &p.LastUpdate) + if err != nil { + return nil, fmt.Errorf("ürün fiyatı bulunamadı: %v", err) + } + return &p, nil +} diff --git a/svc/queries/permission_role_dept.go b/svc/queries/permission_role_dept.go new file mode 100644 index 0000000..49f80ec --- /dev/null +++ b/svc/queries/permission_role_dept.go @@ -0,0 +1,47 @@ +package queries + +/* ====================================================== + ROLE + DEPARTMENT PERMISSIONS +====================================================== */ + +// GET +const GetRoleDepartmentPermissions = ` +SELECT + rdp.module_code, + rdp.action, + rdp.allowed +FROM vw_role_dept_permissions rdp +WHERE rdp.role_id = $1 + AND rdp.department_code = $2 +ORDER BY rdp.module_code, rdp.action +` + +// UPSERT +const UpsertRoleDepartmentPermission = ` +INSERT INTO mk_sys_role_department_permissions +( + role_id, + department_code, + module_code, + action, + allowed +) +VALUES ($1,$2,$3,$4,$5) + +ON CONFLICT ON CONSTRAINT uq_role_dept_module_action +DO UPDATE SET + allowed = EXCLUDED.allowed; + +` + +// ====================================================== +// 📦 MODULES +// ====================================================== + +const GetModuleLookup = ` +SELECT + code AS value, + name AS label +FROM mk_sys_modules +ORDER BY id +` diff --git a/svc/queries/product.go b/svc/queries/product.go new file mode 100644 index 0000000..73953db --- /dev/null +++ b/svc/queries/product.go @@ -0,0 +1,34 @@ +package queries + +import ( + "bssapp-backend/db" + "bssapp-backend/models" +) + +// GetProductList → MSSQL'den ürün listesini döndürür +func GetProductList() ([]models.Product, error) { + rows, err := db.MssqlDB.Query(` + SELECT + ProductCode + FROM ProductFilterWithDescription('TR') + WHERE + ProductAtt42 IN ('SERI', 'AKSESUAR') + AND IsBlocked = 0 + AND LEN(ProductCode) = 13 -- 🔹 yalnızca 13 karakterlik kodlar + ORDER BY ProductCode; + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var list []models.Product + for rows.Next() { + var p models.Product + if err := rows.Scan(&p.ProductCode); err != nil { + return nil, err + } + list = append(list, p) + } + return list, nil +} diff --git a/svc/queries/productcolor.go b/svc/queries/productcolor.go new file mode 100644 index 0000000..f438fe5 --- /dev/null +++ b/svc/queries/productcolor.go @@ -0,0 +1,20 @@ +package queries + +const GetProductColors = ` +DECLARE @ProductCode VARCHAR(30) = @p1; + +SELECT DISTINCT + p.ProductCode, + v.ColorCode, + ISNULL(cd.ColorDescription, '') AS ColorDescription +FROM ProductFilterWithDescription('TR') AS p +INNER JOIN prItemVariant AS v WITH(NOLOCK) + ON v.ItemCode = p.ProductCode +LEFT JOIN cdColorDesc AS cd WITH(NOLOCK) + ON cd.ColorCode = v.ColorCode + AND cd.LangCode = 'TR' +WHERE + p.ProductCode = @ProductCode + AND ISNULL(v.ColorCode, '') <> '' +ORDER BY v.ColorCode; +` diff --git a/svc/queries/productcolorsize.go b/svc/queries/productcolorsize.go new file mode 100644 index 0000000..b6346b9 --- /dev/null +++ b/svc/queries/productcolorsize.go @@ -0,0 +1,66 @@ +package queries + +const GetProductColorSizes = ` +------------------------------------------------------------ +-- 🧩 GetProductColorSizes +-- Kullanım: +-- @p1 = ProductCode +-- @p2 = ColorCode +-- @p3 = ColorCode2 +-- Açıklama: +-- Renk veya ikinci renk boşsa buna göre filtre uygular. +-- Renksiz (aksesuar) ürünlerde ColorCode='' kayıtlarını getirir. +------------------------------------------------------------ + +DECLARE @ProductCode NVARCHAR(30) = @p1; +DECLARE @ColorCode NVARCHAR(30) = @p2; +DECLARE @ColorCode2 NVARCHAR(30) = @p3; + +-- 🔧 Normalize parametreler (boşlukları kırp ve düzelt) +SET @ColorCode = LTRIM(RTRIM(ISNULL(@ColorCode, ''))); +SET @ColorCode2 = LTRIM(RTRIM(ISNULL(@ColorCode2, ''))); + +IF @ColorCode = '' +BEGIN + SET @ColorCode = ''; -- tamamen renksiz ürün +END + +IF @ColorCode2 = '' +BEGIN + SET @ColorCode2 = NULL; -- 2. renk yok +END + +------------------------------------------------------------ +-- 🔹 Ana sorgu +------------------------------------------------------------ +SELECT + Product.ProductCode, + ISNULL(prItemVariant.ColorCode, '') AS ColorCode, + ISNULL(prItemVariant.ItemDim1Code, '') AS ItemDim1Code, + ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code +FROM ProductFilterWithDescription('TR') AS Product +INNER JOIN cdItem WITH(NOLOCK) + ON cdItem.ItemTypeCode = 1 + AND cdItem.ItemCode = Product.ProductCode +LEFT JOIN prProductLot WITH(NOLOCK) + ON prProductLot.ItemTypeCode = cdItem.ItemTypeCode + AND prProductLot.ItemCode = cdItem.ItemCode + AND prProductLot.IsDefault = 1 +LEFT JOIN prItemVariant WITH(NOLOCK) + ON prItemVariant.ItemCode = Product.ProductCode +WHERE + ProductAtt42 IN ('SERI','AKSESUAR') + AND Product.IsBlocked = 0 + AND Product.ProductCode = @ProductCode + AND ( + -- 🔸 1. renk doluysa o renk + (LTRIM(RTRIM(@ColorCode)) <> '' AND prItemVariant.ColorCode = @ColorCode) + -- 🔸 1. renk boşsa renksiz varyantlar + OR (LTRIM(RTRIM(@ColorCode)) = '' AND (prItemVariant.ColorCode IS NULL OR LTRIM(RTRIM(prItemVariant.ColorCode)) = '')) + -- 🔸 2. renk varsa ek olarak dahil et + OR (@ColorCode2 IS NOT NULL AND prItemVariant.ColorCode = @ColorCode2) + ) +ORDER BY + prItemVariant.ItemDim1Code, + prItemVariant.ItemDim2Code; +` diff --git a/svc/queries/productdetail.go b/svc/queries/productdetail.go new file mode 100644 index 0000000..49f3b16 --- /dev/null +++ b/svc/queries/productdetail.go @@ -0,0 +1,36 @@ +package queries + +const GetProductDetail = ` +DECLARE @ProductCode VARCHAR(30) = @p1; + +SELECT + ProductCode, + '' AS BOS7, + ProductDescription, + Product.ProductAtt42, + Product.ProductAtt42Desc AS URUN_ILK_GRUBU, + Product.ProductAtt01, + Product.ProductAtt01Desc AS URUN_ANA_GRUBU, + Product.ProductAtt02, + Product.ProductAtt02Desc AS URUN_ALT_GRUBU, + Product.ProductAtt41, + Product.ProductAtt41Desc AS ICERIK, + Product.ProductAtt10, + Product.ProductAtt10Desc AS MARKA, + Product.ProductAtt11, + Product.ProductAtt11Desc AS DROP_, + Product.ProductAtt29, + Product.ProductAtt29Desc AS KARISIM, + Product.ProductAtt45, + Product.ProductAtt45Desc AS ASKILI_YAN, + Product.ProductAtt44, + Product.ProductAtt44Desc AS KATEGORI, + Product.ProductAtt38, + Product.ProductAtt38Desc AS FIT1, + Product.ProductAtt39, + Product.ProductAtt39Desc AS FIT2 +FROM ProductFilterWithDescription('TR') AS Product +WHERE ProductAtt42 IN ('SERI','AKSESUAR') + AND IsBlocked = 0 + AND ProductCode = @p1; +` diff --git a/svc/queries/productsecondcolor.go b/svc/queries/productsecondcolor.go new file mode 100644 index 0000000..8213c89 --- /dev/null +++ b/svc/queries/productsecondcolor.go @@ -0,0 +1,18 @@ +package queries + +const GetProductSecondColors = ` +SELECT DISTINCT + Product.ProductCode, + ISNULL(prItemVariant.ColorCode, '') AS ColorCode, + ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code +FROM prItemVariant WITH(NOLOCK) + INNER JOIN ProductFilterWithDescription('TR') AS Product + ON prItemVariant.ItemCode = Product.ProductCode + LEFT JOIN cdColorDesc AS ColorDesc WITH(NOLOCK) + ON ColorDesc.ColorCode = prItemVariant.ItemDim2Code + AND ColorDesc.LangCode = 'TR' +WHERE Product.ProductCode = @ProductCode + AND prItemVariant.ColorCode = @ColorCode + AND ISNULL(prItemVariant.ItemDim2Code, '') <> '' +GROUP BY Product.ProductCode, prItemVariant.ItemDim2Code, prItemVariant.ColorCode +` diff --git a/svc/queries/statement_header.go b/svc/queries/statement_header.go new file mode 100644 index 0000000..2dbea7e --- /dev/null +++ b/svc/queries/statement_header.go @@ -0,0 +1,172 @@ +package queries + +import ( + "bssapp-backend/db" + "bssapp-backend/models" + "database/sql" + "fmt" + "strings" +) + +// Ana tabloyu getiren fonksiyon (Vue header tablosu için) +func GetStatements(params models.StatementParams) ([]models.StatementHeader, error) { + // AccountCode normalize: "ZLA0127" → "ZLA 0127" + if len(params.AccountCode) == 7 && strings.ContainsAny(params.AccountCode, "0123456789") { + params.AccountCode = params.AccountCode[:3] + " " + params.AccountCode[3:] + } + + // Parislemler []string → '1','2','3' + parislemFilter := "''" + if len(params.Parislemler) > 0 { + quoted := make([]string, len(params.Parislemler)) + for i, v := range params.Parislemler { + quoted[i] = fmt.Sprintf("'%s'", v) + } + parislemFilter = strings.Join(quoted, ",") + } + + query := fmt.Sprintf(` +;WITH Opening AS ( + SELECT + b.CurrAccCode AS Cari_Kod, + b.DocCurrencyCode AS Para_Birimi, + SUM(c.Debit - c.Credit) AS Devir_Bakiyesi + FROM trCurrAccBook b + LEFT JOIN trCurrAccBookCurrency c + ON c.CurrAccBookID = b.CurrAccBookID + AND c.CurrencyCode = b.DocCurrencyCode + WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%' + AND b.DocumentDate < @startdate + AND EXISTS ( + SELECT 1 + FROM CurrAccBookATAttributesFilter f2 + WHERE f2.CurrAccBookID = b.CurrAccBookID + AND f2.ATAtt01 IN (%s) + ) + GROUP BY b.CurrAccCode, b.DocCurrencyCode +), +Movements AS ( + SELECT + b.CurrAccCode AS Cari_Kod, + d.CurrAccDescription AS Cari_Isim, + CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi, + CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi, + b.RefNumber AS Belge_No, + b.BaseApplicationCode AS Islem_Tipi, + b.LineDescription AS Aciklama, + b.DocCurrencyCode AS Para_Birimi, + c.Debit AS Borc, + c.Credit AS Alacak, + SUM(c.Debit - c.Credit) + OVER (PARTITION BY b.CurrAccCode, c.CurrencyCode + ORDER BY b.DocumentDate, b.CurrAccBookID) AS Hareket_Bakiyesi, + f.ATAtt01 AS Parislemtipi + FROM trCurrAccBook b + LEFT JOIN cdCurrAccDesc d + ON b.CurrAccCode = d.CurrAccCode + LEFT JOIN trCurrAccBookCurrency c + ON b.CurrAccBookID = c.CurrAccBookID + AND b.DocCurrencyCode = c.CurrencyCode + LEFT JOIN CurrAccBookATAttributesFilter f + ON b.CurrAccBookID = f.CurrAccBookID + WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%' + AND b.DocumentDate BETWEEN @startdate AND @enddate + AND EXISTS ( + SELECT 1 + FROM CurrAccBookATAttributesFilter f2 + WHERE f2.CurrAccBookID = b.CurrAccBookID + AND f2.ATAtt01 IN (%s) + ) +) +SELECT + m.Cari_Kod, + m.Cari_Isim, + m.Belge_Tarihi, + m.Vade_Tarihi, + m.Belge_No, + m.Islem_Tipi, + m.Aciklama, + m.Para_Birimi, + m.Borc, + m.Alacak, + ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye, + m.Parislemtipi AS Parislemler +FROM Movements m +LEFT JOIN Opening o + ON o.Cari_Kod = m.Cari_Kod + AND o.Para_Birimi = m.Para_Birimi + +UNION ALL + +-- Devir satırı +SELECT + @Carikod AS Cari_Kod, + MAX(d.CurrAccDescription) AS Cari_Isim, + CONVERT(varchar(10), @startdate, 23) AS Belge_Tarihi, + CONVERT(varchar(10), @startdate, 23) AS Vade_Tarihi, + 'Baslangic_devir' AS Belge_No, + 'Devir' AS Islem_Tipi, + 'Devir Bakiyesi' AS Aciklama, + b.DocCurrencyCode AS Para_Birimi, + SUM(c.Debit) AS Borc, + SUM(c.Credit) AS Alacak, + SUM(c.Debit) - SUM(c.Credit) AS Bakiye, + ( + SELECT STRING_AGG(x.ATAtt01, ',') + FROM ( + SELECT DISTINCT f2.ATAtt01 + FROM CurrAccBookATAttributesFilter f2 + INNER JOIN trCurrAccBook bb + ON f2.CurrAccBookID = bb.CurrAccBookID + WHERE bb.CurrAccCode LIKE '%%' + @Carikod + '%%' + AND bb.DocumentDate < @startdate + AND f2.ATAtt01 IN (%s) + ) x + ) AS Parislemler +FROM trCurrAccBook b +LEFT JOIN cdCurrAccDesc d + ON b.CurrAccCode = d.CurrAccCode +LEFT JOIN trCurrAccBookCurrency c + ON b.CurrAccBookID = c.CurrAccBookID + AND b.DocCurrencyCode = c.CurrencyCode +WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%' + AND b.DocumentDate < @startdate +GROUP BY b.DocCurrencyCode + +ORDER BY Para_Birimi, Belge_Tarihi; +`, parislemFilter, parislemFilter, parislemFilter) + + rows, err := db.MssqlDB.Query(query, + sql.Named("startdate", params.StartDate), + sql.Named("enddate", params.EndDate), + sql.Named("Carikod", params.AccountCode), + sql.Named("LangCode", params.LangCode), + ) + if err != nil { + return nil, fmt.Errorf("MSSQL query error: %v", err) + } + defer rows.Close() + + var results []models.StatementHeader + for rows.Next() { + var r models.StatementHeader + if err := rows.Scan( + &r.CariKod, + &r.CariIsim, + &r.BelgeTarihi, + &r.VadeTarihi, + &r.BelgeNo, + &r.IslemTipi, + &r.Aciklama, + &r.ParaBirimi, + &r.Borc, + &r.Alacak, + &r.Bakiye, + &r.Parislemler, + ); err != nil { + return nil, err + } + results = append(results, r) + } + return results, nil +} diff --git a/svc/queries/statement_header_pdf.go b/svc/queries/statement_header_pdf.go new file mode 100644 index 0000000..c05f7d0 --- /dev/null +++ b/svc/queries/statement_header_pdf.go @@ -0,0 +1,187 @@ +// queries/statements_header_pdf.go +package queries + +import ( + "bssapp-backend/db" + "bssapp-backend/models" + "database/sql" + "fmt" + "log" + "strings" +) + +// küçük yardımcı: boşlukları temizle, her değeri ayrı tırnakla sar +func buildQuotedHList(vals []string) string { + var pp []string + for _, v := range vals { + v = strings.TrimSpace(v) + if v != "" { + pp = append(pp, fmt.Sprintf("'%s'", v)) // '1','2' gibi + } + } + if len(pp) == 0 { + return "" + } + return strings.Join(pp, ",") +} + +/* ============================ HEADER (Ana Tablo) ============================ */ + +func GetStatementsHPDF(accountCode, startDate, endDate string, parislemler []string) ([]models.StatementHeader, []string, error) { + // Account normalize + if len(accountCode) == 7 && strings.ContainsAny(accountCode, "0123456789") { + accountCode = accountCode[:3] + " " + accountCode[3:] + } + + // IN list parse et + inList := buildQuotedHList(parislemler) + parislemCond := "''" + if inList != "" { + parislemCond = inList + } + + query := fmt.Sprintf(` +;WITH Opening AS ( + SELECT + b.CurrAccCode AS Cari_Kod, + b.DocCurrencyCode AS Para_Birimi, + SUM(c.Debit - c.Credit) AS Devir_Bakiyesi + FROM trCurrAccBook b + LEFT JOIN trCurrAccBookCurrency c + ON c.CurrAccBookID = b.CurrAccBookID + AND c.CurrencyCode = b.DocCurrencyCode + WHERE b.CurrAccCode LIKE @Carikod + AND b.DocumentDate < @StartDate + AND EXISTS ( + SELECT 1 + FROM CurrAccBookATAttributesFilter f2 + WHERE f2.CurrAccBookID = b.CurrAccBookID + AND f2.ATAtt01 IN (%s) + ) + GROUP BY b.CurrAccCode, b.DocCurrencyCode +), +Movements AS ( + SELECT + b.CurrAccCode AS Cari_Kod, + d.CurrAccDescription AS Cari_Isim, + CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi, + CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi, + b.RefNumber AS Belge_No, + b.BaseApplicationCode AS Islem_Tipi, + b.LineDescription AS Aciklama, + b.DocCurrencyCode AS Para_Birimi, + c.Debit AS Borc, + c.Credit AS Alacak, + SUM(c.Debit - c.Credit) + OVER (PARTITION BY b.CurrAccCode, c.CurrencyCode + ORDER BY b.DocumentDate, b.CurrAccBookID) AS Hareket_Bakiyesi, + f.ATAtt01 AS Parislemler + FROM trCurrAccBook b + LEFT JOIN cdCurrAccDesc d + ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR' + LEFT JOIN trCurrAccBookCurrency c + ON b.CurrAccBookID = c.CurrAccBookID + AND b.DocCurrencyCode = c.CurrencyCode + LEFT JOIN CurrAccBookATAttributesFilter f + ON b.CurrAccBookID = f.CurrAccBookID + WHERE b.CurrAccCode LIKE @Carikod + AND b.DocumentDate BETWEEN @StartDate AND @EndDate + AND EXISTS ( + SELECT 1 + FROM CurrAccBookATAttributesFilter f2 + WHERE f2.CurrAccBookID = b.CurrAccBookID + AND f2.ATAtt01 IN (%s) + ) +)`, parislemCond, parislemCond) + query += fmt.Sprintf(` +SELECT + m.Cari_Kod, + m.Cari_Isim, + m.Belge_Tarihi, + m.Vade_Tarihi, + m.Belge_No, + m.Islem_Tipi, + m.Aciklama, + m.Para_Birimi, + m.Borc, + m.Alacak, + ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye, + m.Parislemler +FROM Movements m +LEFT JOIN Opening o + ON o.Cari_Kod = m.Cari_Kod + AND o.Para_Birimi = m.Para_Birimi + +UNION ALL + +-- Devir satırı +SELECT + @Carikod AS Cari_Kod, + MAX(d.CurrAccDescription) AS Cari_Isim, + CONVERT(varchar(10), @StartDate, 23) AS Belge_Tarihi, + CONVERT(varchar(10), @StartDate, 23) AS Vade_Tarihi, + 'Baslangic_devir' AS Belge_No, + 'Devir' AS Islem_Tipi, + 'Devir Bakiyesi' AS Aciklama, + b.DocCurrencyCode AS Para_Birimi, + SUM(c.Debit) AS Borc, + SUM(c.Credit) AS Alacak, + SUM(c.Debit) - SUM(c.Credit) AS Bakiye, + ( + SELECT STRING_AGG(x.ATAtt01, ',') + FROM ( + SELECT DISTINCT f2.ATAtt01 + FROM CurrAccBookATAttributesFilter f2 + INNER JOIN trCurrAccBook bb + ON f2.CurrAccBookID = bb.CurrAccBookID + WHERE bb.CurrAccCode LIKE @Carikod + AND bb.DocumentDate < @StartDate + AND f2.ATAtt01 IN (%s) + ) x + ) AS Parislemler +FROM trCurrAccBook b +LEFT JOIN cdCurrAccDesc d + ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR' +LEFT JOIN trCurrAccBookCurrency c + ON b.CurrAccBookID = c.CurrAccBookID + AND b.DocCurrencyCode = c.CurrencyCode +WHERE b.CurrAccCode LIKE @Carikod + AND b.DocumentDate < @StartDate +GROUP BY b.DocCurrencyCode + +ORDER BY Para_Birimi, Belge_Tarihi;`, parislemCond) + + rows, err := db.MssqlDB.Query(query, + sql.Named("Carikod", "%"+accountCode+"%"), + sql.Named("StartDate", startDate), + sql.Named("EndDate", endDate), + ) + if err != nil { + log.Printf("❌ Header sorgu hatası: %v", err) + return nil, nil, fmt.Errorf("header sorgu hatası: %v", err) + } + defer rows.Close() + + var headers []models.StatementHeader + var belgeNos []string + for rows.Next() { + var h models.StatementHeader + if err := rows.Scan( + &h.CariKod, &h.CariIsim, + &h.BelgeTarihi, &h.VadeTarihi, + &h.BelgeNo, &h.IslemTipi, + &h.Aciklama, &h.ParaBirimi, + &h.Borc, &h.Alacak, + &h.Bakiye, &h.Parislemler, + ); err != nil { + log.Printf("❌ Header scan hatası: %v", err) + return nil, nil, err + } + headers = append(headers, h) + if h.BelgeNo != "" { + belgeNos = append(belgeNos, h.BelgeNo) + } + } + log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos)) + return headers, belgeNos, nil +} diff --git a/svc/queries/statements_detail.go b/svc/queries/statements_detail.go new file mode 100644 index 0000000..67b27f9 --- /dev/null +++ b/svc/queries/statements_detail.go @@ -0,0 +1,121 @@ +package queries + +import ( + "bssapp-backend/db" + "bssapp-backend/models" + "database/sql" + "fmt" + "strings" +) + +/* ============================ DETAIL (ALT TABLO) ============================ */ + +func GetStatementDetails(accountCode, startDate, endDate string, parislemler []string) ([]models.StatementDetail, error) { + // Parislemler filtresi hazırlanır (ör: 1,2,3) + inParislem := "" + if len(parislemler) > 0 { + pp := make([]string, len(parislemler)) + for i, v := range parislemler { + pp[i] = strings.TrimSpace(v) + } + inParislem = strings.Join(pp, ",") + } + query := fmt.Sprintf(` +SELECT + CONVERT(varchar(10), a.InvoiceDate, 23) AS Belge_Tarihi, + a.InvoiceNumber AS Belge_Ref_Numarasi, + COALESCE(MAX(AnaGrupDesc.AttributeDescription), '') AS Urun_Ana_Grubu, + COALESCE(MAX(AltGrupDesc.AttributeDescription), '') AS Urun_Alt_Grubu, + COALESCE(MAX(GarsonDesc.AttributeDescription), '') AS Yetiskin_Garson, + COALESCE(MAX(FitDesc.AttributeDescription), '') AS Fit, + COALESCE(MAX(KisaKarDesc.AttributeDescription), '') AS Icerik, + a.ItemCode AS Urun_Kodu, + a.ColorCode AS Urun_Rengi, + SUM(a.Qty1) AS Toplam_Adet, + SUM(ABS(a.Doc_Price)) AS Toplam_Fiyat, + CAST(SUM(a.Qty1 * ABS(a.Doc_Price)) AS numeric(18,2)) AS Toplam_Tutar +FROM AllInvoicesWithAttributes a +LEFT JOIN prItemAttribute AnaGrup + ON a.ItemCode = AnaGrup.ItemCode AND AnaGrup.AttributeTypeCode = 1 +LEFT JOIN cdItemAttributeDesc AnaGrupDesc + ON AnaGrup.AttributeTypeCode = AnaGrupDesc.AttributeTypeCode + AND AnaGrup.AttributeCode = AnaGrupDesc.AttributeCode + AND AnaGrup.ItemTypeCode = AnaGrupDesc.ItemTypeCode + +LEFT JOIN prItemAttribute AltGrup + ON a.ItemCode = AltGrup.ItemCode AND AltGrup.AttributeTypeCode = 2 +LEFT JOIN cdItemAttributeDesc AltGrupDesc + ON AltGrup.AttributeTypeCode = AltGrupDesc.AttributeTypeCode + AND AltGrup.AttributeCode = AltGrupDesc.AttributeCode + AND AltGrup.ItemTypeCode = AltGrupDesc.ItemTypeCode + +LEFT JOIN prItemAttribute Garson + ON a.ItemCode = Garson.ItemCode AND Garson.AttributeTypeCode = 44 +LEFT JOIN cdItemAttributeDesc GarsonDesc + ON Garson.AttributeTypeCode = GarsonDesc.AttributeTypeCode + AND Garson.AttributeCode = GarsonDesc.AttributeCode + AND Garson.ItemTypeCode = GarsonDesc.ItemTypeCode + +LEFT JOIN prItemAttribute FitTbl + ON a.ItemCode = FitTbl.ItemCode AND FitTbl.AttributeTypeCode = 38 +LEFT JOIN cdItemAttributeDesc FitDesc + ON FitTbl.AttributeTypeCode = FitDesc.AttributeTypeCode + AND FitTbl.AttributeCode = FitDesc.AttributeCode + AND FitTbl.ItemTypeCode = FitDesc.ItemTypeCode + +LEFT JOIN prItemAttribute KisaKar + ON a.ItemCode = KisaKar.ItemCode AND KisaKar.AttributeTypeCode = 41 +LEFT JOIN cdItemAttributeDesc KisaKarDesc + ON KisaKar.AttributeTypeCode = KisaKarDesc.AttributeTypeCode + AND KisaKar.AttributeCode = KisaKarDesc.AttributeCode + AND KisaKar.ItemTypeCode = KisaKarDesc.ItemTypeCode +WHERE a.CurrAccCode LIKE @Carikod + AND a.InvoiceDate BETWEEN @StartDate AND @EndDate + %s +GROUP BY a.InvoiceDate, a.InvoiceNumber, a.ItemCode, a.ColorCode +ORDER BY Belge_Tarihi, Belge_Ref_Numarasi, Urun_Kodu;`, + func() string { + if inParislem == "" { + return "" + } + return fmt.Sprintf(`AND EXISTS ( + SELECT 1 + FROM CurrAccBookATAttributesFilter f + WHERE f.CurrAccBookID = a.CurrAccBookID + AND f.ATAtt01 IN (%s) + )`, inParislem) + }(), + ) + rows, err := db.MssqlDB.Query(query, + sql.Named("Carikod", "%"+accountCode+"%"), + sql.Named("StartDate", startDate), + sql.Named("EndDate", endDate), + ) + if err != nil { + return nil, fmt.Errorf("detay sorgu hatası: %v", err) + } + defer rows.Close() + + var results []models.StatementDetail + for rows.Next() { + var d models.StatementDetail + if err := rows.Scan( + &d.BelgeTarihi, + &d.BelgeRefNumarasi, + &d.UrunAnaGrubu, + &d.UrunAltGrubu, + &d.YetiskinGarson, + &d.Fit, + &d.Icerik, + &d.UrunKodu, + &d.UrunRengi, + &d.ToplamAdet, + &d.ToplamFiyat, + &d.ToplamTutar, + ); err != nil { + return nil, err + } + results = append(results, d) + } + return results, nil +} diff --git a/svc/queries/statements_pdf.go b/svc/queries/statements_pdf.go new file mode 100644 index 0000000..a1a84f9 --- /dev/null +++ b/svc/queries/statements_pdf.go @@ -0,0 +1,305 @@ +// queries/statements_pdf.go +package queries + +import ( + "bssapp-backend/db" + "bssapp-backend/models" + "database/sql" + "fmt" + "log" + "strings" +) + +// küçük yardımcı: boşlukları temizle, her değeri ayrı tırnakla sar +func buildQuotedList(vals []string) string { + var pp []string + for _, v := range vals { + v = strings.TrimSpace(v) + if v != "" { + pp = append(pp, fmt.Sprintf("'%s'", v)) // '1','2' gibi + } + } + if len(pp) == 0 { + return "" + } + return strings.Join(pp, ",") +} + +/* ============================ HEADER (Ana Tablo) ============================ */ + +func GetStatementsPDF(accountCode, startDate, endDate string, parislemler []string) ([]models.StatementHeader, []string, error) { + // Account normalize + if len(accountCode) == 7 && strings.ContainsAny(accountCode, "0123456789") { + accountCode = accountCode[:3] + " " + accountCode[3:] + } + + // IN list parse et + inList := buildQuotedList(parislemler) + parislemCond := "''" + if inList != "" { + parislemCond = inList + } + + query := fmt.Sprintf(` +;WITH Opening AS ( + SELECT + b.CurrAccCode AS Cari_Kod, + b.DocCurrencyCode AS Para_Birimi, + SUM(c.Debit - c.Credit) AS Devir_Bakiyesi + FROM trCurrAccBook b + LEFT JOIN trCurrAccBookCurrency c + ON c.CurrAccBookID = b.CurrAccBookID + AND c.CurrencyCode = b.DocCurrencyCode + WHERE b.CurrAccCode LIKE @Carikod + AND b.DocumentDate < @StartDate + AND EXISTS ( + SELECT 1 + FROM CurrAccBookATAttributesFilter f2 + WHERE f2.CurrAccBookID = b.CurrAccBookID + AND f2.ATAtt01 IN (%s) + ) + GROUP BY b.CurrAccCode, b.DocCurrencyCode +), +Movements AS ( + SELECT + b.CurrAccCode AS Cari_Kod, + d.CurrAccDescription AS Cari_Isim, + CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi, + CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi, + b.RefNumber AS Belge_No, + b.BaseApplicationCode AS Islem_Tipi, + b.LineDescription AS Aciklama, + b.DocCurrencyCode AS Para_Birimi, + c.Debit AS Borc, + c.Credit AS Alacak, + SUM(c.Debit - c.Credit) + OVER (PARTITION BY b.CurrAccCode, c.CurrencyCode + ORDER BY b.DocumentDate, b.CurrAccBookID) AS Hareket_Bakiyesi, + f.ATAtt01 AS Parislemler + FROM trCurrAccBook b + LEFT JOIN cdCurrAccDesc d + ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR' + LEFT JOIN trCurrAccBookCurrency c + ON b.CurrAccBookID = c.CurrAccBookID + AND b.DocCurrencyCode = c.CurrencyCode + LEFT JOIN CurrAccBookATAttributesFilter f + ON b.CurrAccBookID = f.CurrAccBookID + WHERE b.CurrAccCode LIKE @Carikod + AND b.DocumentDate BETWEEN @StartDate AND @EndDate + AND EXISTS ( + SELECT 1 + FROM CurrAccBookATAttributesFilter f2 + WHERE f2.CurrAccBookID = b.CurrAccBookID + AND f2.ATAtt01 IN (%s) + ) +)`, parislemCond, parislemCond) + query += fmt.Sprintf(` +SELECT + m.Cari_Kod, + m.Cari_Isim, + m.Belge_Tarihi, + m.Vade_Tarihi, + m.Belge_No, + m.Islem_Tipi, + m.Aciklama, + m.Para_Birimi, + m.Borc, + m.Alacak, + ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye, + m.Parislemler +FROM Movements m +LEFT JOIN Opening o + ON o.Cari_Kod = m.Cari_Kod + AND o.Para_Birimi = m.Para_Birimi + +UNION ALL + +-- Devir satırı +SELECT + @Carikod AS Cari_Kod, + MAX(d.CurrAccDescription) AS Cari_Isim, + CONVERT(varchar(10), @StartDate, 23) AS Belge_Tarihi, + CONVERT(varchar(10), @StartDate, 23) AS Vade_Tarihi, + 'Baslangic_devir' AS Belge_No, + 'Devir' AS Islem_Tipi, + 'Devir Bakiyesi' AS Aciklama, + b.DocCurrencyCode AS Para_Birimi, + SUM(c.Debit) AS Borc, + SUM(c.Credit) AS Alacak, + SUM(c.Debit) - SUM(c.Credit) AS Bakiye, + ( + SELECT STRING_AGG(x.ATAtt01, ',') + FROM ( + SELECT DISTINCT f2.ATAtt01 + FROM CurrAccBookATAttributesFilter f2 + INNER JOIN trCurrAccBook bb + ON f2.CurrAccBookID = bb.CurrAccBookID + WHERE bb.CurrAccCode LIKE @Carikod + AND bb.DocumentDate < @StartDate + AND f2.ATAtt01 IN (%s) + ) x + ) AS Parislemler +FROM trCurrAccBook b +LEFT JOIN cdCurrAccDesc d + ON b.CurrAccCode = d.CurrAccCode AND d.LangCode = 'TR' +LEFT JOIN trCurrAccBookCurrency c + ON b.CurrAccBookID = c.CurrAccBookID + AND b.DocCurrencyCode = c.CurrencyCode +WHERE b.CurrAccCode LIKE @Carikod + AND b.DocumentDate < @StartDate +GROUP BY b.DocCurrencyCode + +ORDER BY Para_Birimi, Belge_Tarihi;`, parislemCond) + + rows, err := db.MssqlDB.Query(query, + sql.Named("Carikod", "%"+accountCode+"%"), + sql.Named("StartDate", startDate), + sql.Named("EndDate", endDate), + ) + if err != nil { + log.Printf("❌ Header sorgu hatası: %v", err) + return nil, nil, fmt.Errorf("header sorgu hatası: %v", err) + } + defer rows.Close() + + var headers []models.StatementHeader + var belgeNos []string + for rows.Next() { + var h models.StatementHeader + if err := rows.Scan( + &h.CariKod, &h.CariIsim, + &h.BelgeTarihi, &h.VadeTarihi, + &h.BelgeNo, &h.IslemTipi, + &h.Aciklama, &h.ParaBirimi, + &h.Borc, &h.Alacak, + &h.Bakiye, &h.Parislemler, + ); err != nil { + log.Printf("❌ Header scan hatası: %v", err) + return nil, nil, err + } + headers = append(headers, h) + if h.BelgeNo != "" { + belgeNos = append(belgeNos, h.BelgeNo) + } + } + log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos)) + return headers, belgeNos, nil +} + +/* ============================ DETAIL (Alt Tablo) ============================ */ + +func GetDetailsMapPDF(belgeNos []string, startDate, endDate string) (map[string][]models.StatementDetail, error) { + result := make(map[string][]models.StatementDetail) + if len(belgeNos) == 0 { + log.Println("⚠️ GetDetailsMapPDF: belge listesi boş") + return result, nil + } + + qs := make([]string, 0, len(belgeNos)) + for _, no := range belgeNos { + safe := strings.ReplaceAll(no, "'", "''") + qs = append(qs, fmt.Sprintf("'%s'", safe)) + } + inBelge := strings.Join(qs, ",") + + query := fmt.Sprintf(` +;WITH BookMap AS ( + SELECT b.RefNumber AS InvoiceNumber, b.CurrAccBookID + FROM trCurrAccBook b + WHERE b.RefNumber IN (%s) +) +SELECT + CONVERT(varchar(10), a.InvoiceDate, 23) AS Belge_Tarihi, + a.InvoiceNumber AS Belge_Ref_Numarasi, + + MAX(ISNULL(AnaGrupDesc.AttributeDescription, '')) AS Urun_Ana_Grubu, + MAX(ISNULL(AltGrupDesc.AttributeDescription, '')) AS Urun_Alt_Grubu, + MAX(ISNULL(GarsonDesc.AttributeDescription, '')) AS Yetiskin_Garson, + MAX(ISNULL(FitDesc.AttributeDescription, '')) AS Fit, + MAX(ISNULL(KisaKarDesc.AttributeDescription, '')) AS Icerik, + + a.ItemCode, a.ColorCode, + SUM(a.Qty1), SUM(ABS(a.Doc_Price)), + CAST(SUM(a.Qty1 * ABS(a.Doc_Price)) AS numeric(18,2)) + +FROM AllInvoicesWithAttributes a +JOIN BookMap bm + ON bm.InvoiceNumber = a.InvoiceNumber + +-- Ana Grup +LEFT JOIN prItemAttribute AnaGrup + ON a.ItemCode = AnaGrup.ItemCode AND AnaGrup.AttributeTypeCode = 1 +LEFT JOIN cdItemAttributeDesc AnaGrupDesc + ON AnaGrup.AttributeTypeCode = AnaGrupDesc.AttributeTypeCode + AND AnaGrup.AttributeCode = AnaGrupDesc.AttributeCode + AND AnaGrup.ItemTypeCode = AnaGrupDesc.ItemTypeCode + +-- Alt Grup +LEFT JOIN prItemAttribute AltGrup + ON a.ItemCode = AltGrup.ItemCode AND AltGrup.AttributeTypeCode = 2 +LEFT JOIN cdItemAttributeDesc AltGrupDesc + ON AltGrup.AttributeTypeCode = AltGrupDesc.AttributeTypeCode + AND AltGrup.AttributeCode = AltGrupDesc.AttributeCode + AND AltGrup.ItemTypeCode = AltGrupDesc.ItemTypeCode + +-- Garson +LEFT JOIN prItemAttribute Garson + ON a.ItemCode = Garson.ItemCode AND Garson.AttributeTypeCode = 44 +LEFT JOIN cdItemAttributeDesc GarsonDesc + ON Garson.AttributeTypeCode = GarsonDesc.AttributeTypeCode + AND Garson.AttributeCode = GarsonDesc.AttributeCode + AND Garson.ItemTypeCode = GarsonDesc.ItemTypeCode + +-- Fit +LEFT JOIN prItemAttribute FitTbl + ON a.ItemCode = FitTbl.ItemCode AND FitTbl.AttributeTypeCode = 38 +LEFT JOIN cdItemAttributeDesc FitDesc + ON FitTbl.AttributeTypeCode = FitDesc.AttributeTypeCode + AND FitTbl.AttributeCode = FitDesc.AttributeCode + AND FitTbl.ItemTypeCode = FitDesc.ItemTypeCode + +-- Kısa Karışım +LEFT JOIN prItemAttribute KisaKar + ON a.ItemCode = KisaKar.ItemCode AND KisaKar.AttributeTypeCode = 41 +LEFT JOIN cdItemAttributeDesc KisaKarDesc + ON KisaKar.AttributeTypeCode = KisaKarDesc.AttributeTypeCode + AND KisaKar.AttributeCode = KisaKarDesc.AttributeCode + AND KisaKar.ItemTypeCode = KisaKarDesc.ItemTypeCode + +WHERE a.InvoiceDate BETWEEN @StartDate AND @EndDate +GROUP BY a.InvoiceDate, a.InvoiceNumber, a.ItemCode, a.ColorCode +ORDER BY a.InvoiceNumber, a.ItemCode, a.ColorCode;`, inBelge) + rows, err := db.MssqlDB.Query(query, + sql.Named("StartDate", startDate), + sql.Named("EndDate", endDate), + ) + if err != nil { + log.Printf("❌ Detay sorgu hatası: %v", err) + return nil, fmt.Errorf("detay sorgu hatası: %v", err) + } + defer rows.Close() + + for rows.Next() { + var d models.StatementDetail + if err := rows.Scan( + &d.BelgeTarihi, + &d.BelgeRefNumarasi, + &d.UrunAnaGrubu, + &d.UrunAltGrubu, + &d.YetiskinGarson, + &d.Fit, + &d.Icerik, + &d.UrunKodu, + &d.UrunRengi, + &d.ToplamAdet, + &d.ToplamFiyat, + &d.ToplamTutar, + ); err != nil { + log.Printf("❌ Detay scan hatası: %v", err) + return nil, err + } + result[d.BelgeRefNumarasi] = append(result[d.BelgeRefNumarasi], d) + } + log.Printf("✅ Detay verileri alındı: %d belge için detay var", len(result)) + return result, nil +} diff --git a/svc/queries/todaycurrencyv3.go b/svc/queries/todaycurrencyv3.go new file mode 100644 index 0000000..b279367 --- /dev/null +++ b/svc/queries/todaycurrencyv3.go @@ -0,0 +1,35 @@ +package queries + +import ( + "bssapp-backend/models" + "database/sql" + "fmt" +) + +// GetTodayCurrencyV3 tek bir döviz türü için güncel kuru döndürür (MSSQL sürümü) +func GetTodayCurrencyV3(db *sql.DB, currencyCode string) (*models.TodayCurrencyV3, error) { + query := ` + SELECT TOP 1 + CurrencyCode, + RelationCurrencyCode, + ExchangeTypeCode, + Rate, + CONVERT(varchar, Date, 23) AS Date + FROM AllExchangeRates + WHERE CurrencyCode = @p1 + AND RelationCurrencyCode = 'TRY' + AND ExchangeTypeCode = 6 + AND Rate > 0 + ORDER BY ABS(DATEDIFF(DAY, Date, GETDATE())) ASC + ` + + row := db.QueryRow(query, currencyCode) + + var c models.TodayCurrencyV3 + err := row.Scan(&c.CurrencyCode, &c.RelationCurrencyCode, &c.ExchangeTypeCode, &c.Rate, &c.Date) + if err != nil { + return nil, fmt.Errorf("kur bilgisi alınamadı: %v", err) + } + + return &c, nil +} diff --git a/svc/queries/user_detail.go b/svc/queries/user_detail.go new file mode 100644 index 0000000..d768b84 --- /dev/null +++ b/svc/queries/user_detail.go @@ -0,0 +1,296 @@ +package queries + +// ====================================================== +// 👤 USER HEADER +// ====================================================== +// ====================================================== +// 👤 USER HEADER (mk_dfusr) +// ====================================================== +const GetUserHeader = ` +SELECT + id, + username, + is_active, + COALESCE(full_name,'') AS full_name, + COALESCE(email,'') AS email, + COALESCE(mobile,'') AS mobile, + COALESCE(address,'') AS address, + + CASE + WHEN password_hash IS NOT NULL AND password_hash <> '' THEN true + ELSE false + END AS has_password + +FROM mk_dfusr +WHERE id = $1 +` + +// ====================================================== +// 🔐 ROLES +// ====================================================== +const GetUserRoles = ` +SELECT r.code +FROM dfrole_usr ur +JOIN dfrole r ON r.id = ur.dfrole_id +WHERE ur.dfusr_id = $1 +ORDER BY r.code +` + +// ====================================================== +// 🏢 DEPARTMENTS +// ====================================================== +const GetUserDepartments = ` +SELECT d.code, d.title +FROM dfusr_dprt ud +JOIN mk_dprt d ON d.id = ud.dprt_id +WHERE ud.dfusr_id = $1 + AND ud.is_active = true +ORDER BY d.code +` + +// ====================================================== +// 🌍 PIYASALAR +// ====================================================== +const GetUserPiyasalar = ` +SELECT p.code, p.title +FROM dfusr_piyasa up +JOIN mk_sales_piy p ON p.code = up.piyasa_code +WHERE up.dfusr_id = $1 + AND up.is_allowed = true +ORDER BY p.code +` + +// ====================================================== +// 🧾 NEBIM USERS +// ====================================================== +const GetUserNebim = ` +SELECT n.username, n.user_group_code +FROM dfusr_nebim_user un +JOIN mk_nebim_user n ON n.id = un.mk_nebim_user_id +WHERE un.dfusr_id = $1 +ORDER BY n.username +` + +// ====================================================== +// ✍️ UPDATE USER HEADER (SAFE) +// ====================================================== +// ====================================================== +// ✍️ UPDATE USER HEADER (mk_dfusr) +// ====================================================== +const UpdateUserHeader = ` +UPDATE mk_dfusr +SET + username = $2, + is_active = $3, + full_name = $4, + email = $5, + mobile = $6, + address = NULLIF($7, ''), + updated_at = NOW() +WHERE id = $1 +` + +// ====================================================== +// ➕ INSERT RELATIONS +// ====================================================== + +// ---------- ROLE +const InsertUserRole = ` +INSERT INTO dfrole_usr (dfusr_id, dfrole_id) +SELECT $1, r.id +FROM dfrole r +WHERE r.code = $2 +ON CONFLICT DO NOTHING +` + +// ---------- DEPARTMENT +const InsertUserDepartment = ` +INSERT INTO dfusr_dprt (dfusr_id, dprt_id, is_active) +SELECT $1, d.id, true +FROM mk_dprt d +WHERE d.code = $2 +ON CONFLICT DO NOTHING +` + +// ---------- PIYASA +const InsertUserPiyasa = ` +INSERT INTO dfusr_piyasa (dfusr_id, piyasa_code, is_allowed) +VALUES ($1, $2, true) +ON CONFLICT DO NOTHING +` + +// ---------- NEBIM USER +const InsertUserNebim = ` +INSERT INTO dfusr_nebim_user (dfusr_id, mk_nebim_user_id) +SELECT $1, n.id +FROM mk_nebim_user n +WHERE n.username = $2 +ON CONFLICT DO NOTHING +` + +// ====================================================== +// 📚 LOOKUPS +// ====================================================== +const GetRoleLookup = ` +SELECT code AS value, code AS label +FROM dfrole +ORDER BY code +` + +const GetDepartmentLookup = ` +SELECT code AS value, title AS label +FROM mk_dprt +WHERE is_active = true +ORDER BY code +` + +const GetPiyasaLookup = ` +SELECT code AS value, title AS label +FROM mk_sales_piy +WHERE is_active = true +ORDER BY code +` + +const GetNebimUserLookup = ` +SELECT + username AS value, + username || ' (' || user_group_code || ')' AS label, + user_group_code +FROM mk_nebim_user +WHERE is_active = true +ORDER BY username +` + +// ====================================================== +// 📦 ORDER — NEW NUMBER +// ====================================================== + +const GetNextOrderNumber = ` +SELECT + COALESCE(MAX(order_number), 0) + 1 +FROM orders +WHERE order_year = EXTRACT(YEAR FROM NOW()) +` + +// ====================================================== +// 🔐 ROLES (ADMIN LIST) +// ====================================================== + +const GetRoles = ` +SELECT + id, + code, + title +FROM dfrole +ORDER BY code +` + +// ====================================================== +// 🏢 DEPARTMENTS (ADMIN LIST) +// ====================================================== + +const GetDepartments = ` +SELECT + id, + code, + title +FROM mk_dprt +WHERE is_active = true +ORDER BY code +` + +// ====================================================== +// 🌍 PIYASALAR (ADMIN LIST) +// ====================================================== + +const GetPiyasalar = ` +SELECT + code, + title +FROM mk_sales_piy +WHERE is_active = true +ORDER BY code +` + +// ====================================================== +// 🔗 ROLE → DEPARTMENT +// ====================================================== + +const DeleteRoleDepartments = ` +DELETE FROM dfrole_dprt +WHERE dfrole_id = $1 +` + +const InsertRoleDepartment = ` +INSERT INTO dfrole_dprt ( + dfrole_id, + dprt_id, + is_allowed +) +SELECT + $1, + d.id, + true +FROM mk_dprt d +WHERE d.code = $2 +ON CONFLICT DO NOTHING +` + +// ====================================================== +// 🔗 ROLE → PIYASA +// ====================================================== + +const DeleteRolePiyasalar = ` +DELETE FROM dfrole_piyasa +WHERE dfrole_id = $1 +` + +const InsertRolePiyasa = ` +INSERT INTO dfrole_piyasa ( + dfrole_id, + piyasa_code, + is_allowed +) +VALUES ($1,$2,true) +ON CONFLICT DO NOTHING +` + +// ====================================================== +// 👤 USER → ROLE +// ====================================================== + +const DeleteUserRoles = ` +DELETE FROM dfrole_usr +WHERE dfusr_id = $1 +` + +// ====================================================== +// 🏢 DEPARTMENTS (ADMIN PERM PAGE SELECT) -> { code, title } +// ====================================================== +const GetDepartmentsForPermissionSelect = ` +SELECT + code AS id, + title AS title +FROM mk_dprt +WHERE is_active = true +ORDER BY title +` + +// ====================================================== +// 🔐 ROLES (ADMIN PERM PAGE SELECT) -> { id, title } +// ====================================================== +const GetRolesForPermissionSelect = ` +SELECT + id::text AS id, + COALESCE(NULLIF(title,''), code) AS title +FROM dfrole +ORDER BY COALESCE(NULLIF(title,''), code) +` +const GetUserLookupForPermission = ` +SELECT + id::text AS id, + username || ' - ' || full_name AS title +FROM mk_dfusr +WHERE is_active = true +ORDER BY username +` diff --git a/svc/queries/user_list.go b/svc/queries/user_list.go new file mode 100644 index 0000000..809bbe4 --- /dev/null +++ b/svc/queries/user_list.go @@ -0,0 +1,106 @@ +package queries + +import ( + "database/sql" + "fmt" +) + +// ======================================================== +// 📌 GetUserList — POSTGRES FINAL (mk_dfusr) +// ======================================================== +func GetUserList(db *sql.DB, search string) (*sql.Rows, error) { + + baseQuery := ` +SELECT + u.id AS id, -- 1 + u.username AS code, -- 2 (frontend uyumu için alias) + u.is_active AS is_active, -- 3 + + -- Nebim eşleşmesi (opsiyonel) + n.username AS nebim_username, -- 4 + n.user_group_code AS user_group_code, -- 5 + + -- Roller + COALESCE(string_agg(DISTINCT r.code, ', '), '') AS role_names, -- 6 + + -- Departmanlar + COALESCE(string_agg(DISTINCT d.title, ', '), '') AS department_names, -- 7 + + -- Piyasalar (ÇOKLU) + COALESCE(string_agg(DISTINCT p.title, ', '), '') AS piyasa_names -- 8 + +FROM mk_dfusr u + +-- ========================== +-- 🔗 Nebim eşleşmesi +-- ========================== +LEFT JOIN dfusr_nebim_user un + ON un.dfusr_id = u.id + +LEFT JOIN mk_nebim_user n + ON n.id = un.mk_nebim_user_id + +-- ========================== +-- 🔐 Roller +-- ========================== +LEFT JOIN dfrole_usr uru + ON uru.dfusr_id = u.id + +LEFT JOIN dfrole r + ON r.id = uru.dfrole_id + +-- ========================== +-- 🏢 Departmanlar +-- ========================== +LEFT JOIN dfusr_dprt ud + ON ud.dfusr_id = u.id + +LEFT JOIN mk_dprt d + ON d.id = ud.dprt_id + +-- ========================== +-- 🌍 Piyasalar +-- ========================== +LEFT JOIN dfusr_piyasa up + ON up.dfusr_id = u.id + +LEFT JOIN mk_sales_piy p + ON p.code = up.piyasa_code + +WHERE 1 = 1 +` + + // 🔍 SEARCH FILTER + if search != "" { + baseQuery += ` + AND ( + u.username ILIKE $1 + OR u.email ILIKE $1 + OR n.username ILIKE $1 + OR r.code ILIKE $1 + OR d.title ILIKE $1 + OR p.title ILIKE $1 + ) +` + } + + // 📌 GROUP + ORDER + baseQuery += ` +GROUP BY + u.id, + u.username, + u.is_active, + n.username, + n.user_group_code + +ORDER BY u.username +` + + // ▶ EXECUTE QUERY + if search != "" { + searchLike := fmt.Sprintf("%%%s%%", search) + return db.Query(baseQuery, searchLike) + } + + return db.Query(baseQuery) +} diff --git a/svc/queries/user_password.go b/svc/queries/user_password.go new file mode 100644 index 0000000..1526a07 --- /dev/null +++ b/svc/queries/user_password.go @@ -0,0 +1,41 @@ +package queries + +// ====================================================== +// 🔐 UPDATE USER PASSWORD (RESET / CHANGE) +// ====================================================== +const UpdateUserPassword = ` +UPDATE dfusr +SET + upass = $2, + last_updated_date = NOW() +WHERE id = $1 + AND is_active = true +` + +// Reset token yaz +const SetUserResetToken = ` +UPDATE dfusr +SET + fpass = $2, + fpass_id = NOW() +WHERE id = $1 + AND is_active = true +` + +// Reset token kontrol +const CheckUserResetToken = ` +SELECT id +FROM dfusr +WHERE + fpass = $1 + AND is_active = true +` + +// Reset sonrası temizle +const ClearUserResetToken = ` +UPDATE dfusr +SET + fpass = NULL, + fpass_id = NULL +WHERE id = $1 +` diff --git a/svc/repository/activitylog_repository.go b/svc/repository/activitylog_repository.go new file mode 100644 index 0000000..73f2365 --- /dev/null +++ b/svc/repository/activitylog_repository.go @@ -0,0 +1,286 @@ +package repository + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" +) + +type ActivityLogRow struct { + CreatedAt time.Time `json:"created_at"` + RequestStartedAt *time.Time `json:"request_started_at,omitempty"` + RequestFinishedAt *time.Time `json:"request_finished_at,omitempty"` + DurationMs *int `json:"duration_ms,omitempty"` + HttpStatus *int `json:"http_status,omitempty"` + + Username string `json:"username"` + RoleCode string `json:"role_code"` + + ActionCategory string `json:"action_category"` + ActionType string `json:"action_type"` + ActionTarget string `json:"action_target"` + Description string `json:"description"` + + // ✅ TARGET USER + TargetDfUsrID int64 `json:"target_dfusr_id"` + TargetUsername string `json:"target_username"` + + // ✅ DIFF + ChangeBefore string `json:"change_before"` + ChangeAfter string `json:"change_after"` + + IpAddress string `json:"ip_address"` + IsSuccess bool `json:"is_success"` + ErrorMsg string `json:"error_message"` + UserAgent string `json:"user_agent"` + SessionID string `json:"session_id"` + + // audit ids + UserUUID string `json:"user_id"` + DfUsrID int64 `json:"dfusr_id"` + + Email string `json:"email"` + IsActive bool `json:"is_active"` +} + +type ActivityLogQuery struct { + Username string + RoleCode string + ActionCategory string + ActionType string + ActionTarget string + Success *bool + + StatusMin *int + StatusMax *int + + DateFrom *time.Time + DateTo *time.Time + + Page int + Limit int +} + +type ActivityLogResult struct { + Total int64 `json:"total"` + Items []ActivityLogRow `json:"items"` +} + +func ListActivityLogs( + ctx context.Context, + db *sql.DB, + q ActivityLogQuery, +) (ActivityLogResult, error) { + + // ---------- defaults ---------- + if q.Page <= 0 { + q.Page = 1 + } + // limit <=0 → unlimited + if q.Limit < 0 { + q.Limit = 50 + } + if q.Limit > 200 { + q.Limit = 200 + } + + offset := 0 + if q.Limit > 0 { + offset = (q.Page - 1) * q.Limit + } + + where := []string{} + args := []interface{}{} + + add := func(cond string, val interface{}) { + where = append(where, cond) + args = append(args, val) + } + + // ---------- filters ---------- + if strings.TrimSpace(q.Username) != "" { + add(fmt.Sprintf("l.username ILIKE $%d", len(args)+1), + "%"+strings.TrimSpace(q.Username)+"%") + } + if strings.TrimSpace(q.RoleCode) != "" { + add(fmt.Sprintf("r.code = $%d", len(args)+1), + strings.TrimSpace(q.RoleCode)) + } + if strings.TrimSpace(q.ActionCategory) != "" { + add(fmt.Sprintf("l.action_category = $%d", len(args)+1), + strings.TrimSpace(q.ActionCategory)) + } + if strings.TrimSpace(q.ActionType) != "" { + add(fmt.Sprintf("l.action_type = $%d", len(args)+1), + strings.TrimSpace(q.ActionType)) + } + if strings.TrimSpace(q.ActionTarget) != "" { + add(fmt.Sprintf("l.action_target ILIKE $%d", len(args)+1), + "%"+strings.TrimSpace(q.ActionTarget)+"%") + } + if q.Success != nil { + add(fmt.Sprintf("l.is_success = $%d", len(args)+1), *q.Success) + } + if q.StatusMin != nil { + add(fmt.Sprintf("l.http_status >= $%d", len(args)+1), *q.StatusMin) + } + if q.StatusMax != nil { + add(fmt.Sprintf("l.http_status <= $%d", len(args)+1), *q.StatusMax) + } + if q.DateFrom != nil { + add(fmt.Sprintf("l.created_at >= $%d", len(args)+1), *q.DateFrom) + } + if q.DateTo != nil { + add(fmt.Sprintf("l.created_at < $%d", len(args)+1), + q.DateTo.Add(24*time.Hour)) + } + + whereSQL := "" + if len(where) > 0 { + whereSQL = "WHERE " + strings.Join(where, " AND ") + } + + // ---------- COUNT ---------- + countSQL := fmt.Sprintf(` + SELECT count(*) + FROM mk_user_activity_log l + LEFT JOIN mk_dfusr u ON u.id = l.dfusr_id + LEFT JOIN dfrole_usr ru ON ru.dfusr_id = u.id + LEFT JOIN dfrole r ON r.id = ru.dfrole_id + %s + `, whereSQL) + + var total int64 + if err := db.QueryRowContext(ctx, countSQL, args...).Scan(&total); err != nil { + return ActivityLogResult{}, err + } + + // ---------- LIST ---------- + listArgs := append([]interface{}{}, args...) + + listSQL := fmt.Sprintf(` +SELECT + l.created_at, + l.request_started_at, + l.request_finished_at, + l.duration_ms, + l.http_status, + + COALESCE(u.username, l.username, '') AS username, + COALESCE(r.code,''), + + COALESCE(l.action_category,''), + COALESCE(l.action_type,''), + COALESCE(l.action_target,''), + COALESCE(l.description,''), + + COALESCE(l.target_dfusr_id, 0), + COALESCE(l.target_username, ''), + COALESCE(l.change_before::text, ''), + COALESCE(l.change_after::text, ''), + + COALESCE(l.ip_address,''), + COALESCE(l.is_success,false), + COALESCE(l.error_message,''), + COALESCE(l.user_agent,''), + COALESCE(l.session_id,''), + + COALESCE(l.user_id::text,''), + COALESCE(l.dfusr_id,0), + + COALESCE(u.email,''), + COALESCE(u.is_active,false) + +FROM mk_user_activity_log l +LEFT JOIN mk_dfusr u ON u.id = l.dfusr_id +LEFT JOIN dfrole_usr ru ON ru.dfusr_id = u.id +LEFT JOIN dfrole r ON r.id = ru.dfrole_id +%s +ORDER BY l.created_at DESC +`, whereSQL) + // LIMIT sadece istenirse + if q.Limit > 0 { + + listSQL += fmt.Sprintf( + " LIMIT $%d OFFSET $%d", + len(listArgs)+1, + len(listArgs)+2, + ) + + listArgs = append(listArgs, q.Limit, offset) + } + + rows, err := db.QueryContext(ctx, listSQL, listArgs...) + if err != nil { + return ActivityLogResult{}, err + } + defer rows.Close() + + items := []ActivityLogRow{} + + for rows.Next() { + var it ActivityLogRow + var started, finished sql.NullTime + var dur, status sql.NullInt64 + + if err := rows.Scan( + &it.CreatedAt, + &started, + &finished, + &dur, + &status, + + &it.Username, + &it.RoleCode, + + &it.ActionCategory, + &it.ActionType, + &it.ActionTarget, + &it.Description, + + // ✅ NEW + &it.TargetDfUsrID, + &it.TargetUsername, + &it.ChangeBefore, + &it.ChangeAfter, + + &it.IpAddress, + &it.IsSuccess, + &it.ErrorMsg, + &it.UserAgent, + &it.SessionID, + + &it.UserUUID, + &it.DfUsrID, + + &it.Email, + &it.IsActive, + ); err != nil { + return ActivityLogResult{}, err + } + + if started.Valid { + it.RequestStartedAt = &started.Time + } + if finished.Valid { + it.RequestFinishedAt = &finished.Time + } + if dur.Valid { + v := int(dur.Int64) + it.DurationMs = &v + } + if status.Valid { + v := int(status.Int64) + it.HttpStatus = &v + } + + items = append(items, it) + } + + return ActivityLogResult{ + Total: total, + Items: items, + }, nil +} diff --git a/svc/repository/mk_user_repository.go b/svc/repository/mk_user_repository.go new file mode 100644 index 0000000..7eb0bce --- /dev/null +++ b/svc/repository/mk_user_repository.go @@ -0,0 +1,352 @@ +package repository + +import ( + "bssapp-backend/models" + "database/sql" + "errors" + "strings" + "time" + + "github.com/lib/pq" +) + +var ErrMkUserNotFound = errors.New("mk_user not found") + +type MkUserRepository struct { + DB *sql.DB +} + +func NewMkUserRepository(db *sql.DB) *MkUserRepository { + return &MkUserRepository{DB: db} +} + +// ------------------------------------------------------- +// 🔍 GET BY USERNAME +// ------------------------------------------------------- +func (r *MkUserRepository) GetByUsername(username string) (*models.MkUser, error) { + username = strings.TrimSpace(username) + + var u models.MkUser + + err := r.DB.QueryRow(`SELECT + u.id, + u.username, + COALESCE(u.email,'') AS email, + u.is_active, + COALESCE(u.password_hash,'') AS password_hash, + u.force_password_change, + + COALESCE(r.id, 0) AS role_id, + COALESCE(r.code, '') AS role_code, + + -- ✅ DEPARTMENTS + COALESCE( + array_agg(DISTINCT d.code) + FILTER (WHERE d.code IS NOT NULL), + '{}' + ) AS department_codes, + + u.password_updated_at, + u.created_at, + u.updated_at, + u.last_login_at + +FROM mk_dfusr u + +LEFT JOIN dfrole_usr ru + ON ru.dfusr_id = u.id + +LEFT JOIN dfrole r + ON r.id = ru.dfrole_id + +-- ✅ USER → DEPT +LEFT JOIN dfusr_dprt ud + ON ud.dfusr_id = u.id + AND ud.is_active = true + +LEFT JOIN mk_dprt d + ON d.id = ud.dprt_id + +WHERE LOWER(u.username) = LOWER($1) + +GROUP BY + u.id, r.id + +LIMIT 1 +`, username).Scan( + &u.ID, + &u.Username, + &u.Email, + &u.IsActive, + &u.PasswordHash, + &u.ForcePasswordChange, + + &u.RoleID, + &u.RoleCode, + + pq.Array(&u.DepartmentCodes), // ✅ + + &u.PasswordUpdatedAt, + + &u.CreatedAt, + &u.UpdatedAt, + &u.LastLoginAt, + ) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrMkUserNotFound + } + return nil, err + } + + return &u, nil +} + +// ------------------------------------------------------- +// 🔍 GET BY ID +// ------------------------------------------------------- +func (r *MkUserRepository) GetByID(id int64) (*models.MkUser, error) { + var u models.MkUser + + err := r.DB.QueryRow(` + SELECT + u.id, + u.username, + COALESCE(u.email,'') AS email, + u.is_active, + COALESCE(u.password_hash,'') AS password_hash, + u.force_password_change, + + COALESCE(r.id, 0) AS role_id, + COALESCE(r.code, '') AS role_code, + + -- ✅ DEPARTMENTS + COALESCE( + array_agg(DISTINCT d.code) + FILTER (WHERE d.code IS NOT NULL), + '{}' + ) AS department_codes, + + u.password_updated_at, + u.created_at, + u.updated_at, + u.last_login_at + +FROM mk_dfusr u + +LEFT JOIN dfrole_usr ru + ON ru.dfusr_id = u.id + +LEFT JOIN dfrole r + ON r.id = ru.dfrole_id + +-- ✅ USER → DEPT +LEFT JOIN dfusr_dprt ud + ON ud.dfusr_id = u.id + AND ud.is_active = true + +LEFT JOIN mk_dprt d + ON d.id = ud.dprt_id + +WHERE LOWER(u.username) = LOWER($1) + +GROUP BY + u.id, r.id + +LIMIT 1 +`, id).Scan( + &u.ID, + &u.Username, + &u.Email, + &u.IsActive, + &u.PasswordHash, + &u.ForcePasswordChange, + + &u.RoleID, + &u.RoleCode, + pq.Array(&u.DepartmentCodes), // ✅ + &u.PasswordUpdatedAt, + &u.CreatedAt, + &u.UpdatedAt, + &u.LastLoginAt, + ) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrMkUserNotFound + } + return nil, err + } + + return &u, nil +} + +// ------------------------------------------------------- +// 🔁 CREATE FROM LEGACY (dfusr → mk_dfusr) +// - id = dfusr.id ✅ +// - role / v3 YOK +// ------------------------------------------------------- +// ------------------------------------------------------- +// 🔁 CREATE FROM LEGACY (dfusr → mk_dfusr) FULL MIGRATION +// ------------------------------------------------------- +func (r *MkUserRepository) CreateFromLegacy( + legacy *models.User, + passwordHash string, +) (*models.MkUser, error) { + + var u models.MkUser + + err := r.DB.QueryRow(` + INSERT INTO mk_dfusr ( + id, + username, + email, + full_name, + mobile, + address, + is_active, + password_hash, + force_password_change, + created_at, + updated_at + ) + VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,true,now(),now() + ) + ON CONFLICT (id) + DO UPDATE SET + email = EXCLUDED.email, + full_name = EXCLUDED.full_name, + mobile = EXCLUDED.mobile, + address = EXCLUDED.address, + password_hash = EXCLUDED.password_hash, + force_password_change= true, + updated_at = now() + RETURNING + id, + username, + COALESCE(email,''), + is_active, + COALESCE(password_hash,''), + force_password_change, + password_updated_at, + created_at, + updated_at, + last_login_at + `, + legacy.ID, + legacy.Username, + legacy.Email, + legacy.FullName, + legacy.Mobile, + legacy.Address, + legacy.IsActive, + passwordHash, + ).Scan( + &u.ID, + &u.Username, + &u.Email, + &u.IsActive, + &u.PasswordHash, + &u.ForcePasswordChange, + &u.PasswordUpdatedAt, + &u.CreatedAt, + &u.UpdatedAt, + &u.LastLoginAt, + ) + + if err != nil { + return nil, err + } + + return &u, nil +} + +// ------------------------------------------------------- +// ➕ CREATE NEW USER (NON-LEGACY) +// - id = sequence (>=1000) +// ------------------------------------------------------- +func (r *MkUserRepository) CreateNewUser( + username string, + email string, + isActive bool, +) (*models.MkUser, error) { + + var u models.MkUser + + err := r.DB.QueryRow(` + INSERT INTO mk_dfusr ( + username, + email, + is_active, + force_password_change, + created_at, + updated_at + ) + VALUES ( + $1,$2,$3,true,now(),now() + ) + RETURNING + id, + username, + COALESCE(email,'') AS email, + is_active, + COALESCE(password_hash,'') AS password_hash, + force_password_change, + password_updated_at, + created_at, + updated_at, + last_login_at + `, + strings.TrimSpace(username), + strings.TrimSpace(email), + isActive, + ).Scan( + &u.ID, + &u.Username, + &u.Email, + &u.IsActive, + &u.PasswordHash, + &u.ForcePasswordChange, + &u.PasswordUpdatedAt, + &u.CreatedAt, + &u.UpdatedAt, + &u.LastLoginAt, + ) + + if err != nil { + return nil, err + } + + return &u, nil +} + +// ------------------------------------------------------- +// 🕒 TOUCH LAST LOGIN +// ------------------------------------------------------- +func (r *MkUserRepository) TouchLastLogin(userID int64) error { + _, err := r.DB.Exec(` + UPDATE mk_dfusr + SET last_login_at = $1, + updated_at = $1 + WHERE id = $2 + `, time.Now(), userID) + return err +} + +// ------------------------------------------------------- +// 🔐 UPDATE PASSWORD +// ------------------------------------------------------- +func (r *MkUserRepository) UpdatePassword(userID int64, newHash string) error { + _, err := r.DB.Exec(` + UPDATE mk_dfusr + SET + password_hash = $1, + force_password_change = false, + password_updated_at = NOW(), + updated_at = NOW() + WHERE id = $2 + `, newHash, userID) + return err +} diff --git a/svc/repository/permission_repository.go b/svc/repository/permission_repository.go new file mode 100644 index 0000000..9586e95 --- /dev/null +++ b/svc/repository/permission_repository.go @@ -0,0 +1,33 @@ +// repository/permission_repository.go +package repository + +import "database/sql" + +type PermissionRepository struct { + DB *sql.DB +} + +func NewPermissionRepository(db *sql.DB) *PermissionRepository { + return &PermissionRepository{DB: db} +} + +func (r *PermissionRepository) GetPermissionsByRoleID(roleID int64) ([]string, error) { + rows, err := r.DB.Query(` + SELECT route + FROM mkdf_rol_per + WHERE role_id = $1 AND can_access = true + `, roleID) + if err != nil { + return nil, err + } + defer rows.Close() + + perms := []string{} + for rows.Next() { + var route string + if err := rows.Scan(&route); err == nil { + perms = append(perms, route) + } + } + return perms, nil +} diff --git a/svc/repository/refresh_token_repository.go b/svc/repository/refresh_token_repository.go new file mode 100644 index 0000000..d090376 --- /dev/null +++ b/svc/repository/refresh_token_repository.go @@ -0,0 +1,80 @@ +package repository + +import ( + "database/sql" + "errors" + "time" +) + +var ErrRefreshTokenInvalid = errors.New("refresh token invalid") + +type RefreshTokenRepository struct { + DB *sql.DB +} + +func NewRefreshTokenRepository(db *sql.DB) *RefreshTokenRepository { + return &RefreshTokenRepository{DB: db} +} + +// Yeni refresh (HASH saklanır) +func (r *RefreshTokenRepository) IssueRefreshToken( + mkUserID int64, + tokenHash string, + expiresAt time.Time, +) error { + _, err := r.DB.Exec(` + INSERT INTO mk_refresh_tokens (mk_user_id, token_hash, expires_at) + VALUES ($1,$2,$3) + `, mkUserID, tokenHash, expiresAt) + return err +} + +// Tek refresh’i revoke et (rotation / logout) +func (r *RefreshTokenRepository) RevokeByHash(hash string) error { + _, err := r.DB.Exec(` + UPDATE mk_refresh_tokens + SET revoked_at = now() + WHERE token_hash = $1 + AND revoked_at IS NULL + `, hash) + return err +} + +// Kullanıcının tüm refresh’lerini revoke et (logout-all / password change) +func (r *RefreshTokenRepository) RevokeAllForUser(mkUserID int64) error { + _, err := r.DB.Exec(` + UPDATE mk_refresh_tokens + SET revoked_at = now() + WHERE mk_user_id = $1 + AND revoked_at IS NULL + `, mkUserID) + return err +} + +// Geçerli refresh’i tüket (validate + rotate) +func (r *RefreshTokenRepository) ConsumeValid(tokenHash string) (int64, error) { + var mkUserID int64 + err := r.DB.QueryRow(` + SELECT mk_user_id + FROM mk_refresh_tokens + WHERE token_hash = $1 + AND revoked_at IS NULL + AND expires_at > now() + `, tokenHash).Scan(&mkUserID) + + if err != nil { + if err == sql.ErrNoRows { + return 0, ErrRefreshTokenInvalid + } + return 0, err + } + + // tek kullanımlık: eskiyi revoke et + _, _ = r.DB.Exec(` + UPDATE mk_refresh_tokens + SET revoked_at = now() + WHERE token_hash = $1 + `, tokenHash) + + return mkUserID, nil +} diff --git a/svc/repository/user_repo.go b/svc/repository/user_repo.go new file mode 100644 index 0000000..e76b874 --- /dev/null +++ b/svc/repository/user_repo.go @@ -0,0 +1,276 @@ +package repository + +import ( + "bssapp-backend/models" + "database/sql" + "errors" + "fmt" + "log" + "strings" +) + +// UserRepository → kullanıcı işlemleri için repository katmanı +type UserRepository struct { + DB *sql.DB +} + +// NewUserRepository → yeni bir UserRepository döner +func NewUserRepository(db *sql.DB) *UserRepository { + return &UserRepository{DB: db} +} + +// ------------------------------------------------------- +// 📋 GetUserList (User Management) +// ------------------------------------------------------- +func (r *UserRepository) GetUserList() ([]models.User, error) { + + query := ` + SELECT + u.id, + u.code, + COALESCE(u.upass,'') as upass, + u.is_active, + COALESCE(u.email,''), + COALESCE(uru.dfrole_id, 0) AS role_id, + COALESCE(dr.code, '') AS role_code + FROM dfusr u + LEFT JOIN dfrole_usr uru ON u.id = uru.dfusr_id + LEFT JOIN dfrole dr ON dr.id = uru.dfrole_id + ORDER BY u.code + ` + + rows, err := r.DB.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var users []models.User + for rows.Next() { + var u models.User + if err := rows.Scan( + &u.ID, + &u.Username, + &u.Upass, // ✅ dfusr.upass + &u.IsActive, + &u.Email, + &u.RoleID, + &u.RoleCode, + ); err != nil { + return nil, err + } + users = append(users, u) + } + return users, nil +} + +// ------------------------------------------------------- +// ➕ CreateUser (legacy dfusr insert) [kullanıyorsan kalsın] +// ------------------------------------------------------- +func (r *UserRepository) CreateUser(u *models.User) (int, error) { + + query := ` + INSERT INTO dfusr ( + code, + upass, + is_active, + email, + v3_username, + v3usergroup + ) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id + ` + + var newID int + err := r.DB.QueryRow( + query, + u.Username, + u.Upass, // ✅ upass + u.IsActive, + u.Email, + u.V3Username, + u.V3UserGroup, + ).Scan(&newID) + + if err != nil { + return 0, err + } + return newID, nil +} + +// ------------------------------------------------------- +// 🔍 GetUserByUsername (legacy) [login için şart değil ama düzelttim] +// ------------------------------------------------------- +func (r *UserRepository) GetUserByUsername(username string) (*models.User, error) { + + clean := strings.TrimSpace(username) + + query := ` + SELECT + u.id, + u.code, + COALESCE(u.upass,'') as upass, + u.is_active, + COALESCE(u.email,''), + + COALESCE(uru.dfrole_id,0) AS role_id, + COALESCE(dr.code,'') AS role_code, + + COALESCE(u.v3_username,'') as v3_username, + COALESCE(u.v3_usergroup,0) as v3_usergroup, + COALESCE(u.force_password_change,false) as force_password_change + + FROM dfusr u + + LEFT JOIN dfrole_usr uru + ON uru.dfusr_id = u.id + + LEFT JOIN dfrole dr + ON dr.id = uru.dfrole_id + + WHERE u.is_active = true + AND ( + LOWER(u.code) = LOWER($1) + OR LOWER(u.email) = LOWER($1) + ) + + LIMIT 1; + ` + + row := r.DB.QueryRow(query, clean) + + var user models.User + + err := row.Scan( + &user.ID, + &user.Username, + &user.Upass, + &user.IsActive, + &user.Email, + + &user.RoleID, + &user.RoleCode, + + &user.V3Username, + &user.V3UserGroup, + &user.ForcePasswordChange, + ) + + if err != nil { + + if errors.Is(err, sql.ErrNoRows) { + return nil, errors.New("kullanıcı bulunamadı") + } + + return nil, err + } + + return &user, nil +} + +// ------------------------------------------------------- +// 🧪 DebugListUsers +// ------------------------------------------------------- +func (r *UserRepository) DebugListUsers() { + rows, err := r.DB.Query(`SELECT id, code FROM dfusr ORDER BY id LIMIT 10`) + if err != nil { + fmt.Println("❌ [DEBUG] Listeleme hatası:", err) + return + } + defer rows.Close() + + fmt.Println("📋 [DEBUG] İlk 10 kullanıcı:") + for rows.Next() { + var id int + var code string + if err := rows.Scan(&id, &code); err == nil { + fmt.Printf(" - %d : %s\n", id, code) + } + } +} + +// ------------------------------------------------------- +// 🔁 SetUserRoles +// ------------------------------------------------------- +func (r *UserRepository) SetUserRoles(userID int, roleIDs []int) error { + + tx, err := r.DB.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + if _, err := tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID); err != nil { + return err + } + + for _, roleID := range roleIDs { + if _, err := tx.Exec( + `INSERT INTO dfrole_usr (dfrole_id, dfusr_id) VALUES ($1, $2)`, + roleID, userID, + ); err != nil { + return err + } + } + + return tx.Commit() +} + +// ------------------------------------------------------- +// 🔐 GetLegacyUserForLogin ✅ LOGIN İÇİN TEK DOĞRU FONKSİYON +// ------------------------------------------------------- +func (r *UserRepository) GetLegacyUserForLogin(login string) (*models.User, error) { + log.Println("🟡 LEGACY LOGIN QUERY HIT:", login) + + login = strings.TrimSpace(login) + + var u models.User + + err := r.DB.QueryRow(` + SELECT + u.id, + u.code, + COALESCE(u.upass,'') as upass, + u.is_active, + COALESCE(u.email,''), + COALESCE(u.dfrole_id,0) as role_id, + COALESCE(dr.code,'') as role_code, + COALESCE(u.force_password_change,false) + FROM dfusr u + LEFT JOIN dfrole dr ON dr.id = u.dfrole_id + WHERE u.is_active = true + AND ( + LOWER(u.code) = LOWER($1) + OR LOWER(u.email) = LOWER($1) + ) + LIMIT 1 + `, login).Scan( + &u.ID, + &u.Username, + &u.Upass, + &u.IsActive, + &u.Email, + &u.RoleID, + &u.RoleCode, + &u.ForcePasswordChange, + ) + + if err != nil { + log.Printf("❌ LEGACY SCAN ERROR: %v", err) + return nil, err + } + + prefix := u.Upass + if len(prefix) > 4 { + prefix = prefix[:4] + } + + log.Printf( + "🧪 LEGACY UPASS OK len=%d prefix=%s", + len(u.Upass), + prefix, + ) + + return &u, nil +} diff --git a/svc/repository/user_role_repo.go b/svc/repository/user_role_repo.go new file mode 100644 index 0000000..b7de144 --- /dev/null +++ b/svc/repository/user_role_repo.go @@ -0,0 +1,32 @@ +package repository + +import "database/sql" + +type UserRole struct { + RoleID int + RoleCode string + IsSystem bool +} + +// repository/user_roles.go +// repository/user_roles.go +func GetUserRolesByUserID(db *sql.DB, userID int64) ([]int, error) { + rows, err := db.Query(` + SELECT role_id + FROM dfrole_usr + WHERE mk_dfusr_id = $1 + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var roles []int + for rows.Next() { + var rid int + if err := rows.Scan(&rid); err == nil { + roles = append(roles, rid) + } + } + return roles, nil +} diff --git a/svc/routes/account.go b/svc/routes/account.go new file mode 100644 index 0000000..363cfd1 --- /dev/null +++ b/svc/routes/account.go @@ -0,0 +1,39 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/authz" + "bssapp-backend/queries" + "encoding/json" + "log" + "net/http" +) + +func GetAccountsHandler(w http.ResponseWriter, r *http.Request) { + + // ✅ AUTH (sadece login kontrolü) + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + // ✅ DEBUG (scope kontrol için faydalı) + log.Println("🔍 PIYASA CTX:", authz.GetPiyasaCodesFromCtx(r.Context())) + + // ✅ QUERY + accounts, err := queries.GetAccounts(r.Context()) + if err != nil { + log.Println("❌ GetAccounts error:", err) + http.Error(w, "db error", http.StatusInternalServerError) + return + } + + // ✅ RESPONSE + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + if err := json.NewEncoder(w).Encode(accounts); err != nil { + log.Println("❌ JSON encode error:", err) + return + } +} diff --git a/svc/routes/activitylogs.go b/svc/routes/activitylogs.go new file mode 100644 index 0000000..df966e6 --- /dev/null +++ b/svc/routes/activitylogs.go @@ -0,0 +1,126 @@ +package routes + +import ( + "bssapp-backend/repository" + "database/sql" + "encoding/json" + "net/http" + "strconv" + "time" +) + +type ActivityLogQuery struct { + Username string + RoleCode string + ActionCategory string + ActionType string + ActionTarget string + Success *bool + + DateFrom *time.Time + DateTo *time.Time + + Page int + Limit int +} + +func AdminActivityLogsHandler(pgDB *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + q := repository.ActivityLogQuery{} + + // pagination + if v := r.URL.Query().Get("page"); v != "" { + if i, err := strconv.Atoi(v); err == nil { + q.Page = i + } + } + + if v := r.URL.Query().Get("limit"); v != "" { + if i, err := strconv.Atoi(v); err == nil { + q.Limit = i + } + } else { + // limit yoksa unlimited + q.Limit = 0 + } + + // filters + q.Username = r.URL.Query().Get("username") + q.RoleCode = r.URL.Query().Get("role_code") + q.ActionCategory = r.URL.Query().Get("action_category") + q.ActionType = r.URL.Query().Get("action_type") + q.ActionTarget = r.URL.Query().Get("action_target") + + // success + if v := r.URL.Query().Get("success"); v != "" { + if v == "true" || v == "1" { + b := true + q.Success = &b + } else if v == "false" || v == "0" { + b := false + q.Success = &b + } + } + + // status range + if v := r.URL.Query().Get("status_min"); v != "" { + if i, err := strconv.Atoi(v); err == nil { + q.StatusMin = &i + } + } + if v := r.URL.Query().Get("status_max"); v != "" { + if i, err := strconv.Atoi(v); err == nil { + q.StatusMax = &i + } + } + + // date range (ISO: 2026-01-03T00:00:00Z veya 2026-01-03) + parseDate := func(s string) (*time.Time, error) { + if s == "" { + return nil, nil + } + // önce RFC3339 dene + if t, err := time.Parse(time.RFC3339, s); err == nil { + return &t, nil + } + // sonra sadece tarih + if t, err := time.Parse("2006-01-02", s); err == nil { + tt := t + return &tt, nil + } + return nil, http.ErrNotSupported + } + + if v := r.URL.Query().Get("date_from"); v != "" { + t, err := parseDate(v) + if err != nil { + http.Error(w, "date_from format hatalı", http.StatusBadRequest) + return + } + q.DateFrom = t + } + if v := r.URL.Query().Get("date_to"); v != "" { + t, err := parseDate(v) + if err != nil { + http.Error(w, "date_to format hatalı", http.StatusBadRequest) + return + } + q.DateTo = t + } + + res, err := repository.ListActivityLogs(r.Context(), pgDB, q) + if err != nil { + http.Error(w, "Loglar alınamadı", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "page": q.Page, + "limit": q.Limit, + "total": res.Total, + "items": res.Items, + }) + } +} diff --git a/svc/routes/admin_piyasa.go b/svc/routes/admin_piyasa.go new file mode 100644 index 0000000..c4de4aa --- /dev/null +++ b/svc/routes/admin_piyasa.go @@ -0,0 +1,61 @@ +package routes + +import ( + "log" + "net/http" + "strconv" + + "bssapp-backend/auth" + "bssapp-backend/internal/authz" + "bssapp-backend/middlewares" + + "github.com/gorilla/mux" +) + +// ===================================================== +// 🔐 ADMIN — PIYASA CACHE SYNC +// ===================================================== +// POST /api/admin/users/{id}/piyasa-sync +func AdminSyncUserPiyasaHandler(w http.ResponseWriter, r *http.Request) { + + // -------------------------------------------------- + // 🔐 AUTH + // -------------------------------------------------- + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", 401) + return + } + + // -------------------------------------------------- + // 🆔 USER ID PARAM + // -------------------------------------------------- + vars := mux.Vars(r) + + idStr := vars["id"] + + targetID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "invalid user id", http.StatusBadRequest) + return + } + + // -------------------------------------------------- + // 🧹 CACHE CLEAR + // -------------------------------------------------- + authz.ClearPiyasaCache(int(targetID)) + middlewares.ClearAuthzScopeCacheForUser(targetID) + + log.Printf( + "🔄 ADMIN PIYASA SYNC | admin=%d target=%d", + claims.ID, + targetID, + ) + + // -------------------------------------------------- + // ✅ OK + // -------------------------------------------------- + w.Header().Set("Content-Type", "application/json") + + w.Write([]byte(`{"status":"ok"}`)) +} diff --git a/svc/routes/admin_reset_password.go b/svc/routes/admin_reset_password.go new file mode 100644 index 0000000..543de20 --- /dev/null +++ b/svc/routes/admin_reset_password.go @@ -0,0 +1,104 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/auditlog" + "bssapp-backend/repository" + "database/sql" + "encoding/json" + "github.com/gorilla/mux" + "golang.org/x/crypto/bcrypt" + "net/http" + "strconv" +) + +type AdminResetPasswordRequest struct { + Password string `json:"password"` // opsiyonel +} + +func AdminResetPasswordHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + // --------------------------------------------------- + // 1️⃣ USER ID + // --------------------------------------------------- + idStr := mux.Vars(r)["id"] + userID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil || userID <= 0 { + http.Error(w, "invalid user id", http.StatusBadRequest) + return + } + + // --------------------------------------------------- + // 2️⃣ PAYLOAD (opsiyonel) + // --------------------------------------------------- + var req AdminResetPasswordRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + // parola verilmediyse backend üretir + password := req.Password + if password == "" { + password = "Temp123!" // 👉 istersek random generator ekleriz + } + + // --------------------------------------------------- + // 3️⃣ HASH + // --------------------------------------------------- + hash, err := bcrypt.GenerateFromPassword( + []byte(password), + bcrypt.DefaultCost, + ) + if err != nil { + http.Error(w, "password hash error", http.StatusInternalServerError) + return + } + + // --------------------------------------------------- + // 4️⃣ UPDATE mk_dfusr + // --------------------------------------------------- + _, err = db.Exec(` + UPDATE mk_dfusr + SET + password_hash = $1, + force_password_change = true, + password_updated_at = NOW(), + updated_at = NOW() + WHERE id = $2 + `, string(hash), userID) + + if err != nil { + http.Error(w, "password reset failed", http.StatusInternalServerError) + return + } + + // --------------------------------------------------- + // 5️⃣ REFRESH TOKEN REVOKE + // --------------------------------------------------- + _ = repository. + NewRefreshTokenRepository(db). + RevokeAllForUser(userID) + + // --------------------------------------------------- + // 6️⃣ AUDIT + // --------------------------------------------------- + auditlog.Write(auditlog.ActivityLog{ + ActionType: "ADMIN_PASSWORD_RESET", + ActionCategory: "security", + ActionTarget: "mk_dfusr.id", + IsSuccess: true, + }) + + // --------------------------------------------------- + // 7️⃣ RESPONSE + // --------------------------------------------------- + _ = json.NewEncoder(w).Encode(map[string]any{ + "success": true, + }) + } +} diff --git a/svc/routes/audit_helper.go b/svc/routes/audit_helper.go new file mode 100644 index 0000000..402d107 --- /dev/null +++ b/svc/routes/audit_helper.go @@ -0,0 +1,60 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/ctxkeys" + "bssapp-backend/internal/auditlog" + "bssapp-backend/permissions" + "bssapp-backend/repository" + "database/sql" + "encoding/json" +) + +// auditLogFromRequest +// routes içinden çağrılır +// auditLogFromRequest +// routes içinden çağrılır +func auditLogFromRequest( + ctx any, + db *sql.DB, + actionType string, + meta map[string]any, +) { + + al := auditlog.ActivityLog{ + ActionType: actionType, + ActionCategory: "ADMIN", + IsSuccess: true, + } + + // JWT → identity + if c, ok := ctx.(interface { + Value(any) any + }); ok { + if claims, ok := c.Value(ctxkeys.UserContextKey).(*auth.Claims); ok && claims != nil { + + // ✅ TEK KİMLİK + al.DfUsrID = claims.ID + al.Username = claims.Username + al.RoleCode = claims.RoleCode + + // 🔗 MULTI ROLE → ADMIN CHECK + roles, err := repository.GetUserRolesByUserID(db, claims.ID) + if err == nil { + _, isAdmin := permissions.ResolveEffectiveRoles(roles) + if isAdmin { + al.RoleCode = "admin" + } + } + } + } + + // meta → description + if meta != nil { + if b, err := json.Marshal(meta); err == nil { + al.Description = string(b) + } + } + + auditlog.Write(al) +} diff --git a/svc/routes/auth_refresh.go b/svc/routes/auth_refresh.go new file mode 100644 index 0000000..f5b177f --- /dev/null +++ b/svc/routes/auth_refresh.go @@ -0,0 +1,92 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/security" + "bssapp-backend/repository" + "database/sql" + "encoding/json" + "net/http" + "time" +) + +func setRefreshCookie(w http.ResponseWriter, plain string, exp time.Time) { + http.SetCookie(w, &http.Cookie{ + Name: "mk_refresh", + Value: plain, + Path: "/", + Expires: exp, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + Secure: false, // prod: true + }) +} + +func AuthRefreshHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // 1) refresh cookie + c, err := r.Cookie("mk_refresh") + if err != nil || c.Value == "" { + http.Error(w, "refresh token missing", http.StatusUnauthorized) + return + } + + hash := security.HashRefreshToken(c.Value) + rtRepo := repository.NewRefreshTokenRepository(db) + + // 2) validate + consume + mkUserID, err := rtRepo.ConsumeValid(hash) + if err != nil { + http.Error(w, "refresh token invalid", http.StatusUnauthorized) + return + } + + // 3) rotate + newPlain, newHash, err := security.GenerateRefreshToken() + if err != nil { + http.Error(w, "refresh gen failed", http.StatusInternalServerError) + return + } + refreshExp := time.Now().Add(14 * 24 * time.Hour) + if err := rtRepo.IssueRefreshToken(mkUserID, newHash, refreshExp); err != nil { + http.Error(w, "refresh store failed", http.StatusInternalServerError) + return + } + setRefreshCookie(w, newPlain, refreshExp) + + // 4) mk user reload + mkRepo := repository.NewMkUserRepository(db) + mkUser, err := mkRepo.GetByID(mkUserID) + if err != nil || !mkUser.IsActive { + http.Error(w, "user invalid", http.StatusUnauthorized) + return + } + if mkUser.ForcePasswordChange { + http.Error(w, "password change required", http.StatusForbidden) + return + } + + // 5) new access token + claims := auth.BuildClaimsFromUser(mkUser, 15*time.Minute) + + token, err := auth.GenerateToken( + claims, + mkUser.Username, + mkUser.ForcePasswordChange, + ) + if err != nil { + http.Error(w, "access token gen failed", http.StatusInternalServerError) + return + } + + // 6) response + _ = json.NewEncoder(w).Encode(map[string]any{ + "success": true, + "token": token, + }) + + } +} diff --git a/svc/routes/customerlist.go b/svc/routes/customerlist.go new file mode 100644 index 0000000..e61b8e8 --- /dev/null +++ b/svc/routes/customerlist.go @@ -0,0 +1,57 @@ +package routes + +import ( + "encoding/json" + "log" + "net/http" + + "bssapp-backend/auth" + "bssapp-backend/queries" +) + +func GetCustomerListHandler(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // -------------------------------------------------- + // 🔐 CLAIMS (DEBUG / TRACE) + // -------------------------------------------------- + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + log.Printf( + "📥 /api/customers | user=%d admin=%v", + claims.ID, + claims.IsAdmin(), + ) + + // -------------------------------------------------- + // 🗄️ QUERY (CONTEXT TAŞIYOR) + // -------------------------------------------------- + list, err := queries.GetCustomerList(r.Context()) + if err != nil { + log.Println("❌ Customer list error:", err) + http.Error(w, "Veritabanı hatası", http.StatusInternalServerError) + return + } + + // -------------------------------------------------- + // 📤 JSON OUTPUT + // -------------------------------------------------- + if err := json.NewEncoder(w).Encode(list); err != nil { + log.Printf("❌ JSON encode error: %v", err) + return + } + + // -------------------------------------------------- + // 📊 RESULT LOG + // -------------------------------------------------- + log.Printf( + "✅ Customer list DONE | user=%d | resultCount=%d", + claims.ID, + len(list), + ) +} diff --git a/svc/routes/first_password_change.go b/svc/routes/first_password_change.go new file mode 100644 index 0000000..8530e65 --- /dev/null +++ b/svc/routes/first_password_change.go @@ -0,0 +1,169 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/auditlog" + "bssapp-backend/internal/security" + "bssapp-backend/models" + "bssapp-backend/repository" + "database/sql" + "encoding/json" + "log" + "net/http" + "time" + + "golang.org/x/crypto/bcrypt" +) + +func FirstPasswordChangeHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // -------------------------------------------------- + // 1️⃣ JWT CLAIMS + // -------------------------------------------------- + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + // -------------------------------------------------- + // 2️⃣ PAYLOAD + // -------------------------------------------------- + var req struct { + CurrentPassword string `json:"current_password"` + NewPassword string `json:"new_password"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + + if req.CurrentPassword == "" || req.NewPassword == "" { + http.Error(w, "password fields required", http.StatusUnprocessableEntity) + return + } + + // -------------------------------------------------- + // 3️⃣ LOAD USER (mk_dfusr) + // -------------------------------------------------- + var currentHash string + err := db.QueryRow(` + SELECT password_hash + FROM mk_dfusr + WHERE id = $1 + `, claims.ID).Scan(¤tHash) + + if err != nil || currentHash == "" { + http.Error(w, "user not found", http.StatusUnauthorized) + return + } + + // -------------------------------------------------- + // 4️⃣ CURRENT PASSWORD CHECK + // -------------------------------------------------- + if bcrypt.CompareHashAndPassword( + []byte(currentHash), + []byte(req.CurrentPassword), + ) != nil { + http.Error(w, "mevcut şifre hatalı", http.StatusUnauthorized) + return + } + + // -------------------------------------------------- + // 5️⃣ PASSWORD POLICY + // -------------------------------------------------- + if err := security.ValidatePassword(req.NewPassword); err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // -------------------------------------------------- + // 6️⃣ HASH NEW PASSWORD + // -------------------------------------------------- + hash, err := bcrypt.GenerateFromPassword( + []byte(req.NewPassword), + bcrypt.DefaultCost, + ) + if err != nil { + http.Error(w, "password hash error", http.StatusInternalServerError) + return + } + + // -------------------------------------------------- + // 7️⃣ UPDATE mk_dfusr + // -------------------------------------------------- + _, err = db.Exec(` + UPDATE mk_dfusr + SET + password_hash = $1, + force_password_change = false, + password_updated_at = NOW(), + updated_at = NOW() + WHERE id = $2 + `, string(hash), claims.ID) + + if err != nil { + http.Error(w, "password update failed", http.StatusInternalServerError) + return + } + + // -------------------------------------------------- + // 8️⃣ REFRESH TOKEN REVOKE + // -------------------------------------------------- + _ = repository. + NewRefreshTokenRepository(db). + RevokeAllForUser(claims.ID) + + // -------------------------------------------------- + // 9️⃣ NEW JWT (TEK DOĞRU YOL) + // -------------------------------------------------- + newClaims := auth.BuildClaimsFromUser( + &models.MkUser{ + ID: claims.ID, + Username: claims.Username, + RoleCode: claims.RoleCode, + V3Username: claims.V3Username, + V3UserGroup: claims.V3UserGroup, + SessionID: claims.SessionID, + ForcePasswordChange: false, + }, + 15*time.Minute, + ) + + newToken, err := auth.GenerateToken( + newClaims, + claims.Username, + false, + ) + if err != nil { + http.Error(w, "token generation failed", http.StatusInternalServerError) + return + } + + // -------------------------------------------------- + // 🔟 AUDIT + // -------------------------------------------------- + auditlog.ForcePasswordChangeCompleted( + r.Context(), + claims.ID, + "self_change", + ) + + // -------------------------------------------------- + // 1️⃣1️⃣ RESPONSE + // -------------------------------------------------- + _ = json.NewEncoder(w).Encode(map[string]any{ + "token": newToken, + "user": map[string]any{ + "id": claims.ID, + "username": claims.Username, + "force_password_change": false, + }, + }) + log.Printf("✅ FIRST-PASS claims user=%d role=%s", claims.ID, claims.RoleCode) + } +} diff --git a/svc/routes/login.go b/svc/routes/login.go new file mode 100644 index 0000000..89685f8 --- /dev/null +++ b/svc/routes/login.go @@ -0,0 +1,733 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/auditlog" + "bssapp-backend/models" + "bssapp-backend/queries" + "bssapp-backend/repository" + "bssapp-backend/services" + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + "golang.org/x/crypto/bcrypt" +) + +/* ====================================================== + 🔐 LOGIN +====================================================== */ + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +func LoginHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // -------------------------------------------------- + // 0️⃣ REQUEST + // -------------------------------------------------- + var req LoginRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Geçersiz JSON", http.StatusBadRequest) + return + } + + login := strings.TrimSpace(req.Username) + pass := req.Password // ⚠️ TRIM YAPMA + + if login == "" || pass == "" { + http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized) + return + } + + mkRepo := repository.NewMkUserRepository(db) + + // ================================================== + // 1️⃣ mk_dfusr ÖNCELİKLİ + // ================================================== + mkUser, err := mkRepo.GetByUsername(login) + if err == nil { + log.Println("🧪 MK USER FROM DB") + log.Printf("🧪 ID=%d role_id=%d role_code='%s' depts=%v", + mkUser.ID, + mkUser.RoleID, + mkUser.RoleCode, + mkUser.DepartmentCodes, + ) + + } + + log.Printf( + "🔎 LOGIN DEBUG | mk_user_found=%v err=%v hash_len=%d", + err == nil, + err, + func() int { + if err == nil { + return len(strings.TrimSpace(mkUser.PasswordHash)) + } + return 0 + }(), + ) + + if err == nil { + + // mk_dfusr authoritative + if strings.TrimSpace(mkUser.PasswordHash) != "" { + + if bcrypt.CompareHashAndPassword( + []byte(mkUser.PasswordHash), + []byte(pass), + ) != nil { + http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized) + return + } + + _ = mkRepo.TouchLastLogin(mkUser.ID) + writeLoginResponse(w, db, mkUser) + return + } + // password_hash boşsa legacy fallback + } else if err != repository.ErrMkUserNotFound { + log.Println("❌ mk_dfusr lookup error:", err) + http.Error(w, "Giriş yapılamadı", http.StatusInternalServerError) + return + } + + // ================================================== + // 2️⃣ dfusr FALLBACK (LEGACY) + // ================================================== + log.Println("🟡 LEGACY LOGIN PATH:", login) + + legacyRepo := repository.NewUserRepository(db) + legacyUser, err := legacyRepo.GetLegacyUserForLogin(login) + if err != nil || !legacyUser.IsActive { + http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized) + return + } + + log.Printf( + "🔎 LOGIN DEBUG | legacy_upass_len=%d prefix=%s", + len(strings.TrimSpace(legacyUser.Upass)), + func() string { + if len(legacyUser.Upass) >= 4 { + return legacyUser.Upass[:4] + } + return legacyUser.Upass + }(), + ) + + if !services.CheckPasswordWithLegacy(legacyUser, pass) { + http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized) + return + } + + // ================================================== + // 3️⃣ MIGRATION (dfusr → mk_dfusr) + // ================================================== + newHash, err := bcrypt.GenerateFromPassword( + []byte(pass), + bcrypt.DefaultCost, + ) + if err != nil { + http.Error(w, "Şifre üretilemedi", http.StatusInternalServerError) + return + } + + mkUser, err = mkRepo.CreateFromLegacy(legacyUser, string(newHash)) + if err != nil { + log.Println("❌ CREATE_FROM_LEGACY FAILED:", err) + http.Error(w, "Kullanıcı migrate edilemedi", http.StatusInternalServerError) + return + } + + // 🔥 KRİTİK: TOKEN GUARD İÇİN GARANTİ + mkUser.ForcePasswordChange = true + + auditlog.Write(auditlog.ActivityLog{ + ActionType: "LEGACY_USER_MIGRATED", + ActionCategory: "security", + Description: "dfusr -> mk_dfusr on login", + IsSuccess: true, + }) + + // ================================================== + // 4️⃣ SUCCESS + // ================================================== + writeLoginResponse(w, db, mkUser) + } +} + +// ====================================================== +// 🔑 LOGIN RESPONSE +// ====================================================== + +func writeLoginResponse(w http.ResponseWriter, db *sql.DB, user *models.MkUser) { + // 🔥 ROLE GARANTİSİ + if user.RoleID == 0 { + + _ = db.QueryRow(` + SELECT dfrole_id + FROM dfrole_usr + WHERE dfusr_id = $1 + LIMIT 1 + `, user.ID).Scan(&user.RoleID) + } + + if user.RoleCode == "" && user.RoleID > 0 { + + _ = db.QueryRow(` + SELECT code + FROM dfrole + WHERE id = $1 + `, user.RoleID).Scan(&user.RoleCode) + } + + log.Println("🧪 LOGIN RESPONSE USER DEBUG") + log.Printf("🧪 user.ID = %d", user.ID) + log.Printf("🧪 user.Username = %s", user.Username) + log.Printf("🧪 user.RoleID = %d", user.RoleID) + log.Printf("🧪 user.RoleCode = '%s'", user.RoleCode) + log.Printf("🧪 user.IsActive = %v", user.IsActive) + + permRepo := repository.NewPermissionRepository(db) + perms, _ := permRepo.GetPermissionsByRoleID(user.RoleID) + + // ✅ CLAIMS BUILD + claims := auth.BuildClaimsFromUser(user, 15*time.Minute) + + token, err := auth.GenerateToken( + claims, + user.Username, + user.ForcePasswordChange, + ) + if err != nil { + http.Error(w, "Token üretilemedi", http.StatusInternalServerError) + return + } + + _ = json.NewEncoder(w).Encode(map[string]any{ + "token": token, + "user": map[string]any{ + "id": user.ID, + "username": user.Username, + "email": user.Email, + "is_active": user.IsActive, + "role_id": user.RoleID, + "role_code": user.RoleCode, + "force_password_change": user.ForcePasswordChange, + "v3_username": user.V3Username, + "v3_usergroup": user.V3UserGroup, + }, + "permissions": perms, + }) +} + +/* ====================================================== + 🔎 LOOKUPS (aynen korunuyor) +====================================================== */ + +type LookupOption struct { + Value string `json:"value"` + Label string `json:"label"` +} + +func GetRoleLookupRoute(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rows, err := db.Query(queries.GetRoleLookup) + if err != nil { + http.Error(w, "role lookup error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var list []LookupOption + for rows.Next() { + var o LookupOption + if err := rows.Scan(&o.Value, &o.Label); err == nil { + list = append(list, o) + } + } + _ = json.NewEncoder(w).Encode(list) + } +} + +func GetDepartmentLookupRoute(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rows, err := db.Query(queries.GetDepartmentLookup) + if err != nil { + http.Error(w, "department lookup error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var list []LookupOption + for rows.Next() { + var o LookupOption + if err := rows.Scan(&o.Value, &o.Label); err == nil { + list = append(list, o) + } + } + _ = json.NewEncoder(w).Encode(list) + } +} + +func GetPiyasaLookupRoute(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rows, err := db.Query(queries.GetPiyasaLookup) + if err != nil { + http.Error(w, "piyasa lookup error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var list []LookupOption + for rows.Next() { + var o LookupOption + if err := rows.Scan(&o.Value, &o.Label); err == nil { + list = append(list, o) + } + } + _ = json.NewEncoder(w).Encode(list) + } +} + +// ====================================================== +// 🧾 NEBIM USER LOOKUP +// GET /api/lookups/nebim-users +// ====================================================== +func GetNebimUserLookupRoute(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + rows, err := db.Query(queries.GetNebimUserLookup) + if err != nil { + http.Error(w, "nebim lookup error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var list []map[string]string + for rows.Next() { + var v, l, g string + if err := rows.Scan(&v, &l, &g); err == nil { + list = append(list, map[string]string{ + "value": v, + "label": l, + "user_group_code": g, + }) + } + } + + _ = json.NewEncoder(w).Encode(list) + } +} +func UserCreateRoute(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + var payload models.UserWrite + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Geçersiz payload", http.StatusBadRequest) + return + } + + if payload.Code == "" { + http.Error(w, "Kullanıcı kodu zorunludur", http.StatusUnprocessableEntity) + return + } + + tx, err := db.Begin() + if err != nil { + http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError) + return + } + defer tx.Rollback() + + var newID int64 + err = tx.QueryRow(` + INSERT INTO mk_dfusr ( + code, + is_active, + full_name, + email, + mobile, + address, + force_password_change, + last_updated_date + ) + VALUES ($1,$2,$3,$4,$5,$6,true,NOW()) + RETURNING id + `, + payload.Code, + payload.IsActive, + payload.FullName, + payload.Email, + payload.Mobile, + payload.Address, + ).Scan(&newID) + + if err != nil { + log.Println("USER INSERT ERROR:", err) + http.Error(w, "Kullanıcı oluşturulamadı", http.StatusInternalServerError) + return + } + + // ROLES + for _, role := range payload.Roles { + _, _ = tx.Exec(queries.InsertUserRole, newID, role) + } + + // DEPARTMENTS + for _, d := range payload.Departments { + _, _ = tx.Exec(queries.InsertUserDepartment, newID, d.Code) + } + + // PIYASALAR + for _, p := range payload.Piyasalar { + _, _ = tx.Exec(queries.InsertUserPiyasa, newID, p.Code) + } + + // NEBIM + for _, n := range payload.NebimUsers { + _, _ = tx.Exec(queries.InsertUserNebim, newID, n.Username) + } + + if err := tx.Commit(); err != nil { + http.Error(w, "Commit başarısız", http.StatusInternalServerError) + return + } + + _ = json.NewEncoder(w).Encode(map[string]any{ + "success": true, + "id": newID, + }) + } +} + +// ====================================================== +// 🔐 ROLES LIST +// GET /api/roles +// ====================================================== + +func GetRolesHandler(db *sql.DB) http.HandlerFunc { + type RoleRow struct { + ID int64 `json:"id"` + Code string `json:"code"` + Title string `json:"title"` + } + + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + rows, err := db.Query(queries.GetRoles) + if err != nil { + http.Error(w, "roles query error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var list []RoleRow + for rows.Next() { + var x RoleRow + if err := rows.Scan(&x.ID, &x.Code, &x.Title); err == nil { + list = append(list, x) + } + } + + _ = json.NewEncoder(w).Encode(list) + } +} + +// ====================================================== +// 🏢 DEPARTMENTS LIST +// GET /api/departments +// ====================================================== + +func GetDepartmentsHandler(db *sql.DB) http.HandlerFunc { + type DeptRow struct { + ID int64 `json:"id"` + Code string `json:"code"` + Title string `json:"title"` + } + + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + rows, err := db.Query(queries.GetDepartments) + if err != nil { + http.Error(w, "departments query error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var list []DeptRow + for rows.Next() { + var x DeptRow + if err := rows.Scan(&x.ID, &x.Code, &x.Title); err == nil { + list = append(list, x) + } + } + + _ = json.NewEncoder(w).Encode(list) + } +} + +// ====================================================== +// 🌍 PIYASALAR LIST +// GET /api/piyasalar +// ====================================================== + +func GetPiyasalarHandler(db *sql.DB) http.HandlerFunc { + type PiyRow struct { + Code string `json:"code"` + Title string `json:"title"` + } + + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + rows, err := db.Query(queries.GetPiyasalar) + if err != nil { + http.Error(w, "piyasalar query error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var list []PiyRow + for rows.Next() { + var x PiyRow + if err := rows.Scan(&x.Code, &x.Title); err == nil { + list = append(list, x) + } + } + + _ = json.NewEncoder(w).Encode(list) + } +} + +// ====================================================== +// 🔗 ROLE → DEPARTMENTS UPDATE +// POST /api/roles/{id}/departments +// body: { "codes": ["D01","D02"] } +// ====================================================== + +type CodesPayload struct { + Codes []string `json:"codes"` +} + +func UpdateRoleDepartmentsHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + roleIDStr := mux.Vars(r)["id"] + roleID, err := strconv.ParseInt(roleIDStr, 10, 64) + if err != nil || roleID <= 0 { + http.Error(w, "Geçersiz role id", http.StatusBadRequest) + return + } + + var payload CodesPayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Geçersiz payload", http.StatusBadRequest) + return + } + + tx, err := db.Begin() + if err != nil { + http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError) + return + } + defer tx.Rollback() + + // reset + if _, err := tx.Exec(queries.DeleteRoleDepartments, roleID); err != nil { + http.Error(w, "Role departments silinemedi", http.StatusInternalServerError) + return + } + + // insert new + for _, code := range payload.Codes { + code = strings.TrimSpace(code) + if code == "" { + continue + } + if _, err := tx.Exec(queries.InsertRoleDepartment, roleID, code); err != nil { + http.Error(w, "Role department eklenemedi", http.StatusInternalServerError) + return + } + } + + if err := tx.Commit(); err != nil { + http.Error(w, "Commit başarısız", http.StatusInternalServerError) + return + } + + _ = json.NewEncoder(w).Encode(map[string]any{"success": true}) + } +} + +// ====================================================== +// 🔗 ROLE → PIYASALAR UPDATE +// POST /api/roles/{id}/piyasalar +// body: { "codes": ["TR","EU"] } (piyasa_code list) +// ====================================================== + +func UpdateRolePiyasalarHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + roleIDStr := mux.Vars(r)["id"] + roleID, err := strconv.ParseInt(roleIDStr, 10, 64) + if err != nil || roleID <= 0 { + http.Error(w, "Geçersiz role id", http.StatusBadRequest) + return + } + + var payload CodesPayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Geçersiz payload", http.StatusBadRequest) + return + } + + tx, err := db.Begin() + if err != nil { + http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError) + return + } + defer tx.Rollback() + + // reset + if _, err := tx.Exec(queries.DeleteRolePiyasalar, roleID); err != nil { + http.Error(w, "Role piyasalar silinemedi", http.StatusInternalServerError) + return + } + + // insert new + for _, code := range payload.Codes { + code = strings.TrimSpace(code) + if code == "" { + continue + } + if _, err := tx.Exec(queries.InsertRolePiyasa, roleID, code); err != nil { + http.Error(w, "Role piyasa eklenemedi", http.StatusInternalServerError) + return + } + } + + if err := tx.Commit(); err != nil { + http.Error(w, "Commit başarısız", http.StatusInternalServerError) + return + } + + _ = json.NewEncoder(w).Encode(map[string]any{"success": true}) + } +} + +// ====================================================== +// 👤 USER → ROLES UPDATE +// POST /api/users/{id}/roles +// body: { "codes": ["admin","user"] } (dfrole.code list) +// ====================================================== + +func UpdateUserRolesHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // ---------- CLAIMS ---------- + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || !claims.IsAdmin() { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + // ---------- TARGET USER ---------- + userIDStr := mux.Vars(r)["id"] + userID, err := strconv.ParseInt(userIDStr, 10, 64) + if err != nil || userID <= 0 { + http.Error(w, "Geçersiz user id", http.StatusBadRequest) + return + } + + // ---------- PAYLOAD ---------- + var payload CodesPayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Geçersiz payload", http.StatusBadRequest) + return + } + + // ---------- BEFORE (ROLE LIST) ---------- + oldRoles, _ := repository.GetUserRolesByUserID(db, userID) + + // ---------- TX ---------- + tx, err := db.Begin() + if err != nil { + http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError) + return + } + defer tx.Rollback() + + // reset + if _, err := tx.Exec(queries.DeleteUserRoles, userID); err != nil { + http.Error(w, "User roles silinemedi", http.StatusInternalServerError) + return + } + + // insert new + for _, roleCode := range payload.Codes { + roleCode = strings.TrimSpace(roleCode) + if roleCode == "" { + continue + } + if _, err := tx.Exec(queries.InsertUserRole, userID, roleCode); err != nil { + http.Error(w, "User role eklenemedi", http.StatusInternalServerError) + return + } + } + + if err := tx.Commit(); err != nil { + http.Error(w, "Commit başarısız", http.StatusInternalServerError) + return + } + + // ---------- AFTER (ROLE LIST) ---------- + newRoles, _ := repository.GetUserRolesByUserID(db, userID) + + // ---------- AUDIT ---------- + auditlog.Enqueue(r.Context(), auditlog.ActivityLog{ + ActionType: "role_change", + ActionCategory: "user_admin", + ActionTarget: fmt.Sprintf("/api/users/%d/roles", userID), + Description: "user roles updated", + + Username: claims.Username, + RoleCode: claims.RoleCode, + DfUsrID: int64(claims.ID), + + TargetDfUsrID: userID, + + ChangeBefore: map[string]any{ + "roles": oldRoles, + }, + ChangeAfter: map[string]any{ + "roles": newRoles, + }, + + IsSuccess: true, + }) + + _ = json.NewEncoder(w).Encode(map[string]any{ + "success": true, + }) + } +} diff --git a/svc/routes/order_list_excel.go b/svc/routes/order_list_excel.go new file mode 100644 index 0000000..90ff2a8 --- /dev/null +++ b/svc/routes/order_list_excel.go @@ -0,0 +1,91 @@ +package routes + +import ( + "bssapp-backend/models" + "bssapp-backend/queries" + "database/sql" + "fmt" + "net/http" + "time" + + "github.com/xuri/excelize/v2" +) + +func OrderListExcelRoute(db *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + search := r.URL.Query().Get("search") + currAcc := r.URL.Query().Get("CurrAccCode") + orderDate := r.URL.Query().Get("OrderDate") + + rows, err := queries.GetOrderListExcel(db, search, currAcc, orderDate) + if err != nil { + http.Error(w, "Veritabanı hatası", 500) + return + } + defer rows.Close() + + f := excelize.NewFile() + sheet := "Orders" + f.SetSheetName("Sheet1", sheet) + + headers := []string{ + "Sipariş No", "Tarih", "Cari Kod", "Cari Adı", + "Temsilci", "Piyasa", "Onay Tarihi", "PB", + "Tutar", "Tutar (USD)", "Açıklama", + } + + for i, h := range headers { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + f.SetCellValue(sheet, cell, h) + } + + rowIdx := 2 + for rows.Next() { + var o models.OrderList + _ = rows.Scan( + &o.OrderHeaderID, + &o.OrderNumber, + &o.OrderDate, + &o.CurrAccCode, + &o.CurrAccDescription, + &o.MusteriTemsilcisi, + &o.Piyasa, + &o.CreditableConfirmedDate, + &o.DocCurrencyCode, + &o.TotalAmount, + &o.TotalAmountUSD, + &o.IsCreditableConfirmed, + &o.Description, + &o.ExchangeRateUSD, + ) + + f.SetSheetRow(sheet, fmt.Sprintf("A%d", rowIdx), &[]interface{}{ + o.OrderNumber, + o.OrderDate, + o.CurrAccCode, + o.CurrAccDescription, + o.MusteriTemsilcisi, + o.Piyasa, + o.CreditableConfirmedDate, + o.DocCurrencyCode, + o.TotalAmount, + o.TotalAmountUSD, + o.Description, + }) + rowIdx++ + } + + filename := fmt.Sprintf( + "order_list_%s.xlsx", + time.Now().Format("2006-01-02_15-04"), + ) + + w.Header().Set("Content-Type", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + w.Header().Set("Content-Disposition", + "attachment; filename="+filename) + + _ = f.Write(w) + }) +} diff --git a/svc/routes/order_pdf.go b/svc/routes/order_pdf.go new file mode 100644 index 0000000..83da361 --- /dev/null +++ b/svc/routes/order_pdf.go @@ -0,0 +1,1387 @@ +package routes + +import ( + "bytes" + "database/sql" + "fmt" + "github.com/gorilla/mux" + "github.com/jung-kurt/gofpdf" + "log" + "math" + "net/http" + "os" + "sort" + "strings" + "time" +) + +/* =========================================================== + 1) SABİTLER / RENKLER / TİPLER + =========================================================== */ + +// Baggi renkleri +var ( + baggiGoldR, baggiGoldG, baggiGoldB = 201, 162, 39 + baggiCreamR, baggiCreamG, baggiCreamB = 255, 254, 249 + baggiGrayBorderR, baggiGrayBorderG, baggiGrayBorderB = 187, 187, 187 +) + +// Beden kategorileri (frontend birebir) +const ( + catAyk = "ayk" + catYas = "yas" + catPan = "pan" + catGom = "gom" + catTak = "tak" + catAksbir = "aksbir" +) + +var categoryOrder = []string{catAyk, catYas, catPan, catGom, catTak, catAksbir} + +var categoryTitle = map[string]string{ + catAyk: " AYAKKABI", + catYas: " YAŞ", + catPan: " PANTOLON", + catGom: " GÖMLEK", + catTak: " TAKIM ELBİSE", + catAksbir: " AKSESUAR", +} + +/* =========================================================== + HEADER MODEL + =========================================================== */ + +type OrderHeader struct { + OrderHeaderID string + OrderNumber string + CurrAccCode string + CurrAccName string + DocCurrency string + OrderDate time.Time + Description string + InternalDesc string + OfficeCode string + CreatedUser string + CustomerRep string // 🆕 Müşteri Temsilcisi +} + +/* =========================================================== + RAW LINE MODEL + =========================================================== */ + +type OrderLineRaw struct { + OrderLineID sql.NullString + ItemCode string + ColorCode string + ItemDim1Code sql.NullString + ItemDim2Code sql.NullString + Qty1 sql.NullFloat64 + Price sql.NullFloat64 + DocCurrencyCode sql.NullString + DeliveryDate sql.NullTime + LineDescription sql.NullString + UrunAnaGrubu sql.NullString + UrunAltGrubu sql.NullString + IsClosed sql.NullBool + WithHoldingTaxType sql.NullString + DOVCode sql.NullString + PlannedDateOfLading sql.NullTime + CostCenterCode sql.NullString + VatCode sql.NullString + VatRate sql.NullFloat64 +} + +/* =========================================================== + PDF SATIR MODELİ + =========================================================== */ + +type PdfRow struct { + Model string + Color string + GroupMain string + GroupSub string + Description string + Category string + SizeQty map[string]int + TotalQty int + Price float64 + Currency string + Amount float64 + Termin string + IsClosed bool + OrderLineIDs map[string]string + + ClosedSizes map[string]bool // 🆕 her beden için IsClosed bilgisi +} + +/* =========================================================== + PDF LAYOUT STRUCT + =========================================================== */ + +type pdfLayout struct { + PageW, PageH float64 + MarginL float64 + MarginR float64 + MarginT float64 + MarginB float64 + + ColModelW float64 + ColRenkW float64 + ColGroupW float64 + ColGroupW2 float64 + ColDescLeft float64 + ColDescRight float64 + ColDescW float64 + ColQtyW float64 + ColPriceW float64 + ColCurW float64 + ColAmountW float64 + ColTerminW float64 + + CentralW float64 + HeaderMainH float64 + HeaderSizeH float64 + RowH float64 +} + +/* genel cell padding */ +const OcellPadX = 2 + +/* +=========================================================== + + PDF LAYOUT OLUŞTURUCU + =========================================================== +*/ + +/* +=========================================================== + + PDF LAYOUT OLUŞTURUCU — AÇIKLAMA TEK KOLON + =========================================================== +*/ +func newPdfLayout(pdf *gofpdf.Fpdf) pdfLayout { + pageW, pageH := pdf.GetPageSize() + + l := pdfLayout{ + PageW: pageW, + PageH: pageH, + MarginL: 10, + MarginR: 10, + MarginT: 10, + MarginB: 12, + + RowH: 7, + HeaderMainH: 8, + HeaderSizeH: 6, + } + + totalW := pageW - l.MarginL - l.MarginR + + /* -------------------------------------------------------- + SOL BLOK GÜNCEL – MODEL/RENK ÇAKIŞMASI GİDERİLDİ + -------------------------------------------------------- */ + l.ColModelW = 24 // eski 18 → genişletildi + l.ColRenkW = 24 // eski 14 → biraz geniş + l.ColGroupW = 20 + l.ColGroupW2 = 20 + + /* -------------------------------------------------------- + AÇIKLAMA = TEK GENİŞ KOLON (kategori listesi + açıklama içerir) + -------------------------------------------------------- */ + l.ColDescLeft = 50 // açıklama başlığı + kategori alanı (artık tek kolon) + l.ColDescRight = 0 // kullanılmıyor (0 bırakılmalı!) + + left := l.ColModelW + l.ColRenkW + l.ColGroupW + l.ColGroupW2 + l.ColDescLeft + + /* -------------------------------------------------------- + SAĞ BLOK + -------------------------------------------------------- */ + l.ColQtyW = 12 + l.ColPriceW = 16 + l.ColCurW = 10 + l.ColAmountW = 20 + l.ColTerminW = 20 + + right := l.ColQtyW + l.ColPriceW + l.ColCurW + l.ColAmountW + l.ColTerminW + + /* -------------------------------------------------------- + ORTA BLOK (BEDEN 16 KOLON) + -------------------------------------------------------- */ + l.CentralW = totalW - left - right + if l.CentralW < 70 { + l.CentralW = 70 + } + + return l +} + +/* =========================================================== + HELPER FONKSİYONLAR + =========================================================== */ + +func safeTrimUpper(s string) string { + return strings.ToUpper(strings.TrimSpace(s)) +} + +func f64(v sql.NullFloat64) float64 { + if !v.Valid { + return 0 + } + return v.Float64 +} + +func s64(v sql.NullString) string { + if !v.Valid { + return "" + } + return v.String +} + +func normalizeBedenLabelGo(v string) string { + // 1️⃣ NULL / boş / whitespace → " " (aksbir null kolonu) + s := strings.TrimSpace(v) + if s == "" { + return " " // 🔥 NULL BEDEN → boş kolon + } + + // 2️⃣ Uppercase + s = strings.ToUpper(s) + + /* -------------------------------------------------- + 🔥 AKSBİR ÖZEL (STD eş anlamlıları) + -------------------------------------------------- */ + switch s { + case "STD", "STANDART", "STANDARD", "ONE SIZE", "ONESIZE": + return "STD" + } + + /* -------------------------------------------------- + 🔢 SADECE "CM" VARSA → NUMERİK KISMI AL + 120CM / 120 CM → 120 + ❌ 105 / 110 / 120 → DOKUNMA + -------------------------------------------------- */ + if strings.HasSuffix(s, "CM") { + num := strings.TrimSpace(strings.TrimSuffix(s, "CM")) + if num != "" { + return num + } + } + + /* -------------------------------------------------- + HARF BEDENLER (DOKUNMA) + -------------------------------------------------- */ + switch s { + case "XS", "S", "M", "L", "XL", + "2XL", "3XL", "4XL", "5XL", "6XL", "7XL": + return s + } + + // 4️⃣ Sayısal veya başka değerler → olduğu gibi + return s +} + +func detectBedenGroupGo(bedenList []string, ana, alt string) string { + ana = safeTrimUpper(ana) + alt = safeTrimUpper(alt) + + for _, b := range bedenList { + switch b { + case "XS", "S", "M", "L", "XL": + return catGom + } + } + + if strings.Contains(ana, "PANTOLON") { + return catPan + } + if strings.Contains(alt, "ÇOCUK") || strings.Contains(alt, "GARSON") { + return catYas + } + return catTak +} +func defaultSizeListFor(cat string) []string { + switch cat { + case catAyk: + return []string{"39", "40", "41", "42", "43", "44", "45"} + case catYas: + return []string{"2", "4", "6", "8", "10", "12", "14"} + case catPan: + return []string{"38", "40", "42", "44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68"} + case catGom: + return []string{"XS", "S", "M", "L", "XL", "2XL", "3XL", "4XL", "5XL", "6XL", "7XL"} + case catTak: + return []string{"44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68", "70", "72", "74"} + case catAksbir: + return []string{"", "44", "STD", "110", "115", "120", "125", "130", "135"} + } + return []string{} +} + +func contains(list []string, v string) bool { + for _, x := range list { + if x == v { + return true + } + } + return false +} + +/* =========================================================== + 2) PDF OLUŞTURUCU (A4 YATAY + FOOTER) + =========================================================== */ + +func newOrderPdf() *gofpdf.Fpdf { + pdf := gofpdf.New("L", "mm", "A4", "") + pdf.SetMargins(10, 10, 10) + pdf.SetAutoPageBreak(false, 12) + + // UTF8 fontlar + pdf.AddUTF8Font("dejavu", "", "fonts/DejaVuSans.ttf") + pdf.AddUTF8Font("dejavu-b", "", "fonts/DejaVuSans-Bold.ttf") + + // Footer: sayfa numarası + pdf.AliasNbPages("") + pdf.SetFooterFunc(func() { + pdf.SetY(-10) + pdf.SetFont("dejavu", "", 8) + txt := fmt.Sprintf("Sayfa %d/{nb}", pdf.PageNo()) + pdf.CellFormat(0, 10, txt, "", 0, "R", false, 0, "") + }) + + return pdf +} + +/* =========================================================== + 3) DB FONKSİYONLARI (HEADER + LINES) + =========================================================== */ + +// HEADER +func getOrderHeaderFromDB(db *sql.DB, orderID string) (*OrderHeader, error) { + row := db.QueryRow(` + SELECT + CAST(h.OrderHeaderID AS varchar(36)), + h.OrderNumber, + h.CurrAccCode, + d.CurrAccDescription, + h.DocCurrencyCode, + h.OrderDate, + h.Description, + h.InternalDescription, + h.OfficeCode, + h.CreatedUserName, + ISNULL(( + SELECT TOP (1) ca.AttributeDescription + FROM BAGGI_V3.dbo.cdCurrAccAttributeDesc AS ca WITH (NOLOCK) + WHERE ca.CurrAccTypeCode = 3 + AND ca.AttributeTypeCode = 2 -- 🟡 Müşteri Temsilcisi + AND ca.AttributeCode = f.CustomerAtt02 + AND ca.LangCode = 'TR' + ), '') AS CustomerRep + FROM BAGGI_V3.dbo.trOrderHeader AS h + LEFT JOIN BAGGI_V3.dbo.cdCurrAccDesc AS d + ON h.CurrAccCode = d.CurrAccCode + LEFT JOIN BAGGI_V3.dbo.CustomerAttributesFilter AS f + ON h.CurrAccCode = f.CurrAccCode + WHERE h.OrderHeaderID = @p1 + `, orderID) + + var h OrderHeader + var orderDate sql.NullTime + + err := row.Scan( + &h.OrderHeaderID, + &h.OrderNumber, + &h.CurrAccCode, + &h.CurrAccName, + &h.DocCurrency, + &orderDate, + &h.Description, + &h.InternalDesc, + &h.OfficeCode, + &h.CreatedUser, + &h.CustomerRep, // 🆕 buradan geliyor + ) + if err != nil { + return nil, err + } + + if orderDate.Valid { + h.OrderDate = orderDate.Time + } + return &h, nil +} + +// LINES +func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) { + rows, err := db.Query(` + SELECT + CAST(L.OrderLineID AS varchar(36)), + L.ItemCode, + L.ColorCode, + L.ItemDim1Code, + L.ItemDim2Code, + L.Qty1, + L.Price, + L.DocCurrencyCode, + L.DeliveryDate, + L.LineDescription, + P.ProductAtt01Desc, + P.ProductAtt02Desc, + L.IsClosed, + L.WithHoldingTaxTypeCode, + L.DOVCode, + L.PlannedDateOfLading, + L.CostCenterCode, + L.VatCode, + L.VatRate + FROM BAGGI_V3.dbo.trOrderLine AS L + LEFT JOIN ProductFilterWithDescription('TR') AS P + ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode)) + WHERE L.OrderHeaderID = @p1 + ORDER BY L.SortOrder, L.OrderLineID + `, orderID) + + if err != nil { + return nil, err + } + defer rows.Close() + + var out []OrderLineRaw + for rows.Next() { + var l OrderLineRaw + if err := rows.Scan( + &l.OrderLineID, + &l.ItemCode, + &l.ColorCode, + &l.ItemDim1Code, + &l.ItemDim2Code, + &l.Qty1, + &l.Price, + &l.DocCurrencyCode, + &l.DeliveryDate, + &l.LineDescription, + &l.UrunAnaGrubu, + &l.UrunAltGrubu, + &l.IsClosed, + &l.WithHoldingTaxType, + &l.DOVCode, + &l.PlannedDateOfLading, + &l.CostCenterCode, + &l.VatCode, + &l.VatRate, + ); err != nil { + return nil, err + } + out = append(out, l) + } + return out, rows.Err() +} + +/* =========================================================== + 4) NORMALIZE + CATEGORY MAP + =========================================================== */ + +func normalizeOrderLinesForPdf(lines []OrderLineRaw) []PdfRow { + type comboKey struct { + Model, Color, Color2 string + } + + merged := make(map[comboKey]*PdfRow) + + for _, raw := range lines { + + // ❌ ARTIK KAPALI SATIRLARI ATMAYACAĞIZ + // if raw.IsClosed.Valid && raw.IsClosed.Bool { + // continue + // } + + model := safeTrimUpper(raw.ItemCode) + color := safeTrimUpper(raw.ColorCode) + color2 := safeTrimUpper(s64(raw.ItemDim2Code)) + displayColor := color + if color2 != "" { + displayColor = fmt.Sprintf("%s-%s", color, color2) + } + + key := comboKey{model, color, color2} + + if _, ok := merged[key]; !ok { + merged[key] = &PdfRow{ + Model: model, + Color: displayColor, + GroupMain: s64(raw.UrunAnaGrubu), + GroupSub: s64(raw.UrunAltGrubu), + Description: s64(raw.LineDescription), + SizeQty: make(map[string]int), + Currency: s64(raw.DocCurrencyCode), + Price: f64(raw.Price), + OrderLineIDs: make(map[string]string), + ClosedSizes: make(map[string]bool), // 🆕 + } + } + row := merged[key] + + // beden + rawBeden := s64(raw.ItemDim1Code) + if raw.ItemDim1Code.Valid { + rawBeden = raw.ItemDim1Code.String + } + normalized := normalizeBedenLabelGo(rawBeden) + + qty := int(math.Round(f64(raw.Qty1))) + if qty > 0 { + row.SizeQty[normalized] += qty + row.TotalQty += qty + + // 🆕 Bu beden kapalı satırdan geldiyse işaretle + if raw.IsClosed.Valid && raw.IsClosed.Bool { + row.ClosedSizes[normalized] = true + } + } + + // OrderLineID eşleştirmesi + if raw.OrderLineID.Valid { + row.OrderLineIDs[normalized] = raw.OrderLineID.String + } + + // Termin + if row.Termin == "" && raw.DeliveryDate.Valid { + row.Termin = raw.DeliveryDate.Time.Format("02.01.2006") + } + } + + // finalize + out := make([]PdfRow, 0, len(merged)) + for _, r := range merged { + var sizes []string + for s := range r.SizeQty { + sizes = append(sizes, s) + } + r.Category = detectBedenGroupGo(sizes, r.GroupMain, r.GroupSub) + r.Amount = float64(r.TotalQty) * r.Price + out = append(out, *r) + } + + // Sıralama: Model → Renk → Category + sort.Slice(out, func(i, j int) bool { + if out[i].Model != out[j].Model { + return out[i].Model < out[j].Model + } + if out[i].Color != out[j].Color { + return out[i].Color < out[j].Color + } + return out[i].Category < out[j].Category + }) + + return out +} + +/* =========================================================== + 5) CATEGORY → SIZE MAP (HEADER İÇİN) + =========================================================== */ + +type CategorySizeMap map[string][]string + +// kategori beden map (global – TÜM GRİD İÇİN TEK HEADER) +func buildCategorySizeMap(rows []PdfRow) CategorySizeMap { + cm := make(CategorySizeMap) + + // Her kategori için sabit default listeleri kullan + for _, cat := range categoryOrder { + base := defaultSizeListFor(cat) + if len(base) > 0 { + cm[cat] = append([]string{}, base...) + } + } + + // İstersen ekstra bedenler varsa ekle (opsiyonel) + for _, r := range rows { + c := r.Category + if c == "" { + c = catTak + } + if _, ok := cm[c]; !ok { + cm[c] = []string{} + } + for size := range r.SizeQty { + if !contains(cm[c], size) { + cm[c] = append(cm[c], size) + } + } + } + + return cm +} + +/* =========================================================== + ORDER HEADER (Logo + Gold Label + Sağ Bilgi Kutusu) + =========================================================== */ + +func drawOrderHeader(pdf *gofpdf.Fpdf, h *OrderHeader, showDesc bool) float64 { + pageW, _ := pdf.GetPageSize() + marginL := 10.0 + y := 8.0 + + /* ---------------------------------------------------- + 1) LOGO + ---------------------------------------------------- */ + logo := "./public/Baggi-Tekstil-A.s-Logolu.jpeg" + if _, err := os.Stat(logo); err == nil { + pdf.ImageOptions(logo, marginL, y, 32, 0, false, gofpdf.ImageOptions{}, 0, "") + } + + /* ---------------------------------------------------- + 2) ALTIN BAŞLIK BAR + ---------------------------------------------------- */ + titleW := 150.0 + titleX := marginL + 40 + titleY := y + 2 + + pdf.SetFillColor(149, 113, 22) // Baggi altın + pdf.Rect(titleX, titleY, titleW, 10, "F") + + pdf.SetFont("dejavu-b", "", 13) + pdf.SetTextColor(255, 255, 255) + pdf.SetXY(titleX+4, titleY+2) + pdf.CellFormat(titleW-8, 6, "BAGGI TEKSTİL - SİPARİŞ FORMU", "", 0, "L", false, 0, "") + + /* ---------------------------------------------------- + 3) SAĞ TARAF BİLGİ KUTUSU + ---------------------------------------------------- */ + boxW := 78.0 + boxH := 30.0 + boxX := pageW - marginL - boxW + boxY := y - 2 + + pdf.SetDrawColor(180, 180, 180) + pdf.Rect(boxX, boxY, boxW, boxH, "") + + pdf.SetFont("dejavu-b", "", 9) + pdf.SetTextColor(149, 113, 22) + rep := strings.TrimSpace(h.CustomerRep) + if rep == "" { + rep = strings.TrimSpace(h.CreatedUser) + } + + info := []string{ + "Formun Basılma Tarihi: " + time.Now().Format("02.01.2006"), + "Sipariş Tarihi: " + h.OrderDate.Format("02.01.2006"), + "Sipariş No: " + h.OrderNumber, + "Müşteri Temsilcisi: " + rep, // 🔥 YENİ EKLENDİ + "Cari Kod: " + h.CurrAccCode, + "Müşteri: " + h.CurrAccName, + } + + iy := boxY + 3 + for _, line := range info { + pdf.SetXY(boxX+3, iy) + pdf.CellFormat(boxW-6, 4.5, line, "", 0, "L", false, 0, "") + iy += 4.5 + } + + /* ---------------------------------------------------- + 4) ALT AYIRICI ÇİZGİ + ---------------------------------------------------- */ + lineY := boxY + boxH + 3 + pdf.SetDrawColor(120, 120, 120) + pdf.Line(marginL, lineY, pageW-marginL, lineY) + pdf.SetDrawColor(200, 200, 200) + + y = lineY + 4 + + /* ---------------------------------------------------- + 5) AÇIKLAMA (Varsa) + ---------------------------------------------------- */ + if showDesc && strings.TrimSpace(h.Description) != "" { + text := strings.TrimSpace(h.Description) + + pdf.SetFont("dejavu", "", 8) // wrap’te kullanılacak font + lineH := 4.0 + textW := pageW - marginL*2 - 52 + + // Kaç satır olacağını hesapla + lines := pdf.SplitLines([]byte(text), textW) + descBoxH := float64(len(lines))*lineH + 4 // min boşluk + + if descBoxH < 10 { + descBoxH = 10 + } + + pdf.SetDrawColor(210, 210, 210) + pdf.Rect(marginL, y, pageW-marginL*2, descBoxH, "") + + // Başlık + pdf.SetFont("dejavu-b", "", 8) + pdf.SetTextColor(149, 113, 22) + pdf.SetXY(marginL+3, y+2) + pdf.CellFormat(40, 4, "Sipariş Genel Açıklaması:", "", 0, "L", false, 0, "") + + // Metin + pdf.SetFont("dejavu", "", 8) + pdf.SetTextColor(30, 30, 30) + pdf.SetXY(marginL+48, y+2) + pdf.MultiCell(textW, lineH, text, "", "L", false) + + y += descBoxH + 3 + } + + return y +} + +/* =========================================================== + GRID HEADER — 2 katmanlı + 16 beden kolonlu + =========================================================== */ + +/* +=========================================================== + + GRID HEADER — AÇIKLAMA içinde kategori listesi + 16 beden kolonu + =========================================================== +*/ +func drawGridHeader(pdf *gofpdf.Fpdf, layout pdfLayout, startY float64, catSizes CategorySizeMap) float64 { + pdf.SetFont("dejavu-b", "", 6) + pdf.SetDrawColor(baggiGrayBorderR, baggiGrayBorderG, baggiGrayBorderB) + pdf.SetFillColor(baggiCreamR, baggiCreamG, baggiCreamB) + pdf.SetTextColor(20, 20, 20) // 🟣 TÜM HEADER YAZILARI SİYAH + + y := startY + x := layout.MarginL + + totalHeaderH := float64(len(categoryOrder)) * layout.HeaderSizeH + + centerLabel := func(h float64) float64 { + return y + (h/2.0 - 3) + } + + /* ---------------------------------------------------- + SOL BLOK (Model – Renk – Grup1 – Grup2) + ---------------------------------------------------- */ + cols := []struct { + w float64 + t string + }{ + {layout.ColModelW, "MODEL"}, + {layout.ColRenkW, "RENK"}, + {layout.ColGroupW, "ÜRÜN ANA"}, + {layout.ColGroupW2, "ÜRÜN ALT"}, + } + + for _, c := range cols { + pdf.Rect(x, y, c.w, totalHeaderH, "DF") + pdf.SetXY(x, centerLabel(totalHeaderH)) + pdf.CellFormat(c.w, 6, c.t, "", 0, "C", false, 0, "") + x += c.w + } + + /* ---------------------------------------------------- + AÇIKLAMA BAŞLIĞI — TEK HÜCRE & DİKEY ORTALAMA + ---------------------------------------------------- */ + descX := x + descW := layout.ColDescLeft + + pdf.Rect(descX, y, descW, totalHeaderH, "DF") + + // AÇIKLAMA yazısı ortalanacak + pdf.SetXY(descX, y+(totalHeaderH/2-3)) + pdf.CellFormat(descW, 6, "AÇIKLAMA", "", 0, "C", false, 0, "") + + /* ---------------------------------------------------- + AÇIKLAMA sağında kategori listesi dikey şekilde + ---------------------------------------------------- */ + catX := descX + 1 + catY := y + 2 + + pdf.SetFont("dejavu", "", 6.2) + for _, cat := range categoryOrder { + label := categoryTitle[cat] + pdf.SetXY(catX+2, catY) + pdf.CellFormat(descW-4, 4.8, label, "", 0, "L", false, 0, "") + catY += layout.HeaderSizeH + } + + /* ---------------------------------------------------- + 16’lı BEDEN BLOĞU + ---------------------------------------------------- */ + x = descX + descW + colW := layout.CentralW / 16.0 + + cy := y + + for _, cat := range categoryOrder { + + // Arka plan + pdf.SetFillColor(baggiCreamR, baggiCreamG, baggiCreamB) + pdf.Rect(x, cy, layout.CentralW, layout.HeaderSizeH, "DF") + + sizes := catSizes[cat] + if len(sizes) == 0 { + sizes = defaultSizeListFor(cat) + } + if cat == catAksbir { + pdf.SetFont("dejavu", "", 5) // sadece aksesuar için küçük font + } else { + pdf.SetFont("dejavu", "", 6) // diğer tüm kategoriler normal font + } + + xx := x + for i := 0; i < 16; i++ { + pdf.Rect(xx, cy, colW, layout.HeaderSizeH, "") + if i < len(sizes) { + pdf.SetXY(xx, cy+1) + pdf.CellFormat(colW, layout.HeaderSizeH-2, sizes[i], "", 0, "C", false, 0, "") + } + xx += colW + } + + cy += layout.HeaderSizeH + } + + /* ---------------------------------------------------- + SAĞ BLOK (ADET – FİYAT – PB – TUTAR – TERMİN) + ---------------------------------------------------- */ + rightX := x + 16*colW + + rightCols := []struct { + w float64 + t string + }{ + {layout.ColQtyW, "ADET"}, + {layout.ColPriceW, "FİYAT"}, + {layout.ColCurW, "PB"}, + {layout.ColAmountW, "TUTAR"}, + {layout.ColTerminW, "TERMİN"}, + } + + for _, c := range rightCols { + pdf.Rect(rightX, y, c.w, totalHeaderH, "DF") + pdf.SetXY(rightX, centerLabel(totalHeaderH)) + pdf.CellFormat(c.w, 6, c.t, "", 0, "C", false, 0, "") + rightX += c.w + } + + return y + totalHeaderH +} + +/* =========================================================== + AÇIKLAMA WRAP + DİNAMİK SATIR YÜKSEKLİĞİ + =========================================================== */ + +// Açıklama kolonunu çok satırlı yazan helper +func drawWrappedCell(pdf *gofpdf.Fpdf, text string, x, y, w, h float64) { + txt := strings.TrimSpace(text) + if txt == "" { + return + } + + lineH := 3.2 + lines := pdf.SplitLines([]byte(txt), w-2*OcellPadX) + cy := y + 1 + + for _, ln := range lines { + if cy+lineH > y+h { + break + } + pdf.SetXY(x+OcellPadX, cy) + pdf.CellFormat(w-2*OcellPadX, lineH, string(ln), "", 0, "L", false, 0, "") + cy += lineH + } +} + +// Açıklamaya göre satır yüksekliği hesaplar +func calcRowHeight(pdf *gofpdf.Fpdf, layout pdfLayout, row PdfRow) float64 { + base := layout.RowH + desc := strings.TrimSpace(row.Description) + if desc == "" { + return base + } + + // Yeni: açıklama genişliği = sol + sağ + descW := layout.ColDescW + + lines := pdf.SplitLines([]byte(desc), descW-2*OcellPadX) + lineH := 3.2 + h := float64(len(lines))*lineH + 2 + + if h < base { + h = base + } + return h +} + +/* =========================================================== + SATIR ÇİZİMİ — 16 kolonlu beden dizilimi (sola yaslı) + =========================================================== */ + +/* +=========================================================== + + SATIR ÇİZİMİ — Tek açıklama sütunu + 16 beden kolonu + =========================================================== +*/ +func drawPdfRow(pdf *gofpdf.Fpdf, layout pdfLayout, y float64, row PdfRow, catSizes CategorySizeMap, rowH float64) float64 { + + pdf.SetFont("dejavu", "", 7) + pdf.SetDrawColor(200, 200, 200) + pdf.SetLineWidth(0.15) + pdf.SetTextColor(0, 0, 0) + + x := layout.MarginL + h := rowH + + centerY := func() float64 { + return y + (h-3.5)/2 + } + + /* ---------------------------------------------------- + 1) SOL BLOK + MODEL – RENK – ÜRÜN ANA – ÜRÜN ALT + ---------------------------------------------------- */ + + cols := []struct { + w float64 + v string + }{ + {layout.ColModelW, row.Model}, + {layout.ColRenkW, row.Color}, + {layout.ColGroupW, row.GroupMain}, + {layout.ColGroupW2, row.GroupSub}, + } + + for _, c := range cols { + pdf.Rect(x, y, c.w, h, "") + pdf.SetXY(x+1.3, centerY()) + pdf.CellFormat(c.w-2.6, 3.5, c.v, "", 0, "L", false, 0, "") + x += c.w + } + + /* ---------------------------------------------------- + 2) AÇIKLAMA (TEK BÜYÜK KOLON) + ---------------------------------------------------- */ + + descW := layout.ColDescLeft // açıklama = sadece sol kolon + + pdf.Rect(x, y, descW, h, "") + drawWrappedCell(pdf, row.Description, x, y, descW, h) + + x += descW + + /* ---------------------------------------------------- + 3) 16 BEDEN KOLONU + ---------------------------------------------------- */ + + colW := layout.CentralW / 16.0 + + // kategorinin beden listesi + sizes := catSizes[row.Category] + + if len(sizes) == 0 { + tmp := make([]string, 0, len(row.SizeQty)) + for s := range row.SizeQty { + tmp = append(tmp, s) + } + sort.Strings(tmp) + sizes = tmp + } + + xx := x + for i := 0; i < 16; i++ { + if i < len(sizes) { + lbl := sizes[i] + + if q, ok := row.SizeQty[lbl]; ok && q > 0 { + // 🔍 Bu beden kapalı mı? + isClosedSize := row.ClosedSizes != nil && row.ClosedSizes[lbl] + + if isClosedSize { + // Gri dolgu + beyaz yazı + pdf.SetFillColor(220, 220, 220) // açık gri zemin + pdf.Rect(xx, y, colW, h, "DF") // doldurulmuş hücre + pdf.SetTextColor(255, 255, 255) // beyaz text + } else { + // Normal hücre: sadece border + pdf.Rect(xx, y, colW, h, "") + pdf.SetTextColor(0, 0, 0) // siyah text + } + + pdf.SetXY(xx, centerY()) + pdf.CellFormat(colW, 3.5, fmt.Sprintf("%d", q), "", 0, "C", false, 0, "") + + // Sonraki işler için text rengini resetle + if isClosedSize { + pdf.SetTextColor(0, 0, 0) + } + } else { + // Bu beden kolonunda quantity yoksa normal boş hücre + pdf.Rect(xx, y, colW, h, "") + } + } else { + // 16 kolonun kalanları (header'da var ama bu kategoride kullanılmayan bedenler) + pdf.Rect(xx, y, colW, h, "") + } + + xx += colW + } + x = xx + + /* ---------------------------------------------------- + 4) SAĞ BLOK: ADET – FİYAT – PB – TUTAR – TERMİN + ---------------------------------------------------- */ + + rightCols := []struct { + w float64 + v string + alg string + }{ + {layout.ColQtyW, fmt.Sprintf("%d", row.TotalQty), "C"}, + {layout.ColPriceW, fmt.Sprintf("%.2f", row.Price), "R"}, + {layout.ColCurW, row.Currency, "C"}, + {layout.ColAmountW, fmt.Sprintf("%.2f", row.Amount), "R"}, + {layout.ColTerminW, row.Termin, "C"}, + } + + for _, c := range rightCols { + pdf.Rect(x, y, c.w, h, "") + pdf.SetXY(x+0.6, centerY()) + pdf.CellFormat(c.w-1.2, 3.5, c.v, "", 0, c.alg, false, 0, "") + x += c.w + } + + return y + h +} + +/* =========================================================== + FORMATLAYICI: TR PARA BİÇİMİ (1.234.567,89) + =========================================================== */ +/* =========================================================== + TOPLAM TUTAR / KDV / KDV DAHİL TOPLAM KUTUSU + (Sipariş Genel Açıklaması ile grid header arasında) +=========================================================== */ + +/* =========================================================== + PREMIUM BAGGI GOLD TOTALS BOX + (Sipariş Açıklaması ile Grid Header arasındaki kutu) +=========================================================== */ + +func drawTotalsBox( + pdf *gofpdf.Fpdf, + layout pdfLayout, + startY float64, + subtotal float64, + hasVat bool, + vatRate float64, + vatAmount float64, + totalWithVat float64, + currency string, +) float64 { + + x := layout.MarginL + w := layout.PageW - layout.MarginL - layout.MarginR + + lineH := 6.0 + rows := 1 + if hasVat { + rows = 3 + } + + boxH := float64(rows)*lineH + 5 + + /* ---------------------------------------------------- + ARKA PLAN + ALTIN ÇERÇEVE + ---------------------------------------------------- */ + pdf.SetFillColor(255, 253, 245) // yumuşak krem + pdf.SetDrawColor(149, 113, 22) // Baggi gold + pdf.SetLineWidth(0.4) + pdf.Rect(x, startY, w, boxH, "DF") + + /* ---------------------------------------------------- + Genel metin stilleri + ---------------------------------------------------- */ + labelX := x + 4 + valueX := x + w - 70 // değerlerin sağda hizalanacağı kolon + + pdf.SetTextColor(149, 113, 22) // Sol başlık gold + pdf.SetFont("dejavu-b", "", 8.5) + + y := startY + 2 + + /* ---------------------------------------------------- + 1️⃣ TOPLAM TUTAR + ---------------------------------------------------- */ + pdf.SetXY(labelX, y) + pdf.CellFormat(80, lineH, "TOPLAM TUTAR", "", 0, "L", false, 0, "") + + pdf.SetTextColor(201, 162, 39) + pdf.SetFont("dejavu-b", "", 9) + + pdf.SetXY(valueX, y) + pdf.CellFormat(65, lineH, + fmt.Sprintf("%s %s", formatCurrencyTR(subtotal), currency), + "", 0, "R", false, 0, "") + + y += lineH + + /* ---------------------------------------------------- + 2️⃣ KDV (opsiyonel) + ---------------------------------------------------- */ + if hasVat { + + pdf.SetTextColor(149, 113, 22) // gold başlık + pdf.SetFont("dejavu-b", "", 8.5) + + pdf.SetXY(labelX, y) + pdf.CellFormat(80, lineH, + fmt.Sprintf("KDV (%%%g)", vatRate), + "", 0, "L", false, 0, "") + + pdf.SetTextColor(20, 20, 20) + pdf.SetFont("dejavu-b", "", 9) + + pdf.SetXY(valueX, y) + pdf.CellFormat(65, lineH, + fmt.Sprintf("%s %s", formatCurrencyTR(vatAmount), currency), + "", 0, "R", false, 0, "") + + y += lineH + + /* ---------------------------------------------------- + 3️⃣ KDV DAHİL TOPLAM + ---------------------------------------------------- */ + pdf.SetTextColor(201, 162, 39) + pdf.SetFont("dejavu-b", "", 8.5) + + pdf.SetXY(labelX, y) + pdf.CellFormat(80, lineH, "KDV DAHİL TOPLAM TUTAR", "", 0, "L", false, 0, "") + + pdf.SetTextColor(20, 20, 20) + pdf.SetFont("dejavu-b", "", 9) + + pdf.SetXY(valueX, y) + pdf.CellFormat(65, lineH, + fmt.Sprintf("%s %s", formatCurrencyTR(totalWithVat), currency), + "", 0, "R", false, 0, "") + } + + /* ---------------------------------------------------- + Kutu altı boşluk + ---------------------------------------------------- */ + return startY + boxH + 4 +} + +/* =========================================================== + GRUP TOPLAM BAR (Frontend tarzı sarı bar) + =========================================================== */ + +func drawGroupSummaryBar(pdf *gofpdf.Fpdf, layout pdfLayout, groupName string, totalQty int, totalAmount float64, currency string) float64 { + y := pdf.GetY() + x := layout.MarginL + w := layout.PageW - layout.MarginL - layout.MarginR + h := 7.0 + + // Açık sarı zemin + pdf.SetFillColor(255, 249, 205) // #fff9cd benzeri + pdf.SetDrawColor(214, 192, 106) + pdf.Rect(x, y, w, h, "DF") + + pdf.SetFont("dejavu-b", "", 8.5) + pdf.SetTextColor(20, 20, 20) + + leftTxt := strings.ToUpper(strings.TrimSpace(groupName)) + if leftTxt == "" { + leftTxt = "GENEL" + } + pdf.SetXY(x+2, y+1.2) + pdf.CellFormat(w*0.40, h-2.4, leftTxt, "", 0, "L", false, 0, "") + + rightTxt := fmt.Sprintf( + "TOPLAM %s ADET: %d TOPLAM %s TUTAR: %s %s", + leftTxt, totalQty, + leftTxt, formatCurrencyTR(totalAmount), currency, + ) + + pdf.SetXY(x+w*0.35, y+1.2) + pdf.CellFormat(w*0.60-2, h-2.4, rightTxt, "", 0, "R", false, 0, "") + + // reset + pdf.SetTextColor(0, 0, 0) + pdf.SetDrawColor(201, 162, 39) + + pdf.SetY(y + h) + return y + h +} + +/* =========================================================== + MULTIPAGE RENDER ENGINE (Header + GridHeader + Rows) + =========================================================== */ + +func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVat bool, vatRate float64) { + layout := newPdfLayout(pdf) + catSizes := buildCategorySizeMap(rows) + + // Grup: ÜRÜN ANA GRUBU + type group struct { + Name string + Rows []PdfRow + Adet int + Tutar float64 + } + + groups := map[string]*group{} + var order []string + + for _, r := range rows { + name := strings.TrimSpace(r.GroupMain) + if name == "" { + name = "GENEL" + } + g, ok := groups[name] + if !ok { + g = &group{Name: name} + groups[name] = g + order = append(order, name) + } + g.Rows = append(g.Rows, r) + g.Adet += r.TotalQty + g.Tutar += r.Amount + } + + groupSummaryH := 7.0 + + // 🔹 Genel toplam (grid içindeki satırlardan) + var subtotal float64 + for _, r := range rows { + subtotal += r.Amount + } + + // 🔹 KDV hesapla (hasVat ve vatRate, OrderPDFHandler'dan geliyor) + var vatAmount float64 + totalWithVat := subtotal + + if hasVat && vatRate > 0 { + vatAmount = subtotal * vatRate / 100.0 + totalWithVat = subtotal + vatAmount + } + + var y float64 + firstPage := true + + newPage := func(showDesc bool, showTotals bool) { + pdf.AddPage() + // Üst header (logo + sağ kutu + genel açıklama) + y = drawOrderHeader(pdf, header, showDesc) + + // 🔸 İlk sayfada, header ile grid arasında TOPLAM kutusu + if showTotals { + y = drawTotalsBox( + pdf, + layout, + y, + subtotal, + hasVat, + vatRate, + vatAmount, + totalWithVat, + header.DocCurrency, + ) + } + + // Grid header + y = drawGridHeader(pdf, layout, y, catSizes) + y += 1 + } + + // İlk sayfa: açıklama + toplam kutusu + newPage(firstPage, true) + firstPage = false + + for _, name := range order { + g := groups[name] + + for _, row := range g.Rows { + rh := calcRowHeight(pdf, layout, row) + + if y+rh+groupSummaryH+2 > layout.PageH-layout.MarginB { + // Sonraki sayfalarda: açıklama yok, toplam kutusu yok + newPage(false, false) + } + y = drawPdfRow(pdf, layout, y, row, catSizes, rh) + pdf.SetY(y) // 🔹 satırın altına imleci getir + } + + // Grup toplam barı için yer kontrolü + if y+groupSummaryH+2 > layout.PageH-layout.MarginB { + newPage(false, false) + } + y = drawGroupSummaryBar(pdf, layout, g.Name, g.Adet, g.Tutar, header.DocCurrency) + y += 1 + } + + // ⚠️ Eski alttaki "Genel Toplam" yazdırma kaldırıldı; toplam kutusu artık üstte. +} + +/* =========================================================== + HTTP HANDLER → /api/order/pdf/{id} + =========================================================== */ + +func OrderPDFHandler(db *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + orderID := mux.Vars(r)["id"] + if orderID == "" { + http.Error(w, "missing order id", http.StatusBadRequest) + return + } + if db == nil { + http.Error(w, "db not initialized", http.StatusInternalServerError) + return + } + + // Header + header, err := getOrderHeaderFromDB(db, orderID) + if err != nil { + log.Println("header error:", err) + http.Error(w, "header not found", http.StatusInternalServerError) + return + } + + // Lines + lines, err := getOrderLinesFromDB(db, orderID) + if err != nil { + log.Println("lines error:", err) + http.Error(w, "lines not found", http.StatusInternalServerError) + return + } + // 🔹 Satırlardan KDV bilgisi yakala (ilk pozitif orana göre) + hasVat := false + var vatRate float64 + + for _, l := range lines { + if l.VatRate.Valid && l.VatRate.Float64 > 0 { + hasVat = true + vatRate = l.VatRate.Float64 + break + } + } + + // Normalize + rows := normalizeOrderLinesForPdf(lines) + + // PDF + pdf := newOrderPdf() + renderOrderGrid(pdf, header, rows, hasVat, vatRate) + + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/pdf") + w.Header().Set( + "Content-Disposition", + fmt.Sprintf("inline; filename=\"ORDER_%s.pdf\"", header.OrderNumber), + ) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(buf.Bytes()) + }) +} diff --git a/svc/routes/order_validate.go b/svc/routes/order_validate.go new file mode 100644 index 0000000..aa93ded --- /dev/null +++ b/svc/routes/order_validate.go @@ -0,0 +1,45 @@ +package routes + +import ( + "bssapp-backend/models" + "bssapp-backend/queries" + "database/sql" + "encoding/json" + "fmt" + "net/http" +) + +type validateOrderRequest struct { + Header models.OrderHeader `json:"header"` + Lines []models.OrderDetail `json:"lines"` +} + +type validateOrderResponse struct { + OK bool `json:"ok"` + Invalid []models.InvalidVariant `json:"invalid"` +} + +func ValidateOrderHandler(mssql *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + var req validateOrderRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, fmt.Sprintf("JSON parse hatası: %v", err), http.StatusBadRequest) + return + } + + invalid, err := queries.ValidateOrderVariants(mssql, req.Lines) + if err != nil { + http.Error(w, fmt.Sprintf("Validate error: %v", err), http.StatusInternalServerError) + return + } + + resp := validateOrderResponse{ + OK: len(invalid) == 0, + Invalid: invalid, + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(resp) + } +} diff --git a/svc/routes/orderinventory.go b/svc/routes/orderinventory.go new file mode 100644 index 0000000..8b4a824 --- /dev/null +++ b/svc/routes/orderinventory.go @@ -0,0 +1,89 @@ +package routes + +import ( + "bssapp-backend/db" + "bssapp-backend/queries" + "context" + "encoding/json" + "log" + "net/http" + "time" +) + +// ✅ GET /api/order-inventory?code=...&color=...&color2=... +func GetOrderInventoryHandler(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + color := r.URL.Query().Get("color") + color2 := r.URL.Query().Get("color2") + + if code == "" { + http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest) + return + } + + // 🔧 Normalize eksik renk parametreleri + if color == "" { + color = " " + } + if color2 == "" { + color2 = " " + } + + log.Printf("🎨 [ORDERINV] İstek alındı -> code=%q | color=%q | color2=%q", code, color, color2) + + // ✅ MSSQL bağlantısını ve timeout'u oluştur + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + rows, err := db.MssqlDB.QueryContext(ctx, queries.GetOrderInventory, code, color, color2) + if err != nil { + log.Printf("❌ [ORDERINV] SQL çalıştırılamadı: %v", err) + http.Error(w, "SQL hatası: "+err.Error(), http.StatusInternalServerError) + return + } + defer func() { + if cerr := rows.Close(); cerr != nil { + log.Printf("⚠️ [ORDERINV] rows.Close() hatası: %v", cerr) + } + }() + + type Row struct { + UrunKodu string `json:"UrunKodu"` + RenkKodu string `json:"RenkKodu"` + RenkAciklamasi string `json:"RenkAciklamasi"` + Beden string `json:"Beden"` + Yaka string `json:"Yaka"` + KullanilabilirAdet float64 `json:"KullanilabilirAdet"` + } + + var list []Row + for rows.Next() { + var r Row + if err := rows.Scan( + &r.UrunKodu, + &r.RenkKodu, + &r.RenkAciklamasi, + &r.Beden, + &r.Yaka, + &r.KullanilabilirAdet, + ); err != nil { + log.Printf("⚠️ [ORDERINV] Scan hatası: %v", err) + continue + } + list = append(list, r) + } + + if err := rows.Err(); err != nil { + log.Printf("❌ [ORDERINV] Rows hatası: %v", err) + http.Error(w, "Veri okuma hatası: "+err.Error(), http.StatusInternalServerError) + return + } + + log.Printf("✅ [ORDERINV] %s / %s / %s -> %d kayıt döndü", code, color, color2, len(list)) + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if err := json.NewEncoder(w).Encode(list); err != nil { + log.Printf("❌ [ORDERINV] JSON encode hatası: %v", err) + http.Error(w, "JSON encode başarısız", http.StatusInternalServerError) + } +} diff --git a/svc/routes/orderlist.go b/svc/routes/orderlist.go new file mode 100644 index 0000000..078cd16 --- /dev/null +++ b/svc/routes/orderlist.go @@ -0,0 +1,126 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/db" + "bssapp-backend/models" + "bssapp-backend/queries" + "database/sql" + "encoding/json" + "log" + "net/http" + "strings" +) + +// ====================================================== +// 📌 OrderListRoute — Sipariş Listeleme API (AUTHZ + SAFE) +// ====================================================== +func OrderListRoute(mssql *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // -------------------------------------------------- + // 🔍 Query Param (RAW + TRIM) + // -------------------------------------------------- + raw := r.URL.Query().Get("search") + search := strings.TrimSpace(raw) + + log.Printf( + "📥 /api/orders/list search raw=%q trimmed=%q lenRaw=%d lenTrim=%d", + raw, + search, + len(raw), + len(search), + ) + + // -------------------------------------------------- + // 🗄️ SQL CALL (WITH CONTEXT) + // -------------------------------------------------- + rows, err := queries.GetOrderList( + r.Context(), + mssql, + db.PgDB, // ✅ artık var + search, + ) + + if err != nil { + log.Printf("❌ SQL sorgu hatası: %v", err) + http.Error(w, "Veritabanı hatası", http.StatusInternalServerError) + return + } + defer rows.Close() + + // -------------------------------------------------- + // 📦 Sonuç Listesi + // -------------------------------------------------- + list := make([]models.OrderList, 0, 100) + count := 0 + + // ================================================== + // 🧠 SCAN — SQL SELECT ile BİRE BİR (14 kolon) + // ================================================== + for rows.Next() { + + var o models.OrderList + + err = rows.Scan( + &o.OrderHeaderID, // 1 + &o.OrderNumber, // 2 + &o.OrderDate, // 3 + + &o.CurrAccCode, // 4 + &o.CurrAccDescription, // 5 + + &o.MusteriTemsilcisi, // 6 + &o.Piyasa, // 7 + + &o.CreditableConfirmedDate, // 8 + &o.DocCurrencyCode, // 9 + + &o.TotalAmount, // 10 + &o.TotalAmountUSD, // 11 + + &o.IsCreditableConfirmed, // 12 + &o.Description, // 13 + + &o.ExchangeRateUSD, // 14 + ) + + if err != nil { + log.Printf( + "⚠️ SCAN HATASI | OrderHeaderID=%v | err=%v", + o.OrderHeaderID, + err, + ) + continue + } + + list = append(list, o) + count++ + } + + if err := rows.Err(); err != nil { + log.Printf("⚠️ rows.Err(): %v", err) + } + + // -------------------------------------------------- + // 📊 RESULT LOG + // -------------------------------------------------- + claims, _ := auth.GetClaimsFromContext(r.Context()) + + log.Printf( + "✅ Order list DONE | user=%d | search=%q | resultCount=%d", + claims.ID, + search, + count, + ) + + // -------------------------------------------------- + // 📤 JSON OUTPUT + // -------------------------------------------------- + if err := json.NewEncoder(w).Encode(list); err != nil { + log.Printf("❌ JSON encode hatası: %v", err) + } + }) +} diff --git a/svc/routes/orderpricelistb2b.go b/svc/routes/orderpricelistb2b.go new file mode 100644 index 0000000..a3b625e --- /dev/null +++ b/svc/routes/orderpricelistb2b.go @@ -0,0 +1,131 @@ +package routes + +import ( + "bssapp-backend/models" + "bssapp-backend/queries" + "database/sql" + "encoding/json" + "fmt" + "net/http" + "strings" +) + +// ✅ Model + pb/currency paramına göre fiyat döner (debug log’lu) +func GetOrderPriceListB2BHandler(pg *sql.DB, mssql *sql.DB) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + + model := r.URL.Query().Get("model") + if model == "" { + model = r.URL.Query().Get("code") + } + + cur := r.URL.Query().Get("currency") + if cur == "" { + cur = r.URL.Query().Get("pb") + } + + if model == "" || cur == "" { + http.Error(w, "Eksik parametre", 400) + return + } + + cur = strings.ToUpper(cur) + + fmt.Printf("💰 [MINPRICE] model=%s cur=%s\n", model, cur) + + var ( + price float64 + rate float64 = 1 + ) + + /* ====================== + USD / EUR → Direkt + ====================== */ + + if cur == "USD" || cur == "EUR" { + + p, err := queries.GetOrderPriceListB2B(pg, model, cur) + + if err != nil { + http.Error(w, cur+" fiyat bulunamadı", 404) + return + } + + price = p.Price + + } else if cur == "TRY" { + + /* ====================== + TRY → USD * Kur + ====================== */ + + // 1️⃣ USD fiyat + p, err := queries.GetOrderPriceListB2B(pg, model, "USD") + if err != nil { + http.Error(w, "USD fiyat bulunamadı", 404) + return + } + + // 2️⃣ USD→TRY satış + r, err := queries.GetCachedCurrencyV3(mssql, "USD") + + if err != nil { + http.Error(w, "USD kuru bulunamadı", 503) + return + } + + rate = r.Rate + price = p.Price * rate + + } else if cur == "GBP" { + + /* ====================== + GBP → USD Bridge + ====================== */ + + // 1️⃣ USD fiyat + p, err := queries.GetOrderPriceListB2B(pg, model, "USD") + if err != nil { + http.Error(w, "USD fiyat bulunamadı", 404) + return + } + + // 2️⃣ USD→TRY + usd, err := queries.GetCachedCurrencyV3(mssql, "USD") + + if err != nil { + http.Error(w, "USD kuru yok", 503) + return + } + + // 3️⃣ GBP→TRY + gbp, err := queries.GetCachedCurrencyV3(mssql, "GBP") + if err != nil { + http.Error(w, "GBP kuru yok", 503) + return + } + + rate = usd.Rate / gbp.Rate + price = p.Price * rate + + } else { + + http.Error(w, "Desteklenmeyen para birimi", 400) + return + } + + resp := models.OrderPriceListB2B{ + ModelCode: model, + CurrencyCode: cur, + Price: price, + RateToTRY: rate, + PriceTRY: price, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + + fmt.Printf("✅ [MINPRICE] %s → %.2f %s\n", model, price, cur) + } +} diff --git a/svc/routes/orders.go b/svc/routes/orders.go new file mode 100644 index 0000000..e5c84f0 --- /dev/null +++ b/svc/routes/orders.go @@ -0,0 +1,239 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/queries" + "bssapp-backend/utils" + "database/sql" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/gorilla/mux" +) + +// ================================ +// POST /api/order/update +// ================================ +func UpdateOrderHandler(w http.ResponseWriter, r *http.Request) { + + // -------------------------------------------------- + // 1️⃣ JWT CLAIMS (TEK KAYNAK) + // -------------------------------------------------- + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + user := utils.UserFromClaims(claims) + + if !ok || claims == nil { + http.Error(w, "Kullanıcı doğrulanamadı", http.StatusUnauthorized) + return + } + + user = utils.UserFromClaims(claims) + if user == nil { + http.Error(w, "Kullanıcı doğrulanamadı", http.StatusUnauthorized) + return + } + + // -------------------------------------------------- + // 2️⃣ REQUEST BODY + // -------------------------------------------------- + var payload struct { + Header models.OrderHeader `json:"header"` + Lines []models.OrderDetail `json:"lines"` + } + + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Geçersiz JSON", http.StatusBadRequest) + return + } + + // -------------------------------------------------- + // 3️⃣ UPDATE + // -------------------------------------------------- + results, err := queries.UpdateOrder( + payload.Header, + payload.Lines, + user, // ✅ *models.User + ) + + if err != nil { + + // ✅ VALIDATION ERROR + var vErr *models.ValidationError + if errors.As(err, &vErr) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(vErr) + return + } + + // ❌ SYSTEM ERROR + utils.LogError("ORDER_UPDATE", err) + + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(map[string]any{ + "code": "ORDER_UPDATE_FAILED", + "message": "Sipariş kaydedilirken beklenmeyen bir hata oluştu.", + }) + return + } + + // -------------------------------------------------- + // 4️⃣ RESPONSE + // -------------------------------------------------- + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "success": true, + "lines": results, + }) +} + +// ------------------------------------------------------------- +// 🟩 CREATE — /api/order/create +// ------------------------------------------------------------- +func CreateOrderHandler(pg *sql.DB, mssql *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // -------------------------------------------------- + // JWT CLAIMS + // -------------------------------------------------- + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + user := utils.UserFromClaims(claims) + + if !ok || claims == nil { + http.Error(w, "Yetkisiz", http.StatusUnauthorized) + return + } + + user = utils.UserFromClaims(claims) + if user == nil { + http.Error(w, "Yetkisiz", http.StatusUnauthorized) + return + } + + var payload struct { + Header models.OrderHeader `json:"header"` + Lines []models.OrderDetail `json:"lines"` + } + + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Geçersiz JSON", http.StatusBadRequest) + return + } + + // -------------------------------------------------- + // INSERT + // -------------------------------------------------- + newID, lineResults, err := queries.InsertOrder( + payload.Header, + payload.Lines, + user, // ✅ *models.User + ) + + if err != nil { + + var vErr *models.ValidationError + if errors.As(err, &vErr) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(vErr) + return + } + + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + orderNo := "" + if payload.Header.OrderNumber.Valid { + orderNo = payload.Header.OrderNumber.String + } + + // -------------------------------------------------- + // RESPONSE + // -------------------------------------------------- + _ = json.NewEncoder(w).Encode(map[string]any{ + "status": "success", + "orderID": newID, + "orderNumber": orderNo, + "lineResults": lineResults, + }) + } +} + +// ------------------------------------------------------------- +// 🟨 GET BY ID — /api/order/get/{id} +// ------------------------------------------------------------- +func GetOrderByIDHandler(mssql *sql.DB) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + orderID := mux.Vars(r)["id"] + if orderID == "" { + http.Error(w, "Eksik parametre: id", http.StatusBadRequest) + return + } + + fmt.Printf("📦 /api/order/get/%s çağrıldı\n", orderID) + + header, lines, err := queries.GetOrderByID(orderID) + + switch { + case errors.Is(err, sql.ErrNoRows): + http.Error(w, fmt.Sprintf("Sipariş bulunamadı: %s", orderID), http.StatusNotFound) + return + + case err != nil: + http.Error(w, fmt.Sprintf("Veritabanı hatası: %v", err), http.StatusInternalServerError) + return + + default: + _ = json.NewEncoder(w).Encode(map[string]any{ + "header": header, + "lines": lines, + }) + } + } +} + +// ------------------------------------------------------------- +// 🔎 ORDER EXISTS — /api/order/check/{id} +// ------------------------------------------------------------- +func OrderExistsHandler(db *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + id := mux.Vars(r)["id"] + + var count int + err := db.QueryRow(` + SELECT COUNT(*) + FROM trOrderHeader + WHERE OrderHeaderID = @p1 + `, id).Scan(&count) + + if err != nil { + http.Error(w, "db error", http.StatusInternalServerError) + return + } + + _ = json.NewEncoder(w).Encode(map[string]any{ + "exists": count > 0, + }) + }) +} diff --git a/svc/routes/password_forgot.go b/svc/routes/password_forgot.go new file mode 100644 index 0000000..a04aa9a --- /dev/null +++ b/svc/routes/password_forgot.go @@ -0,0 +1,116 @@ +package routes + +import ( + "bssapp-backend/internal/auditlog" + "bssapp-backend/internal/mailer" + "bssapp-backend/internal/security" + "database/sql" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "time" +) + +func ForgotPasswordHandler( + db *sql.DB, + mailer *mailer.GraphMailer, +) http.HandlerFunc { + + type request struct { + Email string `json:"email"` + } + + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // ------------------------------------------------------- + // 1️⃣ Request parse (enumeration yok) + // ------------------------------------------------------- + var req request + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondOK(w) + return + } + + email := strings.TrimSpace(strings.ToLower(req.Email)) + if email == "" { + respondOK(w) + return + } + + // ------------------------------------------------------- + // 2️⃣ Aktif kullanıcıyı bul + // ------------------------------------------------------- + var userID int64 + err := db.QueryRow(` + SELECT id + FROM mk_dfusr + WHERE email = $1 + AND is_active = true + `, email).Scan(&userID) + + if err != nil { + // ❗ kullanıcı yok → bilgi sızdırma yok + respondOK(w) + return + } + + // ------------------------------------------------------- + // 3️⃣ Reset token üret + // ------------------------------------------------------- + plain, hash, err := security.GenerateResetToken() + if err != nil { + respondOK(w) + return + } + + expires := time.Now().Add(30 * time.Minute) + + // ------------------------------------------------------- + // 4️⃣ DB’ye SADECE HASH kaydet + // ------------------------------------------------------- + _, _ = db.Exec(` + INSERT INTO dfusr_password_reset ( + dfusr_id, + token, + expires_at + ) + VALUES ($1, $2, $3) + `, userID, hash, expires) + + // ------------------------------------------------------- + // 5️⃣ Reset URL (PLAIN token) + // ------------------------------------------------------- + resetURL := fmt.Sprintf( + "%s/password-reset/%s", + os.Getenv("FRONTEND_URL"), + plain, + ) + + // ------------------------------------------------------- + // 6️⃣ Mail gönder (fail olsa bile enumeration yok) + // ------------------------------------------------------- + _ = mailer.SendPasswordResetMail(email, resetURL) + + // ------------------------------------------------------- + // 7️⃣ AUDIT LOG + // ------------------------------------------------------- + auditlog.Write(auditlog.ActivityLog{ + ActionType: "PASSWORD_FORGOT_REQUEST", + ActionCategory: "security", + ActionTarget: email, + IsSuccess: true, + }) + + respondOK(w) + } +} + +func respondOK(w http.ResponseWriter) { + _ = json.NewEncoder(w).Encode(map[string]bool{ + "success": true, + }) +} diff --git a/svc/routes/password_reset.go b/svc/routes/password_reset.go new file mode 100644 index 0000000..b555139 --- /dev/null +++ b/svc/routes/password_reset.go @@ -0,0 +1,126 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/auditlog" + "bssapp-backend/internal/security" + "database/sql" + "encoding/json" + "net/http" + + "golang.org/x/crypto/bcrypt" +) + +func ChangeOwnPasswordHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // -------------------------------------------------- + // 1️⃣ JWT CLAIMS + // -------------------------------------------------- + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + // -------------------------------------------------- + // 2️⃣ PAYLOAD + // -------------------------------------------------- + var req struct { + CurrentPassword string `json:"current_password"` + NewPassword string `json:"new_password"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + + if req.CurrentPassword == "" || req.NewPassword == "" { + http.Error(w, "current_password and new_password required", http.StatusBadRequest) + return + } + + // -------------------------------------------------- + // 3️⃣ PASSWORD POLICY + // -------------------------------------------------- + if err := security.ValidatePassword(req.NewPassword); err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // -------------------------------------------------- + // 4️⃣ MEVCUT HASH ÇEK + // -------------------------------------------------- + var currentHash string + err := db.QueryRow(` + SELECT password_hash + FROM mk_dfusr + WHERE id = $1 + AND is_active = true + `, claims.ID).Scan(¤tHash) + + if err != nil { + http.Error(w, "user not found", http.StatusUnauthorized) + return + } + + // -------------------------------------------------- + // 5️⃣ CURRENT PASSWORD CHECK + // -------------------------------------------------- + if bcrypt.CompareHashAndPassword( + []byte(currentHash), + []byte(req.CurrentPassword), + ) != nil { + http.Error(w, "current password incorrect", http.StatusUnauthorized) + return + } + + // -------------------------------------------------- + // 6️⃣ NEW HASH + // -------------------------------------------------- + newHash, err := bcrypt.GenerateFromPassword( + []byte(req.NewPassword), + bcrypt.DefaultCost, + ) + if err != nil { + http.Error(w, "hash error", http.StatusInternalServerError) + return + } + + // -------------------------------------------------- + // 7️⃣ UPDATE (⚠️ force_password_change DEĞİŞMEZ) + // -------------------------------------------------- + _, err = db.Exec(` + UPDATE mk_dfusr + SET + password_hash = $1, + password_updated_at = now(), + updated_at = now() + WHERE id = $2 + `, string(newHash), claims.ID) + + if err != nil { + http.Error(w, "password update failed", http.StatusInternalServerError) + return + } + + // -------------------------------------------------- + // 8️⃣ AUDIT + // -------------------------------------------------- + auditlog.Write(auditlog.ActivityLog{ + ActionType: "PASSWORD_CHANGED", + ActionCategory: "security", + ActionTarget: claims.Username, + IsSuccess: true, + }) + + // -------------------------------------------------- + // 9️⃣ RESPONSE + // -------------------------------------------------- + _ = json.NewEncoder(w).Encode(map[string]any{ + "success": true, + }) + } +} diff --git a/svc/routes/password_reset_complete.go b/svc/routes/password_reset_complete.go new file mode 100644 index 0000000..bdcab3a --- /dev/null +++ b/svc/routes/password_reset_complete.go @@ -0,0 +1,152 @@ +// routes/password_reset_complete.go +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/auditlog" + "bssapp-backend/internal/security" + "bssapp-backend/repository" + "database/sql" + "encoding/json" + "net/http" + "time" + + "golang.org/x/crypto/bcrypt" +) + +func CompletePasswordResetHandler(db *sql.DB) http.HandlerFunc { + + type request struct { + Token string `json:"token"` + Password string `json:"password"` + } + + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + var req request + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Token == "" || req.Password == "" { + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + + if err := security.ValidatePassword(req.Password); err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + tokenHash := security.HashToken(req.Token) + + // 1) token doğrula + var userID int64 + var expiresAt time.Time + + err := db.QueryRow(` + SELECT mk_dfusr_id, expires_at + FROM mk_dfusr_password_reset + WHERE token = $1 + AND used_at IS NULL + `, tokenHash).Scan(&userID, &expiresAt) + + if err != nil || time.Now().After(expiresAt) { + http.Error(w, "invalid or expired token", http.StatusUnprocessableEntity) + return + } + + // 2) yeni hash + passHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + http.Error(w, "hash error", http.StatusInternalServerError) + return + } + + // 3) tx: şifre + used_at + tx, err := db.Begin() + if err != nil { + http.Error(w, "tx error", http.StatusInternalServerError) + return + } + defer tx.Rollback() + + // mk_dfusr güncelle + if _, err := tx.Exec(` + UPDATE mk_dfusr + SET password_hash = $1, + force_password_change = false, + password_updated_at = now(), + updated_at = now() + WHERE id = $2 + `, string(passHash), userID); err != nil { + http.Error(w, "update failed", http.StatusInternalServerError) + return + } + + // token tüket + if _, err := tx.Exec(` + UPDATE mk_dfusr_password_reset + SET used_at = now() + WHERE token = $1 + `, tokenHash); err != nil { + http.Error(w, "token update failed", http.StatusInternalServerError) + return + } + + if err := tx.Commit(); err != nil { + http.Error(w, "commit failed", http.StatusInternalServerError) + return + } + + // 4) refresh revoke + _ = repository.NewRefreshTokenRepository(db).RevokeAllForUser(userID) + + // 5) user + perms + jwt + mkRepo := repository.NewMkUserRepository(db) + u, err := mkRepo.GetByID(userID) + if err != nil { + http.Error(w, "user fetch failed", http.StatusInternalServerError) + return + } + + permRepo := repository.NewPermissionRepository(db) + perms, _ := permRepo.GetPermissionsByRoleID(u.RoleID) + + // ✅ CLAIMS BUILD + claims := auth.BuildClaimsFromUser(u, 15*time.Minute) + + // ✅ TOKEN + jwtStr, err := auth.GenerateToken( + claims, + u.Username, + false, + ) + if err != nil { + http.Error(w, "token generation failed", http.StatusInternalServerError) + return + } + + auditlog.Write(auditlog.ActivityLog{ + ActionType: "PASSWORD_RESET_COMPLETED", + ActionCategory: "security", + ActionTarget: u.Username, + IsSuccess: true, + }) + + _ = json.NewEncoder(w).Encode(map[string]any{ + "success": true, + "token": jwtStr, + "user": map[string]any{ + "id": u.ID, + "username": u.Username, + "email": u.Email, + "is_active": u.IsActive, + "role_id": u.RoleID, + "role_code": u.RoleCode, + "force_password_change": false, + "v3_username": u.V3Username, + "v3_usergroup": u.V3UserGroup, + }, + "permissions": perms, + }) + } +} diff --git a/svc/routes/password_reset_validate.go b/svc/routes/password_reset_validate.go new file mode 100644 index 0000000..9c50ead --- /dev/null +++ b/svc/routes/password_reset_validate.go @@ -0,0 +1,63 @@ +package routes + +import ( + "crypto/sha256" + "database/sql" + "encoding/hex" + "net/http" + "time" + + "github.com/gorilla/mux" +) + +// GET /api/password/reset/validate/{token} +func ValidatePasswordResetTokenHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + token := mux.Vars(r)["token"] + if token == "" { + http.NotFound(w, r) + return + } + + // 🔐 plain token -> hash + h := sha256.Sum256([]byte(token)) + tokenHash := hex.EncodeToString(h[:]) + + var ( + userID int64 + expiresAt time.Time + usedAt sql.NullTime + ) + + err := db.QueryRow(` + SELECT user_id, expires_at, used_at + FROM password_reset_tokens + WHERE token_hash = $1 + LIMIT 1 + `, tokenHash).Scan(&userID, &expiresAt, &usedAt) + + if err != nil { + // ❗ bilgi sızdırma yok + http.NotFound(w, r) + return + } + + // ⏰ Süre kontrolü + if time.Now().After(expiresAt) { + http.NotFound(w, r) + return + } + + // 🔁 Tek kullanımlık + if usedAt.Valid { + http.Error(w, "token already used", http.StatusGone) + return + } + + // ✅ TOKEN GEÇERLİ + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"valid":true}`)) + } +} diff --git a/svc/routes/permission_debug.go b/svc/routes/permission_debug.go new file mode 100644 index 0000000..4759fc0 --- /dev/null +++ b/svc/routes/permission_debug.go @@ -0,0 +1,63 @@ +package routes + +import ( + "bssapp-backend/auth" + "database/sql" + "encoding/json" + "net/http" +) + +func DebugPermissionV2(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + // sadece auth kontrolü + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", 401) + return + } + + module := r.URL.Query().Get("module") + action := r.URL.Query().Get("action") + + if module == "" || action == "" { + http.Error(w, "module & action required", 400) + return + } + + rows, err := db.Query(` + SELECT + r.id, + r.code, + rp.allowed + FROM dfrole r + LEFT JOIN mk_sys_role_permissions rp + ON rp.role_id = r.id + AND rp.module_code = $1 + AND rp.action = $2 + ORDER BY r.id + `, module, action) + + if err != nil { + http.Error(w, "db error", 500) + return + } + defer rows.Close() + + type Row struct { + RoleID int `json:"role_id"` + Code string `json:"code"` + Allowed bool `json:"allowed"` + } + + var list []Row + + for rows.Next() { + var r Row + _ = rows.Scan(&r.RoleID, &r.Code, &r.Allowed) + list = append(list, r) + } + + json.NewEncoder(w).Encode(list) + } +} diff --git a/svc/routes/permission_matrix_v2.go b/svc/routes/permission_matrix_v2.go new file mode 100644 index 0000000..bf37ff9 --- /dev/null +++ b/svc/routes/permission_matrix_v2.go @@ -0,0 +1,203 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/auditlog" + "bssapp-backend/permissions" + "database/sql" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +func GetRolePermissionMatrix(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", 401) + return + } + + roleID, _ := strconv.Atoi(mux.Vars(r)["id"]) + + rows, err := db.Query(` + SELECT + module_code, + action, + allowed + FROM mk_sys_role_permissions + WHERE role_id=$1 + `, roleID) + + if err != nil { + http.Error(w, "db error", 500) + return + } + defer rows.Close() + + var list []permissions.PermissionMatrixRow + + for rows.Next() { + + var row permissions.PermissionMatrixRow + + if err := rows.Scan( + &row.Module, + &row.Action, + &row.Allowed, + ); err != nil { + http.Error(w, "scan error", 500) + return + } + + row.Source = "role" + + list = append(list, row) + } + + json.NewEncoder(w).Encode(list) + } +} + +func SaveRolePermissionMatrix(db *sql.DB) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", 401) + return + } + + roleID, _ := strconv.Atoi(mux.Vars(r)["id"]) + + var list []permissions.PermissionMatrixRow + + if err := json.NewDecoder(r.Body).Decode(&list); err != nil { + http.Error(w, "bad payload", 400) + return + } + + repo := permissions.NewPermissionRepository(db) + + // ================= OLD ================= + + oldRows, _ := repo.GetPermissionMatrixForRoles([]int{roleID}) + + oldMap := map[string]bool{} + + for _, p := range oldRows { + key := p.Module + ":" + p.Action + oldMap[key] = p.Allowed + } + + // ================= DIFF ================= + + var changes []map[string]any + + for _, p := range list { + + key := p.Module + ":" + p.Action + + oldVal, ok := oldMap[key] + if !ok { + oldVal = false + } + + if oldVal != p.Allowed { + + changes = append(changes, map[string]any{ + "module": p.Module, + "action": p.Action, + "before": oldVal, + "after": p.Allowed, + }) + } + } + + // ================= SAVE ================= + + tx, err := db.Begin() + if err != nil { + http.Error(w, "tx error", 500) + return + } + defer tx.Rollback() + + stmt, err := tx.Prepare(` + INSERT INTO mk_sys_role_permissions + (role_id,module_code,action,allowed) + VALUES ($1,$2,$3,$4) + + ON CONFLICT + (role_id,module_code,action) + DO UPDATE SET allowed=EXCLUDED.allowed + `) + + if err != nil { + http.Error(w, "prepare error", 500) + return + } + defer stmt.Close() + + for _, p := range list { + + if _, err := stmt.Exec( + roleID, + p.Module, + p.Action, + p.Allowed, + ); err != nil { + http.Error(w, "exec error", 500) + return + } + } + + if err := tx.Commit(); err != nil { + http.Error(w, "commit error", 500) + return + } + + // ================= AUDIT ================= + + if len(changes) > 0 { + + var roleCode string + + _ = db.QueryRow(` + SELECT code FROM dfrole WHERE id=$1 + `, roleID).Scan(&roleCode) + + auditlog.Enqueue(r.Context(), auditlog.ActivityLog{ + + ActionType: "role_permission_change", + ActionCategory: "role_permission", + + ActionTarget: fmt.Sprintf("/api/roles/%d/permissions", roleID), + + Description: "role permission matrix updated", + + Username: claims.Username, + RoleCode: claims.RoleCode, + DfUsrID: int64(claims.ID), + + ChangeBefore: map[string]any{ + "permissions": oldRows, + }, + + ChangeAfter: map[string]any{ + "changes": changes, + }, + + IsSuccess: true, + }) + } + + json.NewEncoder(w).Encode(map[string]bool{ + "success": true, + }) + } +} diff --git a/svc/routes/permissions.go b/svc/routes/permissions.go new file mode 100644 index 0000000..aef0d08 --- /dev/null +++ b/svc/routes/permissions.go @@ -0,0 +1,114 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/permissions" + "database/sql" + "encoding/json" + "net/http" +) + +/* ===================================================== + HANDLER +===================================================== */ + +type PermissionHandler struct { + DB *sql.DB + Repo *permissions.PermissionRepository +} + +func NewPermissionHandler(db *sql.DB) *PermissionHandler { + return &PermissionHandler{ + DB: db, + Repo: permissions.NewPermissionRepository(db), + } +} + +/* ===================================================== + POST /api/permissions/matrix +===================================================== */ + +func (h *PermissionHandler) UpdatePermissionMatrix( + w http.ResponseWriter, + r *http.Request, +) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", 401) + return + } + + var req []permissions.PermissionUpdateRequest + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid payload", 400) + return + } + + if len(req) == 0 { + w.WriteHeader(http.StatusOK) + return + } + + err := h.Repo.UpdatePermissions(req) + if err != nil { + http.Error(w, "db error", 500) + return + } + + json.NewEncoder(w).Encode(map[string]any{ + "success": true, + }) +} +func GetMyPermissionMatrix(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", 401) + return + } + + var roleID int + + err := db.QueryRow(` + SELECT id FROM dfrole WHERE LOWER(code)=LOWER($1) + `, claims.RoleCode).Scan(&roleID) + + if err != nil { + http.Error(w, "role resolve error", 500) + return + } + + repo := permissions.NewPermissionRepository(db) + + raw, err := repo.GetPermissionMatrixForRoles([]int{roleID}) + if err != nil { + http.Error(w, "db error", 500) + return + } + + // 🔥 FRONTEND FORMAT + type Row struct { + Module string `json:"module"` + Action string `json:"action"` + Allowed bool `json:"allowed"` + } + + list := make([]Row, 0, len(raw)) + + for _, p := range raw { + + list = append(list, Row{ + Module: p.ModuleCode, // 👈 burası önemli + Action: p.Action, + Allowed: p.Allowed, + }) + } + + _ = json.NewEncoder(w).Encode(list) + } +} diff --git a/svc/routes/product.go b/svc/routes/product.go new file mode 100644 index 0000000..1893f08 --- /dev/null +++ b/svc/routes/product.go @@ -0,0 +1,27 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/queries" + "encoding/json" + "net/http" +) + +// ✅ GET /api/products +func GetProductListHandler(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + products, err := queries.GetProductList() + if err != nil { + http.Error(w, "Ürün listesi alınamadı: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(products) +} diff --git a/svc/routes/productcolor.go b/svc/routes/productcolor.go new file mode 100644 index 0000000..3a794e6 --- /dev/null +++ b/svc/routes/productcolor.go @@ -0,0 +1,48 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/db" + "bssapp-backend/models" + "bssapp-backend/queries" + "encoding/json" + "log" + "net/http" +) + +func GetProductColorsHandler(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + code := r.URL.Query().Get("code") + if code == "" { + http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest) + return + } + + rows, err := db.MssqlDB.Query(queries.GetProductColors, code) + if err != nil { + http.Error(w, "Renk listesi alınamadı: "+err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + var list []models.ProductColor + for rows.Next() { + var c models.ProductColor + if err := rows.Scan(&c.ProductCode, &c.ColorCode, &c.ColorDescription); err != nil { + log.Println("Satır okunamadı:", err) + continue + } + list = append(list, c) + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(list) + + log.Printf("✅ [1.COLOR] %s -> %d kayıt", code, len(list)) +} diff --git a/svc/routes/productcolorsize.go b/svc/routes/productcolorsize.go new file mode 100644 index 0000000..cb8c048 --- /dev/null +++ b/svc/routes/productcolorsize.go @@ -0,0 +1,73 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/db" + "bssapp-backend/models" + "bssapp-backend/queries" + "encoding/json" + "log" + "net/http" +) + +// ✅ GET /api/product-colorsize?code=S001-DCY18144&color=001&color2=017 +func GetProductColorSizesHandler(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + code := r.URL.Query().Get("code") + color := r.URL.Query().Get("color") + color2 := r.URL.Query().Get("color2") + + // 🔹 Model kodu zorunlu, renk opsiyonel + if code == "" { + http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest) + return + } + + // 🔧 Eksik parametreleri normalize et + if color == "" { + color = " " + } + if color2 == "" { + color2 = " " + } + + log.Printf("🎨 [COLORSIZE] İstek alındı -> code=%q | color=%q | color2=%q", code, color, color2) + + // ✅ MSSQL’e sıralı 3 parametre gönder (adlı değil!) + rows, err := db.MssqlDB.Query(queries.GetProductColorSizes, code, color, color2) + if err != nil { + log.Printf("❌ [COLORSIZE] DB sorgu hatası: %v", err) + http.Error(w, "SQL hatası: "+err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + var list []models.ProductColorSize + for rows.Next() { + var s models.ProductColorSize + if err := rows.Scan(&s.ProductCode, &s.ColorCode, &s.ItemDim1Code, &s.ItemDim2Code); err != nil { + log.Printf("⚠️ [COLORSIZE] Satır okunamadı: %v", err) + continue + } + list = append(list, s) + } + + if err := rows.Err(); err != nil { + log.Printf("⚠️ [COLORSIZE] Row iteration hatası: %v", err) + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if err := json.NewEncoder(w).Encode(list); err != nil { + log.Printf("❌ [COLORSIZE] JSON encode hatası: %v", err) + http.Error(w, "JSON encode başarısız", http.StatusInternalServerError) + return + } + + log.Printf("✅ [COLORSIZE] %s / %s / %s -> %d kayıt döndü", code, color, color2, len(list)) +} diff --git a/svc/routes/productdetail.go b/svc/routes/productdetail.go new file mode 100644 index 0000000..fbaa693 --- /dev/null +++ b/svc/routes/productdetail.go @@ -0,0 +1,65 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/db" + "bssapp-backend/models" + "bssapp-backend/queries" + "encoding/json" + "log" + "net/http" +) + +func GetProductDetailHandler(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + code := r.URL.Query().Get("code") + if code == "" { + http.Error(w, "Eksik parametre: code", http.StatusBadRequest) + return + } + + row := db.MssqlDB.QueryRow(queries.GetProductDetail, code) + + var p models.ProductDetail + err := row.Scan( + &p.ProductCode, + new(string), // BOS7 + new(string), // ProductDescription + new(string), // ProductAtt42 + &p.UrunIlkGrubu, + new(string), // ProductAtt01 + &p.UrunAnaGrubu, + new(string), // ProductAtt02 + &p.UrunAltGrubu, + new(string), // ProductAtt41 + &p.UrunIcerik, + new(string), // ProductAtt10 + new(string), // Marka + new(string), // ProductAtt11 + &p.Drop, + new(string), // ProductAtt29 + new(string), // Karisim + new(string), // ProductAtt45 + &p.AskiliYan, + new(string), // ProductAtt44 + &p.Kategori, + new(string), // ProductAtt38 + &p.Fit1, + new(string), // ProductAtt39 + &p.Fit2, + ) + if err != nil { + log.Println("❌ Ürün detayları alınamadı:", err) + http.Error(w, "Veri okuma hatası: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(p) +} diff --git a/svc/routes/productsecondcolor.go b/svc/routes/productsecondcolor.go new file mode 100644 index 0000000..500a1cd --- /dev/null +++ b/svc/routes/productsecondcolor.go @@ -0,0 +1,67 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/db" + "bssapp-backend/models" + "bssapp-backend/queries" + "database/sql" + "encoding/json" + "log" + "net/http" +) + +// ✅ GET /api/product-secondcolor?code=S001-DCY18144&color=001 +func GetProductSecondColorsHandler(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + code := r.URL.Query().Get("code") + color := r.URL.Query().Get("color") + + if code == "" || color == "" { + http.Error(w, "Eksik parametre: code ve color gerekli", http.StatusBadRequest) + return + } + + log.Printf("🎨 [2.COLOR] İstek alındı -> code=%s color=%s", code, color) + + rows, err := db.MssqlDB.Query( + queries.GetProductSecondColors, + sql.Named("ProductCode", code), + sql.Named("ColorCode", color), + ) + if err != nil { + log.Println("❌ DB sorgu hatası:", err) + http.Error(w, "2. renk listesi alınamadı: "+err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + var list []models.ProductSecondColor + for rows.Next() { + var c models.ProductSecondColor + if err := rows.Scan(&c.ProductCode, &c.ColorCode, &c.ItemDim2Code); err != nil { + log.Println("⚠️ Satır okunamadı:", err) + continue + } + list = append(list, c) + } + + if err := rows.Err(); err != nil { + log.Println("⚠️ Row iteration hatası:", err) + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if err := json.NewEncoder(w).Encode(list); err != nil { + log.Println("❌ JSON encode hatası:", err) + http.Error(w, "JSON encode başarısız", http.StatusInternalServerError) + return + } + + log.Printf("✅ [2.COLOR] %s / %s -> %d kayıt döndü", code, color, len(list)) +} diff --git a/svc/routes/role_department_permissions.go b/svc/routes/role_department_permissions.go new file mode 100644 index 0000000..1d9efbe --- /dev/null +++ b/svc/routes/role_department_permissions.go @@ -0,0 +1,440 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/auditlog" + "bssapp-backend/permissions" + "bssapp-backend/queries" + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +type IdTitleOption struct { + ID string `json:"id"` + Title string `json:"title"` +} +type Row struct { + Route string `json:"route"` + CanAccess bool `json:"can_access"` +} + +type RoleDepartmentPermissionHandler struct { + DB *sql.DB + Repo *permissions.RoleDepartmentPermissionRepo +} + +func NewRoleDepartmentPermissionHandler(db *sql.DB) *RoleDepartmentPermissionHandler { + + return &RoleDepartmentPermissionHandler{ + DB: db, // ✅ EKLENDİ + Repo: permissions.NewRoleDepartmentPermissionRepo(db), + } +} + +/* ====================================================== + GET +====================================================== */ + +func (h *RoleDepartmentPermissionHandler) Get(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + + roleID, err := strconv.Atoi(vars["roleId"]) + if err != nil || roleID <= 0 { + http.Error(w, "invalid roleId", http.StatusBadRequest) + return + } + + deptCode := vars["deptCode"] + if deptCode == "" { + http.Error(w, "invalid deptCode", http.StatusBadRequest) + return + } + + list, err := h.Repo.Get(roleID, deptCode) + if err != nil { + http.Error(w, "db error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(list) +} + +/* ====================================================== + POST +====================================================== */ + +func (h *RoleDepartmentPermissionHandler) Save(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + + roleID, err := strconv.Atoi(vars["roleId"]) + if err != nil || roleID <= 0 { + http.Error(w, "invalid roleId", http.StatusBadRequest) + return + } + + deptCode := vars["deptCode"] + if deptCode == "" { + http.Error(w, "invalid deptCode", http.StatusBadRequest) + return + } + + var list []permissions.RoleDepartmentPermission + if err := json.NewDecoder(r.Body).Decode(&list); err != nil { + http.Error(w, "bad payload", http.StatusBadRequest) + return + } + + // ================= OLD ================= + oldRows, err := h.Repo.Get(roleID, deptCode) + if err != nil { + log.Println("OLD PERM LOAD ERROR:", err) + } + + oldMap := map[string]bool{} + for _, p := range oldRows { + key := p.Module + ":" + p.Action + oldMap[key] = p.Allowed + } + + // ================= DIFF ================= + var changes []map[string]any + for _, p := range list { + key := p.Module + ":" + p.Action + oldVal := oldMap[key] + if oldVal != p.Allowed { + changes = append(changes, map[string]any{ + "module": p.Module, + "action": p.Action, + "before": oldVal, + "after": p.Allowed, + }) + } + } + + // ================= SAVE ================= + if err := h.Repo.Save(roleID, deptCode, list); err != nil { + http.Error(w, "save error", http.StatusInternalServerError) + return + } + + // ================= AUDIT ================= + if len(changes) > 0 { + auditlog.Enqueue(r.Context(), auditlog.ActivityLog{ + ActionType: "role_department_permission_change", + ActionCategory: "role_permission", + ActionTarget: fmt.Sprintf( + "/api/roles/%d/departments/%s/permissions", + roleID, + deptCode, + ), + Description: "role+department permissions updated", + Username: claims.Username, + RoleCode: claims.RoleCode, + DfUsrID: int64(claims.ID), + ChangeBefore: map[string]any{ + "permissions": oldRows, + }, + ChangeAfter: map[string]any{ + "changes": changes, + }, + IsSuccess: true, + }) + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(map[string]bool{"success": true}) +} +func GetModuleLookupRoute(db *sql.DB) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + + rows, err := db.Query(queries.GetModuleLookup) + if err != nil { + http.Error(w, "db error", 500) + return + } + defer rows.Close() + + type Row struct { + Value string `json:"value"` + Label string `json:"label"` + } + + var list []Row + + for rows.Next() { + + var r Row + + if err := rows.Scan( + &r.Value, + &r.Label, + ); err != nil { + http.Error(w, "scan error", 500) + return + } + + list = append(list, r) + } + + json.NewEncoder(w).Encode(list) + } +} +func GetRolesForPermissionSelectRoute(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + rows, err := db.Query(queries.GetRolesForPermissionSelect) + if err != nil { + http.Error(w, "roles select error", 500) + return + } + defer rows.Close() + + list := make([]IdTitleOption, 0) + for rows.Next() { + var o IdTitleOption + if err := rows.Scan(&o.ID, &o.Title); err == nil { + list = append(list, o) + } + } + + _ = json.NewEncoder(w).Encode(list) + } +} + +func GetDepartmentsForPermissionSelectRoute(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + rows, err := db.Query(queries.GetDepartmentsForPermissionSelect) + if err != nil { + http.Error(w, "departments select error", 500) + return + } + defer rows.Close() + + list := make([]IdTitleOption, 0) + for rows.Next() { + var o IdTitleOption + if err := rows.Scan(&o.ID, &o.Title); err == nil { + list = append(list, o) + } + } + + _ = json.NewEncoder(w).Encode(list) + } +} +func GetUsersForPermissionSelectRoute(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + rows, err := db.Query(queries.GetUserLookupForPermission) + if err != nil { + http.Error(w, "users lookup error", http.StatusInternalServerError) + return + } + defer rows.Close() + + list := make([]IdTitleOption, 0, 128) + + for rows.Next() { + + var o IdTitleOption + + if err := rows.Scan(&o.ID, &o.Title); err == nil { + list = append(list, o) + } + } + + _ = json.NewEncoder(w).Encode(list) + } +} + +func (h *PermissionHandler) GetUserOverrides(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + userID, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) + if err != nil || userID <= 0 { + http.Error(w, "invalid id", http.StatusBadRequest) + return + } + + list, err := h.Repo.GetUserOverridesByUserID(userID) + if err != nil { + log.Println("❌ USER OVERRIDE LOAD ERROR:", err) + http.Error(w, "db error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(list) +} +func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", 401) + return + } + + repo := permissions.NewPermissionRepository(db) + + // JWT’den departmanlar + depts := claims.DepartmentCodes + + rows, err := db.Query(` + SELECT DISTINCT + module_code, + action, + path + FROM mk_sys_routes + `) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + defer rows.Close() + + type Row struct { + Route string `json:"route"` + CanAccess bool `json:"can_access"` + } + + list := make([]Row, 0, 64) + + for rows.Next() { + + var module, action, path string + + if err := rows.Scan( + &module, + &action, + &path, + ); err != nil { + continue + } + + allowed, err := repo.ResolvePermissionChain( + int64(claims.ID), + int64(claims.RoleID), + depts, + module, + action, + ) + + if err != nil { + log.Println("PERM RESOLVE ERROR:", err) + continue + } + + list = append(list, Row{ + Route: path, + CanAccess: allowed, + }) + } + + _ = json.NewEncoder(w).Encode(list) + } +} + +func GetMyEffectivePermissions(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", 401) + return + } + + repo := permissions.NewPermissionRepository(db) + + // ✅ JWT'DEN DEPARTMENTS + depts := claims.DepartmentCodes + + log.Printf("🧪 EFFECTIVE PERM | user=%d role=%d depts=%v", + claims.ID, + claims.RoleID, + depts, + ) + + // all system perms + all, err := db.Query(` + SELECT DISTINCT module_code, action + FROM mk_sys_routes + `) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + defer all.Close() + + type Row struct { + Module string `json:"module"` + Action string `json:"action"` + Allowed bool `json:"allowed"` + } + + list := make([]Row, 0, 128) + + for all.Next() { + + var m, a string + if err := all.Scan(&m, &a); err != nil { + continue + } + + allowed, err := repo.ResolvePermissionChain( + int64(claims.ID), + int64(claims.RoleID), + depts, + m, + a, + ) + + if err != nil { + continue + } + + list = append(list, Row{ + Module: m, + Action: a, + Allowed: allowed, + }) + } + + _ = json.NewEncoder(w).Encode(list) + } +} diff --git a/svc/routes/statement_detail.go b/svc/routes/statement_detail.go new file mode 100644 index 0000000..0f5c20f --- /dev/null +++ b/svc/routes/statement_detail.go @@ -0,0 +1,38 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/queries" + "encoding/json" + "net/http" + + "github.com/gorilla/mux" +) + +// GET /api/statements/{accountCode}/details?startdate=...&enddate=...&parislemler=1&parislemler=2 +func GetStatementDetailsHandler(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + accountCode := vars["accountCode"] + + startDate := r.URL.Query().Get("startdate") + endDate := r.URL.Query().Get("enddate") + parislemler := r.URL.Query()["parislemler"] + + details, err := queries.GetStatementDetails(accountCode, startDate, endDate, parislemler) + if err != nil { + http.Error(w, "Error fetching statement details: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if err := json.NewEncoder(w).Encode(details); err != nil { + http.Error(w, "Error encoding response: "+err.Error(), http.StatusInternalServerError) + } +} diff --git a/svc/routes/statement_header.go b/svc/routes/statement_header.go new file mode 100644 index 0000000..71ce07d --- /dev/null +++ b/svc/routes/statement_header.go @@ -0,0 +1,38 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/queries" + "encoding/json" + "net/http" +) + +// GET /api/statements +func GetStatementHeadersHandler(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + params := models.StatementParams{ + StartDate: r.URL.Query().Get("startdate"), + EndDate: r.URL.Query().Get("enddate"), + AccountCode: r.URL.Query().Get("accountcode"), + LangCode: r.URL.Query().Get("langcode"), + Parislemler: r.URL.Query()["parislemler"], + } + + statements, err := queries.GetStatements(params) + if err != nil { + http.Error(w, "Error fetching statements: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if err := json.NewEncoder(w).Encode(statements); err != nil { + http.Error(w, "Error encoding response: "+err.Error(), http.StatusInternalServerError) + } +} diff --git a/svc/routes/statement_header_pdf.go b/svc/routes/statement_header_pdf.go new file mode 100644 index 0000000..85834f1 --- /dev/null +++ b/svc/routes/statement_header_pdf.go @@ -0,0 +1,395 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/queries" + "bytes" + "database/sql" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/jung-kurt/gofpdf" +) + +/* ============================ SABİTLER ============================ */ + +// A4 Landscape (mm) +const ( + hPageWidth = 297.0 + hPageHeight = 210.0 + + hMarginL = 8.0 + hMarginT = 8.0 + hMarginR = 8.0 + hMarginB = 15.0 + + hLineHMain = 5.2 + hCellPadX = 2.0 + + hHeaderRowH = 8.4 + hGroupBarH = 9.0 + + hGridLineWidth = 0.3 + hLogoW = 42.0 +) + +// Kolonlar +var hMainCols = []string{ + "Belge No", "Tarih", "Vade", "İşlem", + "Açıklama", "Para", "Borç", "Alacak", "Bakiye", +} + +var hMainWbase = []float64{ + 34, 30, 28, 24, 60, 20, 36, 36, 36, +} + +// Font dosyaları +const ( + hFontFamilyReg = "dejavu" + hFontFamilyBold = "dejavu-b" + hFontPathReg = "fonts/DejaVuSans.ttf" + hFontPathBold = "fonts/DejaVuSans-Bold.ttf" +) + +// Renkler +var ( + hColorPrimary = [3]int{149, 113, 22} // #957116 + hColorSecondary = [3]int{218, 193, 151} // #dac197 (Baggi secondary) +) + +/* ============================ FONT / FORMAT ============================ */ + +func hEnsureFonts(pdf *gofpdf.Fpdf) { + if _, err := os.Stat(hFontPathReg); err == nil { + pdf.AddUTF8Font(hFontFamilyReg, "", hFontPathReg) + } + if _, err := os.Stat(hFontPathBold); err == nil { + pdf.AddUTF8Font(hFontFamilyBold, "", hFontPathBold) + } +} + +func hNormalizeWidths(base []float64, targetTotal float64) []float64 { + sum := 0.0 + for _, v := range base { + sum += v + } + scale := targetTotal / sum + out := make([]float64, len(base)) + for i, v := range base { + out[i] = v * scale + } + return out +} + +func hFormatCurrencyTR(val float64) string { + s := fmt.Sprintf("%.2f", val) + parts := strings.Split(s, ".") + intPart, decPart := parts[0], parts[1] + + var out []string + for len(intPart) > 3 { + out = append([]string{intPart[len(intPart)-3:]}, out...) + intPart = intPart[:len(intPart)-3] + } + if intPart != "" { + out = append([]string{intPart}, out...) + } + return strings.Join(out, ".") + "," + decPart +} + +func hNeedNewPage(pdf *gofpdf.Fpdf, needH float64) bool { + return pdf.GetY()+needH+hMarginB > hPageHeight +} + +/* ============================ YARDIMCI FONKSİYONLAR ============================ */ + +func hSplitLinesSafe(pdf *gofpdf.Fpdf, text string, width float64) [][]byte { + if width <= 0 { + width = 1 + } + return pdf.SplitLines([]byte(text), width) +} + +func hDrawWrapText(pdf *gofpdf.Fpdf, text string, x, y, width, lineH float64) { + if text == "" { + return + } + lines := hSplitLinesSafe(pdf, text, width) + cy := y + for _, ln := range lines { + pdf.SetXY(x, cy) + pdf.CellFormat(width, lineH, string(ln), "", 0, "L", false, 0, "") + cy += lineH + } +} + +func hCalcRowHeightForText(pdf *gofpdf.Fpdf, text string, colWidth, lineHeight, padX float64) float64 { + if text == "" { + return lineHeight + } + lines := hSplitLinesSafe(pdf, text, colWidth-(2*padX)) + h := float64(len(lines)) * lineHeight + if h < lineHeight { + h = lineHeight + } + return h +} + +/* ============================ HEADER ============================ */ + +func hDrawPageHeader(pdf *gofpdf.Fpdf, cariKod, cariIsim, start, end string) float64 { + logoPath, _ := filepath.Abs("./public/Baggi-Tekstil-A.s-Logolu.jpeg") + + // Logo + pdf.ImageOptions(logoPath, hMarginL, 2, hLogoW, 0, false, gofpdf.ImageOptions{}, 0, "") + + // Başlıklar + pdf.SetFont(hFontFamilyBold, "", 16) + pdf.SetTextColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) + pdf.SetXY(hMarginL+hLogoW+8, hMarginT+2) + pdf.CellFormat(120, 7, "Baggi Software System", "", 0, "L", false, 0, "") + + pdf.SetFont(hFontFamilyBold, "", 12) + pdf.SetXY(hMarginL+hLogoW+8, hMarginT+10) + pdf.CellFormat(120, 6, "Cari Hesap Raporu", "", 0, "L", false, 0, "") + + // Bugünün tarihi (sağ üst) + today := time.Now().Format("02.01.2006") + pdf.SetFont(hFontFamilyReg, "", 9) + pdf.SetXY(hPageWidth-hMarginR-40, hMarginT+3) + pdf.CellFormat(40, 6, "Tarih: "+today, "", 0, "R", false, 0, "") + + // Cari & Tarih kutuları (daha yukarı taşındı) + boxY := hMarginT + hLogoW - 6 + pdf.SetFont(hFontFamilyBold, "", 10) + + pdf.Rect(hMarginL, boxY, 140, 11, "") + pdf.SetXY(hMarginL+2, boxY+3) + pdf.CellFormat(136, 5, fmt.Sprintf("Cari: %s — %s", cariKod, cariIsim), "", 0, "L", false, 0, "") + + pdf.Rect(hPageWidth-hMarginR-140, boxY, 140, 11, "") + pdf.SetXY(hPageWidth-hMarginR-138, boxY+3) + pdf.CellFormat(136, 5, fmt.Sprintf("Tarih Aralığı: %s → %s", start, end), "", 0, "R", false, 0, "") + + // Alt çizgi + y := boxY + 13 + pdf.SetDrawColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) + pdf.Line(hMarginL, y, hPageWidth-hMarginR, y) + pdf.SetDrawColor(200, 200, 200) + + return y + 4 +} + +/* ============================ TABLO ============================ */ + +func hDrawGroupBar(pdf *gofpdf.Fpdf, currency string, sonBakiye float64) { + x := hMarginL + y := pdf.GetY() + w := hPageWidth - hMarginL - hMarginR + h := hGroupBarH + + pdf.SetDrawColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) + pdf.SetLineWidth(0.6) + pdf.Rect(x, y, w, h, "") + + pdf.SetFont(hFontFamilyBold, "", 11.3) + pdf.SetTextColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) + + pdf.SetXY(x+hCellPadX+1.0, y+(h-5.0)/2) + pdf.CellFormat(w*0.6, 5.0, currency, "", 0, "L", false, 0, "") + + txt := "Son Bakiye = " + hFormatCurrencyTR(sonBakiye) + pdf.SetXY(x+w*0.4, y+(h-5.0)/2) + pdf.CellFormat(w*0.6-2.0, 5.0, txt, "", 0, "R", false, 0, "") + + pdf.SetLineWidth(hGridLineWidth) + pdf.SetDrawColor(200, 200, 200) + pdf.SetTextColor(0, 0, 0) + + pdf.Ln(h) +} + +func hDrawMainHeaderRow(pdf *gofpdf.Fpdf, cols []string, widths []float64) { + pdf.SetFont(hFontFamilyBold, "", 10.2) + pdf.SetTextColor(255, 255, 255) + pdf.SetFillColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) + pdf.SetDrawColor(120, 90, 20) + + x := hMarginL + y := pdf.GetY() + for i, c := range cols { + pdf.Rect(x, y, widths[i], hHeaderRowH, "DF") + pdf.SetXY(x+hCellPadX, y+1.6) + pdf.CellFormat(widths[i]-2*hCellPadX, hHeaderRowH-3.2, c, "", 0, "C", false, 0, "") + x += widths[i] + } + pdf.Ln(hHeaderRowH) +} + +func hDrawMainDataRow(pdf *gofpdf.Fpdf, row []string, widths []float64, rowH float64, rowIndex int) { + x := hMarginL + y := pdf.GetY() + + // ✅ Zebra efekti + wTotal := hPageWidth - hMarginL - hMarginR + if rowIndex%2 == 1 { + pdf.SetFillColor(hColorSecondary[0], hColorSecondary[1], hColorSecondary[2]) + pdf.Rect(x, y, wTotal, rowH, "F") + } + + pdf.SetFont(hFontFamilyReg, "", 8.2) + pdf.SetDrawColor(242, 235, 222) + pdf.SetTextColor(30, 30, 30) + + for i, val := range row { + pdf.Rect(x, y, widths[i], rowH, "") + switch { + case i == 4: // Açıklama wrap + hDrawWrapText(pdf, val, x+hCellPadX, y+0.5, widths[i]-2*hCellPadX, hLineHMain) + case i >= 6: + pdf.SetXY(x+hCellPadX, y+(rowH-hLineHMain)/2) + pdf.CellFormat(widths[i]-2*hCellPadX, hLineHMain, val, "", 0, "R", false, 0, "") + default: + pdf.SetXY(x+hCellPadX, y+(rowH-hLineHMain)/2) + pdf.CellFormat(widths[i]-2*hCellPadX, hLineHMain, val, "", 0, "L", false, 0, "") + } + x += widths[i] + } + + // ❌ pdf.Ln(rowH) YOK + // ✅ Manuel olarak Y'yi arttırıyoruz + pdf.SetY(y + rowH) +} + +/* ============================ MAIN HANDLER ============================ */ + +func ExportStatementHeaderReportPDFHandler(mssql *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + started := time.Now() + + accountCode := r.URL.Query().Get("accountcode") + startDate := r.URL.Query().Get("startdate") + endDate := r.URL.Query().Get("enddate") + rawParis := r.URL.Query()["parislemler"] + + var parislemler []string + for _, v := range rawParis { + v = strings.TrimSpace(v) + if v != "" && v != "undefined" && v != "null" { + parislemler = append(parislemler, v) + } + } + + headers, _, err := queries.GetStatementsHPDF(accountCode, startDate, endDate, parislemler) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + type grp struct { + code string + rows []models.StatementHeader + sonBakiye float64 + } + order := []string{} + groups := map[string]*grp{} + + for _, h := range headers { + g, ok := groups[h.ParaBirimi] + if !ok { + g = &grp{code: h.ParaBirimi} + groups[h.ParaBirimi] = g + order = append(order, h.ParaBirimi) + } + g.rows = append(g.rows, h) + } + for _, k := range order { + sort.SliceStable(groups[k].rows, func(i, j int) bool { + ri, rj := groups[k].rows[i], groups[k].rows[j] + if ri.BelgeTarihi == rj.BelgeTarihi { + return ri.BelgeNo < rj.BelgeNo + } + return ri.BelgeTarihi < rj.BelgeTarihi + }) + if n := len(groups[k].rows); n > 0 { + groups[k].sonBakiye = groups[k].rows[n-1].Bakiye + } + } + + pdf := gofpdf.New("L", "mm", "A4", "") + pdf.SetMargins(hMarginL, hMarginT, hMarginR) + pdf.SetAutoPageBreak(false, hMarginB) + hEnsureFonts(pdf) + + wAvail := hPageWidth - hMarginL - hMarginR + mainWn := hNormalizeWidths(hMainWbase, wAvail) + + cariIsim := "" + if len(headers) > 0 { + cariIsim = headers[0].CariIsim + } + + pageNum := 0 + newPage := func() { + pageNum++ + pdf.AddPage() + tableTop := hDrawPageHeader(pdf, accountCode, cariIsim, startDate, endDate) + pdf.SetY(tableTop) + } + + newPage() + + for _, cur := range order { + g := groups[cur] + hDrawGroupBar(pdf, cur, g.sonBakiye) + hDrawMainHeaderRow(pdf, hMainCols, mainWn) + + rowIndex := 0 + for _, h := range g.rows { + row := []string{ + h.BelgeNo, h.BelgeTarihi, h.VadeTarihi, h.IslemTipi, + h.Aciklama, h.ParaBirimi, + hFormatCurrencyTR(h.Borc), + hFormatCurrencyTR(h.Alacak), + hFormatCurrencyTR(h.Bakiye), + } + + rh := hCalcRowHeightForText(pdf, row[4], mainWn[4], hLineHMain, hCellPadX) + if hNeedNewPage(pdf, rh+hHeaderRowH) { + newPage() + hDrawGroupBar(pdf, cur, g.sonBakiye) + hDrawMainHeaderRow(pdf, hMainCols, mainWn) + } + + hDrawMainDataRow(pdf, row, mainWn, rh, rowIndex) + rowIndex++ + } + pdf.Ln(1) + } + + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + http.Error(w, "PDF oluşturulamadı: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/pdf") + w.Header().Set("Content-Disposition", "inline; filename=statement-header-report.pdf") + _, _ = w.Write(buf.Bytes()) + + log.Printf("✅ Header-only PDF üretimi tamamlandı (%s)", time.Since(started)) + } +} diff --git a/svc/routes/statements_pdf.go b/svc/routes/statements_pdf.go new file mode 100644 index 0000000..9908fa7 --- /dev/null +++ b/svc/routes/statements_pdf.go @@ -0,0 +1,642 @@ +// routes/statements_pdf.go +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/queries" + "bytes" + "database/sql" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/jung-kurt/gofpdf" +) + +/* ============================ SABİTLER ============================ */ + +// A4 Landscape (mm) +const ( + pageWidth = 297.0 + pageHeight = 210.0 + + // Kenar boşlukları (mm) + marginL = 8.0 + marginT = 8.0 + marginR = 8.0 + marginB = 15.0 + + // Satır aralıkları (mm) + lineHMain = 6.0 // ana satır biraz iri + lineHDetail = 5.2 + cellPadX = 2.2 + + // Satır yükseklikleri (mm) + headerRowH = 8.4 + subHeaderRowH = 6.2 + groupBarH = 9.0 + + // Çizgi kalınlığı + gridLineWidth = 0.3 + + // Logo genişliği (yükseklik oranlanır) + logoW = 42.0 +) + +// Ana tablo kolonları +var mainCols = []string{ + "Belge No", "Tarih", "Vade", "İşlem", + "Açıklama", "Para", "Borç", "Alacak", "Bakiye", +} + +// Ana tablo kolon genişlikleri (ilk 3 geniş) +var mainWbase = []float64{ + 34, // Belge No + 30, // Tarih + 28, // Vade + 24, // İşlem + 60, // Açıklama (biraz daraltıldı) + 20, // Para + 36, // Borç (genişletildi) + 36, // Alacak (genişletildi) + 36, // Bakiye (genişletildi) +} + +// Detay tablo kolonları ve genişlikleri +var dCols = []string{ + "Ana Grup", "Alt Grup", "Garson", "Fit", "İçerik", + "Ürün", "Renk", "Adet", "Fiyat", "Tutar", +} +var dWbase = []float64{ + 30, 28, 22, 20, 56, 30, 22, 20, 20, 26} + +// Font dosyaları +const ( + fontFamilyReg = "dejavu" + fontFamilyBold = "dejavu-b" + fontPathReg = "fonts/DejaVuSans.ttf" + fontPathBold = "fonts/DejaVuSans-Bold.ttf" +) + +// Kurumsal renkler +var ( + colorPrimary = [3]int{149, 113, 22} // #957116 (altın) + colorSecondary = [3]int{218, 193, 151} // #dac197 (bej) + colorDetailFill = [3]int{242, 235, 222} // secondary’den daha açık (detay satır zemin) +) + +/* ============================ HELPERS ============================ */ + +// Genişlikleri normalize et +func normalizeWidths(base []float64, targetTotal float64) []float64 { + sum := 0.0 + for _, v := range base { + sum += v + } + out := make([]float64, len(base)) + if sum <= 0 { + each := targetTotal / float64(len(base)) + for i := range out { + out[i] = each + } + return out + } + scale := targetTotal / sum + for i, v := range base { + out[i] = v * scale + } + return out +} + +// Türkçe formatlı sayı +func formatCurrencyTR(n float64) string { + s := fmt.Sprintf("%.2f", n) // "864000.00" + parts := strings.Split(s, ".") + intPart := parts[0] + decPart := parts[1] + + // Binlik ayırıcı (.) + var out strings.Builder + for i, c := range intPart { + if (len(intPart)-i)%3 == 0 && i != 0 { + out.WriteRune('.') + } + out.WriteRune(c) + } + + return out.String() + "," + decPart +} + +// Fontları yükle +func ensureFonts(pdf *gofpdf.Fpdf) { + if _, err := os.Stat(fontPathReg); err == nil { + pdf.AddUTF8Font(fontFamilyReg, "", fontPathReg) + } else { + log.Printf("⚠️ Font bulunamadı: %s", fontPathReg) + } + if _, err := os.Stat(fontPathBold); err == nil { + pdf.AddUTF8Font(fontFamilyBold, "", fontPathBold) + } else { + log.Printf("⚠️ Font bulunamadı: %s", fontPathBold) + } +} + +// Güvenli satır kırma +func splitLinesSafe(pdf *gofpdf.Fpdf, text string, width float64) [][]byte { + if width <= 0 { + width = 1 + } + defer func() { + if rec := recover(); rec != nil { + log.Printf("⚠️ splitLinesSafe recover: %v", rec) + } + }() + return pdf.SplitLines([]byte(text), width) +} + +// Metin wrap çizimi +func drawWrapText(pdf *gofpdf.Fpdf, text string, x, y, width, lineH float64) { + if text == "" { + return + } + lines := splitLinesSafe(pdf, text, width) + cy := y + for _, ln := range lines { + pdf.SetXY(x, cy) + pdf.CellFormat(width, lineH, string(ln), "", 0, "L", false, 0, "") + cy += lineH + } +} + +// Wrap satır yüksekliği +func calcRowHeightForText(pdf *gofpdf.Fpdf, text string, colWidth, lineHeight, padX float64) float64 { + if colWidth <= 2*padX { + colWidth = 2*padX + 1 + } + if text == "" { + return lineHeight + } + lines := splitLinesSafe(pdf, text, colWidth-(2*padX)) + if len(lines) == 0 { + return lineHeight + } + h := float64(len(lines)) * lineHeight + if h < lineHeight { + h = lineHeight + } + return h +} + +// Alt tarafa sığmıyor mu? +func needNewPage(pdf *gofpdf.Fpdf, needH float64) bool { + return pdf.GetY()+needH+marginB > pageHeight +} + +// NULL/boş’ları uzun çizgiye çevir +func nullToDash(s string) string { + if strings.TrimSpace(s) == "" { + return "—" + } + return s +} + +/* ============================ HEADER DRAW ============================ */ + +// Küçük yardımcı: kutu içine otomatik 1-2 satır metin (taşarsa 2) +func drawLabeledBox(pdf *gofpdf.Fpdf, x, y, w, h float64, label, value string, align string) { + // Çerçeve + pdf.Rect(x, y, w, h, "") + // İç marj + ix := x + 2 + iy := y + 1.8 + iw := w - 4 + // Etiket (kalın, küçük) + pdf.SetFont(fontFamilyBold, "", 8) + pdf.SetXY(ix, iy) + pdf.CellFormat(iw, 4, label, "", 0, "L", false, 0, "") + + // Değer (normal, 1-2 satır) + pdf.SetFont(fontFamilyReg, "", 8) + vy := iy + 4.2 + lineH := 4.2 + lines := splitLinesSafe(pdf, value, iw) + if len(lines) > 2 { + lines = lines[:2] // en fazla 2 satır göster + } + for _, ln := range lines { + pdf.SetXY(ix, vy) + pdf.CellFormat(iw, lineH, string(ln), "", 0, align, false, 0, "") + vy += lineH + } +} + +func drawPageHeader(pdf *gofpdf.Fpdf, cariKod, cariIsim, start, end string) float64 { + logoPath, _ := filepath.Abs("./public/Baggi-Tekstil-A.s-Logolu.jpeg") + + // Logo + pdf.ImageOptions(logoPath, hMarginL, 2, hLogoW, 0, false, gofpdf.ImageOptions{}, 0, "") + + // Başlıklar + pdf.SetFont(hFontFamilyBold, "", 16) + pdf.SetTextColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) + pdf.SetXY(hMarginL+hLogoW+8, hMarginT+2) + pdf.CellFormat(120, 7, "Baggi Software System", "", 0, "L", false, 0, "") + + pdf.SetFont(hFontFamilyBold, "", 12) + pdf.SetXY(hMarginL+hLogoW+8, hMarginT+10) + pdf.CellFormat(120, 6, "Cari Hesap Raporu", "", 0, "L", false, 0, "") + + // Bugünün tarihi (sağ üst) + today := time.Now().Format("02.01.2006") + pdf.SetFont(hFontFamilyReg, "", 9) + pdf.SetXY(hPageWidth-hMarginR-40, hMarginT+3) + pdf.CellFormat(40, 6, "Tarih: "+today, "", 0, "R", false, 0, "") + + // Cari & Tarih kutuları (daha yukarı taşındı) + boxY := hMarginT + hLogoW - 6 + pdf.SetFont(hFontFamilyBold, "", 10) + + pdf.Rect(hMarginL, boxY, 140, 11, "") + pdf.SetXY(hMarginL+2, boxY+3) + pdf.CellFormat(136, 5, fmt.Sprintf("Cari: %s — %s", cariKod, cariIsim), "", 0, "L", false, 0, "") + + pdf.Rect(hPageWidth-hMarginR-140, boxY, 140, 11, "") + pdf.SetXY(hPageWidth-hMarginR-138, boxY+3) + pdf.CellFormat(136, 5, fmt.Sprintf("Tarih Aralığı: %s → %s", start, end), "", 0, "R", false, 0, "") + + // Alt çizgi + y := boxY + 13 + pdf.SetDrawColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) + pdf.Line(hMarginL, y, hPageWidth-hMarginR, y) + pdf.SetDrawColor(200, 200, 200) + + return y + 4 +} + +/* ============================ GROUP BAR ============================ */ + +func drawGroupBar(pdf *gofpdf.Fpdf, currency string, sonBakiye float64) { + // Kutu alanı (tam genişlik) + x := marginL + y := pdf.GetY() + w := pageWidth - marginL - marginR + h := groupBarH + + // Çerçeve + pdf.SetDrawColor(colorPrimary[0], colorPrimary[1], colorPrimary[2]) + pdf.SetLineWidth(0.6) + pdf.Rect(x, y, w, h, "") + + // Metinler + pdf.SetFont(fontFamilyBold, "", 11.3) // bir tık büyük + pdf.SetTextColor(colorPrimary[0], colorPrimary[1], colorPrimary[2]) + + pdf.SetXY(x+cellPadX+1.0, y+(h-5.0)/2) + pdf.CellFormat(w*0.6, 5.0, fmt.Sprintf("%s", currency), "", 0, "L", false, 0, "") + + txt := "Son Bakiye = " + formatCurrencyTR(sonBakiye) + pdf.SetXY(x+w*0.4, y+(h-5.0)/2) + pdf.CellFormat(w*0.6-2.0, 5.0, txt, "", 0, "R", false, 0, "") + + // Renk/kalınlık geri + pdf.SetLineWidth(gridLineWidth) + pdf.SetDrawColor(200, 200, 200) + pdf.SetTextColor(0, 0, 0) + + pdf.Ln(h) +} + +/* ============================ HEADER ROWS ============================ */ + +func drawMainHeaderRow(pdf *gofpdf.Fpdf, cols []string, widths []float64) { + // Ana başlık: Primary arkaplan, beyaz yazı + pdf.SetFont(fontFamilyBold, "", 10.2) + pdf.SetTextColor(255, 255, 255) + pdf.SetFillColor(colorPrimary[0], colorPrimary[1], colorPrimary[2]) + pdf.SetDrawColor(120, 90, 20) + + x := marginL + y := pdf.GetY() + for i, c := range cols { + pdf.Rect(x, y, widths[i], headerRowH, "DF") + pdf.SetXY(x+cellPadX, y+1.6) + pdf.CellFormat(widths[i]-2*cellPadX, headerRowH-3.2, c, "", 0, "C", false, 0, "") + x += widths[i] + } + pdf.SetY(y + headerRowH) + +} + +func drawDetailHeaderRow(pdf *gofpdf.Fpdf, cols []string, widths []float64) { + // Detay başlık: Secondary (daha açık) + pdf.SetFont(fontFamilyBold, "", 9.2) + pdf.SetFillColor(colorSecondary[0], colorSecondary[1], colorSecondary[2]) + pdf.SetTextColor(0, 0, 0) + pdf.SetDrawColor(160, 140, 100) + + x := marginL + y := pdf.GetY() + for i, c := range cols { + pdf.Rect(x, y, widths[i], subHeaderRowH, "DF") + pdf.SetXY(x+cellPadX, y+1.2) + pdf.CellFormat(widths[i]-2*cellPadX, subHeaderRowH-2.4, c, "", 0, "C", false, 0, "") + x += widths[i] + } + pdf.Ln(subHeaderRowH) +} + +/* ============================ DATA ROWS ============================ */ + +// Ana veri satırı: daha büyük ve kalın görünsün +// Ana veri satırı (zebrasız, sıkı grid – satırlar yapışık) +func drawMainDataRow(pdf *gofpdf.Fpdf, row []string, widths []float64, rowH float64) { + x := marginL + y := pdf.GetY() + + // Arka plan beyaz, çizgiler gri + pdf.SetFont(fontFamilyBold, "", 8.5) + pdf.SetDrawColor(210, 210, 210) + pdf.SetTextColor(30, 30, 30) + pdf.SetFillColor(255, 255, 255) + + for i, val := range row { + // Hücre çerçevesi + pdf.Rect(x, y, widths[i], rowH, "") + switch { + case i == 4: // Açıklama wrap + drawWrapText(pdf, val, x+cellPadX, y+0.8, widths[i]-2*cellPadX, lineHMain) + case i >= 6: // Sayısal sağ + pdf.SetXY(x+cellPadX, y+(rowH-lineHMain)/2) + pdf.CellFormat(widths[i]-2*cellPadX, lineHMain, val, "", 0, "R", false, 0, "") + default: // Normal sol + pdf.SetXY(x+cellPadX, y+(rowH-lineHMain)/2) + pdf.CellFormat(widths[i]-2*cellPadX, lineHMain, val, "", 0, "L", false, 0, "") + } + x += widths[i] + } + + // ❌ pdf.Ln(rowH) yerine: + // ✅ bir alt satıra tam yapışık in + pdf.SetY(y + rowH) +} + +// Detay veri satırı: açık zemin, biraz küçük; zebra opsiyonel +func drawDetailDataRow(pdf *gofpdf.Fpdf, row []string, widths []float64, rowH float64, fillBg bool) { + pdf.SetFont(fontFamilyReg, "", 8.0) + pdf.SetTextColor(60, 60, 60) + pdf.SetDrawColor(220, 220, 220) + + x := marginL + y := pdf.GetY() + + for i, val := range row { + // Zemin + if fillBg { + pdf.SetFillColor(colorDetailFill[0], colorDetailFill[1], colorDetailFill[2]) + pdf.Rect(x, y, widths[i], rowH, "DF") + pdf.SetFillColor(255, 255, 255) // geri + } else { + pdf.Rect(x, y, widths[i], rowH, "") + } + + switch { + case i == 4: // İçerik wrap + drawWrapText(pdf, val, x+cellPadX, y+1.4, widths[i]-2*cellPadX, lineHDetail) + case i >= 7: // Sayısal sağ + pdf.SetXY(x+cellPadX, y+(rowH-lineHDetail)/2) + pdf.CellFormat(widths[i]-2*cellPadX, lineHDetail, val, "", 0, "R", false, 0, "") + default: // Sol + pdf.SetXY(x+cellPadX, y+(rowH-lineHDetail)/2) + pdf.CellFormat(widths[i]-2*cellPadX, lineHDetail, val, "", 0, "L", false, 0, "") + } + x += widths[i] + } + pdf.Ln(rowH) +} + +/* ============================ HANDLER ============================ */ + +func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + started := time.Now() + + defer func() { + if rec := recover(); rec != nil { + log.Printf("❌ PANIC ExportPDFHandler: %v", rec) + http.Error(w, "PDF oluşturulurken hata oluştu", http.StatusInternalServerError) + } + }() + + accountCode := r.URL.Query().Get("accountcode") + startDate := r.URL.Query().Get("startdate") + endDate := r.URL.Query().Get("enddate") + + // parislemler sanitize + rawParis := r.URL.Query()["parislemler"] + parislemler := make([]string, 0, len(rawParis)) + for _, v := range rawParis { + v = strings.TrimSpace(v) + if v == "" || strings.EqualFold(v, "undefined") || strings.EqualFold(v, "null") { + continue + } + parislemler = append(parislemler, v) + } + log.Printf("▶️ ExportPDFHandler: account=%s start=%s end=%s parislemler=%v", + accountCode, startDate, endDate, parislemler) + + // 1) Header verileri + headers, belgeNos, err := queries.GetStatementsPDF(accountCode, startDate, endDate, parislemler) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos)) + + // 2) Detay verileri + detailMap, err := queries.GetDetailsMapPDF(belgeNos, startDate, endDate) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + log.Printf("✅ Detay verileri alındı: %d belge için detay var", len(detailMap)) + + // 3) Gruplama + type grp struct { + code string + rows []models.StatementHeader + sonBakiye float64 + } + order := []string{} + groups := map[string]*grp{} + + for _, h := range headers { + g, ok := groups[h.ParaBirimi] + if !ok { + g = &grp{code: h.ParaBirimi} + groups[h.ParaBirimi] = g + order = append(order, h.ParaBirimi) + } + g.rows = append(g.rows, h) + } + + for _, k := range order { + sort.SliceStable(groups[k].rows, func(i, j int) bool { + ri, rj := groups[k].rows[i], groups[k].rows[j] + if ri.BelgeTarihi == rj.BelgeTarihi { + return ri.BelgeNo < rj.BelgeNo + } + return ri.BelgeTarihi < rj.BelgeTarihi + }) + if n := len(groups[k].rows); n > 0 { + groups[k].sonBakiye = groups[k].rows[n-1].Bakiye + } + } + + // 4) Kolon genişlikleri + wAvail := pageWidth - marginL - marginR + mainWn := normalizeWidths(mainWbase, wAvail) + dWn := normalizeWidths(dWbase, wAvail) + + // 5) PDF init + pdf := gofpdf.New("L", "mm", "A4", "") + pdf.SetMargins(marginL, marginT, marginR) + pdf.SetAutoPageBreak(false, marginB) + ensureFonts(pdf) + pdf.SetFont(fontFamilyReg, "", 8.5) + pdf.SetTextColor(0, 0, 0) + + pageNum := 0 + cariIsim := "" + if len(headers) > 0 { + cariIsim = headers[0].CariIsim + } + + // Sayfa başlatıcı (header yüksekliği dinamik) + newPage := func() { + pageNum++ + pdf.AddPage() + + // drawPageHeader tablo başlangıç yüksekliğini döndürüyor + tableTop := drawPageHeader(pdf, accountCode, cariIsim, startDate, endDate) + + // Sayfa numarası + pdf.SetFont(fontFamilyReg, "", 6) + pdf.SetXY(pageWidth-marginR-28, pageHeight-marginB+3) + pdf.CellFormat(28, 5, fmt.Sprintf("Sayfa %d", pageNum), "", 0, "R", false, 0, "") + + // Tablo Y konumunu ayarla + pdf.SetY(tableTop) + } + + newPage() + + // 6) Gruplar yaz + for _, cur := range order { + g := groups[cur] + + if needNewPage(pdf, groupBarH+headerRowH) { + newPage() + } + drawGroupBar(pdf, cur, g.sonBakiye) + drawMainHeaderRow(pdf, mainCols, mainWn) + + for _, h := range g.rows { + row := []string{ + h.BelgeNo, h.BelgeTarihi, h.VadeTarihi, h.IslemTipi, + h.Aciklama, h.ParaBirimi, + formatCurrencyTR(h.Borc), + formatCurrencyTR(h.Alacak), + formatCurrencyTR(h.Bakiye), + } + + pdf.SetFont(fontFamilyBold, "", 9.6) + rh := calcRowHeightForText(pdf, row[4], mainWn[4], lineHMain, cellPadX) + + if needNewPage(pdf, rh+headerRowH) { + newPage() + drawGroupBar(pdf, cur, g.sonBakiye) + drawMainHeaderRow(pdf, mainCols, mainWn) + } + drawMainDataRow(pdf, row, mainWn, rh) + + // detaylar + details := detailMap[h.BelgeNo] + if len(details) > 0 { + if needNewPage(pdf, subHeaderRowH) { + newPage() + drawGroupBar(pdf, cur, g.sonBakiye) + drawMainHeaderRow(pdf, mainCols, mainWn) + } + drawDetailHeaderRow(pdf, dCols, dWn) + + for i, d := range details { + drow := []string{ + nullToDash(d.UrunAnaGrubu), + nullToDash(d.UrunAltGrubu), + nullToDash(d.YetiskinGarson), + nullToDash(d.Fit), + nullToDash(d.Icerik), + nullToDash(d.UrunKodu), + nullToDash(d.UrunRengi), + formatCurrencyTR(d.ToplamAdet), + formatCurrencyTR(d.ToplamFiyat), + formatCurrencyTR(d.ToplamTutar), + } + + pdf.SetFont(fontFamilyReg, "", 8) + rh2 := calcRowHeightForText(pdf, drow[4], dWn[4], lineHDetail, cellPadX) + + if needNewPage(pdf, rh2) { + newPage() + drawGroupBar(pdf, cur, g.sonBakiye) + drawMainHeaderRow(pdf, mainCols, mainWn) + drawDetailHeaderRow(pdf, dCols, dWn) + } + // zebra: çift indekslerde açık zemin + fill := (i%2 == 0) + drawDetailDataRow(pdf, drow, dWn, rh2, fill) + } + } + } + pdf.Ln(3) + } + + // 7) Çıktı + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + http.Error(w, "PDF oluşturulamadı: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/pdf") + w.Header().Set("Content-Disposition", "inline; filename=statement.pdf") + _, _ = w.Write(buf.Bytes()) + + log.Printf("✅ PDF üretimi tamam: %s", time.Since(started)) + } +} + +/* +NOTLAR: +- Header artık dinamik yüksekliğe sahip (drawPageHeader -> contentTopY döner). +- Logo sol üst köşe, başlık “toolbar” gibi; “Cari / Tarih” kutuları + 1–2 satır metni çerçeve içinde otomatik kırar. +- Grup barı primary renkte çerçeveli ve yazı biraz büyük. +- Ana satırlar kalın & bir tık büyük (detaydan farklı). +- Detay satırlar açık (bej) zeminle zebra (i%2==0) uygulanır. +- SplitLines her yerde splitLinesSafe ile korunuyor (panic yok). +- AddPage sadece newPage()’de. +- Font bulunamazsa gömme fontlarla devam edilir (log yazar). +*/ diff --git a/svc/routes/test_mail.go b/svc/routes/test_mail.go new file mode 100644 index 0000000..3dc107a --- /dev/null +++ b/svc/routes/test_mail.go @@ -0,0 +1,49 @@ +package routes + +import ( + "context" + "encoding/json" + "net/http" + + "bssapp-backend/auth" + "bssapp-backend/internal/mailer" +) + +func TestMailHandler(ml *mailer.GraphMailer) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + var req struct { + To string `json:"to"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid body", http.StatusBadRequest) + return + } + + err := ml.Send( + context.Background(), + mailer.Message{ + To: []string{req.To}, + Subject: "BSSApp Test Mail", + BodyHTML: "

🎉 Test mail başarılı

Microsoft Graph üzerinden gönderildi.

", + }, + ) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(map[string]string{ + "message": "Mail gönderildi", + }) + }) +} diff --git a/svc/routes/todaycurrencyv3.go b/svc/routes/todaycurrencyv3.go new file mode 100644 index 0000000..5704e95 --- /dev/null +++ b/svc/routes/todaycurrencyv3.go @@ -0,0 +1,30 @@ +package routes + +import ( + "bssapp-backend/queries" + "database/sql" + "encoding/json" + "fmt" + "net/http" +) + +// ✅ Handler artık parametre alıyor +func GetTodayCurrencyV3Handler(mssql *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + code := r.URL.Query().Get("code") + if code == "" { + http.Error(w, "Eksik parametre: code", http.StatusBadRequest) + return + } + + currency, err := queries.GetTodayCurrencyV3(mssql, code) // 👈 MSSQL + if err != nil { + http.Error(w, fmt.Sprintf("Kur bulunamadı: %v", err), http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(currency) + } +} diff --git a/svc/routes/user_detail.go b/svc/routes/user_detail.go new file mode 100644 index 0000000..9a00706 --- /dev/null +++ b/svc/routes/user_detail.go @@ -0,0 +1,320 @@ +package routes + +import ( + "bssapp-backend/internal/auditlog" + "bssapp-backend/internal/mailer" + "bssapp-backend/internal/security" + "bssapp-backend/models" + "bssapp-backend/queries" + "bytes" + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "time" + + "github.com/gorilla/mux" +) + +// ====================================================== +// 👤 USER DETAIL ROUTE (GET + PUT) +// URL: /api/users/{id} +// ====================================================== +type LoginUser struct { + ID int64 `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + RoleID int `json:"role_id"` + RoleCode string `json:"role_code"` + ForcePasswordChange bool `json:"force_password_change"` +} + +func UserDetailRoute(db *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + idStr := mux.Vars(r)["id"] + userID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil || userID <= 0 { + http.Error(w, "Geçersiz kullanıcı id", http.StatusBadRequest) + return + } + + switch r.Method { + case http.MethodGet: + handleUserGet(db, w, userID) + case http.MethodPut: + handleUserUpdate(db, w, r, userID) + case http.MethodOptions: + w.WriteHeader(http.StatusOK) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + }) +} + +// ====================================================== +// 📥 GET USER +// ====================================================== +// ====================================================== +// 📥 GET USER (FINAL - mk_dfusr Uyumlu) +// ====================================================== +func handleUserGet(db *sql.DB, w http.ResponseWriter, userID int64) { + + var u models.UserDetail + + // -------------------------------------------------- + // 🟢 HEADER + // -------------------------------------------------- + err := db.QueryRow( + queries.GetUserHeader, + userID, + ).Scan( + &u.ID, + &u.Code, // username + &u.IsActive, + &u.FullName, + &u.Email, + &u.Mobile, + &u.Address, + &u.HasPassword, + ) + + if err != nil { + + // detaylı log (çok önemli) + fmt.Println("❌ [UserDetail] HEADER SCAN ERROR:", err) + + if err == sql.ErrNoRows { + http.Error(w, "Kullanıcı bulunamadı", http.StatusNotFound) + return + } + + http.Error(w, "User header error", http.StatusInternalServerError) + return + } + + // -------------------------------------------------- + // 🟢 ROLES + // -------------------------------------------------- + roleRows, err := db.Query(queries.GetUserRoles, userID) + if err != nil { + fmt.Println("⚠️ [UserDetail] ROLE QUERY:", err) + } else { + defer roleRows.Close() + + for roleRows.Next() { + var code string + + if err := roleRows.Scan(&code); err == nil { + u.Roles = append(u.Roles, code) + } + } + } + + // -------------------------------------------------- + // 🟢 DEPARTMENTS + // -------------------------------------------------- + deptRows, err := db.Query(queries.GetUserDepartments, userID) + if err != nil { + fmt.Println("⚠️ [UserDetail] DEPT QUERY:", err) + } else { + defer deptRows.Close() + + for deptRows.Next() { + + var d models.DeptOption + + if err := deptRows.Scan(&d.Code, &d.Title); err == nil { + u.Departments = append(u.Departments, d) + } + } + } + + // -------------------------------------------------- + // 🟢 PIYASALAR + // -------------------------------------------------- + piyRows, err := db.Query(queries.GetUserPiyasalar, userID) + if err != nil { + fmt.Println("⚠️ [UserDetail] PIYASA QUERY:", err) + } else { + defer piyRows.Close() + + for piyRows.Next() { + + var p models.DeptOption + + if err := piyRows.Scan(&p.Code, &p.Title); err == nil { + u.Piyasalar = append(u.Piyasalar, p) + } + } + } + + // -------------------------------------------------- + // 🟢 NEBIM USERS + // -------------------------------------------------- + nebimRows, err := db.Query(queries.GetUserNebim, userID) + if err != nil { + fmt.Println("⚠️ [UserDetail] NEBIM QUERY:", err) + } else { + defer nebimRows.Close() + + for nebimRows.Next() { + + var n models.NebimOption + + if err := nebimRows.Scan( + &n.Username, + &n.UserGroupCode, + ); err == nil { + + u.NebimUsers = append(u.NebimUsers, n) + } + } + } + + // -------------------------------------------------- + // 🟢 RESPONSE + // -------------------------------------------------- + w.WriteHeader(http.StatusOK) + + if err := json.NewEncoder(w).Encode(u); err != nil { + fmt.Println("❌ [UserDetail] JSON ENCODE:", err) + } +} + +// ====================================================== +// ✍️ UPDATE USER (PUT) +// ====================================================== +func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID int64) { + + raw, _ := io.ReadAll(r.Body) + + fmt.Println("🟥 RAW BODY:", string(raw)) + + // body tekrar okunabilsin diye reset + r.Body = io.NopCloser(bytes.NewBuffer(raw)) + + var payload models.UserWrite + + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + fmt.Printf("🟦 DECODED PAYLOAD: %+v\n", payload) + + fmt.Println("❌ JSON DECODE ERROR:", err) + + http.Error(w, "Geçersiz payload", http.StatusBadRequest) + return + } + + tx, err := db.Begin() + if err != nil { + http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError) + return + } + defer tx.Rollback() + + _, err = tx.Exec( + queries.UpdateUserHeader, + userID, + payload.Code, + payload.IsActive, + payload.FullName, + payload.Email, + payload.Mobile, + payload.Address, + ) + if err != nil { + http.Error(w, "Header güncellenemedi", http.StatusInternalServerError) + return + } + + tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID) + for _, code := range payload.Roles { + tx.Exec(queries.InsertUserRole, userID, code) + } + + tx.Exec(`DELETE FROM dfusr_dprt WHERE dfusr_id = $1`, userID) + for _, d := range payload.Departments { + tx.Exec(queries.InsertUserDepartment, userID, d.Code) + } + + tx.Exec(`DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`, userID) + for _, p := range payload.Piyasalar { + tx.Exec(queries.InsertUserPiyasa, userID, p.Code) + } + + tx.Exec(`DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`, userID) + for _, n := range payload.NebimUsers { + tx.Exec(queries.InsertUserNebim, userID, n.Username) + } + + if err := tx.Commit(); err != nil { + http.Error(w, "Commit başarısız", http.StatusInternalServerError) + return + } + + _ = json.NewEncoder(w).Encode(map[string]any{"success": true}) +} + +// ====================================================== +// 🔐 ADMIN — PASSWORD RESET MAIL +// ====================================================== +func SendPasswordResetMailHandler( + db *sql.DB, + mailer *mailer.GraphMailer, +) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + + userID, _ := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) + + var email string + err := db.QueryRow(` + SELECT email + FROM mk_dfusr + WHERE id = $1 AND is_active = true + `, userID).Scan(&email) + + if err != nil || email == "" { + w.WriteHeader(http.StatusOK) + return + } + + // 🔑 TOKEN (PLAIN + HASH) + plain, hash, err := security.GenerateResetToken() + if err != nil { + w.WriteHeader(http.StatusOK) + return + } + + expires := time.Now().Add(30 * time.Minute) + + // 💾 DB → SADECE HASH + _, _ = db.Exec(` + INSERT INTO dfusr_password_reset (dfusr_id, token, expires_at) + VALUES ($1,$2,$3) + `, userID, hash, expires) + + // 🔗 URL → PLAIN + resetURL := fmt.Sprintf( + "%s/password-reset/%s", + os.Getenv("FRONTEND_URL"), + plain, + ) + + _ = mailer.SendPasswordResetMail(email, resetURL) + + // 🕵️ AUDIT + auditlog.ForcePasswordChangeStarted( + r.Context(), + userID, + "admin_reset", + ) + + w.WriteHeader(http.StatusOK) + } +} diff --git a/svc/routes/user_list.go b/svc/routes/user_list.go new file mode 100644 index 0000000..56ddc08 --- /dev/null +++ b/svc/routes/user_list.go @@ -0,0 +1,81 @@ +package routes + +import ( + "bssapp-backend/models" + "bssapp-backend/queries" + "database/sql" + "encoding/json" + "log" + "net/http" +) + +// ====================================================== +// 📌 UserListRoute — Kullanıcı Listeleme API (FINAL) +// ====================================================== +func UserListRoute(pg *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // -------------------------------------------------- + // 🔍 Query Param + // -------------------------------------------------- + search := r.URL.Query().Get("search") + log.Printf("📥 /api/users/list çağrıldı | search='%s'", search) + + // -------------------------------------------------- + // 🗄️ SQL CALL + // -------------------------------------------------- + rows, err := queries.GetUserList(pg, search) + if err != nil { + log.Printf("❌ SQL sorgu hatası (GetUserList): %v", err) + http.Error(w, "Veritabanı hatası", http.StatusInternalServerError) + return + } + defer rows.Close() + + // -------------------------------------------------- + // 📦 Sonuç Listesi + // -------------------------------------------------- + list := make([]models.UserListRow, 0, 100) + count := 0 + + // ================================================== + // 🧠 SCAN — SQL SELECT ile BİRE BİR (8 kolon) + // ================================================== + for rows.Next() { + + var u models.UserListRow + + err = rows.Scan( + &u.ID, // 1 + &u.Code, // 2 + &u.IsActive, // 3 + &u.NebimUsername, // 4 (nullable) + &u.UserGroupCode, // 5 (nullable) + &u.RoleNames, // 6 + &u.DepartmentNames, // 7 + &u.PiyasaNames, // 8 + ) + + if err != nil { + log.Printf("⚠️ Satır atlandı (SCAN hatası): %v", err) + continue + } + + list = append(list, u) + count++ + } + + if err := rows.Err(); err != nil { + log.Printf("⚠️ rows.Err(): %v", err) + } + + log.Printf("✅ User listesi tamamlandı — %d kayıt gönderildi.", count) + + // -------------------------------------------------- + // 📤 JSON OUTPUT + // -------------------------------------------------- + _ = json.NewEncoder(w).Encode(list) + }) +} diff --git a/svc/routes/userpermissions.go b/svc/routes/userpermissions.go new file mode 100644 index 0000000..be6252b --- /dev/null +++ b/svc/routes/userpermissions.go @@ -0,0 +1,232 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/auditlog" + "bssapp-backend/permissions" + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "strings" + + "github.com/gorilla/mux" +) + +/* ===================================================== + GET USER OVERRIDES + GET /api/users/{id}/permissions +===================================================== */ + +func GetUserPermissionsHandler(db *sql.DB) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + + // ✅ Auth kontrolü yeterli (Yetki middleware'de) + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + idStr := mux.Vars(r)["id"] + + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil || id <= 0 { + http.Error(w, "invalid id", http.StatusBadRequest) + return + } + + repo := permissions.NewPermissionRepository(db) + + rows, err := repo.GetUserOverridesByUserID(id) + if err != nil { + http.Error(w, "db error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + + json.NewEncoder(w).Encode(rows) + } +} + +/* ===================================================== + SAVE USER OVERRIDES + POST /api/users/{id}/permissions +===================================================== */ + +func SaveUserPermissionsHandler(db *sql.DB) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + + // ✅ Auth kontrolü yeterli + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + userIDStr := mux.Vars(r)["id"] + + userID, err := strconv.ParseInt(userIDStr, 10, 64) + if err != nil || userID <= 0 { + http.Error(w, "invalid id", http.StatusBadRequest) + return + } + + /* ================= PAYLOAD ================= */ + + var list []permissions.UserPermissionRequest + + if err := json.NewDecoder(r.Body).Decode(&list); err != nil { + http.Error(w, "bad payload", http.StatusBadRequest) + return + } + + repo := permissions.NewPermissionRepository(db) + + /* ================= OLD ================= */ + + oldRows, _ := repo.GetUserOverridesByUserID(userID) + + oldMap := map[string]bool{} + + for _, p := range oldRows { + key := p.Module + ":" + p.Action + oldMap[key] = p.Allowed + } + + /* ================= NEW ================= */ + + newMap := map[string]bool{} + + for _, p := range list { + key := p.Module + ":" + p.Action + newMap[key] = p.Allowed + } + + /* ================= DIFF ================= */ + + var changes []map[string]any + + for key, newVal := range newMap { + + oldVal := oldMap[key] + + if oldVal != newVal { + + parts := strings.Split(key, ":") + if len(parts) != 2 { + continue + } + + changes = append(changes, map[string]any{ + "module": parts[0], + "action": parts[1], + "before": oldVal, + "after": newVal, + }) + } + } + + /* ================= SAVE ================= */ + + if err := repo.SaveUserOverrides(userID, list); err != nil { + http.Error(w, "db error", http.StatusInternalServerError) + return + } + + /* ================= AUDIT ================= */ + + if len(changes) > 0 && claims != nil { + + var targetUsername string + + _ = db.QueryRow(` + SELECT username + FROM mk_dfusr + WHERE id = $1 + `, userID).Scan(&targetUsername) + + auditlog.Enqueue(r.Context(), auditlog.ActivityLog{ + + ActionType: "user_permission_change", + ActionCategory: "permission", + + ActionTarget: fmt.Sprintf( + "/api/users/%d/permissions", + userID, + ), + + Description: "user permission overrides updated", + + Username: claims.Username, + RoleCode: claims.RoleCode, + DfUsrID: int64(claims.ID), + + TargetDfUsrID: userID, + TargetUsername: targetUsername, + + ChangeBefore: map[string]any{ + "permissions": oldRows, + }, + + ChangeAfter: map[string]any{ + "changes": changes, + }, + + IsSuccess: true, + }) + } + + /* ================= RESPONSE ================= */ + + w.Header().Set("Content-Type", "application/json") + + json.NewEncoder(w).Encode(map[string]any{ + "success": true, + }) + } +} + +/* ===================================================== + OPTIONAL METHOD +===================================================== */ + +func (h *PermissionHandler) SaveUserOverrides(w http.ResponseWriter, r *http.Request) { + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + userID, _ := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) + + var list []permissions.UserPermissionRequest + + if err := json.NewDecoder(r.Body).Decode(&list); err != nil { + http.Error(w, "bad payload", 400) + return + } + + log.Println("➡️ SAVE USER PERMISSIONS") + log.Println("USER ID:", userID) + log.Println("ITEM COUNT:", len(list)) + + if err := h.Repo.SaveUserOverrides(userID, list); err != nil { + + log.Println("🔥 USER PERMISSION SAVE ERROR") + log.Println("ERROR:", err) + + http.Error(w, err.Error(), 500) + return + } + + json.NewEncoder(w).Encode(map[string]bool{ + "success": true, + }) +} diff --git a/svc/services/auth.go b/svc/services/auth.go new file mode 100644 index 0000000..474d071 --- /dev/null +++ b/svc/services/auth.go @@ -0,0 +1,42 @@ +package services + +import ( + "bssapp-backend/models" + "strings" + + "golang.org/x/crypto/bcrypt" +) + +// CheckPasswordWithLegacy +// - SADECE doğrulama yapar +// - DB write YAPMAZ +// - Migration kararını caller verir +func CheckPasswordWithLegacy(user *models.User, plain string) bool { + if user == nil { + return false + } + + plain = strings.TrimSpace(plain) + if plain == "" { + return false + } + + stored := strings.TrimSpace(user.Upass) + if stored == "" { + return false + } + + // 1️⃣ bcrypt hash mi? + if isBcryptHash(stored) { + return bcrypt.CompareHashAndPassword([]byte(stored), []byte(plain)) == nil + } + + // 2️⃣ TAM LEGACY — düz metin (eski kayıtlar) + return stored == plain +} + +func isBcryptHash(s string) bool { + return strings.HasPrefix(s, "$2a$") || + strings.HasPrefix(s, "$2b$") || + strings.HasPrefix(s, "$2y$") +} diff --git a/svc/utils/error.go b/svc/utils/error.go new file mode 100644 index 0000000..527cec1 --- /dev/null +++ b/svc/utils/error.go @@ -0,0 +1,8 @@ +// utils/logger.go +package utils + +import "log" + +func LogError(scope string, err error) { + log.Printf("[%s] %v", scope, err) +} diff --git a/svc/utils/mail.env b/svc/utils/mail.env new file mode 100644 index 0000000..bbd1fe0 --- /dev/null +++ b/svc/utils/mail.env @@ -0,0 +1,6 @@ +SMTP_HOST=smtp.office365.com +SMTP_PORT=587 +SMTP_USERNAME=admin@baggi.com.tr +SMTP_PASSWORD=Tav34909 +SMTP_FROM=admin@baggi.com.tr +SMTP_STARTTLS=true diff --git a/svc/utils/utils.go b/svc/utils/utils.go new file mode 100644 index 0000000..cf9addd --- /dev/null +++ b/svc/utils/utils.go @@ -0,0 +1,17 @@ +package utils + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" +) + +func UserFromClaims(c *auth.Claims) *models.User { + if c == nil { + return nil + } + + return &models.User{ + ID: int(c.ID), + Username: c.Username, + } +} diff --git a/svc/utils/uuid.go b/svc/utils/uuid.go new file mode 100644 index 0000000..b45d29f --- /dev/null +++ b/svc/utils/uuid.go @@ -0,0 +1,11 @@ +// utils/uuid.go +package utils + +import ( + "github.com/google/uuid" +) + +// NewUUID returns a new random UUID (v4) +func NewUUID() string { + return uuid.New().String() +} diff --git a/ui/-.editorconfig b/ui/-.editorconfig new file mode 100644 index 0000000..f654551 --- /dev/null +++ b/ui/-.editorconfig @@ -0,0 +1,7 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] +charset = utf-8 +indent_size = 2 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/ui/.editorconfig b/ui/.editorconfig new file mode 100644 index 0000000..f654551 --- /dev/null +++ b/ui/.editorconfig @@ -0,0 +1,7 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] +charset = utf-8 +indent_size = 2 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/ui/.idea/.gitignore b/ui/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/ui/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/ui/.idea/baggisoftwaresystem.iml b/ui/.idea/baggisoftwaresystem.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/ui/.idea/baggisoftwaresystem.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/ui/.idea/modules.xml b/ui/.idea/modules.xml new file mode 100644 index 0000000..bd6893d --- /dev/null +++ b/ui/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ui/.idea/vcs.xml b/ui/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/ui/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ui/.npmrc b/ui/.npmrc new file mode 100644 index 0000000..eb19082 --- /dev/null +++ b/ui/.npmrc @@ -0,0 +1,5 @@ +# pnpm-related options +shamefully-hoist=true +strict-peer-dependencies=false +# to get the latest compatible packages when creating the project https://github.com/pnpm/pnpm/issues/6463 +resolution-mode=highest diff --git a/ui/.quasar/dev-spa/app.js b/ui/.quasar/dev-spa/app.js new file mode 100644 index 0000000..608c77d --- /dev/null +++ b/ui/.quasar/dev-spa/app.js @@ -0,0 +1,77 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + + + + +import { Quasar } from 'quasar' +import { markRaw } from 'vue' +import RootComponent from 'app/src/App.vue' + +import createStore from 'app/src/stores/index' +import createRouter from 'app/src/router/index' + + + + + +export default async function (createAppFn, quasarUserOptions) { + + + // Create the app instance. + // Here we inject into it the Quasar UI, the router & possibly the store. + const app = createAppFn(RootComponent) + + + app.config.performance = true + + + app.use(Quasar, quasarUserOptions) + + + + + const store = typeof createStore === 'function' + ? await createStore({}) + : createStore + + + app.use(store) + + + + + + const router = markRaw( + typeof createRouter === 'function' + ? await createRouter({store}) + : createRouter + ) + + + // make router instance available in store + + store.use(({ store }) => { store.router = router }) + + + + // Expose the app, the router and the store. + // Note that we are not mounting the app here, since bootstrapping will be + // different depending on whether we are in a browser or on the server. + return { + app, + store, + router + } +} diff --git a/ui/.quasar/dev-spa/client-entry.js b/ui/.quasar/dev-spa/client-entry.js new file mode 100644 index 0000000..9e9e8bf --- /dev/null +++ b/ui/.quasar/dev-spa/client-entry.js @@ -0,0 +1,158 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + +import { createApp } from 'vue' + + + + + + + +import '@quasar/extras/roboto-font/roboto-font.css' + +import '@quasar/extras/material-icons/material-icons.css' + + + + +// We load Quasar stylesheet file +import 'quasar/dist/quasar.sass' + + + + +import 'src/css/app.css' + + +import createQuasarApp from './app.js' +import quasarUserOptions from './quasar-user-options.js' + + + + + + +console.info('[Quasar] Running SPA.') + + + +const publicPath = `/` + + +async function start ({ + app, + router + , store +}, bootFiles) { + + let hasRedirected = false + const getRedirectUrl = url => { + try { return router.resolve(url).href } + catch (err) {} + + return Object(url) === url + ? null + : url + } + const redirect = url => { + hasRedirected = true + + if (typeof url === 'string' && /^https?:\/\//.test(url)) { + window.location.href = url + return + } + + const href = getRedirectUrl(url) + + // continue if we didn't fail to resolve the url + if (href !== null) { + window.location.href = href + window.location.reload() + } + } + + const urlPath = window.location.href.replace(window.location.origin, '') + + for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) { + try { + await bootFiles[i]({ + app, + router, + store, + ssrContext: null, + redirect, + urlPath, + publicPath + }) + } + catch (err) { + if (err && err.url) { + redirect(err.url) + return + } + + console.error('[Quasar] boot error:', err) + return + } + } + + if (hasRedirected === true) return + + + app.use(router) + + + + + + + app.mount('#q-app') + + + +} + +createQuasarApp(createApp, quasarUserOptions) + + .then(app => { + // eventually remove this when Cordova/Capacitor/Electron support becomes old + const [ method, mapFn ] = Promise.allSettled !== void 0 + ? [ + 'allSettled', + bootFiles => bootFiles.map(result => { + if (result.status === 'rejected') { + console.error('[Quasar] boot error:', result.reason) + return + } + return result.value.default + }) + ] + : [ + 'all', + bootFiles => bootFiles.map(entry => entry.default) + ] + + return Promise[ method ]([ + + import(/* webpackMode: "eager" */ 'boot/axios'), + + import(/* webpackMode: "eager" */ 'boot/dayjs') + + ]).then(bootFiles => { + const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function') + start(app, boot) + }) + }) + diff --git a/ui/.quasar/dev-spa/client-prefetch.js b/ui/.quasar/dev-spa/client-prefetch.js new file mode 100644 index 0000000..9bbe3c5 --- /dev/null +++ b/ui/.quasar/dev-spa/client-prefetch.js @@ -0,0 +1,116 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + + +import App from 'app/src/App.vue' +let appPrefetch = typeof App.preFetch === 'function' + ? App.preFetch + : ( + // Class components return the component options (and the preFetch hook) inside __c property + App.__c !== void 0 && typeof App.__c.preFetch === 'function' + ? App.__c.preFetch + : false + ) + + +function getMatchedComponents (to, router) { + const route = to + ? (to.matched ? to : router.resolve(to).route) + : router.currentRoute.value + + if (!route) { return [] } + + const matched = route.matched.filter(m => m.components !== void 0) + + if (matched.length === 0) { return [] } + + return Array.prototype.concat.apply([], matched.map(m => { + return Object.keys(m.components).map(key => { + const comp = m.components[key] + return { + path: m.path, + c: comp + } + }) + })) +} + +export function addPreFetchHooks ({ router, store, publicPath }) { + // Add router hook for handling preFetch. + // Doing it after initial route is resolved so that we don't double-fetch + // the data that we already have. Using router.beforeResolve() so that all + // async components are resolved. + router.beforeResolve((to, from, next) => { + const + urlPath = window.location.href.replace(window.location.origin, ''), + matched = getMatchedComponents(to, router), + prevMatched = getMatchedComponents(from, router) + + let diffed = false + const preFetchList = matched + .filter((m, i) => { + return diffed || (diffed = ( + !prevMatched[i] || + prevMatched[i].c !== m.c || + m.path.indexOf('/:') > -1 // does it has params? + )) + }) + .filter(m => m.c !== void 0 && ( + typeof m.c.preFetch === 'function' + // Class components return the component options (and the preFetch hook) inside __c property + || (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function') + )) + .map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch) + + + if (appPrefetch !== false) { + preFetchList.unshift(appPrefetch) + appPrefetch = false + } + + + if (preFetchList.length === 0) { + return next() + } + + let hasRedirected = false + const redirect = url => { + hasRedirected = true + next(url) + } + const proceed = () => { + + if (hasRedirected === false) { next() } + } + + + + preFetchList.reduce( + (promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({ + store, + currentRoute: to, + previousRoute: from, + redirect, + urlPath, + publicPath + })), + Promise.resolve() + ) + .then(proceed) + .catch(e => { + console.error(e) + proceed() + }) + }) +} diff --git a/ui/.quasar/dev-spa/quasar-user-options.js b/ui/.quasar/dev-spa/quasar-user-options.js new file mode 100644 index 0000000..ac1dae3 --- /dev/null +++ b/ui/.quasar/dev-spa/quasar-user-options.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + +import lang from 'quasar/lang/tr.js' + + + +import {Loading,Dialog,Notify} from 'quasar' + + + +export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} } + diff --git a/ui/.quasar/feature-flags.d.ts b/ui/.quasar/feature-flags.d.ts new file mode 100644 index 0000000..dab07b3 --- /dev/null +++ b/ui/.quasar/feature-flags.d.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +import "quasar/dist/types/feature-flag.d.ts"; + +declare module "quasar/dist/types/feature-flag.d.ts" { + interface QuasarFeatureFlags { + store: true; + } +} diff --git a/ui/.quasar/pinia.d.ts b/ui/.quasar/pinia.d.ts new file mode 100644 index 0000000..5bc0a53 --- /dev/null +++ b/ui/.quasar/pinia.d.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +import { Router } from 'vue-router'; + +declare module 'pinia' { + export interface PiniaCustomProperties { + readonly router: Router; + } +} diff --git a/ui/.quasar/quasar.d.ts b/ui/.quasar/quasar.d.ts new file mode 100644 index 0000000..e37de88 --- /dev/null +++ b/ui/.quasar/quasar.d.ts @@ -0,0 +1,4 @@ +/* eslint-disable */ +/// + +/// diff --git a/ui/.quasar/tsconfig.json b/ui/.quasar/tsconfig.json new file mode 100644 index 0000000..99f52b3 --- /dev/null +++ b/ui/.quasar/tsconfig.json @@ -0,0 +1,94 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "esnext", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "module": "preserve", + "noEmit": true, + "lib": [ + "esnext", + "dom", + "dom.iterable" + ], + "paths": { + "src": [ + "./../src" + ], + "src/*": [ + "./../src/*" + ], + "app": [ + "./.." + ], + "app/*": [ + "./../*" + ], + "components": [ + "./../src/components" + ], + "components/*": [ + "./../src/components/*" + ], + "layouts": [ + "./../src/layouts" + ], + "layouts/*": [ + "./../src/layouts/*" + ], + "pages": [ + "./../src/pages" + ], + "pages/*": [ + "./../src/pages/*" + ], + "assets": [ + "./../src/assets" + ], + "assets/*": [ + "./../src/assets/*" + ], + "boot": [ + "./../src/boot" + ], + "boot/*": [ + "./../src/boot/*" + ], + "stores": [ + "./../src/stores" + ], + "stores/*": [ + "./../src/stores/*" + ], + "#q-app": [ + "./../node_modules/@quasar/app-webpack/types/index.d.ts" + ], + "#q-app/wrappers": [ + "./../node_modules/@quasar/app-webpack/types/app-wrappers.d.ts" + ], + "#q-app/bex/background": [ + "./../node_modules/@quasar/app-webpack/types/bex/entrypoints/background.d.ts" + ], + "#q-app/bex/content": [ + "./../node_modules/@quasar/app-webpack/types/bex/entrypoints/content.d.ts" + ], + "#q-app/bex/private/bex-bridge": [ + "./../node_modules/@quasar/app-webpack/types/bex/bex-bridge.d.ts" + ] + } + }, + "include": [ + "./**/*.d.ts", + "./../**/*" + ], + "exclude": [ + "./../dist", + "./../node_modules", + "./../src-capacitor", + "./../src-cordova", + "./../quasar.config.*.temporary.compiled*" + ] +} \ No newline at end of file diff --git a/ui/.vscode/extensions.json b/ui/.vscode/extensions.json new file mode 100644 index 0000000..f7f50b4 --- /dev/null +++ b/ui/.vscode/extensions.json @@ -0,0 +1,13 @@ +{ + "recommendations": [ + "editorconfig.editorconfig", + "vue.volar", + "wayou.vscode-todo-highlight" + ], + "unwantedRecommendations": [ + "octref.vetur", + "hookyqr.beautify", + "dbaeumer.jshint", + "ms-vscode.vscode-typescript-tslint-plugin" + ] +} \ No newline at end of file diff --git a/ui/.vscode/settings.json b/ui/.vscode/settings.json new file mode 100644 index 0000000..fdb070a --- /dev/null +++ b/ui/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": true +} \ No newline at end of file diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..ebe8f5b --- /dev/null +++ b/ui/README.md @@ -0,0 +1,24 @@ +# Quasar App (baggisowtfaresystem) + +A Quasar Project + +## Install the dependencies +```bash +yarn +# or +npm install +``` + +### Start the app in development mode (hot-code reloading, error reporting, etc.) +```bash +quasar dev +``` + + +### Build the app for production +```bash +quasar build +``` + +### Customize the configuration +See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js). diff --git a/ui/babel.config.js b/ui/babel.config.js new file mode 100644 index 0000000..d907762 --- /dev/null +++ b/ui/babel.config.js @@ -0,0 +1,12 @@ +export default api => { + return { + presets: [ + [ + '@quasar/babel-preset-app', + api.caller(caller => caller && caller.target === 'node') + ? { targets: { node: 'current' } } + : {} + ] + ] + } +} diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..3c8c78f --- /dev/null +++ b/ui/index.html @@ -0,0 +1,21 @@ + + + + <%= productName %> + + + + + + + + + + + + + + + + + diff --git a/ui/jsconfig.json b/ui/jsconfig.json new file mode 100644 index 0000000..f154205 --- /dev/null +++ b/ui/jsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.quasar/tsconfig.json" +} \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..beb6054 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,12102 @@ +{ + "name": "baggisowtfaresystem", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "baggisowtfaresystem", + "version": "0.0.1", + "hasInstallScript": true, + "dependencies": { + "@quasar/extras": "^1.16.4", + "axios": "^1.12.2", + "core-js": "^3.31.1", + "dayjs": "^1.11.18", + "pinia": "^3.0.1", + "quasar": "^2.16.0", + "vue": "3.5.20", + "vue-router": "^4.0.12" + }, + "devDependencies": { + "@quasar/app-webpack": "^4.1.0", + "autoprefixer": "^10.4.2" + }, + "engines": { + "node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18", + "npm": ">= 6.13.4", + "yarn": ">= 1.21.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz", + "integrity": "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-decorators": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-function-sent": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-sent/-/plugin-proposal-function-sent-7.27.1.tgz", + "integrity": "sha512-xA8Bqt8p12TxOFy3os20LxmOoHjyhzRC3zBql57d2W/YarNHgxHB4IlLHf3nXb7N6vSZ6kAdOoK2z5h0evGMhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-throw-expressions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.27.1.tgz", + "integrity": "sha512-pnGZCFdGiN3vHk54wWIvLJV3MXviRjCkSWhPtCkra6AW3AP3AcrRByT5jOnuk6nwu9VYT/B7ujdaLiBnkmc0hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", + "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz", + "integrity": "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", + "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.3", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.3", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.9.0.tgz", + "integrity": "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==", + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz", + "integrity": "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-4.0.1.tgz", + "integrity": "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@types/node": { + "version": "22.18.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz", + "integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@inquirer/core/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/editor": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-3.0.1.tgz", + "integrity": "sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-3.0.1.tgz", + "integrity": "sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-3.0.1.tgz", + "integrity": "sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-2.0.1.tgz", + "integrity": "sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-3.0.1.tgz", + "integrity": "sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-6.0.1.tgz", + "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^3.0.1", + "@inquirer/confirm": "^4.0.1", + "@inquirer/editor": "^3.0.1", + "@inquirer/expand": "^3.0.1", + "@inquirer/input": "^3.0.1", + "@inquirer/number": "^2.0.1", + "@inquirer/password": "^3.0.1", + "@inquirer/rawlist": "^3.0.1", + "@inquirer/search": "^2.0.1", + "@inquirer/select": "^3.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-3.0.1.tgz", + "integrity": "sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-2.0.1.tgz", + "integrity": "sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-3.0.1.tgz", + "integrity": "sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz", + "integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.15.0.tgz", + "integrity": "sha512-7jK0nAXj7g2hiwJ7b3wx569ZohkTFYcgDP18OvaYQ+Bg+D7rzrwaYxkdM6snrxIoKCisbudao8kfJZ4NCLiHjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.1", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@quasar/app-webpack": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@quasar/app-webpack/-/app-webpack-4.3.1.tgz", + "integrity": "sha512-u1fgzDcGkMwf82mLNO2P+4uBE0FM0nR/f8Xrpsim7jMTakhQv5mmMzGcZjnGkxZRQbYx9M4FDT3paSfCPJoxrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quasar/babel-preset-app": "^2.0.3", + "@quasar/render-ssr-error": "^1.0.3", + "@quasar/ssl-certificate": "^1.0.0", + "@quasar/ssr-helpers": "3.0.2", + "@rollup/pluginutils": "^5.1.0", + "@types/chrome": "^0.0.271", + "@types/compression": "^1.7.5", + "@types/cordova": "^11.0.3", + "@types/express": "^4.17.21", + "@types/webpack-bundle-analyzer": "^4.7.0", + "archiver": "^7.0.1", + "browserslist": "^4.23.3", + "chokidar": "^4.0.0", + "ci-info": "^4.0.0", + "compression-webpack-plugin": "^11.1.0", + "confbox": "^0.1.8", + "copy-webpack-plugin": "^12.0.2", + "cross-spawn": "^7.0.6", + "css-loader": "^7.1.2", + "css-minimizer-webpack-plugin": "^7.0.0", + "cssnano": "^7.0.6", + "dot-prop": "^9.0.0", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "elementtree": "0.1.7", + "error-stack-parser": "^2.1.4", + "esbuild": "^0.25.0", + "express": "^4.21.2", + "file-loader": "^6.2.0", + "fs-extra": "^11.2.0", + "hash-sum": "^2.0.0", + "html-minifier-terser": "^7.2.0", + "html-webpack-plugin": "^5.6.0", + "inquirer": "^11.0.2", + "isbinaryfile": "^5.0.2", + "kolorist": "^1.8.0", + "launch-editor-middleware": "^2.9.1", + "lodash": "^4.17.21", + "log-update": "^6.1.0", + "mini-css-extract-plugin": "^2.9.1", + "minimist": "^1.2.8", + "node-loader": "^2.0.0", + "null-loader": "^4.0.1", + "open": "^10.1.0", + "postcss": "^8.4.47", + "postcss-loader": "^8.1.1", + "postcss-rtlcss": "^5.4.0", + "rollup-plugin-visualizer": "^5.12.0", + "sass-embedded": "^1.80.2", + "sass-loader": "^16.0.1", + "semver": "^7.6.3", + "serialize-javascript": "^6.0.2", + "terser-webpack-plugin": "^5.3.10", + "tinyglobby": "^0.2.10", + "ts-essentials": "^10.0.2", + "url-loader": "^4.1.1", + "vue-loader": "^17.4.2", + "vue-style-loader": "^4.1.3", + "webpack": "^5.94.0", + "webpack-5-chain": "8.0.2", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-dev-middleware": "^7.4.2", + "webpack-dev-server": "^5.1.0", + "webpack-hot-middleware": "^2.26.1", + "webpack-merge": "^6.0.1", + "webpack-node-externals": "^3.0.0" + }, + "bin": { + "quasar": "bin/quasar" + }, + "engines": { + "node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18 || ^16", + "npm": ">= 6.14.12", + "yarn": ">= 1.17.3" + }, + "funding": { + "type": "github", + "url": "https://donate.quasar.dev" + }, + "peerDependencies": { + "@electron/packager": ">= 18", + "electron-builder": ">= 22", + "eslint": "^8.57.1 || ^9.0.0", + "pinia": "^2.0.0 || ^3.0.0", + "quasar": "^2.16.0", + "typescript": ">= 5.4", + "vue": "^3.2.29", + "vue-router": "^4.0.12", + "workbox-webpack-plugin": ">= 6" + }, + "peerDependenciesMeta": { + "@electron/packager": { + "optional": true + }, + "electron-builder": { + "optional": true + }, + "eslint": { + "optional": true + }, + "pinia": { + "optional": true + }, + "typescript": { + "optional": true + }, + "workbox-build": { + "optional": true + }, + "workbox-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/@quasar/babel-preset-app": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@quasar/babel-preset-app/-/babel-preset-app-2.0.3.tgz", + "integrity": "sha512-PYvVXU/TBwF1JU+nEKw8VTsbNi4mdhu7l+l9HIqfY0XZGWbDQLOGjBR8TO6A8dn5SUoilvRh85TG3ZQV01VCBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.12.0", + "@babel/helper-compilation-targets": "^7.9.6", + "@babel/helper-module-imports": "^7.8.3", + "@babel/plugin-proposal-decorators": "^7.4.4", + "@babel/plugin-proposal-function-sent": "^7.2.0", + "@babel/plugin-proposal-throw-expressions": "^7.2.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-import-meta": "^7.2.0", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-runtime": "^7.9.0", + "@babel/preset-env": "^7.9.0", + "@babel/runtime": "^7.9.0", + "babel-loader": "^9.1.2", + "babel-plugin-dynamic-import-node": "^2.3.0", + "babel-plugin-module-resolver": "^5.0.0", + "core-js": "^3.6.5", + "core-js-compat": "^3.6.5" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 5.6.0", + "yarn": ">= 1.6.0" + }, + "funding": { + "type": "github", + "url": "https://donate.quasar.dev" + } + }, + "node_modules/@quasar/extras": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.17.0.tgz", + "integrity": "sha512-KqAHdSJfIDauiR1nJ8rqHWT0diqD0QradZKoVIZJAilHAvgwyPIY7MbyR2z4RIMkUIMUSqBZcbshMpEw+9A30w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://donate.quasar.dev" + } + }, + "node_modules/@quasar/render-ssr-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@quasar/render-ssr-error/-/render-ssr-error-1.0.3.tgz", + "integrity": "sha512-A8RF99q6/sOSe1Ighnh5syEIbliD3qUYEJd2HyfFyBPSMF+WYGXon5dmzg4nUoK662NgOggInevkDyBDJcZugg==", + "dev": true, + "license": "MIT", + "dependencies": { + "stack-trace": "^1.0.0-pre2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "github", + "url": "https://donate.quasar.dev" + } + }, + "node_modules/@quasar/ssl-certificate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quasar/ssl-certificate/-/ssl-certificate-1.0.0.tgz", + "integrity": "sha512-RhZF7rO76T7Ywer1/5lCe7xl3CIiXxSAH1xgwOj0wcHTityDxJqIN/5YIj6BxMvlFw8XkJDoB1udEQafoVFA4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^11.1.1", + "selfsigned": "^2.1.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "github", + "url": "https://donate.quasar.dev" + } + }, + "node_modules/@quasar/ssr-helpers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@quasar/ssr-helpers/-/ssr-helpers-3.0.2.tgz", + "integrity": "sha512-JhHDk86o1ucR6toNkpIaAsgA0KuKKYDrOKJHN0O7IEvI2bQOCnmL9ttgRI/A4EeWOe4davqNFfwcwjuk0Ecv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 12.22.1" + }, + "funding": { + "type": "github", + "url": "https://donate.quasar.dev" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.271", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.271.tgz", + "integrity": "sha512-K0qgXvkwA5ic+/eygF1xiypHEvCoBgH5lwrhg3yva2mqJuCWyYm0vpZQ22GksAxgGfo0PWev9Zx3plp2clMlwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cordova": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-11.0.3.tgz", + "integrity": "sha512-kyuRQ40/NWQVhqGIHq78Ehu2Bf9Mlg0LhmSmis6ZFJK7z933FRfYi8tHe/k/0fB+PGfCf95rJC6TO7dopaFvAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", + "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", + "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", + "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/webpack-bundle-analyzer": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz", + "integrity": "sha512-c5i2ThslSNSG8W891BRvOd/RoCjI2zwph8maD22b1adtSns20j+0azDDMCK06DiVrzTgnwiDl5Ntmu1YRJw8Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.20.tgz", + "integrity": "sha512-8TWXUyiqFd3GmP4JTX9hbiTFRwYHgVL/vr3cqhr4YQ258+9FADwvj7golk2sWNGHR67QgmCZ8gz80nQcMokhwg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.20", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.20.tgz", + "integrity": "sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.20.tgz", + "integrity": "sha512-SFcxapQc0/feWiSBfkGsa1v4DOrnMAQSYuvDMpEaxbpH5dKbnEM5KobSNSgU+1MbHCl+9ftm7oQWxvwDB6iBfw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.20", + "@vue/compiler-dom": "3.5.20", + "@vue/compiler-ssr": "3.5.20", + "@vue/shared": "3.5.20", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.20.tgz", + "integrity": "sha512-RSl5XAMc5YFUXpDQi+UQDdVjH9FnEpLDHIALg5J0ITHxkEzJ8uQLlo7CIbjPYqmZtt6w0TsIPbo1izYXwDG7JA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz", + "integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.7" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz", + "integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.7", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz", + "integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.20.tgz", + "integrity": "sha512-hS8l8x4cl1fmZpSQX/NXlqWKARqEsNmfkwOIYqtR2F616NGfsLUm0G6FQBK6uDKUCVyi1YOL8Xmt/RkZcd/jYQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.20.tgz", + "integrity": "sha512-vyQRiH5uSZlOa+4I/t4Qw/SsD/gbth0SW2J7oMeVlMFMAmsG1rwDD6ok0VMmjXY3eI0iHNSSOBilEDW98PLRKw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.20.tgz", + "integrity": "sha512-KBHzPld/Djw3im0CQ7tGCpgRedryIn4CcAl047EhFTCCPT2xFf4e8j6WeKLgEEoqPSl9TYqShc3Q6tpWpz/Xgw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.20", + "@vue/runtime-core": "3.5.20", + "@vue/shared": "3.5.20", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.20.tgz", + "integrity": "sha512-HthAS0lZJDH21HFJBVNTtx+ULcIbJQRpjSVomVjfyPkFSpCwvsPTA+jIzOaUm3Hrqx36ozBHePztQFg6pj5aKg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.20", + "@vue/shared": "3.5.20" + }, + "peerDependencies": { + "vue": "3.5.20" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.20.tgz", + "integrity": "sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-module-resolver": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.2.tgz", + "integrity": "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-babel-config": "^2.1.1", + "glob": "^9.3.3", + "pkg-up": "^3.1.0", + "reselect": "^4.1.7", + "resolve": "^1.22.8" + } + }, + "node_modules/babel-plugin-module-resolver/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/babel-plugin-module-resolver/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/babel-plugin-module-resolver/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", + "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz", + "integrity": "sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/birpc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.6.1.tgz", + "integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-builder": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", + "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", + "dev": true, + "license": "MIT/X11" + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001748", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", + "integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression-webpack-plugin": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-11.1.0.tgz", + "integrity": "sha512-zDOQYp10+upzLxW+VRSjEpRRwBXJdsb5lBMlRxx1g8hckIFBpe3DTI0en2w7h+beuq89576RVzfiXrkdPGrHhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-js": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", + "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", + "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", + "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-7.0.2.tgz", + "integrity": "sha512-nBRWZtI77PBZQgcXMNqiIXVshiQOVLGSf2qX/WZfG8IQfMbeHUMXaBWQmiiSTmPJUflQxHjZjzAmuyO7tpL2Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "cssnano": "^7.0.4", + "jest-worker": "^29.7.0", + "postcss": "^8.4.40", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz", + "integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^7.0.9", + "lilconfig": "^3.1.3" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/cssnano-preset-default": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.9.tgz", + "integrity": "sha512-tCD6AAFgYBOVpMBX41KjbvRh9c2uUjLXRyV7KHSIrwHiq5Z9o0TFfUCoM3TwVrRsRteN3sVXGNvjVNxYzkpTsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^5.0.1", + "postcss-calc": "^10.1.1", + "postcss-colormin": "^7.0.4", + "postcss-convert-values": "^7.0.7", + "postcss-discard-comments": "^7.0.4", + "postcss-discard-duplicates": "^7.0.2", + "postcss-discard-empty": "^7.0.1", + "postcss-discard-overridden": "^7.0.1", + "postcss-merge-longhand": "^7.0.5", + "postcss-merge-rules": "^7.0.6", + "postcss-minify-font-values": "^7.0.1", + "postcss-minify-gradients": "^7.0.1", + "postcss-minify-params": "^7.0.4", + "postcss-minify-selectors": "^7.0.5", + "postcss-normalize-charset": "^7.0.1", + "postcss-normalize-display-values": "^7.0.1", + "postcss-normalize-positions": "^7.0.1", + "postcss-normalize-repeat-style": "^7.0.1", + "postcss-normalize-string": "^7.0.1", + "postcss-normalize-timing-functions": "^7.0.1", + "postcss-normalize-unicode": "^7.0.4", + "postcss-normalize-url": "^7.0.1", + "postcss-normalize-whitespace": "^7.0.1", + "postcss-ordered-values": "^7.0.2", + "postcss-reduce-initial": "^7.0.4", + "postcss-reduce-transforms": "^7.0.1", + "postcss-svgo": "^7.1.0", + "postcss-unique-selectors": "^7.0.4" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/cssnano-utils": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.1.tgz", + "integrity": "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.232", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz", + "integrity": "sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==", + "dev": true, + "license": "ISC" + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-babel-config": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-2.1.2.tgz", + "integrity": "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.3" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz", + "integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/html-webpack-plugin/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-11.1.0.tgz", + "integrity": "sha512-CmLAZT65GG/v30c+D2Fk8+ceP6pxD6RL+hIUOWAltCmeyEqWYwqu9v76q03OvjyZ3AB0C1Ala2stn1z/rMqGEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/prompts": "^6.0.1", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "ansi-escapes": "^4.3.2", + "mute-stream": "^1.0.0", + "run-async": "^3.0.0", + "rxjs": "^7.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.6.tgz", + "integrity": "sha512-I+NmIfBHUl+r2wcDd6JwE9yWje/PIVY/R5/CmV8dXLZd5K+L9X2klAOwfAHNnondLXkbHyTAleQAWonpTJBTtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/javascript-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", + "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/launch-editor": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", + "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/launch-editor-middleware": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/launch-editor-middleware/-/launch-editor-middleware-2.11.1.tgz", + "integrity": "sha512-6xpn4pJz5mDg2kUH7L6gK5BuZcZPdVwoSs/DhfebefwLyszNXqFFjksGup/w4CTRzzrr8FSEufDzb/gKFLle6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "launch-editor": "^2.11.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.49.0.tgz", + "integrity": "sha512-L9uC9vGuc4xFybbdOpRLoOAOq1YEBBsocCs5NVW32DfU+CZWWIn3OVF+lB8Gp4ttBVSMazwrTrjv8ussX/e3VQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", + "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-loader": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-loader/-/node-loader-2.1.0.tgz", + "integrity": "sha512-OwjPkyh8+7jW8DMd/iq71uU1Sspufr/C2+c3t0p08J3CrM9ApZ4U53xuisNrDXOHyGi5OYHgtfmmh+aK9zJA6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.3" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/null-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", + "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/null-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/null-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/null-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/null-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz", + "integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", + "integrity": "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12 || ^20.9 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.38" + } + }, + "node_modules/postcss-colormin": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.4.tgz", + "integrity": "sha512-ziQuVzQZBROpKpfeDwmrG+Vvlr0YWmY/ZAk99XD+mGEBuEojoFekL41NCsdhyNUtZI7DPOoIWIR7vQQK9xwluw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-convert-values": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.7.tgz", + "integrity": "sha512-HR9DZLN04Xbe6xugRH6lS4ZQH2zm/bFh/ZyRkpedZozhvh+awAfbA0P36InO4fZfDhvYfNJeNvlTf1sjwGbw/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-discard-comments": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.4.tgz", + "integrity": "sha512-6tCUoql/ipWwKtVP/xYiFf1U9QgJ0PUvxN7pTcsQ8Ns3Fnwq1pU5D5s1MhT/XySeLq6GXNvn37U46Ded0TckWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.1.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.2.tgz", + "integrity": "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-discard-empty": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.1.tgz", + "integrity": "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.1.tgz", + "integrity": "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-loader": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.2.0.tgz", + "integrity": "sha512-tHX+RkpsXVcc7st4dSdDGliI+r4aAQDuv+v3vFYHixb6YgjreG5AG4SEB0kDK8u2s6htqEEpKlkhSBUTvWKYnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^2.5.1", + "semver": "^7.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-merge-longhand": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.5.tgz", + "integrity": "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^7.0.5" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-merge-rules": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.6.tgz", + "integrity": "sha512-2jIPT4Tzs8K87tvgCpSukRQ2jjd+hH6Bb8rEEOUDmmhOeTcqDg5fEFK8uKIu+Pvc3//sm3Uu6FRqfyv7YF7+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^5.0.1", + "postcss-selector-parser": "^7.1.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.1.tgz", + "integrity": "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.1.tgz", + "integrity": "sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^5.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-minify-params": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.4.tgz", + "integrity": "sha512-3OqqUddfH8c2e7M35W6zIwv7jssM/3miF9cbCSb1iJiWvtguQjlxZGIHK9JRmc8XAKmE2PFGtHSM7g/VcW97sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "cssnano-utils": "^5.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.5.tgz", + "integrity": "sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "postcss-selector-parser": "^7.1.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.1.tgz", + "integrity": "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.1.tgz", + "integrity": "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.1.tgz", + "integrity": "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.1.tgz", + "integrity": "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-string": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.1.tgz", + "integrity": "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.1.tgz", + "integrity": "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.4.tgz", + "integrity": "sha512-LvIURTi1sQoZqj8mEIE8R15yvM+OhbR1avynMtI9bUzj5gGKR/gfZFd8O7VMj0QgJaIFzxDwxGl/ASMYAkqO8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.1.tgz", + "integrity": "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.1.tgz", + "integrity": "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-ordered-values": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.2.tgz", + "integrity": "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-utils": "^5.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.4.tgz", + "integrity": "sha512-rdIC9IlMBn7zJo6puim58Xd++0HdbvHeHaPgXsimMfG1ijC5A9ULvNLSE0rUKVJOvNMcwewW4Ga21ngyJjY/+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.1.tgz", + "integrity": "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-rtlcss": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-5.7.1.tgz", + "integrity": "sha512-zE68CuARv5StOG/UQLa0W1Y/raUTzgJlfjtas43yh3/G1BFmoPEaHxPRHgeowXRFFhW33FehrNgsljxRLmPVWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "rtlcss": "4.3.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.0.tgz", + "integrity": "sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^4.0.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.4.tgz", + "integrity": "sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.1.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quasar": { + "version": "2.18.5", + "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.18.5.tgz", + "integrity": "sha512-5ItDSsNjqBVRrC7SqcdvT1F5mghVyJ/KmaWNwnaT5mM91a7gWpT/d7wTCIFxxDbWLZdkHKI+cpdudEqnfcSw9A==", + "license": "MIT", + "engines": { + "node": ">= 10.18.1", + "npm": ">= 6.13.4", + "yarn": ">= 1.21.1" + }, + "funding": { + "type": "github", + "url": "https://donate.quasar.dev" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup-plugin-visualizer": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz", + "integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.4.0", + "picomatch": "^4.0.2", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "rolldown": "1.x", + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rtlcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", + "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", + "dev": true, + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", + "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.93.2.tgz", + "integrity": "sha512-FvQdkn2dZ8DGiLgi0Uf4zsj7r/BsiLImNa5QJ10eZalY6NfZyjrmWGFcuCN5jNwlDlXFJnftauv+UtvBKLvepQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.5.0", + "buffer-builder": "^0.2.0", + "colorjs.io": "^0.5.0", + "immutable": "^5.0.2", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-all-unknown": "1.93.2", + "sass-embedded-android-arm": "1.93.2", + "sass-embedded-android-arm64": "1.93.2", + "sass-embedded-android-riscv64": "1.93.2", + "sass-embedded-android-x64": "1.93.2", + "sass-embedded-darwin-arm64": "1.93.2", + "sass-embedded-darwin-x64": "1.93.2", + "sass-embedded-linux-arm": "1.93.2", + "sass-embedded-linux-arm64": "1.93.2", + "sass-embedded-linux-musl-arm": "1.93.2", + "sass-embedded-linux-musl-arm64": "1.93.2", + "sass-embedded-linux-musl-riscv64": "1.93.2", + "sass-embedded-linux-musl-x64": "1.93.2", + "sass-embedded-linux-riscv64": "1.93.2", + "sass-embedded-linux-x64": "1.93.2", + "sass-embedded-unknown-all": "1.93.2", + "sass-embedded-win32-arm64": "1.93.2", + "sass-embedded-win32-x64": "1.93.2" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.93.2.tgz", + "integrity": "sha512-BbSucRP6PVRZGIwlEBkp+6VQl2GWdkWFMN+9EuOTPrLxCJZoq+yhzmbjspd3PeM8+7WJ7AdFu/uRYdO8tor1iQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", + "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true, + "license": "ISC" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "1.0.0-pre2", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz", + "integrity": "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylehacks": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.6.tgz", + "integrity": "sha512-iitguKivmsueOmTO0wmxURXBP8uqOO+zikLGZ7Mm9e/94R4w5T999Js2taS/KBOnQ/wdC3jN3vNSrkGDrlnqQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "postcss-selector-parser": "^7.1.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svgo": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", + "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.4.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/svgo/node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" + }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", + "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/thingies": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/ts-essentials": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-10.1.1.tgz", + "integrity": "sha512-4aTB7KLHKmUvkjNj8V+EdnmuVTiECzn3K+zIbRthumvHu+j44x3w63xpfs0JL3NGIzGXqoQ7AV591xHO+XrOTw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/url-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/url-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vue": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.20.tgz", + "integrity": "sha512-2sBz0x/wis5TkF1XZ2vH25zWq3G1bFEPOfkBcx2ikowmphoQsPH6X0V3mmPCXA2K1N/XGTnifVyDQP4GfDDeQw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.20", + "@vue/compiler-sfc": "3.5.20", + "@vue/runtime-dom": "3.5.20", + "@vue/server-renderer": "3.5.20", + "@vue/shared": "3.5.20" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-loader": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.4.2.tgz", + "integrity": "sha512-yTKOA4R/VN4jqjw4y5HrynFL8AK0Z3/Jt7eOJXEitsm0GMRHDBjCfCiuTiLP7OESvsZYo2pATCWhDqxC5ZrM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "watchpack": "^2.4.0" + }, + "peerDependencies": { + "webpack": "^4.1.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/vue-style-loader": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", + "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-sum": "^1.0.2", + "loader-utils": "^1.0.2" + } + }, + "node_modules/vue-style-loader/node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-style-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/vue-style-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webpack": { + "version": "5.102.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.0.tgz", + "integrity": "sha512-hUtqAR3ZLVEYDEABdBioQCIqSoguHbFn1K7WlPPWSuXmx0031BD73PSE35jKyftdSh4YLDoQNgK4pqBt5Q82MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.2.3", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-5-chain": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/webpack-5-chain/-/webpack-5-chain-8.0.2.tgz", + "integrity": "sha512-gpzlChffrVUu5YwIw9i240/wdcglw53mSEV/7WoK7L/ddfb6Al8/sRjztyPYV8VgJAmkapH5T1AOUfMFryQ/VA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "deepmerge": "^1.5.2", + "javascript-stringify": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-dev-server/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-server/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-hot-middleware": { + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz", + "integrity": "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-html-community": "0.0.8", + "html-entities": "^2.1.0", + "strip-ansi": "^6.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..b033588 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,45 @@ +{ + "name": "baggisowtfaresystem", + "version": "0.0.1", + "description": "A Quasar Project", + "productName": "Baggi SS", + "author": "MehmetKececi", + "private": true, + "type": "module", + "scripts": { + "test": "echo \"No test specified\" && exit 0", + "dev": "quasar dev", + "build": "quasar build", + "postinstall": "quasar prepare" + }, + "dependencies": { + "@quasar/extras": "^1.16.4", + "axios": "^1.12.2", + "core-js": "^3.31.1", + "dayjs": "^1.11.18", + "pinia": "^3.0.1", + "quasar": "^2.16.0", + "vue": "3.5.20", + "vue-router": "^4.0.12" + }, + "devDependencies": { + "@quasar/app-webpack": "^4.1.0", + "autoprefixer": "^10.4.2" + }, + "browserslist": [ + "last 10 Chrome versions", + "last 10 Firefox versions", + "last 4 Edge versions", + "last 7 Safari versions", + "last 8 Android versions", + "last 8 ChromeAndroid versions", + "last 8 FirefoxAndroid versions", + "last 10 iOS versions", + "last 5 Opera versions" + ], + "engines": { + "node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18", + "npm": ">= 6.13.4", + "yarn": ">= 1.21.1" + } +} diff --git a/ui/postcss.config.js b/ui/postcss.config.js new file mode 100644 index 0000000..513a8fd --- /dev/null +++ b/ui/postcss.config.js @@ -0,0 +1,9 @@ +// https://github.com/michael-ciniawsky/postcss-load-config +import autoprefixer from 'autoprefixer' + +export default { + plugins: [ + // to edit target browsers: use "browserslist" field in package.json + autoprefixer + ] +} diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 0000000..580e207 Binary files /dev/null and b/ui/public/favicon.ico differ diff --git a/ui/public/icons/baggi-icon.png b/ui/public/icons/baggi-icon.png new file mode 100644 index 0000000..2584b73 Binary files /dev/null and b/ui/public/icons/baggi-icon.png differ diff --git a/ui/public/icons/favicon-128x128.png b/ui/public/icons/favicon-128x128.png new file mode 100644 index 0000000..a7acfdf Binary files /dev/null and b/ui/public/icons/favicon-128x128.png differ diff --git a/ui/public/icons/favicon-16x16.png b/ui/public/icons/favicon-16x16.png new file mode 100644 index 0000000..e401497 Binary files /dev/null and b/ui/public/icons/favicon-16x16.png differ diff --git a/ui/public/icons/favicon-32x32.png b/ui/public/icons/favicon-32x32.png new file mode 100644 index 0000000..5940268 Binary files /dev/null and b/ui/public/icons/favicon-32x32.png differ diff --git a/ui/public/icons/favicon-96x96.png b/ui/public/icons/favicon-96x96.png new file mode 100644 index 0000000..0245b4b Binary files /dev/null and b/ui/public/icons/favicon-96x96.png differ diff --git a/ui/public/images/Baggi-Exclusive-Logo.jpg b/ui/public/images/Baggi-Exclusive-Logo.jpg new file mode 100644 index 0000000..19694f1 Binary files /dev/null and b/ui/public/images/Baggi-Exclusive-Logo.jpg differ diff --git a/ui/public/images/Baggi-Fabrika-resmi.jpg b/ui/public/images/Baggi-Fabrika-resmi.jpg new file mode 100644 index 0000000..07b80b6 Binary files /dev/null and b/ui/public/images/Baggi-Fabrika-resmi.jpg differ diff --git a/ui/public/images/Baggi-tekstilas-logolu.jpg b/ui/public/images/Baggi-tekstilas-logolu.jpg new file mode 100644 index 0000000..8742b5d Binary files /dev/null and b/ui/public/images/Baggi-tekstilas-logolu.jpg differ diff --git a/ui/quasar.config.js b/ui/quasar.config.js new file mode 100644 index 0000000..520371a --- /dev/null +++ b/ui/quasar.config.js @@ -0,0 +1,78 @@ +// quasar.config.js +import { defineConfig } from '#q-app/wrappers' + +export default defineConfig(() => { + return { + + // ✅ UYGULAMA KİMLİĞİ (WEB'DE GÖRÜNEN İSİM) + productName: 'Baggi BSS', + productDescription: 'Baggi Tekstil Business Support System', + + // 🔹 Boot dosyaları + boot: ['axios', 'dayjs'], + + // 🔹 Global CSS + css: ['app.css'], + + // 🔹 Ekstra icon/font setleri + extras: [ + 'roboto-font', + 'material-icons' + ], + + // 🔹 Derleme Ayarları + build: { + vueRouterMode: 'hash', + env: { + VITE_API_BASE_URL: 'http://localhost:8080/api' + }, + esbuildTarget: { + browser: ['es2022', 'firefox115', 'chrome115', 'safari14'], + node: 'node20' + } + }, + + // 🔹 Geliştirme Sunucusu + devServer: { + server: { type: 'http' }, + port: 9000, + open: true + }, + + // 🔹 Quasar Framework ayarları + framework: { + config: { + notify: { position: 'top', timeout: 2500 } + }, + lang: 'tr', + plugins: ['Loading', 'Dialog', 'Notify'] + }, + + animations: [], + + ssr: { + prodPort: 3000, + middlewares: ['render'], + pwa: false + }, + + pwa: { + workboxMode: 'GenerateSW' + }, + + capacitor: { + hideSplashscreen: true + }, + + electron: { + preloadScripts: ['electron-preload'], + inspectPort: 5858, + bundler: 'packager', + builder: { appId: 'baggisowtfaresystem' } + }, + + bex: { + extraScripts: [] + } + } +}) diff --git a/ui/quasar.config.js.temporary.compiled.1770820662971.mjs b/ui/quasar.config.js.temporary.compiled.1770820662971.mjs new file mode 100644 index 0000000..b4f411c --- /dev/null +++ b/ui/quasar.config.js.temporary.compiled.1770820662971.mjs @@ -0,0 +1,83 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * 1. DO NOT edit this file directly as it won't do anything. + * 2. EDIT the original quasar.config file INSTEAD. + * 3. DO NOT git commit this file. It should be ignored. + * + * This file is still here because there was an error in + * the original quasar.config file and this allows you to + * investigate the Node.js stack error. + * + * After you fix the original file, this file will be + * deleted automatically. + **/ + + +// quasar.config.js +import { defineConfig } from "@quasar/app-webpack/wrappers"; +var quasar_config_default = defineConfig(() => { + return { + // ✅ UYGULAMA KİMLİĞİ (WEB'DE GÖRÜNEN İSİM) + productName: "Baggi BSS", + productDescription: "Baggi Tekstil Business Support System", + // 🔹 Boot dosyaları + boot: ["axios", "dayjs"], + // 🔹 Global CSS + css: ["app.css"], + // 🔹 Ekstra icon/font setleri + extras: [ + "roboto-font", + "material-icons" + ], + // 🔹 Derleme Ayarları + build: { + vueRouterMode: "hash", + env: { + VITE_API_BASE_URL: "http://localhost:8080/api" + }, + esbuildTarget: { + browser: ["es2022", "firefox115", "chrome115", "safari14"], + node: "node20" + } + }, + // 🔹 Geliştirme Sunucusu + devServer: { + server: { type: "http" }, + port: 9e3, + open: true + }, + // 🔹 Quasar Framework ayarları + framework: { + config: { + notify: { position: "top", timeout: 2500 } + }, + lang: "tr", + plugins: ["Loading", "Dialog", "Notify"] + }, + animations: [], + ssr: { + prodPort: 3e3, + middlewares: ["render"], + pwa: false + }, + pwa: { + workboxMode: "GenerateSW" + }, + capacitor: { + hideSplashscreen: true + }, + electron: { + preloadScripts: ["electron-preload"], + inspectPort: 5858, + bundler: "packager", + builder: { appId: "baggisowtfaresystem" } + }, + bex: { + extraScripts: [] + } + }; +}); +export { + quasar_config_default as default +}; diff --git a/ui/src/App.vue b/ui/src/App.vue new file mode 100644 index 0000000..b22f395 --- /dev/null +++ b/ui/src/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/assets/quasar-logo-vertical.svg b/ui/src/assets/quasar-logo-vertical.svg new file mode 100644 index 0000000..8210831 --- /dev/null +++ b/ui/src/assets/quasar-logo-vertical.svg @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/ui/src/boot/.gitkeep b/ui/src/boot/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ui/src/boot/axios.js b/ui/src/boot/axios.js new file mode 100644 index 0000000..400dbfd --- /dev/null +++ b/ui/src/boot/axios.js @@ -0,0 +1,21 @@ +import { boot } from 'quasar/wrappers' +import axios from 'axios' + +export const api = axios.create({ + baseURL: 'http://localhost:8080/api', + timeout: 180000, + withCredentials: true // refresh cookie kullanıyorsan kalsın +}) + +export default boot(() => { + api.interceptors.request.use((config) => { + const token = localStorage.getItem('token') // ✅ senin authStore key’in + + if (token) { + config.headers = config.headers || {} + config.headers.Authorization = `Bearer ${token}` + } + + return config + }) +}) diff --git a/ui/src/boot/dayjs.js b/ui/src/boot/dayjs.js new file mode 100644 index 0000000..ba3697e --- /dev/null +++ b/ui/src/boot/dayjs.js @@ -0,0 +1,14 @@ +// src/boot/dayjs.js +import dayjs from 'dayjs' +import customParseFormat from 'dayjs/plugin/customParseFormat.js' +import relativeTime from 'dayjs/plugin/relativeTime.js' +import localizedFormat from 'dayjs/plugin/localizedFormat.js' +import 'dayjs/locale/tr.js' + +// 🔹 Plugin’leri aktif et +dayjs.extend(customParseFormat) +dayjs.extend(relativeTime) +dayjs.extend(localizedFormat) +dayjs.locale('tr') + +export default dayjs diff --git a/ui/src/components/EssentialLink.vue b/ui/src/components/EssentialLink.vue new file mode 100644 index 0000000..54afb06 --- /dev/null +++ b/ui/src/components/EssentialLink.vue @@ -0,0 +1,44 @@ + + + diff --git a/ui/src/composables/usePermission.js b/ui/src/composables/usePermission.js new file mode 100644 index 0000000..a17fd33 --- /dev/null +++ b/ui/src/composables/usePermission.js @@ -0,0 +1,20 @@ +import { computed } from 'vue' +import { usePermissionStore } from 'stores/permissionStore' + +export function usePermission () { + + const perm = usePermissionStore() + + return { + + canRead: (m) => computed(() => perm.hasPermission(m, 'read')), + canWrite: (m) => computed(() => perm.hasPermission(m, 'write')), + canUpdate:(m) => computed(() => perm.hasPermission(m, 'update')), + canDelete:(m) => computed(() => perm.hasPermission(m, 'delete')), + canExport:(m) => computed(() => perm.hasPermission(m, 'export')), + + canApi: (p) => computed(() => perm.hasApiPermission(p)), + + hasModule: (m) => computed(() => perm.hasModule(m)) + } +} diff --git a/ui/src/css/app.css b/ui/src/css/app.css new file mode 100644 index 0000000..2a742bc --- /dev/null +++ b/ui/src/css/app.css @@ -0,0 +1,1532 @@ +/* =========================================================== + GLOBAL CUSTOM CSS + =========================================================== */ + .with-bg { + position: relative; + min-height: 100%; +} +.with-bg::before { + content: ""; + position: absolute; + inset: 0; + background: url('/images/Baggi-tekstilas-logolu.jpg') no-repeat center top; + background-size: 400px auto; + opacity: 0.15; + pointer-events: none; + z-index: 0; +} +.with-bg > * { + position: relative; + z-index: 1; +} + +.q-page { + margin-top: 5px; +} + +@media (max-width: 768px) { + .with-bg::before { + background-size: 260px auto; + } +} + +/* ===== ÜST BLOKLAR (SABİT) ===== */ +.filter-sticky { + position: sticky; + top: 56px; /* q-header yüksekliği */ + z-index: 300; + background: #fff; +} + +.filter-collapsible { + background: #fff; +} + +/* ===== TABLO SCROLL ===== */ +.table-scroll { + margin-top: 0; /* 🔹 Boşluğu kaldır */ + height: calc(100vh - 56px); /* 🔹 Header yüksekliği kadar kısalt */ + overflow-y: auto; + overflow-x: auto; + position: relative; +} + +.sticky-table .q-table__middle { + overflow: visible !important; + max-height: none !important; +} + +.sticky-table .q-table__top { + position: sticky; + top: 0; + z-index: 220; + background: #fff; + box-shadow: 0 2px 4px rgba(0,0,0,0.08); +} + +.sticky-table thead th { + position: sticky; + top: 40px; + z-index: 210; + background: #fff; +} + +/* 🔹 Toggle bar */ +.sticky-bar { + position: sticky; + top: 0; /* tablo scroll başladığında en üstte kalsın */ + z-index: 230; + background: #fff; + padding: 4px 8px; + border-bottom: 1px solid #ddd; +} + +/* ===== KOLON DARALTMA + WRAP ===== */ +.sticky-table thead th { + resize: horizontal; + overflow: auto; + min-width: 80px; + max-width: 400px; +} + +.sticky-table td { + min-width: 80px; + max-width: 400px; + white-space: normal !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + line-height: 1.2rem; + padding: 4px 8px !important; + font-weight: 600; + font-size: 0.95rem; +} + +/* ===== GÖRSEL ===== */ +.baggi-ppct { + display: block; + margin: 30px auto 0; + max-width: 400px; + opacity: 0.4; +} + +.col-desc { + white-space: normal !important; + word-break: break-word !important; + overflow-wrap: break-word; + font-size: 0.75rem !important; + line-height: 1.1rem; + width: 220px !important; + max-width: 220px !important; + min-width: 180px !important; +} + +/* ===== TABLO GÖRÜNÜM ===== */ +.custom-table { font-size: 0.8rem; } +.custom-table th { background: #fff; font-weight: 800; color: #222; } +.custom-table td { font-weight: 600; color: #333; } + +.custom-subtable { font-size: 0.72rem; background: #fafafa; } +.custom-subtable th { background: #f9f9f9; font-weight: 500; color: #555; } +.custom-subtable td { font-weight: 400; color: #666; } + +/* dar sütunlar için */ +.col-narrow { + font-size: 0.72rem; + padding: 2px 6px !important; + max-width: 90px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ===== GRUP SATIRI ===== */ +.group-row { + background: #f1f1f1 !important; + font-weight: 700 !important; + color: #222; + border-top: 2px solid #ccc; + border-bottom: 2px solid #ccc; +} + +/* ===== BALANCE CARD ===== */ +.balance-card { + width: 100%; + min-height: 120px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; +} + +.q-table td[data-col="belge_no"], +.q-table td[data-col="Belge_No"], +.q-table td[data-col="BELGE_NO"] { + color: var(--q-primary) !important; + font-weight: 600 !important; +} + +/* =========================================================== + PERMISSIONS PAGE (FINAL) + =========================================================== */ + +/* Toolbar */ +.permissions-toolbar { + position: sticky; + top: 42px; /* q-header yüksekliği */ + z-index: 300; + background: #fff; + display: flex; + align-items: center; + gap: 12px; + padding: 8px 16px; + border-bottom: 1px solid #ddd; +} + +/* Table scroll alanı */ +.permissions-table-scroll { + height: calc(100vh - 112px); /* header (56) + toolbar (56) */ + overflow-y: auto; + overflow-x: auto; + position: relative; +} + +/* Tablo gövdesi */ +.permissions-table .q-table__middle { + overflow: auto !important; + max-height: none !important; + padding-top: 0px; /* 🔑 Başlık yüksekliği kadar boşluk bırak */ +} + +/* Sticky başlıklar – toolbar’ın altında */ +.permissions-table thead th { + position: sticky; + top:10px; /* toolbar altında hizalanır */ + z-index: 210; + background: #fff; + box-shadow: 0 2px 4px rgba(0,0,0,0.08); +} + +/* Hücreler */ +.permissions-table td { + min-width: 80px; + max-width: 400px; + white-space: normal !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + line-height: 1.2rem; + padding: 4px 8px !important; + font-weight: 600; + font-size: 0.95rem; + background: #fff; +} + +/* İlk kolon (role) sabit */ +.permissions-table .permissions-sticky-col { + position: sticky; + left: 0; + z-index: 205; + background: #fff; + box-shadow: 2px 0 4px rgba(0,0,0,0.04); +} + +/* =========================================================== + 1️⃣ ROOT & GLOBAL RESET + =========================================================== */ +:root { + --header-h: 0px; + --filter-h: 72px; + --save-h: 60px; + --grid-header-h: 172px; + --sub-header-h: 34px; + --drawer-w: 240px; + + /* Grid kolon genişlikleri */ + --col-model: 90px; + --col-renk: 80px; + --col-ana: 100px; + --col-alt: 100px; + --col-aciklama: 140px; + --col-adet: 70px; + --col-fiyat: 70px; + --col-pb: 70px; + --col-tutar: 70px; + --col-termin: 142px; /* 🔹 termin tarihi kolon genişliği */ + + /* Beden blok ölçüleri */ + --grp-title-w: 90px; + --grp-title-gap: 4px; + --beden-w: 44px; + --beden-h: 28px; + --beden-count: 16; + + /* Tema renkleri */ + --baggi-gold: #c9a227; + --baggi-gold-pale: #fff9e6; + --baggi-gold-light: #fff7d2; + --baggi-cream: #fffef9; + --baggi-gray-border: #bbb; +} + +*, *::before, *::after { box-sizing: border-box; } +html, body { height: 100%; margin: 0; } +body { + background: #fff; + color: #222; + font-family: Inter, "Segoe UI", Arial, sans-serif; + font-size: 14px; + line-height: 1.4; +} +#q-app, .q-page-container { margin: 0; padding: 0; } +.q-layout__page { top: 0 !important; } +/* =========================================================== + 2️⃣ PAGE STRUCTURE & SCROLL + =========================================================== */ +.order-page { + display: flex; + flex-direction: column; + height: calc(100vh - var(--header-h)); + overflow-y: auto; + overflow-x: visible; + background: #fff; +} + +.body--drawer-left-open .q-page-container { + margin-left: var(--drawer-w); + width: calc(100% - var(--drawer-w)); +} +.body--drawer-left-closed .q-page-container { + margin-left: 0; width: 100%; +} + +/* 🔸 Yatay scroll sadece grid alanında */ +.order-scroll-x { + flex: 1; + overflow-x: auto; + overflow-y: visible; + background: #fff; +} +/* 🔸 Scrollbar stili */ + +.order-page::-webkit-scrollbar-thumb, +.order-scroll-x::-webkit-scrollbar { + height: 8px; width: 8px; +} +.order-scroll-x::-webkit-scrollbar-thumb { + background: #c0a75e; + border-radius: 4px; +} +.order-scroll-x::-webkit-scrollbar-track { + background: #f9f5e6; +} + +/* =========================================================== + 3️⃣ STICKY STACK (HEADER + TOOLBARS) + =========================================================== */ +.q-header { + position: sticky; + top: 0; + z-index: 1000; + box-shadow: 0 1px 2px rgba(0,0,0,0.08); +} + +.sticky-stack { + position: sticky; + top: var(--header-h); + margin-top: 0 !important; + z-index: 950; + display: flex; + flex-direction: column; + background: #fff; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); +} + +/* 🔹 Filtre bar */ +.filter-bar { + background: #fafafa; + border-bottom: 1px solid #ddd; + padding: 12px 24px; + margin-top:0 !important; +} + +/* 🔹 Save toolbar */ +.save-toolbar { + background: var(--baggi-gold-pale); + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding: 10px 16px; + display: flex; + justify-content: space-between; + align-items: center; + z-index: 940; +} +.save-toolbar .label { font-weight: 700; color: #6a5314; } +.save-toolbar .value { font-weight: 700; color: #000; } +.save-toolbar .q-btn { + font-weight: 600; + border-radius: 6px; + text-transform: none; +} +/* =========================================================== + 4️⃣ GRID HEADER (ANA BAŞLIK BLOKU) + =========================================================== */ +.order-grid-header { + position: sticky; + top: calc(var(--header-h) + var(--filter-h) + var(--save-h)); + z-index: 700; + display: grid; + grid-auto-flow: column; + grid-template-columns: + var(--col-model) + var(--col-renk) + var(--col-ana) + var(--col-alt) + var(--col-aciklama) + calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w)*var(--beden-count))) + var(--col-adet) + var(--col-fiyat) + var(--col-pb) + var(--col-tutar) + var(--col-termin); + background: var(--baggi-cream); + border-bottom: 2px solid var(--baggi-gray-border); + box-shadow: 0 2px 3px rgba(0,0,0,0.05); +} + +/* Sabit kolonlar */ +.order-grid-header .col-fixed { + display: flex; + justify-content: center; + align-items: center; + writing-mode: vertical-lr; + transform: rotate(180deg); + background: var(--baggi-gold-light); + border: 1px solid #aaa; + font-weight: 700; + font-size: 12.5px; + height: var(--grid-header-h); +} + +.order-grid-header .aciklama-col { + background: #fff9c4; + border-right: 2px solid #a6a6a6; +} +/* =========================================================== + 5️⃣ BEDEN BLOKLARI & SAĞ TOPLAM + =========================================================== */ +.order-grid-header .beden-block { + display: flex; + flex-direction: column; + height: var(--grid-header-h); + background: #fff; + border: 1px solid #ccc; +} + +.order-grid-header .grp-row { + display: flex; + align-items: center; + height: var(--beden-h); +} + +.order-grid-header .grp-title { + width: var(--grp-title-w); + text-align: right; + font-weight: 700; + font-size: 12px; + padding-right: 4px; +} + +.order-grid-header .grp-body { + display: grid; + grid-auto-flow: column; + grid-auto-columns: var(--beden-w); +} +.order-grid-header .grp-cell.hdr { + width: var(--beden-w); + height: var(--beden-h); + border: 1px solid #bbb; + font-size: 11.5px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; +} + +.order-grid-header .total-row { + display: flex; + align-items: stretch; + justify-content: space-between; + background: #fff59d; +} +.order-grid-header .total-cell { + width: var(--col-adet); + display: flex; + justify-content: center; + align-items: center; + writing-mode: vertical-lr; + transform: rotate(180deg); + border-right: 1px solid #bbb; + background: var(--baggi-gold-pale); + font-weight: 700; + font-size: 12px; +} +/* =========================================================== + 6️⃣ SUB-HEADER (ÜRÜN GRUBU BAR) — TAM HİZALANMIŞ + =========================================================== */ + .order-sub-header { + padding-right: 0 !important; /* 🔹 Ekstra sağ boşluğu kaldır */ + margin-right: 0 !important; +} +.order-sub-header { + position: sticky; + top: calc( + var(--header-h) + + var(--filter-h) + + var(--save-h) + + var(--grid-header-h) + ); + z-index: 650; + + /* 🔹 Header ile birebir grid düzeni */ + display: grid; + grid-auto-flow: column; + grid-template-columns: + var(--col-model) + var(--col-renk) + var(--col-ana) + var(--col-alt) + var(--col-aciklama) + calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count))) + var(--col-adet) + var(--col-fiyat) + var(--col-pb) + var(--col-tutar) + var(--col-termin); + + align-items: center; + justify-items: stretch; + height: var(--sub-header-h); + min-height: var(--sub-header-h); + + /* 🔹 Görsel */ + background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%); + border-top: 1px solid #d6c06a; + border-bottom: 1px solid #d6c06a; + + /* 🔹 Hatalı hizalamaları engelle */ + box-sizing: border-box; + overflow: hidden; + padding: 0 !important; + margin: 0 !important; + padding-right: 0 !important; /* ✅ sağ taşmayı önler */ +} + +/* 🔹 Genişlik eşitleme */ +:root { + --col-termin: 142px; /* ✅ q-input genişliğiyle birebir */ +} + +/* 🔹 Sub-header hover efekti */ +.order-sub-header:hover { + background: linear-gradient(90deg, #fff9cf 0%, #fff3b0 70%, #ffe88f 100%); +} + + +/* 🔹 Sol taraf (MODEL–AÇIKLAMA alanı) */ +.order-sub-header .sub-left { + grid-column: 1 / span 5; + font-weight: 800; + padding-left: 6px; + color: #2b1f05; + display: flex; + align-items: center; +} + +/* 🔹 Orta beden bloğu (header’la aynı yapı) */ +.order-sub-header .sub-center { + grid-column: 6 / 7; + display: grid; + grid-auto-flow: column; + grid-auto-columns: var(--beden-w); + justify-content: start; + align-items: center; + width: calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count))); + padding-left: var(--grp-title-w); + margin-left: var(--grp-title-gap); + height: 100%; + box-sizing: border-box; +} + +.order-sub-header .beden-cell { + width: var(--beden-w); + height: 100%; + border: 1px solid #d8c16b; + border-right: none; + background: #fffdf3; + font-size: 12px; + font-weight: 600; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; +} +.order-sub-header .beden-cell:last-child { + border-right: 1px solid #d8c16b; +} + +/* 🔹 Sağ taraf (adet–fiyat–pb–tutar–termin toplamları) */ +.order-sub-header .sub-right { + grid-column: 7 / -1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-end; + text-align: right; + padding-right: 0px; + font-weight: 900; + color: #3b2f09; + line-height: 1.3; + text-transform: uppercase; + font-size: 13.5px; + transform: translateX(-60px); +} + +.order-sub-header:hover { + background: linear-gradient(90deg,#fff9cf 0%,#fff3b0 70%,#ffe88f 100%); +} + +:root { + --sub-header-h: 60px; +} + + +/* Taşmayı engelle (ihtiyaten) */ +.order-sub-header { + overflow: hidden; +} + +/* =========================================================== + 7️⃣ GRID BODY & SATIRLAR — TAM HİZALANMIŞ + =========================================================== */ +.order-grid-body { + position: relative; + background: #fff; + margin-top: 0 !important; + padding-top: var(--sub-header-h); + z-index: 100; +} + +.summary-row { + display: grid; + grid-template-columns: + var(--col-model) + var(--col-renk) + var(--col-ana) + var(--col-alt) + var(--col-aciklama) + calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count))) + var(--col-adet) + var(--col-fiyat) + var(--col-pb) + var(--col-tutar) + var(--col-termin);} + +.summary-row:hover { + background: #fffce0; +} +.summary-row.is-editing { + background: #fff3cd; + outline: 2px solid #caa83f; +} + +.summary-row .cell { + display: flex; + align-items: center; + justify-content: center; + height: var(--beden-h); + padding: 4px 6px; + font-size: 13px; + color: #222; + box-sizing: border-box; +} +.summary-row.row-closed { + opacity: 0.55; + background: #f5f5f5; + pointer-events: none; +} +.summary-row.row-closed:hover { + background: #f5f5f5 !important; +} + +.summary-row:nth-child(odd) { background: #fffef9; } + +/* 🔹 Beden blok hizalaması */ +.summary-row .grp-area { + display: flex; + flex-direction: column; + justify-content: center; + transform: translateX(calc(var(--grp-title-w) - var(--beden-w))); +} +.summary-row .grp-row { + display: grid; + grid-auto-flow: column; + grid-auto-columns: var(--beden-w); +} +.summary-row .grp-row .cell.beden { + width: var(--beden-w); + height: var(--beden-h); + border: 1px solid #ddd; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; +} +.cell.beden.ghost { + opacity: 0; + pointer-events: none; + border: 1px solid transparent !important; +} + +/* 🔹 Sağ kolonlar */ +.summary-row .cell.adet, +.summary-row .cell.fiyat, +.summary-row .cell.pb, +.summary-row .cell.tutar, +.summary-row .cell.termin { + font-weight: 600; + color: #000; + border-left: none !important; + height: 100%; +} + +.summary-row .cell.tutar { + text-align: right; + justify-content: flex-end; + padding-right: 8px; + border-right: none !important; +} + +.summary-row .cell.termin { + background: #fffef9; + justify-content: center; + align-items: center; + min-width: var(--col-termin); +} +.summary-row .cell.termin .q-input { + width: 100%; + max-width: 142px !important; + box-sizing: border-box; +} +.summary-row .cell.termin input { + text-align: center; + font-size: 13px; +} + +/* =========================================================== + 9️⃣ ORDER EDITOR (ALT FORM) + =========================================================== */ +.editor { + position: relative; + z-index: 50; + background: #fffef9; + border-top: 1px solid #ddd; + margin-top: 24px; + padding: 16px; +} +.editor::before { + content: ""; + display: block; + height: 4px; + background: linear-gradient(to right,#c9a227,#e5d28b,#fff7d2); + margin-bottom: 12px; + border-radius: 2px; +} +.editor .q-btn:hover { background: #d2b04d; } +.editor .q-input, +.editor .q-select { margin-bottom: 8px; font-size: 14px; } +.cell.termin .termin-label { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 13px; + color: #222; + background: #fffef9; + border-left: 1px solid #ccc; + box-sizing: border-box; +} + +/* =========================================================== + 🔟 RESPONSIVE + MİNÖR DÜZEN + =========================================================== */ +@media (max-width: 1024px) { + :root { --beden-w: 40px; --col-aciklama: 120px; } + .order-grid-header .col-fixed { font-size: 11px; } + .order-sub-header { font-size: 12.5px; } +} +@media (max-width: 768px) { + :root { + --beden-w: 36px; + --col-model: 70px; + --col-renk: 60px; + --col-aciklama: 100px; + } + .order-page { font-size: 13px; } + .order-grid-header .total-cell { font-size: 10.5px; } +} + +.summary-row .cell { + display: flex; + justify-content: center; + align-items: center; + padding: 4px 6px; + height: auto; + text-align: center; + white-space: normal; + word-wrap: break-word; +} + +.summary-row .grp-area, +.summary-row .grp-row, +.summary-row .grp-row .cell.beden { + align-items: center; + height: 100%; +} +.summary-row .cell.aciklama { + grid-column: 5 / 6 !important; /* sadece 5. kolon */ + position: relative !important; + width: calc(var(--col-aciklama) + 92px) !important; /* 🔹 74px genişletme */ + margin-right: -92px !important; /* 🔹 bedenle tam hizalanır */ + white-space: normal !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + line-height: 1.4 !important; + padding: 6px 12px !important; + font-size: 13px !important; + text-align: left !important; + display: flex !important; + flex-direction: column !important; + align-items: flex-start !important; + justify-content: flex-start !important; + min-height: 36px !important; + background: #fff !important; + box-sizing: border-box !important; + border-right: 1px solid #ccc !important; + z-index: 10 !important; +} +/* 🧩 Grid çizgi kontrastı güçlendirme */ +.summary-row .cell, +.order-grid-header .col-fixed, +.summary-row .grp-row .cell.beden { + border-color: #bbb !important; /* 🔹 daha belirgin çizgi */ +} + +.summary-row .cell:not(:last-child) { + border-right: 1px solid #bdbdbd !important; +} +/* =========================================================== + 🧱 ALT GRID ÇİZGİLERİ – TÜM SATIRLAR İÇİN + =========================================================== */ +.summary-row { + border-bottom: 1px solid #ccc; /* 🔹 satır alt çizgisi */ +} + +.summary-row:last-child { + border-bottom: 2px solid #b7a33a; /* 🔹 son satırda Baggi gold tonu */ +} + +/* 🔹 Hücrelerin alt çizgisi (beden dahil) */ +.summary-row .cell, +.summary-row .grp-row .cell.beden { + border-bottom: 1px solid #ddd !important; +} + +/* 🔹 Hover olduğunda grid çizgileri kaybolmasın */ +.summary-row:hover .cell, +.summary-row:hover .grp-row .cell.beden { + border-bottom: 1px solid #ccc !important; +} +.summary-row:hover { + background: #fffce0; +} + +.summary-row.is-editing { + background: #fff3cd; + outline: 2px solid #caa83f; + z-index: 2; +} + +.editor .q-btn:hover { + background: #d2b04d; + color: #fff; +} + +/* 🔹 Hover olduğunda grid çizgileri kaybolmasın */ +.summary-row:hover .cell, +.summary-row:hover .grp-row .cell.beden { + border-bottom: 1px solid #ccc !important; +} + +/* =========================================================== + 🎨 STOK RENKLERİ (LOW–MID–HIGH) + =========================================================== */ +.stok-red { + color: #e53935; /* 🔴 Kırmızı */ + font-weight: 600; +} + +.stok-yellow { + color: #f9a825; /* 🟡 Sarı */ + font-weight: 600; +} + +.stok-green { + color: #43a047; /* 🟢 Yeşil */ + font-weight: 600; +} +.q-banner.rounded-borders { + border-radius: 8px; +} +.order-gateway { + background: linear-gradient(145deg, #fff 0%, #fafafa 100%); + height: 100%; +} + +.order-btn { + font-size: 1.2rem; + padding: 20px 40px; + border-radius: 12px; + min-width: 280px; + transition: all 0.2s ease; +} +.order-btn:hover { + transform: translateY(-3px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} +/* =========================================================== + 🧭 DRAWER AÇIKKEN GRID HİZALAMA FIX + =========================================================== */ + +/* Drawer açıkken içerik kaymaması */ +.body--drawer-left-open .order-page { + width: calc(100vw - var(--drawer-w)); /* viewport'tan drawer genişliği kadar düş */ + overflow-x: visible; /* dış overflow’u kes */ +} + +/* Scroll konteyner sadece grid içinde çalışsın */ +.order-scroll-x { + max-width: 100%; + overflow-x: auto; + overflow-y: visible; + background: #fff; + box-sizing: border-box; +} + +/* Scrollbar ve sağ boşluğu dengeler */ +.order-grid-header, +.order-sub-header, +.order-grid-body { + min-width: fit-content; + width: 100%; + box-sizing: border-box; +} +/* =========================================================== + 🧱 DRAWER AÇIKKEN TAM HİZALAMA FIX (v2) + =========================================================== */ + +/* Drawer açıkken tüm üst bloklar sağdan taşmasın */ +.body--drawer-left-open .filter-bar, +.body--drawer-left-open .save-toolbar, +.body--drawer-left-open .order-grid-header, +.body--drawer-left-open .order-sub-header, +.body--drawer-left-open .order-grid-body { + width: calc(100vw - var(--drawer-w)); /* drawer genişliği kadar daralt */ + margin-left: 0; + margin-right: 0; + overflow-x: hidden; + box-sizing: border-box; +} + +/* Drawer kapalıyken tam genişlik */ +.body--drawer-left-closed .filter-bar, +.body--drawer-left-closed .save-toolbar, +.body--drawer-left-closed .order-grid-header, +.body--drawer-left-closed .order-sub-header, +.body--drawer-left-closed .order-grid-body { + width: 100vw; +} + +/* Order grid sağ sınırı altın kenarlıkla bitir (optik kapanış) */ +.order-grid-header, +.order-sub-header, +.order-grid-body { + border-right: 2px solid var(--baggi-gold); +} +/* =========================================================== + 🎯 SAĞ ALT BOŞLUK FİNAL FIX + =========================================================== */ + +/* Drawer açıkken tüm grid konteynerleri sağdan tam sıfırla */ +.body--drawer-left-open .order-page, +.body--drawer-left-open .filter-bar, +.body--drawer-left-open .save-toolbar, +.body--drawer-left-open .order-grid-header, +.body--drawer-left-open .order-sub-header, +.body--drawer-left-open .order-grid-body { + width: calc(100vw - var(--drawer-w) - 8px); /* 🔹 scrollbar toleransı */ + padding-right: 0 !important; + margin-right: 0 !important; + overflow-x: visible !important; +} + +/* Son altın kenarlık hizasını koru */ +.order-grid-body { + border-right: 2px solid var(--baggi-gold); +} +/* =========================================================== + 🎯 GRID SAĞ HİZALAMA (FILTER + SAVE + HEADER) + =========================================================== */ + +/* Ana scroll container referansı */ +.order-scroll-x { + display: flex; + flex-direction: column; + align-items: flex-start; /* hizalama sola */ + overflow-x: auto; + overflow-y: visible; + background: #fff; +} + +/* Filter ve Save barlar grid genişliğini takip etsin */ +.filter-bar, +.save-toolbar, +.order-grid-header, +.order-sub-header { + width: fit-content; /* içeriğe göre genişlik */ + min-width: 100%; /* minimum ekran kadar */ + box-sizing: border-box; +} + +/* Grid body’nin genişliği kadar sağ hizalama */ +.order-grid-body { + width: fit-content; + box-sizing: border-box; +} +/* 🔒 Kapalı satır */ +.summary-row.row-closed { + background: #e6e6e6 !important; + opacity: 0.65; + pointer-events: none; /* tüm inputları disable eder */ +} + +/* Hover iptal */ +.summary-row.row-closed:hover { + background: #e6e6e6 !important; +} + +/* Edit efekti de kapansın */ +.summary-row.row-closed.is-editing { + outline: none !important; +} + +/* Sağ kenarda taşma veya padding olmasın */ +.filter-bar, +.save-toolbar, +.order-grid-header, +.order-sub-header, +.order-grid-body { + margin-right: 0 !important; + padding-right: 0 !important; + border-right: none !important; /* altın çizgi istemiyorsan kaldırılır */ +} +.summary-row.row-error { + background: rgba(193, 0, 21, 0.08); +} + +.row-error-icon { + position: absolute; + left: 4px; + top: 50%; + transform: translateY(-50%); +} + + +/* Drawer açık/kapalı fark etmeden */ +.body--drawer-left-open .order-scroll-x, +.body--drawer-left-closed .order-scroll-x { + width: 100%; + overflow-x: auto; +} + +/* =============================== + ORDER LIST (ol-) — Sticky Stack + =============================== */ + +:root { + /* Quasar header yüksekliği */ + --ol-header-h: 56px; + /* Filter bar yüksekliği (px) — inputlar tek satırsa 56 idealdir */ + --ol-filter-h: 96px; +} + +/* q-page tek scroller: header altından başlar */ +.ol-page { + height: calc(100vh - var(--ol-header-h)); + overflow: auto; /* 🔑 tek scroll container */ + background: #fff; + display: flex; + flex-direction: column; +} + +/* Filter bar: q-header’ın altında sticky */ +.ol-filter-bar { + position: sticky; + top: 0; /* 🔑 .ol-page scroller’ında en üst */ + z-index: 600; + background: #fff; + border-bottom: 1px solid #ddd; + padding: 10px 16px; + box-shadow: 0 1px 2px rgba(0,0,0,0.06); + min-height: var(--ol-filter-h); + display: flex; + align-items: center; +} + +/* QTable: sticky thead, zebra aktif ve çakışma yok */ +.ol-table .q-table__middle { + overflow: visible !important; /* sticky thead için güvenli */ + max-height: none !important; +} + +/* thead sabitleme: filter bar’ın ALTINA oturur */ +.ol-table thead th { + position: sticky; + top: var(--ol-filter-h); /* 🔑 filter yüksekliği kadar boşluk */ + z-index: 500; + background: #fff; + box-shadow: 0 2px 4px rgba(0,0,0,0.08); + font-weight: 700; +} + +/* Zebra */ +.ol-table .q-table__body .q-tr:nth-child(odd) { + background-color: #f7f7f7 !important; +} +.ol-table .q-table__body .q-tr:nth-child(even) { + background-color: #ffffff !important; +} +.ol-table .q-table__body .q-tr:hover { + background-color: #fff7d1 !important; + transition: background-color .15s ease; +} + +/* Hücreler */ +.ol-table .q-td { + font-size: .9rem; + line-height: 1.3; + padding: 6px 8px !important; +} + +/* Güvenli z-index hiyerarşisi */ +.q-header { z-index: 1000 !important; } /* header en üstte */ +.q-drawer { z-index: 950 !important; } /* drawer header’ın altında */ + +/* Mobile */ +@media (max-width: 768px) { + :root { --ol-filter-h: 64px; } /* input kırılıyorsa biraz artır */ + .ol-filter-bar { padding: 8px 12px; } +} +/* =========================================================== + 🟡 ORDERLIST ZEBRA FIX (v3) + =========================================================== */ + +/* Her iki tr katmanını da hedefliyoruz (Quasar q-tr + native tr) */ +.ol-table tbody tr:nth-child(odd), +.ol-table .q-table__body .q-tr:nth-child(odd) { + background-color: #faf8ef !important; /* açık krem tonu */ +} + +.ol-table tbody tr:nth-child(even), +.ol-table .q-table__body .q-tr:nth-child(even) { + background-color: #ffffff !important; +} + +/* Hover tonu: hafif Baggi gold dokunuşu */ +.ol-table tbody tr:hover, +.ol-table .q-table__body .q-tr:hover { + background-color: #fff4cc !important; + transition: background-color 0.2s ease; +} +/* ========================================================= + 📌 OrderList — Toplam USD Banner + ========================================================= */ +.ol-qbanner { + background: #f9fafb; + border: 1px solid #e0e0e0; + border-radius: 6px; + padding: 8px 12px; +} + +.ol-qbanner-amount { + color: #1976d2; /* Quasar primary */ + margin-left: 6px; +} +/* ========================================================= + 📌 ORDER LIST (ol-) — MULTILINE COLUMN FIX + ========================================================= */ + +/* Ortak çok satırlı davranış */ +.ol-col-multiline { + white-space: normal !important; + overflow: hidden; + word-break: break-word; + line-height: 1.25rem; +} + +/* 🧾 Cari Adı — 2 SATIR */ +.ol-col-cari { + max-width: 200px; + min-width: 150px; + + display: -webkit-box; + -webkit-box-orient: vertical; + + font-weight: 600; + font-size: 0.88rem; +} + +/* 📝 Açıklama — 5 SATIR */ +.ol-col-desc { + max-width: 220px; + min-width: 160px; + + display: -webkit-box; + -webkit-box-orient: vertical; + + font-size: 0.82rem; + color: #444; +} + +/* Hücre padding iyileştirme */ +.ol-table .q-td.ol-col-cari, +.ol-table .q-td.ol-col-desc { + padding-top: 6px !important; + padding-bottom: 6px !important; +} + +/* Header hizalama */ +.ol-table th.ol-col-cari, +.ol-table th.ol-col-desc { + white-space: nowrap; +} +/* ========================================================= + ORDER LIST — FILTER BAR FLEX FIX (FINAL) + ========================================================= */ + +.ol-filter-bar { + position: sticky; + top: 0; + z-index: 600; + background: #fff; + border-bottom: 1px solid #ddd; + padding: 10px 12px; +} + +/* 🔑 TEK SATIR */ +.ol-filter-row { + display: flex; + align-items: flex-end; /* 🔑 tüm input & butonlar aynı çizgi */ + gap: 12px; + flex-wrap: nowrap; /* ❌ alt satıra düşme */ +} + +/* Inputlar */ +.ol-filter-input { + min-width: 180px; + max-width: 260px; +} + +/* Arama biraz geniş */ +.ol-search { + min-width: 280px; +} + +/* Butonlar */ +.ol-filter-actions { + display: flex; + gap: 8px; + align-items: center; + white-space: nowrap; +} + +/* Toplam banner */ +.ol-filter-total { + margin-left: auto; /* 🔑 sağa yasla */ + background: #f9fafb; + border: 1px solid #e0e0e0; + border-radius: 6px; + padding: 8px 12px; + white-space: nowrap; +} + +/* Mobile fallback */ +@media (max-width: 1200px) { + .ol-filter-row { + flex-wrap: wrap; + row-gap: 8px; + } + + .ol-filter-total { + width: 100%; + justify-content: flex-end; + } +} + +.order-gateway { + min-height: 100vh; +} + +.draft-card { + width: 320px; + max-width: 90vw; +} +/* =========================================================== + 🕵️ ACTIVITY LOGS (act-) — GLOBAL + =========================================================== */ + +/* Sayfa konteyneri */ +.act-page { + height: calc(100vh - 56px); /* q-header */ + overflow: auto; + background: #fff; +} + +/* Sticky filter bar */ +.act-filter-bar { + position: sticky; + top: 0; + z-index: 620; + background: #fff; + border-bottom: 1px solid #ddd; + padding: 10px 16px; + box-shadow: 0 1px 2px rgba(0,0,0,0.06); +} + +/* Filtre satırı */ +.act-filter-row { + display: flex; + align-items: flex-end; + gap: 12px; + flex-wrap: nowrap; +} + +/* Input genişlikleri */ +.act-filter-input { + min-width: 160px; + max-width: 240px; +} + +.act-filter-wide { + min-width: 260px; +} + +/* Aksiyon butonları */ +.act-filter-actions { + display: flex; + gap: 8px; + margin-left: auto; + white-space: nowrap; +} + +/* Tablo */ +.act-table { + font-size: 0.85rem; +} + +/* Sticky thead */ +.act-table thead th { + position: sticky; + top: 56px; /* filter bar yüksekliği */ + z-index: 500; + background: #fff; + box-shadow: 0 2px 4px rgba(0,0,0,0.08); + font-weight: 700; +} + +/* Zebra */ +.act-table tbody tr:nth-child(odd) { + background: #faf8ef; +} +.act-table tbody tr:nth-child(even) { + background: #fff; +} +.act-table tbody tr:hover { + background: #fff4cc; +} + +/* Hücreler */ +.act-table .q-td { + padding: 6px 8px !important; + line-height: 1.25; + font-weight: 600; +} + +/* Başarılı / hatalı satır */ +.act-row-success { + background: rgba(67, 160, 71, 0.06); +} +.act-row-fail { + background: rgba(211, 47, 47, 0.06); +} + +/* Badge renkleri */ +.act-badge-ok { + background: #43a047; +} +.act-badge-fail { + background: #e53935; +} + +/* Dar kolon */ +.act-col-narrow { + max-width: 90px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Route kolonu */ +.act-col-route { + max-width: 260px; + white-space: normal; + word-break: break-word; + font-size: 0.8rem; +} + +/* IP / UA */ +.act-col-meta { + font-size: 0.75rem; + color: #555; +} +/* ================================ + UserList — Piyasa Wrap (NO CUT) + ================================ */ + +/* Hücre yukarıdan başlasın */ +.ol-col-piyasa { + vertical-align: top; + padding-top: 6px !important; + padding-bottom: 6px !important; +} + +/* Chip konteyner */ +.piyasa-wrap { + display: flex; + flex-wrap: wrap; /* 🔑 alt satıra insin */ + align-content: flex-start; + + row-gap: 4px; + column-gap: 6px; + + /* ❌ KESİNLİKLE YOK */ + max-height: none; + overflow: visible; +} + +/* Chip */ +.piyasa-chip { + flex: 0 0 calc(25% - 6px); /* 🔑 satırda 4 adet */ + max-width: calc(25% - 6px); + + font-size: 11px; + font-weight: 600; + line-height: 1.1; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.user-detail-page { + background: #fafafa; +} +.image-preview { + width: 100%; + border-radius: 6px; +} + +.image-thumb { + width: 100%; + border-radius: 4px; +} + +.workorder-page { + padding-bottom: 80px; +} +/* =============================== + PERMISSIONS — BAGGI THEME + =============================== */ + +.permissions-page { + background: #fff; + height: calc(100vh - 56px); + display: flex; + flex-direction: column; +} + +/* Scroll alanı */ +.permissions-table-scroll { + flex: 1; + overflow: auto; + background: #fff; +} + +/* Tablo */ +.permissions-table { + font-size: 0.85rem; +} + +/* Sticky header */ +.permissions-table thead th { + position: sticky; + top: 0; + z-index: 300; + background: var(--baggi-cream); + font-weight: 800; + color: #222; + box-shadow: 0 2px 3px rgba(0,0,0,0.06); +} + +/* Hücreler */ +.permissions-table td { + font-weight: 600; + padding: 6px 8px !important; + color: #333; +} + +/* Zebra */ +.permissions-table tbody tr:nth-child(odd) { + background: #fffef7; +} +.permissions-table tbody tr:nth-child(even) { + background: #ffffff; +} +.permissions-table tbody tr:hover { + background: #fff4cc; +} + +/* Sol kolon sabit */ +.permissions-sticky-col { + position: sticky; + left: 0; + z-index: 250; + background: #fff; + box-shadow: 2px 0 4px rgba(0,0,0,0.04); + font-weight: 700; +} + +/* Header checkbox */ +.permissions-table .q-th .column { + gap: 2px; +} + diff --git a/ui/src/css/quasar.variables.sass b/ui/src/css/quasar.variables.sass new file mode 100644 index 0000000..de35157 --- /dev/null +++ b/ui/src/css/quasar.variables.sass @@ -0,0 +1,8 @@ +$primary : #957116e8 // baggi altın +$secondary : #dac197 // baggi ikinci ton +$accent : #ff9800 // turuncu +$dark : #5a4c4c // siyah-gri +$positive : #21ba45 // yeşil +$negative : #c10015 // kırmızı +$info : #31ccec // açık mavi +$warning : #f2c037 // sarı \ No newline at end of file diff --git a/ui/src/layouts/EmptyLayout.vue b/ui/src/layouts/EmptyLayout.vue new file mode 100644 index 0000000..bf35919 --- /dev/null +++ b/ui/src/layouts/EmptyLayout.vue @@ -0,0 +1,7 @@ + diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue new file mode 100644 index 0000000..a58a79d --- /dev/null +++ b/ui/src/layouts/MainLayout.vue @@ -0,0 +1,302 @@ + + + diff --git a/ui/src/pages/ActivityLogs.vue b/ui/src/pages/ActivityLogs.vue new file mode 100644 index 0000000..9748805 --- /dev/null +++ b/ui/src/pages/ActivityLogs.vue @@ -0,0 +1,341 @@ + + + + + diff --git a/ui/src/pages/ChangePassword.vue b/ui/src/pages/ChangePassword.vue new file mode 100644 index 0000000..4d51503 --- /dev/null +++ b/ui/src/pages/ChangePassword.vue @@ -0,0 +1,120 @@ + + + + diff --git a/ui/src/pages/Dashboard.vue b/ui/src/pages/Dashboard.vue new file mode 100644 index 0000000..2bffd6f --- /dev/null +++ b/ui/src/pages/Dashboard.vue @@ -0,0 +1,9 @@ + + + diff --git a/ui/src/pages/ErrorNotFound.vue b/ui/src/pages/ErrorNotFound.vue new file mode 100644 index 0000000..4b53e5a --- /dev/null +++ b/ui/src/pages/ErrorNotFound.vue @@ -0,0 +1,27 @@ + + + diff --git a/ui/src/pages/FirstPasswordChange.vue b/ui/src/pages/FirstPasswordChange.vue new file mode 100644 index 0000000..ad0a69d --- /dev/null +++ b/ui/src/pages/FirstPasswordChange.vue @@ -0,0 +1,120 @@ + + + + + + + diff --git a/ui/src/pages/MainPage.vue b/ui/src/pages/MainPage.vue new file mode 100644 index 0000000..1d2b0ab --- /dev/null +++ b/ui/src/pages/MainPage.vue @@ -0,0 +1,306 @@ + + + + + + diff --git a/ui/src/pages/MainPanel.vue b/ui/src/pages/MainPanel.vue new file mode 100644 index 0000000..172bc15 --- /dev/null +++ b/ui/src/pages/MainPanel.vue @@ -0,0 +1,9 @@ + + + diff --git a/ui/src/pages/MePassword.vue b/ui/src/pages/MePassword.vue new file mode 100644 index 0000000..637cbc5 --- /dev/null +++ b/ui/src/pages/MePassword.vue @@ -0,0 +1,107 @@ + + + diff --git a/ui/src/pages/OrderEntry.vue b/ui/src/pages/OrderEntry.vue new file mode 100644 index 0000000..2afd629 --- /dev/null +++ b/ui/src/pages/OrderEntry.vue @@ -0,0 +1,2987 @@ + + + diff --git a/ui/src/pages/OrderGateway.vue b/ui/src/pages/OrderGateway.vue new file mode 100644 index 0000000..5e06f90 --- /dev/null +++ b/ui/src/pages/OrderGateway.vue @@ -0,0 +1,272 @@ + + + + diff --git a/ui/src/pages/OrderList.vue b/ui/src/pages/OrderList.vue new file mode 100644 index 0000000..bc3f043 --- /dev/null +++ b/ui/src/pages/OrderList.vue @@ -0,0 +1,408 @@ + + + + diff --git a/ui/src/pages/OrderPdf.vue b/ui/src/pages/OrderPdf.vue new file mode 100644 index 0000000..cc340bc --- /dev/null +++ b/ui/src/pages/OrderPdf.vue @@ -0,0 +1 @@ + diff --git a/ui/src/pages/PermissionMatrix.vue b/ui/src/pages/PermissionMatrix.vue new file mode 100644 index 0000000..e4cd983 --- /dev/null +++ b/ui/src/pages/PermissionMatrix.vue @@ -0,0 +1,301 @@ + + + + diff --git a/ui/src/pages/ProductionWorker.vue b/ui/src/pages/ProductionWorker.vue new file mode 100644 index 0000000..a4c56f2 --- /dev/null +++ b/ui/src/pages/ProductionWorker.vue @@ -0,0 +1,224 @@ + + diff --git a/ui/src/pages/ProductionWorkerGateway.vue b/ui/src/pages/ProductionWorkerGateway.vue new file mode 100644 index 0000000..d2c4b70 --- /dev/null +++ b/ui/src/pages/ProductionWorkerGateway.vue @@ -0,0 +1,211 @@ + + + diff --git a/ui/src/pages/ResetPassword.vue b/ui/src/pages/ResetPassword.vue new file mode 100644 index 0000000..b8ffe92 --- /dev/null +++ b/ui/src/pages/ResetPassword.vue @@ -0,0 +1,264 @@ + + + + diff --git a/ui/src/pages/RoleDepartmentPermissionPage.vue b/ui/src/pages/RoleDepartmentPermissionPage.vue new file mode 100644 index 0000000..e6d2cab --- /dev/null +++ b/ui/src/pages/RoleDepartmentPermissionPage.vue @@ -0,0 +1,435 @@ + + + + + + + + diff --git a/ui/src/pages/StatementHeaderReport.vue b/ui/src/pages/StatementHeaderReport.vue new file mode 100644 index 0000000..515e9a7 --- /dev/null +++ b/ui/src/pages/StatementHeaderReport.vue @@ -0,0 +1,103 @@ + + + + diff --git a/ui/src/pages/StatementReport.vue b/ui/src/pages/StatementReport.vue new file mode 100644 index 0000000..491987f --- /dev/null +++ b/ui/src/pages/StatementReport.vue @@ -0,0 +1,499 @@ + + + diff --git a/ui/src/pages/TestMail.vue b/ui/src/pages/TestMail.vue new file mode 100644 index 0000000..55ddcee --- /dev/null +++ b/ui/src/pages/TestMail.vue @@ -0,0 +1,63 @@ + + + diff --git a/ui/src/pages/UserDetail.vue b/ui/src/pages/UserDetail.vue new file mode 100644 index 0000000..5c0721a --- /dev/null +++ b/ui/src/pages/UserDetail.vue @@ -0,0 +1,383 @@ + + + diff --git a/ui/src/pages/UserGateway.vue b/ui/src/pages/UserGateway.vue new file mode 100644 index 0000000..86af299 --- /dev/null +++ b/ui/src/pages/UserGateway.vue @@ -0,0 +1,108 @@ + + + + + + diff --git a/ui/src/pages/UserList.vue b/ui/src/pages/UserList.vue new file mode 100644 index 0000000..f0c76b0 --- /dev/null +++ b/ui/src/pages/UserList.vue @@ -0,0 +1,272 @@ + + + diff --git a/ui/src/pages/UserPermissionPage.vue b/ui/src/pages/UserPermissionPage.vue new file mode 100644 index 0000000..bde7e1a --- /dev/null +++ b/ui/src/pages/UserPermissionPage.vue @@ -0,0 +1,357 @@ + + + diff --git a/ui/src/pages/UserSync.vue b/ui/src/pages/UserSync.vue new file mode 100644 index 0000000..dd4c278 --- /dev/null +++ b/ui/src/pages/UserSync.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/ui/src/pages/statementofaccount.vue b/ui/src/pages/statementofaccount.vue new file mode 100644 index 0000000..9231b0f --- /dev/null +++ b/ui/src/pages/statementofaccount.vue @@ -0,0 +1,521 @@ + + + diff --git a/ui/src/router/index.js b/ui/src/router/index.js new file mode 100644 index 0000000..c74e02c --- /dev/null +++ b/ui/src/router/index.js @@ -0,0 +1,96 @@ +import { route } from 'quasar/wrappers' +import { createRouter, createWebHashHistory } from 'vue-router' + +import routes from 'src/router/routes.js' + +import { useAuthStore } from 'stores/authStore' +import { usePermissionStore } from 'stores/permissionStore' + + +export default route(function () { + + const router = createRouter({ + history: createWebHashHistory(), + routes + }) + + + /* ============================================================ + 🔐 GLOBAL GUARD + ============================================================ */ + router.beforeEach(async (to, from, next) => { + + const auth = useAuthStore() + const perm = usePermissionStore() + + + /* ================= PUBLIC ================= */ + + if (to.meta?.public === true) { + return next() + } + + + /* ================= LOGIN ================= */ + + if (!auth.isAuthenticated) { + return next('/login') + } + + + /* ================= PASSWORD ================= */ + + if ( + auth.mustChangePassword && + to.path !== '/first-password-change' + ) { + return next('/first-password-change') + } + + + /* ================= ADMIN ================= */ + + if (auth.isAdmin) { + return next() + } + + + /* ================= LOAD PERMS ================= */ + + if (!perm.loaded) { + try { + await perm.fetchPermissions() + } catch (e) { + console.error('Permission load failed', e) + } + } + + + /* ================= CHECK ================= */ + + const required = to.meta?.permission + + if (!required) { + return next() + } + + + const allowed = perm.hasApiPermission(required) + + if (!allowed) { + + console.warn('⛔ ACCESS DENIED:', { + path: to.fullPath, + permission: required + }) + + return next('/unauthorized') + } + + + next() + }) + + + return router +}) diff --git a/ui/src/router/meta.d.js b/ui/src/router/meta.d.js new file mode 100644 index 0000000..f750359 --- /dev/null +++ b/ui/src/router/meta.d.js @@ -0,0 +1,10 @@ +// src/router/meta.js + +/** + * Route meta fields reference + * + * @typedef {Object} RouteMeta + * @property {boolean} [public] - Auth gerekmez + * @property {string} [permission] - Backend route permission (/api/...) + */ +export {} diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js new file mode 100644 index 0000000..0f50c21 --- /dev/null +++ b/ui/src/router/routes.js @@ -0,0 +1,260 @@ +// src/router/routes.js + +const routes = [ + + /* ========================================================== + 🌍 ROOT + ========================================================== */ + { + path: '/', + redirect: '/login' + }, + + + /* ========================================================== + 🔐 PUBLIC + ========================================================== */ + { + path: '/login', + component: () => import('layouts/EmptyLayout.vue'), + meta: { public: true }, + children: [ + { + path: '', + name: 'login', + component: () => import('pages/MainPage.vue') + } + ] + }, + + { + path: '/first-password-change', + component: () => import('layouts/EmptyLayout.vue'), + meta: { public: true }, + children: [ + { + path: '', + name: 'first-password-change', + component: () => import('pages/FirstPasswordChange.vue') + } + ] + }, + + { + path: '/password-reset/:token', + component: () => import('layouts/EmptyLayout.vue'), + meta: { public: true }, + children: [ + { + path: '', + name: 'password-reset', + component: () => import('pages/ResetPassword.vue') + } + ] + }, + + + /* ========================================================== + 🏠 MAIN APP + ========================================================== */ + { + path: '/app', + component: () => import('layouts/MainLayout.vue'), + + children: [ + + /* ================= DASHBOARD ================= */ + + { + path: '', + name: 'dashboard', + component: () => import('pages/Dashboard.vue'), + meta: { permission: 'system:read' } + }, + + + /* ================= PERMISSIONS ================= */ + + { + path: 'permissions', + name: 'permissions', + component: () => import('pages/PermissionMatrix.vue'), + meta: { permission: 'system:read' } + }, + + { + path: 'role-dept-permissions', + name: 'role-dept-permissions', + component: () => import('pages/RoleDepartmentPermissionPage.vue'), + meta: { permission: 'user:update' } + }, + + { + path: 'user-permissions', + name: 'user-permissions', + component: () => import('pages/UserPermissionPage.vue'), + meta: { permission: 'user:update' } + }, + + + /* ================= FINANCE ================= */ + + { + path: 'statementofaccount', + name: 'statementofaccount', + component: () => import('pages/statementofaccount.vue'), + meta: { permission: 'finance:view' } + }, + + { + path: 'statementreport', + name: 'statementreport', + component: () => import('pages/StatementReport.vue'), + meta: { permission: 'finance:view' } + }, + + { + path: 'statementheaderreport', + name: 'statementheaderreport', + component: () => import('pages/StatementHeaderReport.vue'), + meta: { permission: 'finance:view' } + }, + + + /* ================= USERS ================= */ + + { + path: 'users', + name: 'user-gateway', + component: () => import('pages/UserGateway.vue'), + meta: { permission: 'user:view' } + }, + + { + path: 'users/list', + name: 'user-list', + component: () => import('pages/UserList.vue'), + meta: { permission: 'user:view' } + }, + + { + path: 'users/new', + name: 'user-new', + component: () => import('pages/UserDetail.vue'), + meta: { + mode: 'new', + permission: 'user:insert' + } + }, + + { + path: 'users/edit/:id', + name: 'user-edit', + component: () => import('pages/UserDetail.vue'), + props: true, + meta: { + mode: 'edit', + permission: 'user:update' + } + }, + + { + path: 'users/view/:id', + name: 'user-view', + component: () => import('pages/UserDetail.vue'), + props: true, + meta: { + mode: 'view', + permission: 'user:view' + } + }, + + + /* ================= LOGS ================= */ + + { + path: 'activity-logs', + name: 'activity-logs', + component: () => import('pages/ActivityLogs.vue'), + meta: { permission: 'user:view' } + }, + + + /* ================= TEST MAIL ================= */ + + { + path: 'test-mail', + name: 'test-mail', + component: () => import('pages/TestMail.vue'), + meta: { permission: 'user:insert' } + }, + + + /* ================= ORDERS ================= */ + + { + path: 'order-gateway', + name: 'order-gateway', + component: () => import('pages/OrderGateway.vue'), + meta: { permission: 'order:view' } + }, + + { + path: 'order-entry/:orderHeaderID', + name: 'order-entry', + component: () => import('pages/OrderEntry.vue'), + props: true, + meta: { + mode: 'new', + permission: 'order:insert' + } + }, + + { + path: 'order-edit/:orderHeaderID', + name: 'order-edit', + component: () => import('pages/OrderEntry.vue'), + props: true, + meta: { + mode: 'edit', + permission: 'order:update' + } + }, + + { + path: 'order-list', + name: 'order-list', + component: () => import('pages/OrderList.vue'), + meta: { permission: 'order:view' } + }, + + { + path: 'order-pdf/:id', + name: 'order-pdf', + component: () => import('pages/OrderPdf.vue'), + props: true, + meta: { permission: 'order:export' } + }, + + + /* ================= PASSWORD ================= */ + + { + path: 'change-password', + name: 'change-password', + component: () => import('pages/ChangePassword.vue') + } + ] + }, + + + /* ========================================================== + ❌ 404 + ========================================================== */ + { + path: '/:catchAll(.*)*', + component: () => import('pages/ErrorNotFound.vue') + } +] + +export default routes diff --git a/ui/src/services/api.js b/ui/src/services/api.js new file mode 100644 index 0000000..7c85019 --- /dev/null +++ b/ui/src/services/api.js @@ -0,0 +1,67 @@ +// src/services/api.js +import axios from 'axios' +import qs from 'qs' +import { useAuthStore } from 'stores/authStore' + +const api = axios.create({ + baseURL: 'http://localhost:8080/api', + timeout: 180000, + paramsSerializer: params => + qs.stringify(params, { arrayFormat: 'repeat' }) +}) + +// REQUEST +api.interceptors.request.use((config) => { + const auth = useAuthStore() + const url = config.url || '' + + const isPublic = + url.startsWith('/auth/login') || + url.startsWith('/auth/refresh') || + url.startsWith('/password/forgot') || + url.startsWith('/password/reset') + + + if (!isPublic && auth?.token) { + config.headers ||= {} + config.headers.Authorization = `Bearer ${auth.token}` + } + + return config +}) + +// RESPONSE +let isLoggingOut = false +api.interceptors.response.use( + r => r, + async (error) => { + if (error?.response?.status === 401 && !isLoggingOut) { + isLoggingOut = true + try { + useAuthStore().clearSession() + } finally { + isLoggingOut = false + } + } + return Promise.reject(error) + } +) + +// HELPERS +export const get = (u, p = {}, c = {}) => + api.get(u, { params: p, ...c }).then(r => r.data) + +export const post = (u, b = {}, c = {}) => + api.post(u, b, c).then(r => r.data) + +export const put = (u, b = {}, c = {}) => + api.put(u, b, c).then(r => r.data) + +export const del = (u, p = {}, c = {}) => + api.delete(u, { params: p, ...c }).then(r => r.data) + +export const download = (u, p = {}, c = {}) => + api.get(u, { params: p, responseType: 'blob', ...c }) + .then(r => r.data) + +export default api diff --git a/ui/src/services/orderService.js b/ui/src/services/orderService.js new file mode 100644 index 0000000..91e9935 --- /dev/null +++ b/ui/src/services/orderService.js @@ -0,0 +1,62 @@ +// src/services/orderService.js +import { get, post, put } from './api' + +/** + * 🔹 Tek bir siparişi ID’ye göre getirir. + * @param {string} id - OrderHeaderID (GUID) + */ +export async function getOrderById(id) { + try { + const data = await get(`/order/get/${id}`) + return data + } catch (err) { + console.error('❌ getOrderById hatası:', err.message) + throw err + } +} + +/** + * 🔹 Yeni sipariş oluşturur (insert). + * Backend: POST /api/order/create + * @param {Object} header - Sipariş başlığı (OrderHeader tablosu) + * @param {Array} lines - Satırlar (OrderLine tablosu) + */ +export async function createOrder(header, lines) { + const payload = { + header, + lines, + username: header?.CreatedUserName || 'system', + } + + try { + const data = await post('/order/create', payload) + console.log('✅ Sipariş oluşturuldu:', data) + return data + } catch (err) { + console.error('❌ createOrder hatası:', err.message) + throw err + } +} + +/** + * 🔹 Mevcut siparişi günceller (update). + * Backend: PUT /api/order/update + * @param {Object} header - Sipariş başlığı (OrderHeader tablosu) + * @param {Array} lines - Satırlar (OrderLine tablosu) + */ +export async function updateOrder(header, lines) { + const payload = { + header, + lines, + username: header?.LastUpdatedUserName || 'system', + } + + try { + const data = await put('/order/update', payload) + console.log('✅ Sipariş güncellendi:', data) + return data + } catch (err) { + console.error('❌ updateOrder hatası:', err.message) + throw err + } +} diff --git a/ui/src/stores/OrdernewListStore.js b/ui/src/stores/OrdernewListStore.js new file mode 100644 index 0000000..c71aea4 --- /dev/null +++ b/ui/src/stores/OrdernewListStore.js @@ -0,0 +1,167 @@ +// src/stores/OrdernewListStore.js +import { defineStore } from 'pinia' +import api from 'src/services/api' + +let lastRequestId = 0 + +export const useOrderListStore = defineStore('orderlist', { + state: () => ({ + orders: [], + loading: false, + error: null, + + filters: { + search: '', + CurrAccCode: '', + OrderDate: '' + } + }), + + getters: { + filteredOrders (state) { + let result = state.orders + + if (state.filters.CurrAccCode) { + result = result.filter(o => o.CurrAccCode === state.filters.CurrAccCode) + } + + if (state.filters.OrderDate) { + result = result.filter(o => + o.OrderDate?.startsWith(state.filters.OrderDate) + ) + } + + return result + }, + + totalVisibleUSD (state) { + return state.filteredOrders.reduce( + (sum, o) => sum + Number(o.TotalAmountUSD || 0), + 0 + ) + } + }, + + actions: { + async fetchOrders () { + + // ============================== + // 📌 REQUEST ID + // ============================== + const rid = ++lastRequestId + + // ============================== + // 📌 SEARCH SNAPSHOT + // ============================== + const raw = this.filters.search ?? '' + const trimmed = String(raw).trim() + + // ============================== + // 📌 REQUEST LOG + // ============================== + console.groupCollapsed( + `%c[orders] FETCH rid=${rid}`, + 'color:#1976d2;font-weight:bold' + ) + + console.log('raw =', JSON.stringify(raw), 'len=', String(raw).length) + console.log('trimmed =', JSON.stringify(trimmed), 'len=', trimmed.length) + console.log('filters =', JSON.parse(JSON.stringify(this.filters))) + console.log('lastRID =', lastRequestId) + + console.groupEnd() + + this.loading = true + this.error = null + + try { + + // ============================== + // 📌 PARAMS + // ============================== + const params = {} + if (trimmed) params.search = trimmed + + // ============================== + // 📌 API CALL + // ============================== + const res = await api.get('/orders/list', { params }) + + // ============================== + // 📌 STALE CHECK + // ============================== + if (rid !== lastRequestId) { + console.warn( + `[orders] IGNORE stale response rid=${rid} last=${lastRequestId}` + ) + return + } + + // ============================== + // 📌 DATA + // ============================== + const data = res?.data + this.orders = Array.isArray(data) ? data : [] + + // ============================== + // 📌 RESPONSE LOG + // ============================== + console.groupCollapsed( + `%c[orders] RESPONSE rid=${rid} count=${this.orders.length}`, + 'color:#2e7d32;font-weight:bold' + ) + + console.log('status =', res?.status) + + console.log( + 'sample =', + this.orders.slice(0, 5).map(o => ({ + id: o.OrderHeaderID, + no: o.OrderNumber, + code: o.CurrAccCode, + name: o.CurrAccDescription + })) + ) + + // ============================== + // 📌 DUPLICATE CHECK + // ============================== + const ids = this.orders.map(o => String(o.OrderHeaderID)) + const dup = ids.filter((v, i) => ids.indexOf(v) !== i) + + if (dup.length) { + console.warn( + 'DUPLICATE OrderHeaderID sample =', + dup.slice(0, 10) + ) + } + + console.groupEnd() + + } catch (err) { + + if (rid !== lastRequestId) return + + console.error( + '[orders] FETCH FAILED', + err?.response?.status, + err?.response?.data || err + ) + + this.orders = [] + + this.error = + err?.response?.data || + err?.message || + 'Sipariş listesi alınamadı' + + } finally { + + if (rid === lastRequestId) { + this.loading = false + } + + } + } + } +}) diff --git a/ui/src/stores/UserDetailStore.js b/ui/src/stores/UserDetailStore.js new file mode 100644 index 0000000..6d1dd0a --- /dev/null +++ b/ui/src/stores/UserDetailStore.js @@ -0,0 +1,238 @@ +// src/stores/userDetailStore.js +import { defineStore } from 'pinia' +import api, { get, post, put } from 'src/services/api' + +export const useUserDetailStore = defineStore('userDetail', { + state: () => ({ + sendingPasswordMail: false, + lastPasswordMailSentAt: null, + hasPassword: false, + + /* ================= FLAGS ================= */ + loading: false, + saving: false, + error: null, + + /* ================= FORM ================= */ + form: { + id: null, + code: '', + full_name: '', + email: '', + mobile: '', + is_active: true, + address: '', + roles: [], + departments: [], + piyasalar: [], + nebim_users: [] + }, + + /* ================= LOOKUPS ================= */ + roleOptions: [], + departmentOptions: [], + piyasaOptions: [], + nebimUserOptions: [] + }), + + actions: { + /* ===================================================== + 🔄 RESET (NEW MODE) + ===================================================== */ + resetForm () { + this.form = { + id: null, + code: '', + full_name: '', + email: '', + mobile: '', + is_active: true, + address: '', + roles: [], + departments: [], + piyasalar: [], + nebim_users: [] + } + this.error = null + this.hasPassword = false + this.lastPasswordMailSentAt = null + }, + + /* ===================================================== + 🔐 ADMIN RESET PASSWORD + ===================================================== */ + async adminResetPassword (id, payload) { + // token otomatik (interceptor) + await post(`/users/${id}/admin-reset-password`, payload) + this.hasPassword = true + }, + + /* ===================================================== + ✉️ SEND PASSWORD MAIL + ===================================================== */ + async sendPasswordMail (id) { + this.sendingPasswordMail = true + this.error = null + + try { + await post(`/users/${id}/send-password-mail`, {}) + + // UI takip (DB’siz): sadece “son gönderim” gösterir + this.lastPasswordMailSentAt = new Date().toLocaleString('tr-TR') + } catch (e) { + this.error = 'Parola maili gönderilemedi' + throw e + } finally { + this.sendingPasswordMail = false + } + }, + + /* ===================================================== + 📦 PAYLOAD BUILDER (BACKEND SÖZLEŞMESİYLE UYUMLU) + ===================================================== */ + buildPayload () { + return { + code: this.form.code, + full_name: this.form.full_name, + email: this.form.email, + mobile: this.form.mobile, + is_active: this.form.is_active, + address: this.form.address, + + roles: this.form.roles, + + // ✅ TEK DEPARTMAN (string → backend array) + departments: this.form.departments + ? [{ code: this.form.departments }] + : [], + + piyasalar: (this.form.piyasalar || []).map(code => ({ code })), + + nebim_users: (this.form.nebim_users || []).map(username => { + const opt = (this.nebimUserOptions || []).find(x => x.value === username) + return { + username, + user_group_code: opt?.user_group_code || '' + } + }) + } + }, + + /* ===================================================== + 📥 GET USER (EDIT MODE) + ===================================================== */ + async fetchUser (id) { + this.loading = true + this.error = null + + try { + const data = await get(`/users/${id}`) + + this.form.id = data.id + this.form.code = data.code || '' + this.form.full_name = data.full_name || '' + this.form.email = data.email || '' + this.form.mobile = data.mobile || '' + this.form.is_active = !!data.is_active + this.form.address = data.address || '' + + this.form.roles = data.roles || [] + this.form.departments = (data.departments || []).map(x => x.code) + this.form.piyasalar = (data.piyasalar || []).map(x => x.code) + this.form.nebim_users = (data.nebim_users || []).map(x => x.username) + + this.hasPassword = !!data.has_password + } catch (e) { + this.error = 'Kullanıcı bilgileri alınamadı' + throw e + } finally { + this.loading = false + } + }, + + /* ===================================================== + ✍️ UPDATE USER (PUT) + ===================================================== */ + async saveUser (id) { + this.saving = true + this.error = null + + try { + console.log('🟦 saveUser() START', id) + + const payload = this.buildPayload() + console.log('📤 PUT payload', payload) + + await put(`/users/${id}`, payload) + + console.log('✅ PUT OK → REFETCH USER') + await this.fetchUser(id) + + console.log('🔄 USER REFRESHED', { + hasPassword: this.hasPassword, + roles: this.form.roles, + departments: this.form.departments + }) + } catch (e) { + console.error('❌ saveUser FAILED', e) + this.error = 'Kullanıcı güncellenemedi' + throw e + } finally { + this.saving = false + } + }, + + /* ===================================================== + ➕ CREATE USER (POST) + ===================================================== */ + async createUser () { + this.saving = true + this.error = null + + try { + console.log('🟢 createUser() START') + + const payload = this.buildPayload() + console.log('📤 POST payload', payload) + + const data = await post('/users', payload) + + console.log('✅ CREATE OK response', data) + + const newId = data?.id + if (!newId) { + throw new Error('CREATE response id yok') + } + + console.log('🔁 FETCH NEW USER id=', newId) + await this.fetchUser(newId) + + return newId + } catch (e) { + console.error('❌ createUser FAILED', e) + this.error = 'Kullanıcı oluşturulamadı' + throw e + } finally { + this.saving = false + } + }, + + /* ===================================================== + 📚 LOOKUPS (NEW + EDIT ORTAK) + ===================================================== */ + async fetchLookups () { + // token otomatik + const [roles, depts, piyasalar, nebims] = await Promise.all([ + api.get('/lookups/roles'), + api.get('/lookups/departments'), + api.get('/lookups/piyasalar'), + api.get('/lookups/nebim-users') + ]) + + this.roleOptions = roles?.data || roles || [] + this.departmentOptions = depts?.data || depts || [] + this.piyasaOptions = piyasalar?.data || piyasalar || [] + this.nebimUserOptions = nebims?.data || nebims || [] + } + } +}) diff --git a/ui/src/stores/UserListStore.js b/ui/src/stores/UserListStore.js new file mode 100644 index 0000000..66d2c52 --- /dev/null +++ b/ui/src/stores/UserListStore.js @@ -0,0 +1,72 @@ +// src/stores/userListStore.js +import { defineStore } from 'pinia' +import api from 'src/services/api' + +export const useUserListStore = defineStore('userlist', { + state: () => ({ + users: [], + loading: false, + error: null, + + filters: { + search: '', + onlyActive: false + } + }), + + getters: { + filteredUsers(state) { + let result = state.users + const term = state.filters.search?.toLowerCase() || '' + + if (term) { + result = result.filter(u => + u.code?.toLowerCase().includes(term) || + u.nebim_username?.toLowerCase().includes(term) || + u.role_names?.toLowerCase().includes(term) || + u.department_names?.toLowerCase().includes(term) || + u.piyasa_names?.toLowerCase().includes(term) + ) + } + + if (state.filters.onlyActive) { + result = result.filter(u => u.is_active) + } + + return result + } + }, + + actions: { + async fetchUsers() { + this.loading = true + this.error = null + + try { + const params = {} + + if (this.filters.search) { + params.search = this.filters.search + } + + const { data } = await api.get( + '/users/list', + { params } + ) + + this.users = Array.isArray(data) ? data : [] + + console.log('✅ User listesi alındı:', this.users.length) + + } catch (err) { + console.error('❌ User listesi alınamadı:', err) + this.users = [] + this.error = + err?.message || + 'Kullanıcı listesi alınamadı' + } finally { + this.loading = false + } + } + } +}) diff --git a/ui/src/stores/accountStore.js b/ui/src/stores/accountStore.js new file mode 100644 index 0000000..35b351b --- /dev/null +++ b/ui/src/stores/accountStore.js @@ -0,0 +1,43 @@ +// src/stores/accountStore.js +import { defineStore } from 'pinia' +import api from 'src/services/api' + +export const useAccountStore = defineStore('account', { + state: () => ({ + accountOptions: [], + loading: false, + error: null + }), + + actions: { + async fetchAccounts () { + this.loading = true + this.error = null + + try { + // 🔐 Token interceptor ile otomatik eklenir + const { data } = await api.get('/accounts') + + this.accountOptions = (Array.isArray(data) ? data : []).map(acc => ({ + label: `${acc.display_code || ''} ${acc.account_name || ''}`.trim(), + value: acc.account_code + })) + + } catch (err) { + console.error('❌ Error fetching accounts:', err) + + if (err?.response?.status === 401) { + this.error = 'Cari hesapları görüntüleme yetkiniz yok.' + } else { + this.error = + err?.response?.data?.message || + err?.message || + 'Cari hesaplar yüklenemedi' + } + } finally { + this.loading = false + } + } + + } +}) diff --git a/ui/src/stores/activityLogStore.js b/ui/src/stores/activityLogStore.js new file mode 100644 index 0000000..9eb3c3c --- /dev/null +++ b/ui/src/stores/activityLogStore.js @@ -0,0 +1,102 @@ +import { defineStore } from 'pinia' +import { get } from 'src/services/api' + +export const useActivityLogStore = defineStore('activityLogStore', { + state: () => ({ + loading: false, + + rows: [], + total: 0, + + pagination: { + page: 1, + rowsPerPage: 0, // ✅ SINIRSIZ + sortBy: 'created_at', + descending: true + } + , + + filters: { + username: '', + actionCategory: null, + actionType: '', + success: null, + dateFrom: '', + dateTo: '' + } + }), + + actions: { + async fetchLogs () { + this.loading = true + try { + const params = {} + + if (this.pagination.rowsPerPage > 0) { + params.page = this.pagination.page + params.limit = this.pagination.rowsPerPage + } + + + if (this.filters.username) + params.username = this.filters.username + + if (this.filters.actionCategory) + params.action_category = this.filters.actionCategory + + if (this.filters.actionType) + params.action_type = this.filters.actionType + + if (this.filters.success !== null) + params.success = this.filters.success + + if (this.filters.dateFrom) + params.date_from = this.filters.dateFrom + + if (this.filters.dateTo) + params.date_to = this.filters.dateTo + + const data = await get('/activity-logs', params) + + this.rows = data.items || [] + this.total = data.total || 0 + } finally { + this.loading = false + } + }, + quickRoleChange () { + + this.filters.actionCategory = 'role_permission' + this.filters.actionType = 'role_department_permission_change' + + this.pagination.page = 1 + + this.fetchLogs() + } + , + + onTableRequest (props) { + const { page, rowsPerPage, sortBy, descending } = props.pagination + + this.pagination.page = page + this.pagination.rowsPerPage = rowsPerPage + this.pagination.sortBy = sortBy + this.pagination.descending = descending + + this.fetchLogs() + } +, + resetFilters () { + this.filters = { + username: '', + actionCategory: null, + actionType: '', + success: null, + dateFrom: '', + dateTo: '' + } + this.pagination.page = 1 + this.fetchLogs() + } + } +}) diff --git a/ui/src/stores/authStore.js b/ui/src/stores/authStore.js new file mode 100644 index 0000000..e17eda2 --- /dev/null +++ b/ui/src/stores/authStore.js @@ -0,0 +1,117 @@ +// src/stores/authStore.js +import { defineStore } from 'pinia' +import api from 'src/services/api' +import { usePermissionStore } from 'stores/permissionStore' + +export const useAuthStore = defineStore('auth', { + state: () => { + let user = null + + try { + const raw = localStorage.getItem('user') + if (raw && raw !== 'undefined' && raw !== 'null') { + user = JSON.parse(raw) + } + } catch { + console.warn('⚠️ Invalid user in localStorage, cleared') + localStorage.removeItem('user') + } + + return { + token: localStorage.getItem('token'), + user, + forcePasswordChange: localStorage.getItem('forcePasswordChange') === '1' + } + }, + + getters: { + isAuthenticated: s => !!s.token, + mustChangePassword: s => !!s.forcePasswordChange, + + // 🔥 TEK ADMIN KURALI + isAdmin: s => + String(s.user?.role_code || '').toLowerCase() === 'admin' + }, + + actions: { + /* ========================================================= + 🔐 SESSION + ========================================================= */ + setSession ({ token, user }) { + this.token = token + this.user = user ?? null + this.forcePasswordChange = !!user?.force_password_change + + localStorage.setItem('token', token) + + if (user) { + localStorage.setItem('user', JSON.stringify(user)) + } else { + localStorage.removeItem('user') + } + + localStorage.setItem( + 'forcePasswordChange', + this.forcePasswordChange ? '1' : '0' + ) + }, + + clearSession () { + this.token = null + this.user = null + this.forcePasswordChange = false + + localStorage.removeItem('token') + localStorage.removeItem('user') + localStorage.removeItem('forcePasswordChange') + + usePermissionStore().clear() + }, + + /* ========================================================= + 🔐 LOGIN + ========================================================= */ + async login (username, password) { + const res = await api.post('/auth/login', { username, password }) + + const token = + res?.token || + res?.data?.token || + res?.access_token || + res?.data?.access_token + + const user = + res?.user || + res?.data?.user + + // ✅ JWT doğrulama + const tokenStr = typeof token === 'string' ? token.trim() : '' + const looksLikeJwt = tokenStr.split('.').length === 3 + + if (!tokenStr || !looksLikeJwt) { + console.error('❌ LOGIN RESPONSE (unexpected):', res) + throw new Error('Invalid login token') + } + + this.setSession({ token: tokenStr, user }) + + // 🔥 PERMISSIONS + const perm = usePermissionStore() + await perm.fetchPermissions() + + + + + // 🧪 DEBUG (istersen sonra kaldır) + console.log('🔐 AUTH DEBUG', { + isAdmin: this.isAdmin, + users: perm.hasPermission('/api/users/list'), + orders: perm.hasPermission('/api/orders/list'), + logs: perm.hasPermission('/api/activity-logs'), + permissions: perm.hasPermission('/api/permissions/matrix') + }) + + return true + } + } +}) diff --git a/ui/src/stores/deneme b/ui/src/stores/deneme new file mode 100644 index 0000000..ebdca69 --- /dev/null +++ b/ui/src/stores/deneme @@ -0,0 +1,3023 @@ + + + diff --git a/ui/src/stores/deneme2 b/ui/src/stores/deneme2 new file mode 100644 index 0000000..60520f2 --- /dev/null +++ b/ui/src/stores/deneme2 @@ -0,0 +1,1128 @@ +/* =========================================================== + GLOBAL CUSTOM CSS + =========================================================== */ + .with-bg { + position: relative; + min-height: 100%; +} +.with-bg::before { + content: ""; + position: absolute; + inset: 0; + background: url('/images/Baggi-tekstilas-logolu.jpg') no-repeat center top; + background-size: 400px auto; + opacity: 0.15; + pointer-events: none; + z-index: 0; +} +.with-bg > * { + position: relative; + z-index: 1; +} + +.q-page { + margin-top: 5px; +} + +@media (max-width: 768px) { + .with-bg::before { + background-size: 260px auto; + } +} + +/* ===== ÜST BLOKLAR (SABİT) ===== */ +.filter-sticky { + position: sticky; + top: 56px; /* q-header yüksekliği */ + z-index: 300; + background: #fff; +} + +.filter-collapsible { + background: #fff; +} + +/* ===== TABLO SCROLL ===== */ +.table-scroll { + margin-top: 0; /* 🔹 Boşluğu kaldır */ + height: calc(100vh - 56px); /* 🔹 Header yüksekliği kadar kısalt */ + overflow-y: auto; + overflow-x: auto; + position: relative; +} + +.sticky-table .q-table__middle { + overflow: visible !important; + max-height: none !important; +} + +.sticky-table .q-table__top { + position: sticky; + top: 0; + z-index: 220; + background: #fff; + box-shadow: 0 2px 4px rgba(0,0,0,0.08); +} + +.sticky-table thead th { + position: sticky; + top: 40px; + z-index: 210; + background: #fff; +} + +/* 🔹 Toggle bar */ +.sticky-bar { + position: sticky; + top: 0; /* tablo scroll başladığında en üstte kalsın */ + z-index: 230; + background: #fff; + padding: 4px 8px; + border-bottom: 1px solid #ddd; +} + +/* ===== KOLON DARALTMA + WRAP ===== */ +.sticky-table thead th { + resize: horizontal; + overflow: auto; + min-width: 80px; + max-width: 400px; +} + +.sticky-table td { + min-width: 80px; + max-width: 400px; + white-space: normal !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + line-height: 1.2rem; + padding: 4px 8px !important; + font-weight: 600; + font-size: 0.95rem; +} + +/* ===== GÖRSEL ===== */ +.baggi-ppct { + display: block; + margin: 30px auto 0; + max-width: 400px; + opacity: 0.4; +} + +.col-desc { + white-space: normal !important; + word-break: break-word !important; + overflow-wrap: break-word; + font-size: 0.75rem !important; + line-height: 1.1rem; + width: 220px !important; + max-width: 220px !important; + min-width: 180px !important; +} + +/* ===== TABLO GÖRÜNÜM ===== */ +.custom-table { font-size: 0.8rem; } +.custom-table th { background: #fff; font-weight: 800; color: #222; } +.custom-table td { font-weight: 600; color: #333; } + +.custom-subtable { font-size: 0.72rem; background: #fafafa; } +.custom-subtable th { background: #f9f9f9; font-weight: 500; color: #555; } +.custom-subtable td { font-weight: 400; color: #666; } + +/* dar sütunlar için */ +.col-narrow { + font-size: 0.72rem; + padding: 2px 6px !important; + max-width: 90px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ===== GRUP SATIRI ===== */ +.group-row { + background: #f1f1f1 !important; + font-weight: 700 !important; + color: #222; + border-top: 2px solid #ccc; + border-bottom: 2px solid #ccc; +} + +/* ===== BALANCE CARD ===== */ +.balance-card { + width: 100%; + min-height: 120px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; +} + +.q-table td[data-col="belge_no"], +.q-table td[data-col="Belge_No"], +.q-table td[data-col="BELGE_NO"] { + color: var(--q-primary) !important; + font-weight: 600 !important; +} + +/* =========================================================== + PERMISSIONS PAGE (FINAL) + =========================================================== */ + +/* Toolbar */ +.permissions-toolbar { + position: sticky; + top: 42px; /* q-header yüksekliği */ + z-index: 300; + background: #fff; + display: flex; + align-items: center; + gap: 12px; + padding: 8px 16px; + border-bottom: 1px solid #ddd; +} + +/* Table scroll alanı */ +.permissions-table-scroll { + height: calc(100vh - 112px); /* header (56) + toolbar (56) */ + overflow-y: auto; + overflow-x: auto; + position: relative; +} + +/* Tablo gövdesi */ +.permissions-table .q-table__middle { + overflow: auto !important; + max-height: none !important; + padding-top: 0px; /* 🔑 Başlık yüksekliği kadar boşluk bırak */ +} + +/* Sticky başlıklar – toolbar’ın altında */ +.permissions-table thead th { + position: sticky; + top:10px; /* toolbar altında hizalanır */ + z-index: 210; + background: #fff; + box-shadow: 0 2px 4px rgba(0,0,0,0.08); +} + +/* Hücreler */ +.permissions-table td { + min-width: 80px; + max-width: 400px; + white-space: normal !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + line-height: 1.2rem; + padding: 4px 8px !important; + font-weight: 600; + font-size: 0.95rem; + background: #fff; +} + +/* İlk kolon (role) sabit */ +.permissions-table .permissions-sticky-col { + position: sticky; + left: 0; + z-index: 205; + background: #fff; + box-shadow: 2px 0 4px rgba(0,0,0,0.04); +} + +/* =========================================================== + 1️⃣ ROOT & GLOBAL RESET + =========================================================== */ +:root { + --header-h: 0px; + --filter-h: 72px; + --save-h: 60px; + --grid-header-h: 172px; + --sub-header-h: 34px; + --drawer-w: 240px; + + /* Grid kolon genişlikleri */ + --col-model: 90px; + --col-renk: 80px; + --col-ana: 100px; + --col-alt: 100px; + --col-aciklama: 140px; + --col-adet: 70px; + --col-fiyat: 70px; + --col-pb: 70px; + --col-tutar: 70px; + --col-termin: 142px; /* 🔹 termin tarihi kolon genişliği */ + + /* Beden blok ölçüleri */ + --grp-title-w: 90px; + --grp-title-gap: 4px; + --beden-w: 44px; + --beden-h: 28px; + --beden-count: 16; + + /* Tema renkleri */ + --baggi-gold: #c9a227; + --baggi-gold-pale: #fff9e6; + --baggi-gold-light: #fff7d2; + --baggi-cream: #fffef9; + --baggi-gray-border: #bbb; +} + +*, *::before, *::after { box-sizing: border-box; } +html, body { height: 100%; margin: 0; } +body { + background: #fff; + color: #222; + font-family: Inter, "Segoe UI", Arial, sans-serif; + font-size: 14px; + line-height: 1.4; +} +#q-app, .q-page-container { margin: 0; padding: 0; } +.q-layout__page { top: 0 !important; } +/* =========================================================== + 2️⃣ PAGE STRUCTURE & SCROLL + =========================================================== */ +.order-page { + display: flex; + flex-direction: column; + height: calc(100vh - var(--header-h)); + overflow-y: auto; + overflow-x: hidden; + background: #fff; +} + +.body--drawer-left-open .q-page-container { + margin-left: var(--drawer-w); + width: calc(100% - var(--drawer-w)); +} +.body--drawer-left-closed .q-page-container { + margin-left: 0; width: 100%; +} + +/* 🔸 Yatay scroll sadece grid alanında */ +.order-scroll-x { + flex: 1; + overflow-x: auto; + overflow-y: visible; + background: #fff; +} + +/* 🔸 Scrollbar stili */ +.order-page::-webkit-scrollbar, +.order-scroll-x::-webkit-scrollbar { + height: 8px; width: 8px; +} +.order-page::-webkit-scrollbar-thumb, +.order-scroll-x::-webkit-scrollbar-thumb { + background: #c0a75e; + border-radius: 4px; +} +.order-page::-webkit-scrollbar-track, +.order-scroll-x::-webkit-scrollbar-track { + background: #f9f5e6; +} +/* =========================================================== + 3️⃣ STICKY STACK (HEADER + TOOLBARS) + =========================================================== */ +.q-header { + position: sticky; + top: 0; + z-index: 1000; + box-shadow: 0 1px 2px rgba(0,0,0,0.08); +} + +.sticky-stack { + position: sticky; + top: var(--header-h); + margin-top: 0 !important; + z-index: 950; + display: flex; + flex-direction: column; + background: #fff; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); +} + +/* 🔹 Filtre bar */ +.filter-bar { + background: #fafafa; + border-bottom: 1px solid #ddd; + padding: 12px 24px; + margin-top:0 !important; +} + +/* 🔹 Save toolbar */ +.save-toolbar { + background: var(--baggi-gold-pale); + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding: 10px 16px; + display: flex; + justify-content: space-between; + align-items: center; + z-index: 940; +} +.save-toolbar .label { font-weight: 700; color: #6a5314; } +.save-toolbar .value { font-weight: 700; color: #000; } +.save-toolbar .q-btn { + font-weight: 600; + border-radius: 6px; + text-transform: none; +} +/* =========================================================== + 4️⃣ GRID HEADER (ANA BAŞLIK BLOKU) + =========================================================== */ +.order-grid-header { + position: sticky; + top: calc(var(--header-h) + var(--filter-h) + var(--save-h)); + z-index: 700; + display: grid; + grid-auto-flow: column; + grid-template-columns: + var(--col-model) + var(--col-renk) + var(--col-ana) + var(--col-alt) + var(--col-aciklama) + calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w)*var(--beden-count))) + var(--col-adet) + var(--col-fiyat) + var(--col-pb) + var(--col-tutar) + var(--col-termin); + background: var(--baggi-cream); + border-bottom: 2px solid var(--baggi-gray-border); + box-shadow: 0 2px 3px rgba(0,0,0,0.05); +} + +/* Sabit kolonlar */ +.order-grid-header .col-fixed { + display: flex; + justify-content: center; + align-items: center; + writing-mode: vertical-lr; + transform: rotate(180deg); + background: var(--baggi-gold-light); + border: 1px solid #aaa; + font-weight: 700; + font-size: 12.5px; + height: var(--grid-header-h); +} + +.order-grid-header .aciklama-col { + background: #fff9c4; + border-right: 2px solid #a6a6a6; +} +/* =========================================================== + 5️⃣ BEDEN BLOKLARI & SAĞ TOPLAM + =========================================================== */ +.order-grid-header .beden-block { + display: flex; + flex-direction: column; + height: var(--grid-header-h); + background: #fff; + border: 1px solid #ccc; +} + +.order-grid-header .grp-row { + display: flex; + align-items: center; + height: var(--beden-h); +} + +.order-grid-header .grp-title { + width: var(--grp-title-w); + text-align: right; + font-weight: 700; + font-size: 12px; + padding-right: 4px; +} + +.order-grid-header .grp-body { + display: grid; + grid-auto-flow: column; + grid-auto-columns: var(--beden-w); +} +.order-grid-header .grp-cell.hdr { + width: var(--beden-w); + height: var(--beden-h); + border: 1px solid #bbb; + font-size: 11.5px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; +} + +.order-grid-header .total-row { + display: flex; + align-items: stretch; + justify-content: space-between; + background: #fff59d; +} +.order-grid-header .total-cell { + width: var(--col-adet); + display: flex; + justify-content: center; + align-items: center; + writing-mode: vertical-lr; + transform: rotate(180deg); + border-right: 1px solid #bbb; + background: var(--baggi-gold-pale); + font-weight: 700; + font-size: 12px; +} +/* =========================================================== + 6️⃣ SUB-HEADER (ÜRÜN GRUBU BAR) — TAM HİZALANMIŞ + =========================================================== */ + .order-sub-header { + padding-right: 0 !important; /* 🔹 Ekstra sağ boşluğu kaldır */ + margin-right: 0 !important; +} +.order-sub-header { + position: sticky; + top: calc( + var(--header-h) + + var(--filter-h) + + var(--save-h) + + var(--grid-header-h) + ); + z-index: 650; + + /* 🔹 Header ile birebir grid düzeni */ + display: grid; + grid-auto-flow: column; + grid-template-columns: + var(--col-model) + var(--col-renk) + var(--col-ana) + var(--col-alt) + var(--col-aciklama) + calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count))) + var(--col-adet) + var(--col-fiyat) + var(--col-pb) + var(--col-tutar) + var(--col-termin); + + align-items: center; + justify-items: stretch; + height: var(--sub-header-h); + min-height: var(--sub-header-h); + + /* 🔹 Görsel */ + background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%); + border-top: 1px solid #d6c06a; + border-bottom: 1px solid #d6c06a; + + /* 🔹 Hatalı hizalamaları engelle */ + box-sizing: border-box; + overflow: hidden; + padding: 0 !important; + margin: 0 !important; + padding-right: 0 !important; /* ✅ sağ taşmayı önler */ +} + +/* 🔹 Genişlik eşitleme */ +:root { + --col-termin: 142px; /* ✅ q-input genişliğiyle birebir */ +} + +/* 🔹 Sub-header hover efekti */ +.order-sub-header:hover { + background: linear-gradient(90deg, #fff9cf 0%, #fff3b0 70%, #ffe88f 100%); +} + + +/* 🔹 Sol taraf (MODEL–AÇIKLAMA alanı) */ +.order-sub-header .sub-left { + grid-column: 1 / span 5; + font-weight: 800; + padding-left: 6px; + color: #2b1f05; + display: flex; + align-items: center; +} + +/* 🔹 Orta beden bloğu (header’la aynı yapı) */ +.order-sub-header .sub-center { + grid-column: 6 / 7; + display: grid; + grid-auto-flow: column; + grid-auto-columns: var(--beden-w); + justify-content: start; + align-items: center; + width: calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count))); + padding-left: var(--grp-title-w); + margin-left: var(--grp-title-gap); + height: 100%; + box-sizing: border-box; +} + +.order-sub-header .beden-cell { + width: var(--beden-w); + height: 100%; + border: 1px solid #d8c16b; + border-right: none; + background: #fffdf3; + font-size: 12px; + font-weight: 600; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; +} +.order-sub-header .beden-cell:last-child { + border-right: 1px solid #d8c16b; +} + +/* 🔹 Sağ taraf (adet–fiyat–pb–tutar–termin toplamları) */ +.order-sub-header .sub-right { + grid-column: 7 / -1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-end; + text-align: right; + padding-right: 6px; + font-weight: 900; + color: #3b2f09; + line-height: 1.3; + text-transform: uppercase; + font-size: 13.5px; +} + +.order-sub-header:hover { + background: linear-gradient(90deg,#fff9cf 0%,#fff3b0 70%,#ffe88f 100%); +} + +:root { + --sub-header-h: 60px; +} +/* SUB-HEADER sağ yazıyı 3 beden kolonu sola kaydır */ +.order-sub-header .sub-right { + transform: translateX(calc(-1 * var(--beden-w) * 4)); +} + +/* Taşmayı engelle (ihtiyaten) */ +.order-sub-header { + overflow: hidden; +} + +/* =========================================================== + 7️⃣ GRID BODY & SATIRLAR — TAM HİZALANMIŞ + =========================================================== */ +.order-grid-body { + position: relative; + background: #fff; + margin-top: 0 !important; + padding-top: 0; + z-index: 100; +} + +.summary-row { + display: grid; + grid-template-columns: + var(--col-model) + var(--col-renk) + var(--col-ana) + var(--col-alt) + var(--col-aciklama) + calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count))) + var(--col-adet) + var(--col-fiyat) + var(--col-pb) + var(--col-tutar) + var(--col-termin);} + +.summary-row:hover { + background: #fffce0; +} +.summary-row.is-editing { + background: #fff3cd; + outline: 2px solid #caa83f; +} + +.summary-row .cell { + display: flex; + align-items: center; + justify-content: center; + height: var(--beden-h); + padding: 4px 6px; + font-size: 13px; + color: #222; + box-sizing: border-box; +} + +.summary-row:nth-child(odd) { background: #fffef9; } + +/* 🔹 Beden blok hizalaması */ +.summary-row .grp-area { + display: flex; + flex-direction: column; + justify-content: center; + transform: translateX(calc(var(--grp-title-w) - var(--beden-w))); +} +.summary-row .grp-row { + display: grid; + grid-auto-flow: column; + grid-auto-columns: var(--beden-w); +} +.summary-row .grp-row .cell.beden { + width: var(--beden-w); + height: var(--beden-h); + border: 1px solid #ddd; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; +} +.cell.beden.ghost { + opacity: 0; + pointer-events: none; + border: 1px solid transparent !important; +} + +/* 🔹 Sağ kolonlar */ +.summary-row .cell.adet, +.summary-row .cell.fiyat, +.summary-row .cell.pb, +.summary-row .cell.tutar, +.summary-row .cell.termin { + font-weight: 600; + color: #000; + border-left: none !important; + height: 100%; +} + +.summary-row .cell.tutar { + text-align: right; + justify-content: flex-end; + padding-right: 8px; + border-right: none !important; +} + +.summary-row .cell.termin { + background: #fffef9; + justify-content: center; + align-items: center; + min-width: var(--col-termin); +} +.summary-row .cell.termin .q-input { + width: 100%; + max-width: 142px !important; + box-sizing: border-box; +} +.summary-row .cell.termin input { + text-align: center; + font-size: 13px; +} + +/* =========================================================== + 9️⃣ ORDER EDITOR (ALT FORM) + =========================================================== */ +.editor { + position: relative; + z-index: 50; + background: #fffef9; + border-top: 1px solid #ddd; + margin-top: 24px; + padding: 16px; +} +.editor::before { + content: ""; + display: block; + height: 4px; + background: linear-gradient(to right,#c9a227,#e5d28b,#fff7d2); + margin-bottom: 12px; + border-radius: 2px; +} +.editor .q-btn:hover { background: #d2b04d; } +.editor .q-input, +.editor .q-select { margin-bottom: 8px; font-size: 14px; } +.cell.termin .termin-label { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 13px; + color: #222; + background: #fffef9; + border-left: 1px solid #ccc; + box-sizing: border-box; +} + +/* =========================================================== + 🔟 RESPONSIVE + MİNÖR DÜZEN + =========================================================== */ +@media (max-width: 1024px) { + :root { --beden-w: 40px; --col-aciklama: 120px; } + .order-grid-header .col-fixed { font-size: 11px; } + .order-sub-header { font-size: 12.5px; } +} +@media (max-width: 768px) { + :root { + --beden-w: 36px; + --col-model: 70px; + --col-renk: 60px; + --col-aciklama: 100px; + } + .order-page { font-size: 13px; } + .order-grid-header .total-cell { font-size: 10.5px; } +} + +.summary-row .cell { + display: flex; + justify-content: center; + align-items: center; + padding: 4px 6px; + height: auto; + text-align: center; + white-space: normal; + word-wrap: break-word; +} + +.summary-row .grp-area, +.summary-row .grp-row, +.summary-row .grp-row .cell.beden { + align-items: center; + height: 100%; +} +.summary-row .cell.aciklama { + grid-column: 5 / 6 !important; /* sadece 5. kolon */ + position: relative !important; + width: calc(var(--col-aciklama) + 92px) !important; /* 🔹 74px genişletme */ + margin-right: -92px !important; /* 🔹 bedenle tam hizalanır */ + white-space: normal !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + line-height: 1.4 !important; + padding: 6px 12px !important; + font-size: 13px !important; + text-align: left !important; + display: flex !important; + flex-direction: column !important; + align-items: flex-start !important; + justify-content: flex-start !important; + min-height: 36px !important; + background: #fff !important; + box-sizing: border-box !important; + border-right: 1px solid #ccc !important; + z-index: 10 !important; +} +/* 🧩 Grid çizgi kontrastı güçlendirme */ +.summary-row .cell, +.order-grid-header .col-fixed, +.summary-row .grp-row .cell.beden { + border-color: #bbb !important; /* 🔹 daha belirgin çizgi */ +} + +.summary-row .cell:not(:last-child) { + border-right: 1px solid #bdbdbd !important; +} +/* =========================================================== + 🧱 ALT GRID ÇİZGİLERİ – TÜM SATIRLAR İÇİN + =========================================================== */ +.summary-row { + border-bottom: 1px solid #ccc; /* 🔹 satır alt çizgisi */ +} + +.summary-row:last-child { + border-bottom: 2px solid #b7a33a; /* 🔹 son satırda Baggi gold tonu */ +} + +/* 🔹 Hücrelerin alt çizgisi (beden dahil) */ +.summary-row .cell, +.summary-row .grp-row .cell.beden { + border-bottom: 1px solid #ddd !important; +} + +/* 🔹 Hover olduğunda grid çizgileri kaybolmasın */ +.summary-row:hover .cell, +.summary-row:hover .grp-row .cell.beden { + border-bottom: 1px solid #ccc !important; +} +.summary-row:hover { + background: #fffce0; +} + +.summary-row.is-editing { + background: #fff3cd; + outline: 2px solid #caa83f; + z-index: 2; +} + +.editor .q-btn:hover { + background: #d2b04d; + color: #fff; +} + +/* 🔹 Hover olduğunda grid çizgileri kaybolmasın */ +.summary-row:hover .cell, +.summary-row:hover .grp-row .cell.beden { + border-bottom: 1px solid #ccc !important; +} + +/* =========================================================== + 🎨 STOK RENKLERİ (LOW–MID–HIGH) + =========================================================== */ +.stok-red { + color: #e53935; /* 🔴 Kırmızı */ + font-weight: 600; +} + +.stok-yellow { + color: #f9a825; /* 🟡 Sarı */ + font-weight: 600; +} + +.stok-green { + color: #43a047; /* 🟢 Yeşil */ + font-weight: 600; +} +.q-banner.rounded-borders { + border-radius: 8px; +} +.order-gateway { + background: linear-gradient(145deg, #fff 0%, #fafafa 100%); + height: 100%; +} + +.order-btn { + font-size: 1.2rem; + padding: 20px 40px; + border-radius: 12px; + min-width: 280px; + transition: all 0.2s ease; +} +.order-btn:hover { + transform: translateY(-3px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} +/* =========================================================== + 🧭 DRAWER AÇIKKEN GRID HİZALAMA FIX + =========================================================== */ + +/* Drawer açıkken içerik kaymaması */ +.body--drawer-left-open .order-page { + width: calc(100vw - var(--drawer-w)); /* viewport'tan drawer genişliği kadar düş */ + overflow-x: hidden; /* dış overflow’u kes */ +} + +/* Scroll konteyner sadece grid içinde çalışsın */ +.order-scroll-x { + max-width: 100%; + overflow-x: auto; + overflow-y: visible; + background: #fff; + box-sizing: border-box; +} + +/* Scrollbar ve sağ boşluğu dengeler */ +.order-grid-header, +.order-sub-header, +.order-grid-body { + min-width: fit-content; + width: 100%; + box-sizing: border-box; +} +/* =========================================================== + 🧱 DRAWER AÇIKKEN TAM HİZALAMA FIX (v2) + =========================================================== */ + +/* Drawer açıkken tüm üst bloklar sağdan taşmasın */ +.body--drawer-left-open .filter-bar, +.body--drawer-left-open .save-toolbar, +.body--drawer-left-open .order-grid-header, +.body--drawer-left-open .order-sub-header, +.body--drawer-left-open .order-grid-body { + width: calc(100vw - var(--drawer-w)); /* drawer genişliği kadar daralt */ + margin-left: 0; + margin-right: 0; + overflow-x: hidden; + box-sizing: border-box; +} + +/* Drawer kapalıyken tam genişlik */ +.body--drawer-left-closed .filter-bar, +.body--drawer-left-closed .save-toolbar, +.body--drawer-left-closed .order-grid-header, +.body--drawer-left-closed .order-sub-header, +.body--drawer-left-closed .order-grid-body { + width: 100vw; +} + +/* Order grid sağ sınırı altın kenarlıkla bitir (optik kapanış) */ +.order-grid-header, +.order-sub-header, +.order-grid-body { + border-right: 2px solid var(--baggi-gold); +} +/* =========================================================== + 🎯 SAĞ ALT BOŞLUK FİNAL FIX + =========================================================== */ + +/* Drawer açıkken tüm grid konteynerleri sağdan tam sıfırla */ +.body--drawer-left-open .order-page, +.body--drawer-left-open .filter-bar, +.body--drawer-left-open .save-toolbar, +.body--drawer-left-open .order-grid-header, +.body--drawer-left-open .order-sub-header, +.body--drawer-left-open .order-grid-body { + width: calc(100vw - var(--drawer-w) - 8px); /* 🔹 scrollbar toleransı */ + padding-right: 0 !important; + margin-right: 0 !important; + overflow-x: hidden !important; +} + +/* Son altın kenarlık hizasını koru */ +.order-grid-body { + border-right: 2px solid var(--baggi-gold); +} +/* =========================================================== + 🎯 GRID SAĞ HİZALAMA (FILTER + SAVE + HEADER) + =========================================================== */ + +/* Ana scroll container referansı */ +.order-scroll-x { + display: flex; + flex-direction: column; + align-items: flex-start; /* hizalama sola */ + overflow-x: auto; + overflow-y: visible; + background: #fff; +} + +/* Filter ve Save barlar grid genişliğini takip etsin */ +.filter-bar, +.save-toolbar, +.order-grid-header, +.order-sub-header { + width: fit-content; /* içeriğe göre genişlik */ + min-width: 100%; /* minimum ekran kadar */ + box-sizing: border-box; +} + +/* Grid body’nin genişliği kadar sağ hizalama */ +.order-grid-body { + width: fit-content; + box-sizing: border-box; +} + +/* Sağ kenarda taşma veya padding olmasın */ +.filter-bar, +.save-toolbar, +.order-grid-header, +.order-sub-header, +.order-grid-body { + margin-right: 0 !important; + padding-right: 0 !important; + border-right: none !important; /* altın çizgi istemiyorsan kaldırılır */ +} + +/* Drawer açık/kapalı fark etmeden */ +.body--drawer-left-open .order-scroll-x, +.body--drawer-left-closed .order-scroll-x { + width: 100%; + overflow-x: auto; +} + +/* =============================== + ORDER LIST (ol-) — Sticky Stack + =============================== */ + +:root { + /* Quasar header yüksekliği */ + --ol-header-h: 56px; + /* Filter bar yüksekliği (px) — inputlar tek satırsa 56 idealdir */ + --ol-filter-h: 96px; +} + +/* q-page tek scroller: header altından başlar */ +.ol-page { + height: calc(100vh - var(--ol-header-h)); + overflow: auto; /* 🔑 tek scroll container */ + background: #fff; + display: flex; + flex-direction: column; +} + +/* Filter bar: q-header’ın altında sticky */ +.ol-filter-bar { + position: sticky; + top: 0; /* 🔑 .ol-page scroller’ında en üst */ + z-index: 600; + background: #fff; + border-bottom: 1px solid #ddd; + padding: 10px 16px; + box-shadow: 0 1px 2px rgba(0,0,0,0.06); + min-height: var(--ol-filter-h); + display: flex; + align-items: center; +} + +/* QTable: sticky thead, zebra aktif ve çakışma yok */ +.ol-table .q-table__middle { + overflow: visible !important; /* sticky thead için güvenli */ + max-height: none !important; +} + +/* thead sabitleme: filter bar’ın ALTINA oturur */ +.ol-table thead th { + position: sticky; + top: var(--ol-filter-h); /* 🔑 filter yüksekliği kadar boşluk */ + z-index: 500; + background: #fff; + box-shadow: 0 2px 4px rgba(0,0,0,0.08); + font-weight: 700; +} + +/* Zebra */ +.ol-table .q-table__body .q-tr:nth-child(odd) { + background-color: #f7f7f7 !important; +} +.ol-table .q-table__body .q-tr:nth-child(even) { + background-color: #ffffff !important; +} +.ol-table .q-table__body .q-tr:hover { + background-color: #fff7d1 !important; + transition: background-color .15s ease; +} + +/* Hücreler */ +.ol-table .q-td { + font-size: .9rem; + line-height: 1.3; + padding: 6px 8px !important; +} + +/* Güvenli z-index hiyerarşisi */ +.q-header { z-index: 1000 !important; } /* header en üstte */ +.q-drawer { z-index: 950 !important; } /* drawer header’ın altında */ + +/* Mobile */ +@media (max-width: 768px) { + :root { --ol-filter-h: 64px; } /* input kırılıyorsa biraz artır */ + .ol-filter-bar { padding: 8px 12px; } +} +/* =========================================================== + 🟡 ORDERLIST ZEBRA FIX (v3) + =========================================================== */ + +/* Her iki tr katmanını da hedefliyoruz (Quasar q-tr + native tr) */ +.ol-table tbody tr:nth-child(odd), +.ol-table .q-table__body .q-tr:nth-child(odd) { + background-color: #faf8ef !important; /* açık krem tonu */ +} + +.ol-table tbody tr:nth-child(even), +.ol-table .q-table__body .q-tr:nth-child(even) { + background-color: #ffffff !important; +} + +/* Hover tonu: hafif Baggi gold dokunuşu */ +.ol-table tbody tr:hover, +.ol-table .q-table__body .q-tr:hover { + background-color: #fff4cc !important; + transition: background-color 0.2s ease; +} diff --git a/ui/src/stores/deneme3 b/ui/src/stores/deneme3 new file mode 100644 index 0000000..84a44eb --- /dev/null +++ b/ui/src/stores/deneme3 @@ -0,0 +1,653 @@ +// src/stores/orderentryStore.js +import { defineStore } from 'pinia' +import axios from 'axios' +import qs from 'qs' +import { useAuthStore } from 'src/stores/authStore' +import dayjs from 'src/boot/dayjs' +import { ref, watch } from 'vue' + +/* ========================================================== + Reaktif shared referanslar (bazı UI yardımcıları) +========================================================== */ +const stockMap = ref({}) // { "48": 12, "50": 7, ... } +const bedenStock = ref([]) // [{beden:'48', stok:12}, ...] +const sizeCache = ref({}) // beden/stok cache (component tarafı çağırıyor) + +/* ========================================================== + STORE +========================================================== */ +export const useOrderentryStore = defineStore('orderentry', { + state: () => ({ + /* 🔹 Ana durumlar */ + orders: [], // grid kaynak array’i (summaryRows ile senkron) + loading: false, + selected: null, // UI’de seçili satır + error: null, + + /* 🔹 Cari */ + customers: [], + selectedCustomer: null, + + /* 🔹 Ürün zinciri */ + products: [], + colors: [], + secondColors: [], + inventory: [], + + selectedProduct: null, + selectedColor: null, + selectedColor2: null, + + /* 🔹 Transaction & Storage */ + activeTransactionId: null, + persistKey: 'bss_orderentry_data', // ♻️ kalıcı depolama key’i + lastSnapshotKey: 'bss_orderentry_snapshot', // son-kaydedilen-sipariş + + /* 🔹 Düzenleme durumu */ + editingIndex: -1, + currentOrderId: null, // edit modunda header ID + header: {}, // backend header modeli + mode: 'new' // 'new' | 'edit' + }), + + getters: { + /* 🔹 Toplam adet */ + totalQty(state) { + return state.orders.reduce((sum, r) => sum + (Number(r.adet) || 0), 0) + }, + /* 🔹 Toplam tutar (string fix2) */ + totalAmount(state) { + const n = state.orders.reduce((s, r) => s + (Number(r.tutar) || 0), 0) + return isNaN(n) ? '0.00' : n.toFixed(2) + }, + /* 🔹 Müşteri bazlı gruplanmış (opsiyonel) */ + groupedByCustomer(state) { + const out = {} + for (const row of state.orders) { + const k = row.musteri || '—' + if (!out[k]) out[k] = [] + out[k].push(row) + } + return out + }, + /* 🔹 2. renk var mı? */ + hasSecondColor(state) { + return Array.isArray(state.secondColors) && state.secondColors.length > 0 + }, + /* 🔹 Envanter toplamı */ + totalInventoryQty(state) { + return state.inventory.reduce((s, r) => s + (Number(r.kullanilabilir) || 0), 0) + } + }, + actions: { + /* ========================================================== + STORAGE — Kalıcı kayıt yardımcıları + ========================================================== */ + saveToStorage() { + try { + const payload = { + orders: this.orders, + header: this.header, + currentOrderId: this.currentOrderId, + selectedCustomer: this.selectedCustomer, + activeTransactionId: this.activeTransactionId, + mode: this.mode, + savedAt: dayjs().toISOString() + } + localStorage.setItem(this.persistKey, JSON.stringify(payload)) + } catch (err) { + console.warn('⚠️ localStorage kaydı başarısız:', err) + } + }, + + /* Kayıt sonrası görüntülenecek "snapshot". + UI’yi temizlesen bile sayfa yenilenince bu snapshot geri yüklenebilir. */ + saveSnapshot(tag = 'post-submit') { + try { + const snap = { + tag, + orders: this.orders, + header: this.header, + currentOrderId: this.currentOrderId, + selectedCustomer: this.selectedCustomer, + mode: this.mode, + savedAt: dayjs().toISOString() + } + localStorage.setItem(this.lastSnapshotKey, JSON.stringify(snap)) + } catch (e) { + console.warn('⚠️ saveSnapshot hatası:', e) + } + }, + + loadFromStorage() { + try { + const raw = localStorage.getItem(this.persistKey) + if (!raw) return + const data = JSON.parse(raw) + if (Array.isArray(data.orders)) this.orders = data.orders + this.header = data.header || {} + this.currentOrderId = data.currentOrderId || null + this.selectedCustomer = data.selectedCustomer || null + this.activeTransactionId = data.activeTransactionId || null + this.mode = data.mode || 'new' + console.log(`♻️ Storage yüklendi • mode:${this.mode} • rows:${this.orders.length}`) + } catch (err) { + console.warn('⚠️ localStorage okuma hatası:', err) + } + }, + + loadSnapshot() { + try { + const raw = localStorage.getItem(this.lastSnapshotKey) + if (!raw) return null + return JSON.parse(raw) + } catch (e) { + console.warn('⚠️ loadSnapshot hatası:', e) + return null + } + }, + + clearStorage() { + localStorage.removeItem(this.persistKey) + // snapshot’ı silmiyoruz → kullanıcı isterse elle siler + }, + /* ========================================================== + TRANSACTION STATE + ========================================================== */ + setTransaction(id) { + this.activeTransactionId = id + this.saveToStorage() + }, + + async initTransaction() { + if (this.activeTransactionId) { + console.log('🔹 Aktif transaction:', this.activeTransactionId) + return this.activeTransactionId + } + try { + const dummyId = Math.floor(100000 + Math.random() * 900000) + this.activeTransactionId = dummyId + this.saveToStorage() + console.log('🧩 Dummy Transaction başlatıldı:', dummyId) + return dummyId + } catch (err) { + console.error('❌ Dummy transaction başlatılamadı:', err) + return null + } + }, + + clearTransaction() { + this.activeTransactionId = null + this.saveToStorage() + }, + + /* Orders’ı otomatik kaydeden watcher (component’ten çağrılır) */ + watchOrders() { + watch( + () => this.orders, + () => { + // her değişimde full storage yaz + this.saveToStorage() + }, + { deep: true } + ) + }, + /* ========================================================== + CRUD — Frontend grid’i ile senkron temel aksiyonlar + ========================================================== */ + + addRow(row) { + if (!row) return + this.orders.push({ ...row }) + this.saveToStorage() + }, + + updateRow(idOrIndex, patch) { + if (idOrIndex == null) return + let idx = -1 + if (typeof idOrIndex === 'number') { + idx = idOrIndex + } else { + // id ile bul + idx = this.orders.findIndex(r => r.id === idOrIndex) + } + if (idx >= 0 && this.orders[idx]) { + this.orders[idx] = { ...this.orders[idx], ...patch } + this.saveToStorage() + } + }, + + removeRow(idOrIndex) { + let idx = -1 + if (typeof idOrIndex === 'number') { + idx = idOrIndex + } else { + idx = this.orders.findIndex(r => r.id === idOrIndex) + } + if (idx >= 0) { + this.orders.splice(idx, 1) + this.saveToStorage() + } + }, + /* ========================================================== + PRICE / LIMIT — Minimum fiyat sorgusu (model + PB) + Beklenen response: { price, priceTRY, rateToTRY } + ========================================================== */ + async fetchMinPrice(modelCode, pb) { + if (!modelCode || !pb) return null + try { + const baseURL = 'http://localhost:8080' + const res = await axios.get(`${baseURL}/api/min-price`, { + params: { code: modelCode, pb } + }) + const d = res?.data || null + if (!d) return null + + // normalize + return { + price: Number(d.price ?? d.Price ?? 0), + priceTRY: Number(d.priceTRY ?? d.PriceTRY ?? d.price_try ?? 0), + rateToTRY: Number(d.rateToTRY ?? d.RateToTRY ?? d.rate ?? 1) + } + } catch (e) { + console.warn('⚠️ fetchMinPrice hata:', e) + return null + } + }, + /* ========================================================== + LOAD (EDIT MODE) — Sunucudan Siparişi Açma + ========================================================== */ + async openById(id) { + if (!id) return + this.loading = true + try { + const auth = useAuthStore() + const res = await axios.get(`http://localhost:8080/api/order/get/${id}`, { + headers: { Authorization: `Bearer ${auth.token}` } + }) + const data = res.data || {} + + // 🔹 sql.Null* flatten helper + const flat = (v) => { + if (v === null || v === undefined) return null + if (typeof v === 'object' && 'Valid' in v) { + return v.Valid + ? v.String ?? v.Float64 ?? v.Int32 ?? v.Time ?? null + : null + } + return v + } + + /* ============================================================ + 🧾 HEADER MAPPING (73 kolon) + ============================================================ */ + const h = data.header || {} + const header = { + // Görünen alanlar + OrderHeaderID: flat(h.OrderHeaderID) || '', + OrderNumber: flat(h.OrderNumber) || '', + OrderDate: flat(h.OrderDate) + ? String(flat(h.OrderDate)).substring(0, 10) + : '', + AverageDueDate: flat(h.AverageDueDate) + ? String(flat(h.AverageDueDate)).substring(0, 10) + : '', + Description: flat(h.Description) || '', + CurrAccCode: flat(h.CurrAccCode) || '', + DocCurrencyCode: flat(h.DocCurrencyCode) || 'TRY', + + // Arka plan alanlar (backend roundtrip) + OrderTypeCode: flat(h.OrderTypeCode) || 1, + ProcessCode: flat(h.ProcessCode) || 'WS', + IsCancelOrder: flat(h.IsCancelOrder) || 0, + OrderTime: flat(h.OrderTime) || '', + DocumentNumber: flat(h.DocumentNumber) || '', + PaymentTerm: flat(h.PaymentTerm) || '', + InternalDescription: flat(h.InternalDescription) || '', + CurrAccTypeCode: flat(h.CurrAccTypeCode) || '', + SubCurrAccID: flat(h.SubCurrAccID) || '', + ContactID: flat(h.ContactID) || '', + ShipmentMethodCode: flat(h.ShipmentMethodCode) || '', + ShippingPostalAddressID: flat(h.ShippingPostalAddressID) || '', + BillingPostalAddressID: flat(h.BillingPostalAddressID) || '', + GuarantorContactID: flat(h.GuarantorContactID) || '', + GuarantorContactID2: flat(h.GuarantorContactID2) || '', + RoundsmanCode: flat(h.RoundsmanCode) || '', + DeliveryCompanyCode: flat(h.DeliveryCompanyCode) || '', + TaxTypeCode: flat(h.TaxTypeCode) || '', + WithHoldingTaxTypeCode: flat(h.WithHoldingTaxTypeCode) || '', + DOVCode: flat(h.DOVCode) || '', + TaxExemptionCode: flat(h.TaxExemptionCode) || 0, + CompanyCode: flat(h.CompanyCode) || 1, + OfficeCode: flat(h.OfficeCode) || '101', + StoreTypeCode: flat(h.StoreTypeCode) || 5, + StoreCode: flat(h.StoreCode) || 0, + POSTerminalID: flat(h.POSTerminalID) || 0, + WarehouseCode: flat(h.WarehouseCode) || '1-0-12', + ToWarehouseCode: flat(h.ToWarehouseCode) || '', + OrdererCompanyCode: flat(h.OrdererCompanyCode) || 1, + OrdererOfficeCode: flat(h.OrdererOfficeCode) || '101', + OrdererStoreCode: flat(h.OrdererStoreCode) || '', + GLTypeCode: flat(h.GLTypeCode) || '', + LocalCurrencyCode: flat(h.LocalCurrencyCode) || 'TRY', + ExchangeRate: flat(h.ExchangeRate) || 1, + DiscountReasonCode: flat(h.DiscountReasonCode) || 0, + SurplusOrderQtyToleranceRate: flat(h.SurplusOrderQtyToleranceRate) || 0, + IncotermCode1: flat(h.IncotermCode1) || '', + IncotermCode2: flat(h.IncotermCode2) || '', + PaymentMethodCode: flat(h.PaymentMethodCode) || '', + IsInclutedVat: flat(h.IsInclutedVat) || 0, + IsCreditSale: flat(h.IsCreditSale) || 1, + IsCreditableConfirmed: flat(h.IsCreditableConfirmed) || 1, + CreditableConfirmedUser: flat(h.CreditableConfirmedUser) || '', + CreditableConfirmedDate: flat(h.CreditableConfirmedDate) || '', + ApplicationCode: flat(h.ApplicationCode) || 'Order', + ApplicationID: flat(h.ApplicationID) || '', + CreatedUserName: flat(h.CreatedUserName) || '', + CreatedDate: flat(h.CreatedDate) || '', + LastUpdatedUserName: flat(h.LastUpdatedUserName) || '', + LastUpdatedDate: flat(h.LastUpdatedDate) || '', + IsProposalBased: flat(h.IsProposalBased) || 0 + } + + this.header = header + this.currentOrderId = header.OrderHeaderID || id + this.mode = 'edit' + + // 🔹 Cari görünümü (QSelect) + this.selectedCustomer = { + value: header.CurrAccCode || '', + label: `${header.CurrAccCode || ''} - ${flat(h.CurrAccDescription) || ''}` + } + + /* ============================================================ + 📦 LINES MAPPING (57 kolon) + ============================================================ */ + this.orders = (data.lines || []).map((l, idx) => ({ + // Görünen alanlar + id: flat(l.OrderLineID) || `row-${idx + 1}`, + model: flat(l.ItemCode), + renk: flat(l.ColorCode), + renk2: flat(l.ItemDim2Code), + fiyat: Number(flat(l.Price) || 0), + pb: flat(l.DocCurrencyCode) || flat(l.PriceCurrencyCode) || 'USD', + adet: Number(flat(l.Qty1) || 0), + tutar: Number(flat(l.Price) || 0) * Number(flat(l.Qty1) || 0), + aciklama: flat(l.LineDescription) || '', + terminTarihi: flat(l.DeliveryDate) + ? String(flat(l.DeliveryDate)).substring(0, 10) + : '', + urunAnaGrubu: flat(l.ProductGroup) || '', + urunAltGrubu: flat(l.ProductSubGroup) || '', + grpKey: l.grpKey || 'tak', + bedenMap: l.BedenMap || {}, + + // Backend roundtrip alanları + SortOrder: flat(l.SortOrder) || 0, + ItemTypeCode: flat(l.ItemTypeCode) || 1, + ItemDim1Code: flat(l.ItemDim1Code) || '', + ItemDim3Code: flat(l.ItemDim3Code) || '', + Qty2: flat(l.Qty2) || 0, + CancelQty1: flat(l.CancelQty1) || 0, + CancelQty2: flat(l.CancelQty2) || 0, + CancelDate: flat(l.CancelDate) || null, + OrderCancelReasonCode: flat(l.OrderCancelReasonCode) || '', + ClosedDate: flat(l.ClosedDate) || null, + IsClosed: flat(l.IsClosed) || false, + VatRate: flat(l.VatRate) || 10, + PCTRate: flat(l.PCTRate) || 0, + PriceCurrencyCode: flat(l.PriceCurrencyCode) || 'TRY', + PriceExchangeRate: flat(l.PriceExchangeRate) || header.ExchangeRate || 1, + CreatedUserName: flat(l.CreatedUserName) || '', + CreatedDate: flat(l.CreatedDate) || '', + LastUpdatedUserName: flat(l.LastUpdatedUserName) || '', + LastUpdatedDate: flat(l.LastUpdatedDate) || '', + SurplusOrderQtyToleranceRate: + flat(l.SurplusOrderQtyToleranceRate) || 0 + })) + + /* ============================================================ + 💾 LOCAL STORAGE + ============================================================ */ + localStorage.setItem( + `bssapp:order:last:${id}`, + JSON.stringify({ header, lines: this.orders }) + ) + + console.log(`📦 Sipariş (${id}) yüklendi • rows:${this.orders.length}`) + } catch (err) { + console.error('❌ openById hatası:', err) + this.error = err.message + } finally { + this.loading = false + } +} +, + /* ========================================================== + NEW TEMPLATE — Yeni sipariş başlatma + ========================================================== */ + newOrderTemplate() { + const today = dayjs().format('YYYY-MM-DD') + const due = dayjs().add(30, 'day').format('YYYY-MM-DD') + + this.header = { + OrderHeaderID: '', + OrderTypeCode: 1, + ProcessCode: 'WS', + OrderNumber: '', + OrderDate: today, + AverageDueDate: due, + Description: '', + CurrAccCode: '', + CurrAccDescription: '', + DocCurrencyCode: 'USD', + LocalCurrencyCode: 'TRY', + ExchangeRate: 1, + CompanyCode: 1, + OfficeCode: '101', + StoreTypeCode: 5, + WarehouseCode: '1-0-12', + IsCreditSale: true, + CreatedUserName: '', + CreatedDate: today, + LastUpdatedUserName: '', + LastUpdatedDate: today + } + + this.orders = [] + this.currentOrderId = null + this.activeTransactionId = null + this.selectedCustomer = null + this.mode = 'new' + this.error = null + + // Temiz bir başlangıcı storage’a yaz + this.saveToStorage() + console.log('🧾 Yeni sipariş template yüklendi.') + }, + /* ========================================================== + SUBMIT — Create/Update (SQL tablo INSERT/UPDATE) + ➜ Kayıt sonrası: transaction kapanır AMA snapshot tutulur. + ========================================================== */ + async submitAll() { + const auth = useAuthStore() + const baseURL = 'http://localhost:8080' + + const toNullable = (v, type = 'string') => { + if (v === null || v === undefined || v === '') { + if (type === 'number') return { Float64: 0, Valid: false } + if (type === 'time') return { Time: null, Valid: false } + return { String: '', Valid: false } + } + if (type === 'number') return { Float64: Number(v), Valid: true } + if (type === 'time') return { Time: v, Valid: true } + return { String: String(v), Valid: true } + } + + try { + this.loading = true + + // Header payload (backend’in beklediği Null* formatıyla) + const h = this.header || {} + const headerPayload = { + OrderHeaderID: h.OrderHeaderID || this.currentOrderId || '', + OrderTypeCode: toNullable(1, 'number'), + ProcessCode: toNullable('WS'), + OrderNumber: toNullable(h.OrderNumber), + OrderDate: toNullable(h.OrderDate || dayjs().format('YYYY-MM-DD'), 'time'), + AverageDueDate: toNullable(h.AverageDueDate || dayjs().add(30, 'day').format('YYYY-MM-DD'), 'time'), + Description: toNullable(h.Description || ''), + CurrAccCode: toNullable(h.CurrAccCode || this.selectedCustomer?.value || ''), + CurrAccDescription: toNullable(h.CurrAccDescription || this.selectedCustomer?.label || ''), + DocCurrencyCode: toNullable(h.DocCurrencyCode || 'USD'), + LocalCurrencyCode: toNullable(h.LocalCurrencyCode || 'TRY'), + ExchangeRate: toNullable(h.ExchangeRate || 1, 'number'), + CompanyCode: toNullable(1, 'number'), + OfficeCode: toNullable('101'), + StoreTypeCode: toNullable(5, 'number'), + WarehouseCode: toNullable(h.WarehouseCode || '1-0-12'), + IsCreditSale: true, + CreatedUserName: toNullable(auth.user?.Username || 'admin'), + CreatedDate: toNullable(h.CreatedDate || dayjs().format('YYYY-MM-DD'), 'time'), + LastUpdatedUserName: toNullable(auth.user?.Username || 'admin'), + LastUpdatedDate: toNullable(dayjs().format('YYYY-MM-DD HH:mm:ss'), 'time') + } + + // Lines payload + const linesPayload = this.orders.map((l, idx) => ({ + OrderLineID: l.id || '', + SortOrder: idx + 1, + ItemTypeCode: toNullable(1, 'number'), + ItemCode: toNullable(l.model), + ColorCode: toNullable(l.renk), + ItemDim1Code: toNullable(Object.keys(l.bedenMap?.[l.grpKey] || {})[0] || ''), + ItemDim2Code: toNullable(l.renk2), + Qty1: toNullable(Number(l.adet || 0), 'number'), + Price: toNullable(Number(l.fiyat || 0), 'number'), + DocCurrencyCode: toNullable(l.pb || 'USD'), + VatRate: toNullable(10, 'number'), + PCTRate: toNullable(0, 'number'), + DeliveryDate: toNullable(l.terminTarihi || null, 'time'), + LineDescription: toNullable(l.aciklama || ''), + IsClosed: false, + CreatedUserName: toNullable(auth.user?.Username || 'admin'), + CreatedDate: toNullable(dayjs().format('YYYY-MM-DD HH:mm:ss'), 'time'), + LastUpdatedUserName: toNullable(auth.user?.Username || 'admin'), + LastUpdatedDate: toNullable(dayjs().format('YYYY-MM-DD HH:mm:ss'), 'time') + })) + + // Final payload + const payload = { + header: headerPayload, + lines: linesPayload, + user: auth.user?.Username || 'admin' + } + + let res + if (this.currentOrderId) { + // UPDATE + res = await axios.post(`${baseURL}/api/order/update`, payload, { + headers: { Authorization: `Bearer ${auth.token}` } + }) + console.log('✅ UPDATE ok:', res.data) + } else { + // CREATE + res = await axios.post(`${baseURL}/api/order/create`, payload, { + headers: { Authorization: `Bearer ${auth.token}` } + }) + console.log('✅ CREATE ok:', res.data) + if (res.data?.orderID) { + this.currentOrderId = res.data.orderID + this.header.OrderHeaderID = res.data.orderID + this.mode = 'edit' + } + } + + // 🟩 Kayıt sonrası: snapshot’ı al ve storage’a da yaz + this.saveSnapshot('post-submit') + this.saveToStorage() + + // 🧹 Transaction’ı kapat (UI temizliği ayrı fonksiyonda) + this.clearTransaction() + this.afterSubmit({ keepLocalStorage: true }) // 👈 önemli + + } catch (err) { + console.error('❌ submitAll hatası:', err) + this.error = err.message + throw err + } finally { + this.loading = false + } + }, + /* ========================================================== + AFTER SUBMIT — UI temizliği (snapshot kalır!) + keepLocalStorage=true → persistKey SİLİNMEZ + ========================================================== */ + afterSubmit(opts = { keepLocalStorage: true }) { + try { + // Snapshot zaten kaydedildi; istenirse persistKey’i bırak + if (!opts?.keepLocalStorage) { + localStorage.removeItem(this.persistKey) + } else { + // son hal zaten saveToStorage ile yazıldı — dokunma + } + + // UI temizliği (hafızada formu boşaltalım) + // Ama edit’e dönmek istersen, snapshot/loadFromStorage ile geri getirirsin. + this.orders = [] + // header’ı hafızadan temizliyoruz ama snapshot yerinde. + this.header = {} + this.selectedCustomer = null + this.editingIndex = -1 + // currentOrderId’yi istersen koruyabilirsin; biz editte geri yüklüyoruz. + // burada null’lıyoruz: + this.currentOrderId = null + this.mode = 'new' + this.loading = false + this.error = null + + console.log('🧹 afterSubmit: UI temizlendi, snapshot storage’da.') + } catch (err) { + console.warn('⚠️ afterSubmit temizleme hatası:', err) + } + }, + + /* ========================================================== + MANUAL UPDATE — mevcut header/lines yapılarına göre + (İsteğe bağlı kullanılır) + ========================================================== */ + async updateOrder() { + if (!this.currentOrderId) { + console.warn('⚠️ currentOrderId yok, update yapılamaz.') + return + } + try { + const auth = useAuthStore() + const payload = { + header: this.header, + lines: this.orders, + username: auth.user?.Username || 'admin' + } + const res = await axios.post( + 'http://localhost:8080/api/order/update', + payload, + { headers: { Authorization: `Bearer ${auth.token}` } } + ) + console.log('✅ Güncelleme tamamlandı:', res.data) + + // kayıt sonrası snapshot + persist + this.saveSnapshot('manual-update') + this.saveToStorage() + } catch (err) { + console.error('❌ updateOrder hatası:', err) + this.error = err.message + } + } + } // actions +}) // defineStore +// (opsiyonel) Bu referanslara component tarafından erişmek istersen: +export const sharedOrderEntryRefs = { + stockMap, + bedenStock, + sizeCache +} diff --git a/ui/src/stores/downloadstHeadStore.js b/ui/src/stores/downloadstHeadStore.js new file mode 100644 index 0000000..ad60b1f --- /dev/null +++ b/ui/src/stores/downloadstHeadStore.js @@ -0,0 +1,50 @@ +// src/stores/downloadstHeadStore.js +import { defineStore } from 'pinia' +import { download } from 'src/services/api' + +export const useDownloadstHeadStore = defineStore('downloadstHead', { + actions: { + // 📄 Statement Header PDF indir / aç + async handlestHeadDownload ( + accountCode, + startDate, + endDate, + parislemler + ) { + try { + // ✅ Params (axios paramsSerializer array=repeat destekliyor) + const params = { + accountcode: accountCode, + startdate: startDate, + enddate: endDate + } + + if (Array.isArray(parislemler) && parislemler.length > 0) { + params.parislemler = parislemler.filter( + p => p !== undefined && p !== null && p !== '' + ) + } + + // 🔥 API CALL (TOKEN + BLOB + ERROR HANDLING OTOMATİK) + const blob = await download( + '/exportstamentheaderreport-pdf', + params + ) + + const pdfUrl = window.URL.createObjectURL(blob) + window.open(pdfUrl, '_blank') + + return { ok: true, message: '📄 PDF hazırlandı' } + + } catch (err) { + console.error('❌ PDF açma hatası:', err) + return { + ok: false, + message: + err?.message || + '❌ PDF açma hatası' + } + } + } + } +}) diff --git a/ui/src/stores/downloadstpdfStore.js b/ui/src/stores/downloadstpdfStore.js new file mode 100644 index 0000000..d857edd --- /dev/null +++ b/ui/src/stores/downloadstpdfStore.js @@ -0,0 +1,51 @@ +// src/stores/downloadstpdfStore.js +import { defineStore } from 'pinia' +import { download } from 'src/services/api' + +export const useDownloadstpdfStore = defineStore('downloadstpdf', { + actions: { + /* ========================================================== + 📄 PDF İNDİR / AÇ + ========================================================== */ + async downloadPDF(accountCode, startDate, endDate, parislemler = []) { + try { + // 🔹 Query params + const params = { + accountcode: accountCode, + startdate: startDate, + enddate: endDate + } + + if (Array.isArray(parislemler) && parislemler.length > 0) { + params.parislemler = parislemler.filter( + p => p !== undefined && p !== null && p !== '' + ) + } + + // 🔥 MERKEZİ API — BLOB + const blob = await download('/export-pdf', params) + + // 🔹 Blob → URL + const pdfUrl = window.URL.createObjectURL( + new Blob([blob], { type: 'application/pdf' }) + ) + + // 🔹 Yeni sekmede aç + window.open(pdfUrl, '_blank') + + console.log('✅ PDF yeni sekmede açıldı') + return { ok: true, message: '📄 PDF hazırlandı' } + + } catch (err) { + console.error('❌ PDF açma hatası:', err) + + return { + ok: false, + message: + err?.message || + '❌ PDF alınamadı' + } + } + } + } +}) diff --git a/ui/src/stores/example-store.js b/ui/src/stores/example-store.js new file mode 100644 index 0000000..83e8390 --- /dev/null +++ b/ui/src/stores/example-store.js @@ -0,0 +1,15 @@ +import { defineStore } from 'pinia'; + +export const useCounterStore = defineStore('counter', { + state: () => ({ + counter: 0, + }), + getters: { + doubleCount: (state) => state.counter * 2, + }, + actions: { + increment() { + this.counter++; + }, + }, +}); diff --git a/ui/src/stores/index.js b/ui/src/stores/index.js new file mode 100644 index 0000000..a260a8c --- /dev/null +++ b/ui/src/stores/index.js @@ -0,0 +1,20 @@ +import { defineStore } from '#q-app/wrappers' +import { createPinia } from 'pinia' + +/* + * If not building with SSR mode, you can + * directly export the Store instantiation; + * + * The function below can be async too; either use + * async/await or return a Promise which resolves + * with the Store instance. + */ + +export default defineStore((/* { ssrContext } */) => { + const pinia = createPinia() + + // You can add Pinia plugins here + // pinia.use(SomePiniaPlugin) + + return pinia +}) diff --git a/ui/src/stores/mailTestStore.js b/ui/src/stores/mailTestStore.js new file mode 100644 index 0000000..c3e4091 --- /dev/null +++ b/ui/src/stores/mailTestStore.js @@ -0,0 +1,29 @@ +import { defineStore } from 'pinia' +import { post } from 'src/services/api' + +export const useMailTestStore = defineStore('mailTest', { + state: () => ({ + loading: false, + lastResult: null + }), + + actions: { + async sendTestMail (to) { + this.loading = true + try { + const data = await post('/test-mail', { + to + }) + + this.lastResult = data + return true + } catch (err) { + this.lastResult = err + throw err + } finally { + this.loading = false + } + } + } +}) + diff --git a/ui/src/stores/mePasswordStore.js b/ui/src/stores/mePasswordStore.js new file mode 100644 index 0000000..61bc99b --- /dev/null +++ b/ui/src/stores/mePasswordStore.js @@ -0,0 +1,38 @@ +// src/stores/mePasswordStore.js +import { defineStore } from 'pinia' +import api from 'src/services/api' + +export const useMePasswordStore = defineStore('mePassword', { + state: () => ({ + loading: false, + error: null + }), + + actions: { + async changePassword (currentPassword, newPassword) { + this.loading = true + this.error = null + + try { + // 🔐 Token interceptor ile otomatik + await api.post('/me/password', { + current_password: currentPassword, + new_password: newPassword + }) + + return true + + } catch (e) { + // 🔥 api.js normalize error + this.error = + e?.message || + 'Şifre güncellenemedi' + + throw e + + } finally { + this.loading = false + } + } + } +}) diff --git a/ui/src/stores/orderentryStore.js b/ui/src/stores/orderentryStore.js new file mode 100644 index 0000000..3019e53 --- /dev/null +++ b/ui/src/stores/orderentryStore.js @@ -0,0 +1,3340 @@ +/* =========================================================== + 📦 orderentryStore.js (v3.4 CLEAN — AUTH + LOCAL PERSIST + AUTO RESUME) +=========================================================== */ + +import { defineStore } from 'pinia' +import api from 'src/services/api' +import dayjs from 'src/boot/dayjs' +import { ref, toRaw, nextTick } from 'vue' // ✅ düzeltildi +import { useAuthStore } from 'src/stores/authStore' + +// =========================================================== +// 🔹 Shared Reactive Referanslar (Global, Reaktif Nesneler) +// =========================================================== +/* =========================================================== + 🔹 BEDEN ŞEMALARI — STORE SOURCE OF TRUTH +=========================================================== */ +// ⬆️ orderentryStore.js EN ÜSTÜNE +// =========================================================== +// 🔑 COMBO KEY CONTRACT (Frontend ↔ Backend) — v1 +// - trim + UPPER +// - dim1 boşsa " " +// - dim2 boşsa "" +// =========================================================== +const BEDEN_EMPTY = '_' + +const norm = (v) => (v == null ? '' : String(v)).trim() +const normUpper = (v) => norm(v).toUpperCase() + +export function buildComboKey(row, beden) { + const model = normUpper(row?.model || row?.ItemCode) + const renk = normUpper(row?.renk || row?.ColorCode) + const renk2 = normUpper(row?.renk2 || row?.ItemDim2Code) + const bdn = normUpper(beden) + const bedenFinal = bdn === '' ? BEDEN_EMPTY : bdn + + // 🔒 KANONİK SIRA + return `${model}||${renk}||${renk2}||${bedenFinal}` +} + + + + + + +export const BEDEN_SCHEMA = [ + { key: 'ayk', title: 'AYAKKABI', values: ['39','40','41','42','43','44','45'] }, + { key: 'yas', title: 'YAŞ', values: ['2','4','6','8','10','12','14'] }, + { key: 'pan', title: 'PANTOLON', values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68'] }, + { key: 'gom', title: 'GÖMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] }, + { key: 'tak', title: 'TAKIM ELBİSE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] }, + { key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110CM', '115CM', '120CM', '125CM', '130CM', '135CM'] } +] + +export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => { + m[g.key] = g + return m +}, {}) + + +export const stockMap = ref({}) +export const bedenStock = ref([]) +export const sizeCache = ref({}) +// =========================================================== +// 🔹 Shared Reactive Referanslar (Global, Reaktif Nesneler) +// =========================================================== +// ======================== +// 🧰 GLOBAL DATE NORMALIZER +// ======================== + +function newGuid() { + return crypto.randomUUID() +} + + + +// 🔑 Her beden satırı için deterministik clientKey üretimi +function makeLineClientKey(row, grpKey, beden) { + const base = + row.clientRowKey || + row.clientKey || + row.id || + row._id || + row.tmpId || + `${row.model || ''}|${row.renk || ''}|${row.renk2 || ''}` + + return `${base}::${grpKey}::${beden}` +} + + + +// =========================================================== +// 🧩 Pinia Store — ORDER ENTRY STORE (REV 2025-11-03.2) +// =========================================================== +export const useOrderEntryStore = defineStore('orderentry', { + state: () => ({ + + isControlledSubmit: false, + + allowRouteLeaveOnce: false, + schemaMap: {}, + productCache: {}, + _lastSavedFingerprint: null, + + activeNewHeaderId: localStorage.getItem("bss_active_new_header") || null, + + loading: false, + selected: null, + error: null, + + customers: [], + selectedCustomer: null, + + products: [], + colors: [], + secondColors: [], + inventory: [], + + selectedProduct: null, + selectedColor: null, + selectedColor2: null, + + OrderHeaderID: null, + + // Persist config + persistKey: 'bss_orderentry_data', + lastSnapshotKey: 'bss_orderentry_snapshot', + + // Editor state + editingKey: null, + currentOrderId: null, + mode: 'new', + + // Grid state + orders: [], + header: {}, + summaryRows: [], + + lastSavedAt: null, + + // Guards + preventPersist: false, + _uiBusy: false, + _unsavedChanges: false, + }), + + getters: { + getDraftKey() { + // NEW taslak → GLOBAL ama tekil + return 'bss_orderentry_new_draft' + }, + + getEditKey() { + // EDIT → OrderHeaderID’ye bağlı + const id = this.header?.OrderHeaderID + return id ? `bss_orderentry_edit:${id}` : null + } + , + hasUnsavedChanges(state) { + try { + return ( + state._lastSavedFingerprint !== + state._persistFingerprint?.() + ) + } catch { + return false + } + }, + + getPersistKey: (state) => + state.header?.OrderHeaderID + ? `${state.persistKey}:${state.header.OrderHeaderID}` + : state.persistKey, + + getSnapshotKey: (state) => + state.header?.OrderHeaderID + ? `${state.lastSnapshotKey}:${state.header.OrderHeaderID}` + : state.lastSnapshotKey, + + totalQty: (state) => + (state.orders || []).reduce((sum, r) => sum + (Number(r?.adet) || 0), 0), + + hasAnyClosedLine(state) { + return Array.isArray(state.summaryRows) && + state.summaryRows.some(r => r?.isClosed === true) + }, + + totalAmount(state) { + if (!Array.isArray(state.summaryRows)) return 0 + return state.summaryRows.reduce( + (sum, r) => sum + Number(r?.tutar || 0), + 0 + ) + } + }, + + actions: { + + normalizeComboUI(row) { + return buildComboKey(row, BEDEN_EMPTY) + } + + , + /* =========================================================== + 🧩 initSchemaMap — BEDEN ŞEMA İNİT + - TEK SOURCE OF TRUTH: BEDEN_SCHEMA +=========================================================== */ + initSchemaMap() { + if (this.schemaMap && Object.keys(this.schemaMap).length > 0) { + return + } + + const map = {} + + for (const g of BEDEN_SCHEMA) { + map[g.key] = { + key: g.key, + title: g.title, + values: [...g.values] + } + } + + this.schemaMap = map + + console.log( + '🧩 schemaMap INIT edildi:', + Object.keys(this.schemaMap) + ) + }, + + + getRowKey(row) { + if (!row) return null + return row.OrderLineID || row.id || null + } + + + , + updateHeaderTotals() { + try { + if (!Array.isArray(this.summaryRows)) return 0 + + const total = this.summaryRows.reduce( + (sum, r) => sum + Number(r?.tutar || 0), + 0 + ) + + // Header sadece GÖSTERİM / BACKEND için + if (this.header) { + this.header.TotalAmount = Number(total.toFixed(2)) + } + + return total + } catch (err) { + console.error('❌ updateHeaderTotals hata:', err) + return 0 + } + } + + , + /* =========================================================== + 🚨 showInvalidVariantDialog — FINAL + ----------------------------------------------------------- + ✔ prItemVariant olmayan satırları listeler + ✔ Satıra tıkla → scroll + highlight + ✔ Kaydı BLOKLAYAN tek UI noktası + =========================================================== */ + async showInvalidVariantDialog($q, invalidList = []) { + if (!Array.isArray(invalidList) || invalidList.length === 0) return + + return new Promise(resolve => { + $q.dialog({ + title: '🚨 Tanımsız Ürün Kombinasyonları', + message: ` +
+ ${invalidList.map((v, i) => ` +
+
+ #${i + 1} | Item: ${v.itemCode} +
+
+ Beden: ${v.dim1 || '(boş)'} | + Renk: ${v.colorCode || '-'} | + Qty: ${v.qty1} +
+
+ Sebep: ${v.reason || 'Tanımsız ürün kombinasyonu'} +
+
+ `).join('')} +
+ `, + html: true, + ok: { + label: 'Düzelt', + color: 'negative' + }, + cancel: false, + persistent: true + }) + .onOk(() => resolve()) + .onDismiss(() => resolve()) + .onShown(() => { + // Satıra tıklama → scroll + highlight + const nodes = document.querySelectorAll('.invalid-row') + nodes.forEach(n => { + n.addEventListener('click', () => { + const ck = n.getAttribute('data-clientkey') + this.scrollToInvalidRow?.(ck) + }) + }) + }) + }) + } + , + /* =========================================================== + 🎯 scrollToInvalidRow — FINAL + ----------------------------------------------------------- + ✔ ClientKey bazlı scroll + ✔ Hem summaryRows hem orders destekli + ✔ Highlight otomatik kalkar + =========================================================== */ + scrollToInvalidRow(clientKey) { + if (!clientKey) return + + // 1️⃣ Store içindeki satırı bul + const idx = this.summaryRows?.findIndex( + r => r.clientKey === clientKey + ) + + if (idx === -1) { + console.warn('❌ Satır bulunamadı:', clientKey) + return + } + + // 2️⃣ DOM node + const el = document.querySelector( + `[data-clientkey="${clientKey}"]` + ) + + if (!el) { + console.warn('❌ DOM satırı bulunamadı:', clientKey) + return + } + + // 3️⃣ Scroll + el.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }) + + // 4️⃣ Highlight + el.classList.add('invalid-highlight') + + setTimeout(() => { + el.classList.remove('invalid-highlight') + }, 2500) + } + , + + async checkHeaderExists(orderHeaderID) { + try { + if (!orderHeaderID) return false + + const res = await api.get(`/orders/check/${orderHeaderID}`) + + // Backend “true/false” döner varsayımı + return res?.data?.exists === true + } catch (err) { + console.warn("⚠ checkHeaderExists hata:", err) + return false + } + } + , + + async fetchOrderPdf(orderId) { + try { + const resp = await api.get(`/order/pdf/${orderId}`, { + responseType: 'blob' + }) + return resp.data + } catch (err) { + console.error("❌ fetchOrderPdf hata:", err) + throw err + } + } + , + + async downloadOrderPdf(id = null) { + try { + const orderId = id || this.header?.OrderHeaderID + if (!orderId) { + console.error('❌ PDF ID bulunamadı') + return + } + + const res = await api.get(`/order/pdf/${orderId}`, { + responseType: 'blob' + }) + + const blob = new Blob([res.data], { type: 'application/pdf' }) + const url = URL.createObjectURL(blob) + + window.open(url, '_blank') + setTimeout(() => URL.revokeObjectURL(url), 60_000) + + } catch (err) { + console.error('❌ PDF açma hatası:', err) + throw err + } + } + + + + + + , + setActiveNewHeader(id) { + this.activeNewHeaderId = id || null + if (id) localStorage.setItem("bss_active_new_header", id) + else localStorage.removeItem("bss_active_new_header") + }, + + getActiveNewHeaderId() { + return this.activeNewHeaderId || localStorage.getItem("bss_active_new_header") + }, + + /* =========================================================== + 🧩 initFromRoute (v5.6 — groupedRows TOUCH YOK) + ----------------------------------------------------------- + - Route ID ve bss_last_txn arasında en dolu snapshot'ı seçer + - header + orders + summaryRows restore edilir + - groupedRows hydrate edilmez / resetlenmez / dokunulmaz + - Route ID farklıysa router.replace ile URL düzeltilir + =========================================================== */ + async initFromRoute(orderId, router = null) { // ✅ NEW MODE → SADECE global draft + if (this.mode === 'new') { + const raw = localStorage.getItem(this.getDraftKey) + if (raw) { + try { + const payload = JSON.parse(raw) + this.header = payload.header || {} + this.orders = payload.orders || [] + this.summaryRows = payload.summaryRows || this.orders + console.log('♻️ NEW draft restore edildi (global)') + return + } catch {} + } + console.log('⚪ NEW draft yok, boş başlatılıyor') + return + } + if (!this.schemaMap || !Object.keys(this.schemaMap).length) { + this.initSchemaMap() + } + + try { + console.log('🧩 [initFromRoute] orderId:', orderId) + + const lastTxn = localStorage.getItem('bss_last_txn') || null + + const readPayload = (id) => { + if (!id) return null + const raw = localStorage.getItem(`bss_orderentry_data:${id}`) + if (!raw) return null + try { + return JSON.parse(raw) + } catch { + return null + } + } + + const fromRoute = readPayload(orderId) + const fromLast = readPayload(lastTxn) + + const hasData = (p) => + !!p && ( + (Array.isArray(p.orders) && p.orders.length > 0) || + (Array.isArray(p.summaryRows) && p.summaryRows.length > 0) + ) + + let chosenId = null + let chosenPayload = null + + if (hasData(fromRoute)) { + chosenId = orderId + chosenPayload = fromRoute + console.log('✅ [initFromRoute] Route ID snapshot seçildi:', chosenId) + } else if (hasData(fromLast)) { + chosenId = lastTxn + chosenPayload = fromLast + console.log('✅ [initFromRoute] lastTxn snapshot seçildi:', chosenId) + } + + /* ------------------------------------------------------- + 🚫 SNAPSHOT YOK → BOŞ BAŞLA + -------------------------------------------------------- */ + if (!chosenId || !chosenPayload) { + console.log('⚪ [initFromRoute] Snapshot yok, boş başlatılıyor') + + this.header = { + ...(this.header || {}), + OrderHeaderID: orderId || lastTxn || crypto.randomUUID() + } + + this.orders = [] + this.summaryRows = [] + + // ❗ groupedRows'a DOKUNMA + return + } + + /* ------------------------------------------------------- + ✅ SNAPSHOT RESTORE (SAFE CLONE) + -------------------------------------------------------- */ + this.header = { + ...(chosenPayload.header || {}), + OrderHeaderID: chosenId + } + + const orders = Array.isArray(chosenPayload.orders) + ? [...chosenPayload.orders] + : [] + + const summaryRows = Array.isArray(chosenPayload.summaryRows) + ? [...chosenPayload.summaryRows] + : orders + + this.orders = orders + this.summaryRows = summaryRows + + // ❗ groupedRows hydrate edilmez, resetlenmez + + /* ------------------------------------------------------- + 🔁 lastTxn SENKRON + -------------------------------------------------------- */ + try { + localStorage.setItem('bss_last_txn', chosenId) + } catch (e) { + console.warn('⚠️ bss_last_txn yazılamadı:', e) + } + + /* ------------------------------------------------------- + 🔁 ROUTE DÜZELTME (GEREKİRSE) + -------------------------------------------------------- */ + if (router && orderId && orderId !== chosenId) { + console.log('🔁 [initFromRoute] Route ID düzeltiliyor →', chosenId) + await router.replace({ + name: 'order-entry', + params: { orderHeaderID: chosenId } + }) + } + + console.log( + '✅ [initFromRoute] Restore tamam. Satır sayısı:', + this.summaryRows.length + ) + + } catch (err) { + console.error('❌ [initFromRoute] hata:', err) + } + } + + , + + /* =========================================================== + 🆕 startNewOrder (v8.3 — FINAL & STABLE) + =========================================================== */ + async startNewOrder({ $q }) { + + if (!this.schemaMap || !Object.keys(this.schemaMap).length) { + this.initSchemaMap() + } + + const headerId = crypto.randomUUID() + + let orderNumber = `LOCAL-${dayjs().format("YYMMDD-HHmmss")}` + + try { + const res = await api.get("/order/new-number") + if (res?.data?.OrderNumber) { + orderNumber = res.data.OrderNumber + } + } catch { + console.info('ℹ️ Backend order number yok, LOCAL kullanıldı') + } + + this.mode = 'new' + this.isControlledSubmit = false + this.allowRouteLeaveOnce = false + + this.header = { + OrderHeaderID: headerId, + OrderNumber: orderNumber, + OrderDate: new Date().toISOString().slice(0, 10), + CurrAccCode: null, + DocCurrencyCode: 'USD', + PriceCurrencyCode: 'USD', + PriceExchangeRate: 1 + } + + this.orders = [] + this.summaryRows = [] + + // ✅ fingerprint bazlı sistem için reset + this._lastSavedFingerprint = null + + // ✅ NEW draft hemen yazılır + this.persistLocalStorage?.() + + return this.header + } + + + + , + dedupeActiveLinesByCombo(lines) { + const map = new Map() + for (const ln of lines) { + const key = buildComboKey({ + model: ln.ItemCode, + renk: ln.ColorCode, + renk2: ln.ItemDim2Code + }, ln.ItemDim1Code) + + if (!map.has(key)) { + ln.ComboKey = key + map.set(key, ln) + continue + } + + const ex = map.get(key) + ex.Qty1 = (Number(ex.Qty1) || 0) + (Number(ln.Qty1) || 0) + + // OrderLineID boşsa doldur (editte önemli) + if (!ex.OrderLineID && ln.OrderLineID) ex.OrderLineID = ln.OrderLineID + } + return Array.from(map.values()) + } + + + , + /* =========================================================== + 🧹 Core reset helper — sadece state'i sıfırlar + =========================================================== */ + resetCoreState() { + this.orders = [] + this.summaryRows = [] + this.groupedRows = [] + this.header = {} + this.editingKey = null + this.currentOrderId = null + },resetForNewOrder() { + // mevcut her şeyi temizle + this.header = { + OrderHeaderID: this.header?.OrderHeaderID || null, + OrderDate: new Date().toISOString().slice(0,10), + CurrAccCode: null, + DocCurrencyCode: 'TRY', + PriceCurrencyCode: 'TRY', + // ihtiyaç duyduğun diğer default header alanları + } + + this.orders = [] + this.summaryRows = [] + this.productCache = {} + this.stockMap = {} + + this.setMode('new') + } + , + resetForEdit() { + // EDIT modda grid temizlenmez — sadece UI state resetlenir + this.editingKey = null + this.groupedRows = [] + this.mode = 'edit' + } + + ,markAsSaved() { + try { + this._lastSavedFingerprint = this._persistFingerprint() + console.log('✅ markAsSaved → fingerprint senkron') + } catch (e) { + console.warn('⚠️ markAsSaved hata:', e) + } + } + ,clearLocalSnapshot() { + try { + const id = this.header?.OrderHeaderID + if (!id) return + localStorage.removeItem(`bss_orderentry_data:${id}`) + console.log('🧹 Local snapshot temizlendi:', id) + } catch (e) { + console.warn('⚠️ clearLocalSnapshot hata:', e) + } + },/* =========================================================== + 🧹 HARD CLEAN — ALL ORDERENTRY SNAPSHOTS +=========================================================== */ + clearAllOrderSnapshots () { + Object.keys(localStorage) + .filter(k => + k.startsWith('bss_orderentry_data:') || + k.startsWith('bss_orderentry_edit:') + ) + .forEach(k => { + console.log('🧹 snapshot silindi:', k) + localStorage.removeItem(k) + }) + + localStorage.removeItem('bss_last_txn') + } + + + + + , + /* =========================================================== + 🧹 Store Hard Reset — Submit Sonrası Temizlik (FIXED) + - Grid, header, toplamlar, local state'ler sıfırlanır + - persistKey / lastSnapshotKey NULL yapılmaz (config sabit kalır) + - localStorage txn/snapshot temizliği güvenli yapılır + =========================================================== */ + hardResetAfterSubmit() { + try { + // 🔑 mevcut id’yi yakala (local temizliği için) + const id = this.header?.OrderHeaderID || null + + /* ------------------------------------------------------- + 1) Grid ve satırlar + -------------------------------------------------------- */ + this.orders = [] + this.summaryRows = [] + this.groupedRows = [] + + /* ------------------------------------------------------- + 2) Header & meta + -------------------------------------------------------- */ + this.header = {} + + /* ------------------------------------------------------- + 3) Mode & edit state + -------------------------------------------------------- */ + this.mode = 'new' + this.editingKey = null + this.currentOrderId = null + + /* ------------------------------------------------------- + 4) Snapshot / transaction meta + ⚠️ persistKey / lastSnapshotKey store config → NULL YAPMA + -------------------------------------------------------- */ + this.activeTransactionId = null + this.submitted = false + + // fingerprint / debounce meta varsa sıfırla + this._lastSavedFingerprint = null + this._lastPersistFingerprint = null + if (this._persistTimeout) { + clearTimeout(this._persistTimeout) + this._persistTimeout = null + } + + /* ------------------------------------------------------- + 5) LocalStorage temizlik (opsiyonel ama submit sonrası doğru) + -------------------------------------------------------- */ + try { + if (id) { + localStorage.removeItem(`bss_orderentry_data:${id}`) + localStorage.removeItem(`bss_orderentry_snapshot:${id}`) + } + localStorage.removeItem('bss_last_txn') + localStorage.removeItem('bss_active_new_header') + } catch (e) { + console.warn('⚠️ hardResetAfterSubmit localStorage temizliği hata:', e) + } + + console.log('🧹 Store resetlendi (submit sonrası).') + } catch (err) { + console.error('❌ hardResetAfterSubmit hata:', err) + } + } + + , + + /* =========================================================== + ✏️ openExistingForEdit (v12 — FINAL & CLEAN) + ----------------------------------------------------------- + ✔ Backend authoritative (orderlist açılışı local'i dikkate almaz) + ✔ mode=new → backend çağrısı YOK + ✔ normalizeOrderLines → grpKey + bedenMap garanti + ✔ isClosed varsa → view, yoksa → edit + ✔ Form sync opsiyonel + ✔ İlk açılışta snapshot yazılır (edit boyunca persist ile güncellenir) + =========================================================== */ + async openExistingForEdit( + orderId, + { $q = null, form = null, productCache = null } = {} + ) { + // 🔑 schemaMap garanti + if (!this.schemaMap || !Object.keys(this.schemaMap).length) { + this.initSchemaMap?.() + } + + if (!orderId) return false + + /* ======================================================= + 🟦 NEW MODE — ASLA backend çağrısı yok + ======================================================= */ + if (this.mode === 'new') { + console.log('⚪ openExistingForEdit skip (mode=new)') + return false + } + + // productCache hem ref hem reactive olabilir → güvenli oku + const pc = + productCache?.value + ? productCache.value + : (productCache && typeof productCache === 'object' ? productCache : {}) + + try { + // geçici varsayım (sonra isClosed durumuna göre set edilecek) + this.setMode?.('edit') + + /* ======================================================= + 🔹 BACKEND — authoritative load + ======================================================= */ + const res = await api.get(`/order/get/${orderId}`) + const backend = res?.data + + if (!backend?.header) { + throw new Error('Backend header yok') + } + + /* ======================================================= + 🔹 HEADER — SADECE BACKEND + (orderlist açılışında local merge YOK) + ======================================================= */ + this.header = { + ...backend.header, + OrderHeaderID: backend.header.OrderHeaderID || orderId + } + + /* ======================================================= + 🔹 NORMALIZE LINES (TEK KAYNAK) + normalizeOrderLines şu alanları üretmeli: + ✔ row.grpKey + ✔ row.bedenMap[grpKey] + ✔ row.isClosed (boolean) + ======================================================= */ + const normalized = this.normalizeOrderLines( + backend.lines || [], + this.header.DocCurrencyCode || 'USD', + pc + ) + + this.orders = Array.isArray(normalized) ? normalized : [] + this.summaryRows = [...this.orders] + + /* ======================================================= + 🔹 MODE KARARI (BACKEND SATIRLARI ÜZERİNDEN) + - herhangi bir isClosed=true → view + - değilse → edit + ======================================================= */ + const hasClosedLine = (this.summaryRows || []).some(r => r?.isClosed === true) + this.setMode?.(hasClosedLine ? 'view' : 'edit') + + /* ======================================================= + 🔹 FORM SYNC (opsiyonel) + ======================================================= */ + if (form) { + Object.assign(form, this.header) + } + + /* ======================================================= + 🔹 LOCAL SNAPSHOT (edit boyunca tutulacak temel) + - Açılışta snapshot yaz + - Sonraki değişikliklerde zaten persistLocalStorage çağrıları var + ======================================================= */ + this.persistLocalStorage?.() + try { + localStorage.setItem('bss_last_txn', String(orderId)) + } catch {} + + console.log('✅ openExistingForEdit OK:', { + id: orderId, + rows: this.summaryRows.length, + mode: this.mode, + hasClosedLine + }) + + return true + } catch (err) { + console.error('❌ openExistingForEdit hata:', err) + + // new değilse uyar + if (this.mode !== 'new') { + $q?.notify?.({ + type: 'negative', + message: 'Sipariş yüklenemedi' + }) + } + + return false + } + } + + , + /* =========================================================== + ♻️ hydrateFromLocalStorage (v5.5 — FIXED & CLEAN) + ----------------------------------------------------------- + - Tek assign (double overwrite YOK) + - groupedRows hydrate edilmez + - mode ASLA set edilmez + - header + rows güvenli restore + =========================================================== */ + async hydrateFromLocalStorage(orderId, log = false) {if (this.mode === 'new') { + return this.hydrateFromLocalStorageIfExists() + } + + try { + const key = `bss_orderentry_data:${orderId}` + const payload = JSON.parse(localStorage.getItem(key) || 'null') + + if (!payload) { + log && console.log('ℹ️ hydrate → snapshot yok:', orderId) + return null + } + + // 🔑 source bilgisi (mode set edilmez) + this.source = payload.source || 'local' + + /* ------------------------------------------------------- + MSSQL tarih helper’ları + -------------------------------------------------------- */ + const safeDateTime = v => { + if (!v) return null + const d = dayjs(v) + return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : null + } + + const safeDateOnly = v => { + if (!v) return null + const d = dayjs(v) + return d.isValid() ? d.format('YYYY-MM-DD') : null + } + + const safeTimeOnly = v => { + if (!v) return null + const d = dayjs(v) + return d.isValid() ? d.format('HH:mm:ss') : null + } + + /* ------------------------------------------------------- + HEADER + -------------------------------------------------------- */ + this.header = { + ...(payload.header || {}), + OrderHeaderID: payload.header?.OrderHeaderID ?? orderId, + OrderNumber : payload.header?.OrderNumber ?? null + } + + const h = this.header + h.CreatedDate = safeDateTime(h.CreatedDate) + h.LastUpdatedDate = safeDateTime(h.LastUpdatedDate) + h.CreditableConfirmedDate = safeDateTime(h.CreditableConfirmedDate) + h.OrderDate = safeDateOnly(h.OrderDate) + h.OrderTime = safeTimeOnly(h.OrderTime) + this.header = h + + /* ------------------------------------------------------- + ROWS (TEK KAYNAK) + -------------------------------------------------------- */ + const orders = Array.isArray(payload.orders) + ? payload.orders + : [] + + this.orders = orders + + this.summaryRows = Array.isArray(payload.summaryRows) + ? payload.summaryRows + : orders + + // ❌ groupedRows hydrate edilmez (computed olmalı) + this.groupedRows = [] + + /* ------------------------------------------------------- + SNAPSHOT ÖZET + -------------------------------------------------------- */ + const output = { + type : payload.submitted === true ? 'submitted' : 'draft', + source : this.source, + headerId : orderId, + orderNumber: this.header?.OrderNumber ?? null, + rows : this.summaryRows.length, + submitted : + payload.submitted === true || + payload.header?.IsSubmitted === true + } + + log && console.log('♻️ hydrate sonuc (FIXED):', output) + return output + + } catch (err) { + console.warn('⚠️ hydrateFromLocalStorage hata:', err) + return null + } + } + + + , + hydrateFromLocalStorageIfExists() { + try { + let raw = null + + if (this.mode === 'new') { + raw = localStorage.getItem(this.getDraftKey) // ✅ + } + + if (this.mode === 'edit') { + const key = this.getEditKey // ✅ + if (key) raw = localStorage.getItem(key) + } + + if (!raw) return false + + const payload = JSON.parse(raw) + + this.header = payload.header || {} + this.orders = payload.orders || [] + this.summaryRows = payload.summaryRows || this.orders + + console.log('♻️ hydrate OK:', this.mode) + return true + + } catch (err) { + console.warn('hydrateFromLocalStorageIfExists hata:', err) + return false + } + } + + , + + /* =========================================================== + 🔀 mergeOrders (local + backend) + normalizeISO → kaldırıldı + safe MSSQL helpers eklendi + =========================================================== */ + mergeOrders(local, backend, preferLocal = true) { + if (!backend && !local) return { header: {}, orders: [] } + + const safeMerge = (base = {}, override = {}) => { + const out = { ...base } + for (const [k, v] of Object.entries(override || {})) { + if (v === undefined || v === null) continue + if (typeof v === 'string' && v.trim() === '') continue + out[k] = v + } + return out + } + + // Header merge + const header = safeMerge(backend?.header || {}, local?.header || {}) + header.OrderHeaderID = + backend?.header?.OrderHeaderID || + local?.header?.OrderHeaderID || + header.OrderHeaderID || + null + + const getKey = (r) => + (r.OrderLineID || + `${r.model || r.ItemCode}_${r.renk || r.ColorCode}_${r.renk2 || r.ColorCode2}` + ).toString().toUpperCase() + + const map = new Map() + + // Backend satırları + for (const b of (backend?.lines || backend?.orders || [])) { + map.set(getKey(b), { ...b, _src: 'backend' }) + } + + // Local satırları merge et + for (const l of (local?.orders || [])) { + const k = getKey(l) + if (map.has(k)) { + const merged = safeMerge(map.get(k), l) + merged._src = preferLocal ? 'local' : 'backend' + map.set(k, merged) + } else { + map.set(k, { ...l, _src: 'local-only' }) + } + } + + const mergedOrders = Array.from(map.values()) + console.log(`🧩 mergeOrders → ${mergedOrders.length} satır birleşti (ID:${header.OrderHeaderID})`) + + // ==================================================== + // 🕒 HEADER TARİHLERİNİ MSSQL FORMATINA NORMALİZE ET + // ==================================================== + const safeDateTime = v => { + if (!v) return null + const d = dayjs(v) + return d.isValid() ? d.format("YYYY-MM-DD HH:mm:ss") : null + } + + const safeDateOnly = v => { + if (!v) return null + const d = dayjs(v) + return d.isValid() ? d.format("YYYY-MM-DD") : null + } + + const safeTimeOnly = v => { + if (!v) return null + const d = dayjs(v) + return d.isValid() ? d.format("HH:mm:ss") : null + } + + header.CreatedDate = safeDateTime(header.CreatedDate) + header.LastUpdatedDate = safeDateTime(header.LastUpdatedDate) + header.CreditableConfirmedDate = safeDateTime(header.CreditableConfirmedDate) + + header.OrderDate = safeDateOnly(header.OrderDate) + header.OrderTime = safeTimeOnly(header.OrderTime) + + return { header, orders: mergedOrders } + + } + , + + markRowSource(row) { + if (row._src === 'local-only') return '🟠 Offline' + if (row._src === 'local') return '🔵 Local' + return '⚪ Backend' + } + , + + /* =========================================================== + 🔄 mergeAndPersistBackendOrder (edit mode) + =========================================================== */ + mergeAndPersistBackendOrder(orderId, backendPayload) { + const key = `bss_orderentry_data:${orderId}` + const localPayload = JSON.parse(localStorage.getItem(key) || 'null') + + const merged = this.mergeOrders(localPayload, backendPayload, true) + + localStorage.setItem(key, JSON.stringify({ + ...merged, + source: 'db', + mode: 'edit', + updatedAt: new Date().toISOString() + })) + + console.log(`💾 mergeAndPersistBackendOrder → ${orderId} localStorage’a yazıldı`) + } + , + + persistLocalStorage() { + try { + if (this.preventPersist || this._uiBusy) return + + const payload = { + mode: this.mode, + header: toRaw(this.header || {}), + orders: toRaw(this.orders || []), + summaryRows: toRaw(this.summaryRows || []), + updatedAt: new Date().toISOString() + } + + /* =============================== + 🟢 NEW MODE — GLOBAL TEK TASLAK + =============================== */ + if (this.mode === 'new') { + localStorage.setItem(this.getDraftKey, JSON.stringify(payload)) + + // 🔒 sadece aktif new header bilgisi + this.setActiveNewHeader?.(this.header?.OrderHeaderID) + + return + } + + /* =============================== + 🔵 EDIT MODE — ID BAZLI + =============================== */ + if (this.mode === 'edit') { + const key = this.getEditKey + if (!key) return + localStorage.setItem(key, JSON.stringify(payload)) + } + + } catch (e) { + console.warn('persistLocalStorage error:', e) + } + } + + + + , + clearEditSnapshotIfExists() { + if (this.mode !== 'edit') return + + const key = this.getEditKey // ✅ + if (!key) return + + localStorage.removeItem(key) + console.log('🧹 EDIT snapshot silindi:', key) + } + + + + + ,/* =========================================================== + 🧠 _persistFingerprint — kritik state’leri tek stringe indirger + - X3: orders+header yetmez → mode, summaryRows, id/no, map’ler dahil +=========================================================== */ + _persistFingerprint() { + // 🔹 orders: çok büyürse pahalı olabilir ama snapshot tutarlılığı için önemli + // (istersen burada sadece length + rowKey listesi gibi optimize ederiz) + const ordersSnap = JSON.stringify(this.orders || []) + + // 🔹 header: sadece kritik alanları al (tam header yerine daha stabil) + const h = this.header || {} + const headerSnap = JSON.stringify({ + OrderHeaderID: h.OrderHeaderID || '', + OrderNumber: h.OrderNumber || '', + CurrAccCode: h.CurrAccCode || '', + DocCurrencyCode: h.DocCurrencyCode || '', + ExchangeRate: h.ExchangeRate ?? null + }) + + // 🔹 summaryRows: hash yerine şimdilik “length + rowKey listesi” (hafif + etkili) + const sr = Array.isArray(this.summaryRows) ? this.summaryRows : [] + const summaryMeta = JSON.stringify({ + len: sr.length, + keys: sr.map(r => this.getRowKey?.(r) || r?.key || r?.id || '').filter(Boolean) + }) + + // 🔹 comboLineIds / lineIdMap gibi kritik map’ler + // (sende hangisi varsa onu otomatik topluyoruz) + const mapSnap = JSON.stringify({ + lineIdMap: this.lineIdMap || null, + comboLineIds: this.comboLineIds || null, + comboLineIdMap: this.comboLineIdMap || null, + comboLineIdSet: this.comboLineIdSet ? Array.from(this.comboLineIdSet) : null + }) + + // 🔹 mode + const modeSnap = String(this.mode || 'new') + + // ✅ Tek fingerprint + return `${modeSnap}|${headerSnap}|${summaryMeta}|${mapSnap}|${ordersSnap}` + } + , + /* =========================================================== + 🕒 _safePersistDebounced — snapshot değişmediği sürece yazmaz (X3) + - fingerprint: mode + header(id/no) + summaryRows meta + lineIdMap/combo + orders + =========================================================== */ + _safePersistDebounced(delay = 1200) { + clearTimeout(this._persistTimeout) + + this._persistTimeout = setTimeout(() => { + try { + // ✅ Persist guard’ları (varsa) + if (this.preventPersist) return + if (this._uiBusy) return + + const fp = this._persistFingerprint() + + if (fp === this._lastPersistFingerprint) { + return + } + + this._lastPersistFingerprint = fp + + this.persistLocalStorage() + console.log(`🕒 Otomatik LocalStorage senkron (${this.orders?.length || 0} satır).`) + } catch (err) { + console.warn('⚠️ Debounce persist hata:', err) + } + }, delay) + } + + , + + /* =========================================================== + 💰 fetchMinPrice — model/pb için min fiyat + =========================================================== */ + async fetchMinPrice(model, currency, $q) { + try { + const res = await api.get('/min-price', { + params: { model, currency } + }) + const data = res?.data || {} + console.log('💰 [store.fetchMinPrice] yanıt:', data) + return { + price: Number(data.price || 0), + rateToTRY: Number(data.rateToTRY || 1), + priceTRY: Number(data.priceTRY || 0) + } + } catch (err) { + console.error('❌ [store.fetchMinPrice] Min fiyat alınamadı:', err) + $q?.notify?.({ + type: 'warning', + message: 'Min. fiyat bilgisi alınamadı, kontrol atlandı ⚠️', + position: 'top-right' + }) + return { price: 0, rateToTRY: 1, priceTRY: 0 } + } + } + , + applyCurrencyToLines(newPB) { + if (!newPB) return + + // 🔹 Header + if (this.header) { + this.header.DocCurrencyCode = newPB + this.header.PriceCurrencyCode = newPB + } + + // 🔹 Lines + if (Array.isArray(this.orders)) { + this.orders = this.orders.map(r => ({ + ...r, + pb: newPB, + DocCurrencyCode: newPB, + PriceCurrencyCode: newPB + })) + } + + // 🔹 Summary + if (Array.isArray(this.summaryRows)) { + this.summaryRows = this.summaryRows.map(r => ({ + ...r, + pb: newPB, + DocCurrencyCode: newPB, + PriceCurrencyCode: newPB + })) + } + + // ❗ totalAmount SET ETME + // ✔️ TEK MERKEZ + this.updateHeaderTotals?.() + } + , + + /* =========================================================== + 💠 HEADER SET & CURRENCY PROPAGATION + =========================================================== */ + setHeaderFields(fields, opts = {}) { + const { + applyCurrencyToLines = false, + immediatePersist = false + } = opts + + // 1️⃣ HEADER + this.header = { + ...(this.header || {}), + ...fields + } + + // 2️⃣ SATIRLARA GERÇEKTEN YAY + if (applyCurrencyToLines && Array.isArray(this.summaryRows)) { + this.summaryRows = this.summaryRows.map(r => ({ + ...r, + pb: fields.DocCurrencyCode ?? r.pb, + DocCurrencyCode: fields.DocCurrencyCode ?? r.DocCurrencyCode, + PriceCurrencyCode: fields.PriceCurrencyCode ?? fields.DocCurrencyCode ?? r.PriceCurrencyCode + })) + } + + // 3️⃣ STORE ORDERS REFERANSI + this.orders = [...this.summaryRows] + + + // 4️⃣ PERSIST + if (immediatePersist) { + this.persistLocalStorage('header-change') + } + } + + , + + applyHeaderCurrencyToOrders() { + if (!Array.isArray(this.orders)) return + + const doc = this.header?.DocCurrencyCode ?? null + const prc = this.header?.PriceCurrencyCode ?? null + const rate = this.header?.PriceExchangeRate ?? null + + let cnt = 0 + + for (const r of this.orders) { + if (doc) r.DocCurrencyCode = doc + if (prc) r.PriceCurrencyCode = prc + if (rate != null) r.PriceExchangeRate = rate + cnt++ + } + + console.log(`💱 ${cnt} satırda PB güncellendi → Doc:${doc} Price:${prc} Rate:${rate}`) + } + + + ,/* =========================================================== + 📸 saveSnapshot — küçük debug snapshot +=========================================================== */ + saveSnapshot(tag = 'snapshot') { + try { + const id = this.header?.OrderHeaderID + if (!id) return + + const key = `bss_orderentry_snapshot:${id}` + + const snap = { + tag, + mode: this.mode, + orders: toRaw(this.orders || []), + header: toRaw(this.header || {}), + savedAt: dayjs().toISOString() + } + + localStorage.setItem(key, JSON.stringify(snap)) + console.log(`📸 Snapshot kaydedildi [${key}]`) + } catch (err) { + console.warn('⚠️ saveSnapshot hata:', err) + } + } + , + + /* =========================================================== + ♻️ loadFromStorage — eski generic persist için + =========================================================== */ + loadFromStorage(force = false) { + try { + const raw = localStorage.getItem(this.getPersistKey) + if (!raw) { + console.info('ℹ️ LocalStorage boş, grid başlatılmadı.') + return false + } + + if (!force && this.mode === 'edit') { + console.info('⚠️ Edit modda local restore atlandı (force=false).') + return false + } + + const data = JSON.parse(raw) + + this.orders = Array.isArray(data.orders) ? data.orders : [] + this.header = data.header || {} + this.currentOrderId = data.currentOrderId || null + this.selectedCustomer = data.selectedCustomer || null + + // 🔧 Temiz ID + this.header.OrderHeaderID = data.header?.OrderHeaderID || null + + this.mode = data.mode || 'new' + this.lastSavedAt = data.savedAt || null + + console.log(`♻️ Storage yüklendi • txn:${this.header.OrderHeaderID} (${this.orders.length} satır)`) + + // Header PB -> satırlara + this.applyHeaderCurrencyToOrders() + this._safePersistDebounced(200) + + return data + } catch (err) { + console.warn('⚠️ localStorage okuma hatası:', err) + return false + } + } + , + + clearStorage() { + try { + localStorage.removeItem(this.getPersistKey) + console.log(`🗑️ LocalStorage temizlendi [${this.getPersistKey}]`) + } catch (err) { + console.warn('⚠️ clearStorage hatası:', err) + } + } + , + clearNewDraft() { + localStorage.removeItem(this.getDraftKey) // ✅ + localStorage.removeItem('bss_last_txn') + console.log('🧹 NEW taslak temizlendi') + } + + , +// =========================================================== +// 🔹 isSameCombo — STORE LEVEL (TEK KAYNAK) +// - model ZORUNLU eşleşir +// - renk / renk2 boşsa → joker +// =========================================================== + isSameCombo(a, b) { + if (!a || !b) return false + + const n = v => (v == null ? '' : String(v).trim().toUpperCase()) + + const A = { model: n(a.model), renk: n(a.renk), renk2: n(a.renk2) } + const B = { model: n(b.model), renk: n(b.renk), renk2: n(b.renk2) } + + if (!A.model || !B.model) return false + + const renkOk = (A.renk === B.renk) || !A.renk || !B.renk + const renk2Ok = (A.renk2 === B.renk2) || !A.renk2 || !B.renk2 + + return A.model === B.model && renkOk && renk2Ok + }, + + + +// =========================================================== +// 🔹 saveOrUpdateRowUnified (v6.6 — COMBO SAFE + FIXED STOCK+PRICE + UI) +// - v6.5 korunur (stok+min fiyat + this.loadProductSizes) +// - ✅ NEW MODE: dupIdx artık _deleteSignal satırlarını BAŞTAN hariç tutar +// - EDIT MODE: sameCombo → update, combo değişti → delete + insert (korundu) +// - lineIdMap koruması korunur +// =========================================================== + async saveOrUpdateRowUnified({ + form, + recalcVat = null, + resetEditor = null, + stockMap = null, + loadProductSizes = null, + $q = null + }) { + try { + console.log('🔥 saveOrUpdateRowUnified v6.6', { + model: form?.model, + mode: this.mode, + editingKey: this.editingKey + }) + + const getKey = + typeof this.getRowKey === 'function' + ? this.getRowKey + : (r => r?.clientKey || r?.id || r?.OrderLineID) + + const rows = Array.isArray(this.summaryRows) + ? [...this.summaryRows] + : [] + + /* ======================================================= + 1️⃣ ZORUNLU KONTROLLER + ======================================================= */ + if (!form?.model) { + $q?.notify?.({ type: 'warning', message: 'Model seçiniz' }) + return false + } + + if (!form.pb) { + form.pb = this.header?.DocCurrencyCode || 'USD' + } + + /* ======================================================= + 2️⃣ STOK KONTROLÜ (FIXED) + - stok guard’dan önce this.loadProductSizes(form,true,$q) + - opsiyonel callback loadProductSizes(true) + - tek dialog + doğru await + ======================================================= */ + + // ✅ store fonksiyonu + try { + if (typeof this.loadProductSizes === 'function') { + await this.loadProductSizes(form, true, $q) + } + } catch (err) { + console.warn('⚠ this.loadProductSizes hata:', err) + } + + // ✅ dışarıdan callback geldiyse + try { + if (typeof loadProductSizes === 'function') { + await loadProductSizes(true) + } + } catch (err) { + console.warn('⚠ loadProductSizes hata:', err) + } + + const stockMapLocal = stockMap?.value || stockMap || {} + const bedenLabels = form.bedenLabels || [] + const bedenValues = form.bedenler || [] + + const overLimit = [] + for (let i = 0; i < bedenLabels.length; i++) { + const lbl = String(bedenLabels[i] ?? '').trim() + const stok = Number(stockMapLocal?.[lbl] ?? 0) + const girilen = Number(bedenValues?.[i] ?? 0) + + if (stok > 0 && girilen > stok) { + overLimit.push({ beden: lbl, stok, girilen }) + } + } + + if (overLimit.length && $q) { + const msg = overLimit + .map(x => `• ${x.beden}: ${x.girilen} (Stok: ${x.stok})`) + .join('
') + + const stokOK = await new Promise(resolve => { + $q.dialog({ + title: 'Stok Uyarısı', + message: `Bazı bedenlerde stoktan fazla giriş yaptınız:

${msg}`, + html: true, + ok: { label: 'Devam', color: 'primary' }, + cancel: { label: 'İptal', color: 'negative' } + }) + .onOk(() => resolve(true)) + .onCancel(() => resolve(false)) + .onDismiss(() => resolve(false)) + }) + + if (!stokOK) return false + } + + /* ======================================================= + 3️⃣ FİYAT (MIN) KONTROLÜ (FIXED) + ======================================================= */ + let fiyatOK = true + try { + let minFiyat = 0 + + if (typeof this.fetchMinPrice === 'function') { + const p = await this.fetchMinPrice(form.model, form.pb, $q) + minFiyat = Number(p?.price || 0) + } else if (Number(form.minFiyat || 0) > 0) { + minFiyat = Number(form.minFiyat) + } + + const girilen = Number(form.fiyat || 0) + + if (minFiyat > 0 && girilen > 0 && girilen < minFiyat && $q) { + fiyatOK = await new Promise(resolve => { + $q.dialog({ + title: 'Fiyat Uyarısı', + message: + `Min. Fiyat: ${minFiyat} ${form.pb}
` + + `Girdiğiniz: ${girilen} ${form.pb}`, + html: true, + ok: { label: 'Devam', color: 'primary' }, + cancel: { label: 'İptal', color: 'negative' } + }) + .onOk(() => resolve(true)) + .onCancel(() => resolve(false)) + .onDismiss(() => resolve(false)) + }) + } + } catch (err) { + console.warn('⚠ Min fiyat hata:', err) + } + if (!fiyatOK) return false + + /* ======================================================= + 4️⃣ TOPLAM HESABI + ======================================================= */ + const adet = (form.bedenler || []).reduce((a, b) => a + Number(b || 0), 0) + form.adet = adet + form.tutar = Number((adet * Number(form.fiyat || 0)).toFixed(2)) + + const newRow = toSummaryRowFromForm(form) + + /* ======================================================= + 5️⃣ EDIT MODE (editingKey ZORUNLU) + ======================================================= */ + if (this.editingKey) { + const idx = rows.findIndex(r => getKey(r) === this.editingKey) + if (idx === -1) { + this.editingKey = null + resetEditor?.(true) + return false + } + + const prev = rows[idx] + + if (this.isRowLocked?.(prev)) { + $q?.notify?.({ type: 'warning', message: 'Satır kapalı' }) + this.editingKey = null + resetEditor?.(true) + return false + } + + // ✅ kritik: store-level + const sameCombo = this.isSameCombo(prev, newRow) + + const preservedLineIdMap = + (prev?.lineIdMap && typeof prev.lineIdMap === 'object') + ? { ...prev.lineIdMap } + : (newRow?.lineIdMap && typeof newRow.lineIdMap === 'object') + ? { ...newRow.lineIdMap } + : {} + + /* ===== SAME COMBO → UPDATE ===== */ + if (sameCombo) { + rows[idx] = { + ...prev, + ...newRow, + id: prev.id, + OrderLineID: prev.OrderLineID || null, + lineIdMap: preservedLineIdMap + } + + this.summaryRows = rows + this.orders = rows + + this.updateHeaderTotals?.() + this.persistLocalStorage?.() + + this.editingKey = null + resetEditor?.(true) + recalcVat?.() + + $q?.notify?.({ type: 'positive', message: 'Satır güncellendi' }) + return true + } + + /* ===== COMBO CHANGED → DELETE + INSERT ===== */ + const grpKey = + prev?.grpKey || + Object.keys(prev?.bedenMap || {})[0] || + 'tak' + + const emptyMap = {} + const srcMap = + (prev?.bedenMap?.[grpKey] && typeof prev.bedenMap[grpKey] === 'object') + ? prev.bedenMap[grpKey] + : (preservedLineIdMap && typeof preservedLineIdMap === 'object') + ? preservedLineIdMap + : null + + if (srcMap) { + for (const beden of Object.keys(srcMap)) emptyMap[beden] = 0 + } else { + emptyMap['STD'] = 0 + } + + const deleteRow = { + ...prev, + id: `DEL::${prev.id || prev.OrderLineID || crypto.randomUUID()}`, + _deleteSignal: true, + adet: 0, + Qty1: 0, + tutar: 0, + ComboKey: '', + + OrderLineID: prev.OrderLineID || null, + + grpKey, + bedenMap: { [grpKey]: emptyMap }, + lineIdMap: preservedLineIdMap, + comboLineIds: { ...(prev.comboLineIds || {}) } + } + + const insertedRow = { + ...newRow, + id: crypto.randomUUID(), + OrderLineID: null, + lineIdMap: {} + } + + rows.splice(idx, 1, insertedRow) + + this.summaryRows = rows + this.orders = [...rows, deleteRow] + + this.updateHeaderTotals?.() + this.persistLocalStorage?.() + + this.editingKey = null + resetEditor?.(true) + recalcVat?.() + + $q?.notify?.({ type: 'positive', message: 'Kombinasyon değişti' }) + return true + } + + /* ======================================================= + 6️⃣ NEW MODE (MERGE / INSERT) — COMBO SAFE + - aynı combo → bedenMap merge (satır sayısı artmaz) + - farklı combo → yeni satır + - ✅ FIX: _deleteSignal satırlarını dup aramasında hariç tut + ======================================================= */ + const dupIdx = rows.findIndex(r => + !r?._deleteSignal && + this.isSameCombo(r, newRow) + ) + + // helper: bedenMap çıkar (gruplu ya da düz) + const extractMap = (row) => { + const grpKey = + row?.grpKey || + Object.keys(row?.bedenMap || {})[0] || + 'GENEL' + + const grouped = row?.bedenMap?.[grpKey] + const flat = (row?.bedenMap && typeof row.bedenMap === 'object' && !grouped) + ? row.bedenMap + : null + + return { grpKey, map: (grouped || flat || {}) } + } + + if (dupIdx !== -1) { + const prev = rows[dupIdx] + + // delete satırına merge yapma (ek güvenlik) + if (prev?._deleteSignal !== true) { + const { grpKey: prevGrp, map: prevMap } = extractMap(prev) + const { grpKey: newGrp, map: newMap } = extractMap(newRow) + + // hangi grpKey kullanılacak? + const grpKey = newRow?.grpKey || prevGrp || newGrp || 'GENEL' + + // MERGE: bedenleri topluyoruz (override değil) + const merged = { ...(prevMap || {}) } + for (const [k, v] of Object.entries(newMap || {})) { + const beden = (k == null || String(k).trim() === '') ? ' ' : String(k).trim() + merged[beden] = Number(merged[beden] || 0) + Number(v || 0) + } + + // toplam adet/tutar recalc + const totalAdet = Object.values(merged).reduce((a, b) => a + Number(b || 0), 0) + const price = Number(newRow?.fiyat ?? prev?.fiyat ?? 0) + const totalTutar = Number((totalAdet * price).toFixed(2)) + + rows[dupIdx] = { + ...prev, + ...newRow, + + // kritik korumalar + id: prev.id, + OrderLineID: prev.OrderLineID || null, + lineIdMap: { ...(prev.lineIdMap || {}) }, + + // MERGED bedenMap + grpKey, + bedenMap: { [grpKey]: merged }, + + // adet/tutar + adet: totalAdet, + tutar: totalTutar, + + updatedAt: dayjs().toISOString() + } + + this.summaryRows = rows + this.orders = rows + + this.updateHeaderTotals?.() + this.persistLocalStorage?.() + resetEditor?.(true) + recalcVat?.() + + $q?.notify?.({ type: 'positive', message: 'Aynı kombinasyon bulundu, bedenler birleştirildi' }) + return true + } + } + + // dup yoksa (veya dup delete satırıydı) → yeni satır + rows.push({ + ...newRow, + id: newRow.id || crypto.randomUUID(), + OrderLineID: null, + lineIdMap: { ...(newRow.lineIdMap || {}) } + }) + + this.summaryRows = rows + this.orders = rows + + this.updateHeaderTotals?.() + this.persistLocalStorage?.() + resetEditor?.(true) + recalcVat?.() + + $q?.notify?.({ type: 'positive', message: 'Yeni satır eklendi' }) + return true + + } catch (err) { + console.error('❌ saveOrUpdateRowUnified:', err) + $q?.notify?.({ type: 'negative', message: 'Satır kaydı başarısız' }) + return false + } + } + + + + + , + + /* =========================================================== + 🔄 setTransaction — yeni transaction ID set et + =========================================================== */ + setTransaction(id, autoResume = true) { + if (!id) return + + // 🔧 temiz ID + this.header.OrderHeaderID = id + + localStorage.setItem('bss_last_txn', id) + console.log('🔄 Transaction değiştirildi:', id) + + if (autoResume) { + const hasData = Array.isArray(this.orders) && this.orders.length > 0 + if (!hasData) { + const ok = this.hydrateFromLocalStorage(id,true) + if (ok) console.info('📦 Local kayıt geri yüklendi (boş grid için).') + } else { + console.log('🚫 Grid dolu, auto-resume atlandı (mevcut satırlar korundu).') + } + } + } + , + + + /* =========================================================== + 🧹 clearTransaction — sadece NEW MODE taslaklarını temizler + =========================================================== */ + clearTransaction() { + try { + const id = this.header?.OrderHeaderID + if (id) { + localStorage.removeItem(`bss_orderentry_data:${id}`) + } + + this.orders = [] + this.summaryRows = [] + this.groupedRows = [] + this.header = {} + this.mode = 'new' + + localStorage.removeItem('bss_last_txn') + + console.log('🧹 Transaction temizlendi') + } catch (err) { + console.warn('⚠️ clearTransaction hata:', err) + } + } + , + + + // ======================================================= + // 🔒 KİLİT KONTROLÜ — Sadece EDIT modunda, backend satırı + // ======================================================= + isRowLocked(row) { + if (!row) return false + // Sadece edit modunda, + // ve backend'den gelen gerçek OrderLineID varsa, + // ve IsClosed=1 ise satır kilitli + return ( + this.mode === 'edit' && + !!row.OrderLineID && + row.isClosed === true + ) + }, + + + findExistingIndexByForm(form) { + return this.orders.findIndex(r => this.isSameCombo(r, form)) + }, + + addRow(row) { + if (!row) return + + const existingIndex = this.orders.findIndex(r => { + const sameId = r.id && row.id && r.id === row.id + const sameCombo = this.isSameCombo(r, row) + return sameId || sameCombo + }) + + if (existingIndex !== -1) { + const old = this.orders[existingIndex] + this.orders[existingIndex] = { + ...old, + adet: Number(row.adet ?? old.adet ?? 0), + fiyat: Number(row.fiyat ?? old.fiyat ?? 0), + tutar: Number(row.fiyat ?? old.fiyat ?? 0) * Number(row.adet ?? old.adet ?? 0), + ItemDim1Code: row.ItemDim1Code || old.ItemDim1Code, + aciklama: row.aciklama || old.aciklama, + updatedAt: dayjs().toISOString() + } + console.log(`⚠️ Aynı kombinasyon bulundu, satır güncellendi: ${row.model} ${row.renk || ''} ${row.renk2 || ''}`) + } else { + this.orders.push(toRaw(row)) + console.log(`➕ Yeni kombinasyon eklendi: ${row.model} ${row.renk || ''} ${row.renk2 || ''}`) + } + + this.persistLocalStorage() + this.saveSnapshot('after-add') + }, + + updateRow(index, patch) { + if (index < 0 || index >= this.orders.length) return + this.orders[index] = { + ...this.orders[index], + ...toRaw(patch), + updatedAt: dayjs().toISOString() + } + this.persistLocalStorage() + this.saveSnapshot('after-update') + console.log(`✏️ Satır güncellendi (store): #${index}`) + }, + + + removeRow(index) { + if (index < 0 || index >= this.orders.length) return + + const removed = this.orders.splice(index, 1) + if (Array.isArray(this.summaryRows)) { + this.summaryRows.splice(index, 1) + } + + this.persistLocalStorage() + this.saveSnapshot('after-remove') + console.log(`🗑️ Satır silindi: ${removed[0]?.model || '(model yok)'}`) + }, + removeSelectedRow(row, $q = null) { + if (!row) return + + // 1) Kilitli satır silinemez + if (this.isRowLocked(row)) { + $q?.notify?.({ + type: 'warning', + message: '🔒 Bu satır (IsClosed=1) kapatılmış. Silinemez.' + }) + return false + } + + // 2) Kullanıcıya onay sor + return new Promise(resolve => { + $q?.dialog({ + title: 'Satır Sil', + message: `${row.model} / ${row.renk} / ${row.renk2} kombinasyonu silinsin mi?`, + ok: { label: 'Evet', color: 'negative' }, + cancel: { label: 'Vazgeç' } + }) + .onOk(() => { + this.removeRowInternal(row) + resolve(true) + }) + .onCancel(() => resolve(false)) + }) + } + , + removeRowInternal(row) { + if (!row) return false + + // 1️⃣ Kilit kontrolü + if (this.isRowLocked(row)) { + console.warn('🔒 Kilitli satır silinemez.') + return false + } + + const getKey = + typeof this.getRowKey === 'function' + ? this.getRowKey + : (r => r?.clientKey || r?.id || r?.OrderLineID) + + const rowKey = getKey(row) + if (!rowKey) return false + + const idx = this.summaryRows.findIndex(r => getKey(r) === rowKey) + if (idx === -1) return false + + console.log('🗑️ X2 removeRowInternal →', row) + + // 🔐 UI BUSY + this._uiBusy = true + this.preventPersist = true + + try { + // 2️⃣ UI’dan kaldır + this.summaryRows.splice(idx, 1) + + // orders = UI satırları (temiz kopya) + this.orders = [...this.summaryRows] + + // 3️⃣ EDIT MODE → DELETE SİNYALİ + if (this.mode === 'edit') { + const grpKey = + row.grpKey || + Object.keys(row.bedenMap || {})[0] || + 'tak' + + // ✅ lineIdMap referansı (varsa) + const lineIdMap = + (row.lineIdMap && typeof row.lineIdMap === 'object') + ? { ...row.lineIdMap } + : {} + + const emptyMap = {} + + // Öncelik: bedenMap[grpKey] → lineIdMap → fallback + if (row.bedenMap && row.bedenMap[grpKey]) { + for (const beden of Object.keys(row.bedenMap[grpKey] || {})) { + emptyMap[beden] = 0 + } + } else if (Object.keys(lineIdMap).length) { + for (const beden of Object.keys(lineIdMap)) { + emptyMap[beden] = 0 + } + } else { + emptyMap['STD'] = 0 + } + + const deleteSignalRow = { + ...row, + + // 🔴 UI KEY + id: `DEL::${row.id || row.OrderLineID || crypto.randomUUID()}`, + + // 🔴 BACKEND DELETE SIGNAL + adet: 0, + Qty1: 0, + tutar: 0, + + // 🔴 CRITICAL: duplicate guard'a girmesin + ComboKey: '', + + // 🔴 legacy tekil alan (varsa kalsın) + OrderLineID: row.OrderLineID || null, + + // ✅ CRITICAL + grpKey, + bedenMap: { [grpKey]: emptyMap }, + lineIdMap, + comboLineIds: { ...(row.comboLineIds || {}) }, + + _deleteSignal: true + } + + console.log('📡 DELETE sinyali üretildi:', deleteSignalRow) + + this.orders.push(deleteSignalRow) + } + + // 4️⃣ Totals (persist YOK) + this.updateHeaderTotals?.() + + } finally { + // 🔓 GUARD KAPAT + this.preventPersist = false + this._uiBusy = false + } + + // 5️⃣ TEK VE KONTROLLÜ persist + this.persistLocalStorage() + + return true + } + + , + + + /* =========================================================== + 📦 normalizeOrderLines (v9 — lineIdMap FIXED + AKSBİR SAFE) + ----------------------------------------------------------- + ✔ grpKey SADECE burada set edilir + ✔ detectBedenGroup SADECE store’da kullanılır + ✔ aksbir → ' ' bedeni = GERÇEK adet + ✔ backend satırlarında BEDEN → OrderLineID map’i üretilir + =========================================================== */ + normalizeOrderLines(lines, pbFallback = 'USD') { + if (!Array.isArray(lines)) return [] + + const merged = Object.create(null) + + const makeBaseKey = (model, renk, renk2) => + `${model || ''}||${renk || ''}||${renk2 || ''}` + + for (const raw of lines) { + if (!raw) continue + + const isClosed = + raw.IsClosed === true || + raw.isClosed === true || + raw.IsClosed?.Bool === true + + /* ======================================================= + 1️⃣ UI / SNAPSHOT KAYNAKLI SATIR + ------------------------------------------------------- + ✔ ComboKey YOK + ✔ Sadece model / renk / renk2 bazında gruplanır + ======================================================= */ + if (raw.bedenMap && Object.keys(raw.bedenMap).length) { + const model = (raw.model || raw.ItemCode || '').trim() + const renk = (raw.renk || raw.ColorCode || '').trim() + const renk2 = (raw.renk2 || raw.ItemDim2Code || '').trim() + + // ❗ BEDEN YOK → bu SADECE üst seviye grup anahtarı + const modelKey = `${model}||${renk}||${renk2}` + + const grpKey = raw.grpKey || 'tak' + const srcMap = raw.bedenMap[grpKey] || {} + + const adet = Object.values(srcMap).reduce((a, b) => a + (Number(b) || 0), 0) + const fiyat = Number(raw.fiyat || 0) + const pb = raw.pb || raw.DocCurrencyCode || pbFallback + const tutar = Number(raw.tutar ?? adet * fiyat) + + merged[modelKey] ??= [] + merged[modelKey].push({ + ...raw, + grpKey, + bedenMap: { [grpKey]: { ...srcMap } }, + adet, + fiyat, + pb, + tutar, + isClosed + }) + continue + } + + + /* ======================================================= + 2️⃣ BACKEND / LEGACY SATIR (FIXED) + ------------------------------------------------------- + ✔ ComboKey YOK + ✔ Sadece model / renk / renk2 bazlı gruplanır + ✔ BEDEN sadece bedenMap + lineIdMap için kullanılır + ======================================================= */ + const model = (raw.Model || raw.ItemCode || '').trim() + const renk = (raw.ColorCode || '').trim() + const renk2 = (raw.ItemDim2Code || '').trim() + + // ❗ BEDEN HARİÇ — üst seviye grup anahtarı + const modelKey = `${model}||${renk}||${renk2}` + + merged[modelKey] ??= [] + + const beden = ( + raw.ItemDim1Code == null || String(raw.ItemDim1Code).trim() === '' + ? ' ' + : String(raw.ItemDim1Code).trim().toUpperCase() + ) + + const qty = Number(raw.Qty1 || raw.Qty || 0) + + let entry = merged[modelKey][0] + if (!entry) { + entry = { + id: raw.OrderLineID || crypto.randomUUID(), + + model, + renk, + renk2, + + urunAnaGrubu: raw.UrunAnaGrubu || 'GENEL', + urunAltGrubu: raw.UrunAltGrubu || '', + kategori: raw.Kategori || '', + + aciklama: raw.LineDescription || '', + fiyat: Number(raw.Price || 0), + pb: raw.DocCurrencyCode || pbFallback, + + __tmpMap: {}, // beden → qty + lineIdMap: {}, // beden → OrderLineID + + adet: 0, + tutar: 0, + + terminTarihi: raw.DeliveryDate || null, + isClosed + } + + merged[modelKey].push(entry) + } + + /* ------------------------------------------------------- + 🔑 BEDEN → OrderLineID (DETERMINISTIC & SAFE) + -------------------------------------------------------- */ + const rawLineId = + raw.OrderLineID || + raw.OrderLineId || + raw.orderLineID || + null + + if (rawLineId) { + entry.lineIdMap[beden] = String(rawLineId) + } + + if (qty > 0) { + entry.__tmpMap[beden] = (entry.__tmpMap[beden] || 0) + qty + entry.adet += qty + entry.tutar += qty * entry.fiyat + } + } + + /* ======================================================= + 3️⃣ FINAL — grpKey KESİN + AKSBİR FIX + ======================================================= */ + const out = [] + + for (const rows of Object.values(merged)) { + for (const row of rows) { + if (!row.__tmpMap) { + out.push(row) + continue + } + + const bedenList = Object.keys(row.__tmpMap) + + // 🔒 TEK VE KESİN KARAR + const grpKey = detectBedenGroup( + bedenList, + row.urunAnaGrubu, + row.kategori + ) + + row.grpKey = grpKey + row.bedenMap = { [grpKey]: { ...row.__tmpMap } } + + /* =================================================== + 🔒 AKSBİR — BOŞLUK BEDEN GERÇEK ADETİ ALIR + ❗ STD’ye dönme YOK + ❗ 0 yazma YOK + =================================================== */ + if (grpKey === 'aksbir') { + row.bedenMap[grpKey] ??= {} + row.bedenMap[grpKey][' '] = Number(row.adet || 0) + } + + delete row.__tmpMap + out.push(row) + } + } + + console.log( + `📦 normalizeOrderLines (v9 + lineIdMap) → ${out.length} satır` + ) + + return out + } + + , + + /** + * =========================================================== + * loadProductSizes — FINAL v4.2 (EDITOR SAFE) + * ----------------------------------------------------------- + * ✔ grpKey SADECE form.grpKey + * ✔ schemaMap TEK OTORİTE + * ✔ edit modda BEDEN LABEL DOKUNULMAZ + * ✔ ' ' (boş beden) korunur + * =========================================================== + */ + async loadProductSizes(form, forceRefresh = false, $q = null) { + if (!form?.model) return + + const store = this + const prevBusy = !!store._uiBusy + const prevPrevent = !!store.preventPersist + store._uiBusy = true + store.preventPersist = true + + try { + const grpKey = form.grpKey + if (!grpKey) { + console.warn('⛔ loadProductSizes iptal → grpKey yok') + return + } + + const colorKey = form.renk || 'nocolor' + const color2Key = form.renk2 || 'no2color' + const cacheKey = `${form.model}_${colorKey}_${color2Key}_${grpKey}` + + /* ======================================================= + ♻️ CACHE (LABEL DOKUNMADAN) + ======================================================= */ + if (!forceRefresh && sizeCache.value?.[cacheKey]) { + const cached = sizeCache.value[cacheKey] + bedenStock.value = [...cached.stockArray] + stockMap.value = { ...cached.stockMap } + console.log(`♻️ loadProductSizes CACHE → ${grpKey}`) + return + } + + /* ======================================================= + 📡 API + ======================================================= */ + const params = { code: form.model } + if (form.renk) params.color = form.renk + if (form.renk2) params.color2 = form.renk2 + + const res = await api.get('/product-colorsize', { params }) + const data = Array.isArray(res?.data) ? res.data : [] + + if (!data.length) { + bedenStock.value = [] + stockMap.value = {} + return + } + + /* ======================================================= + 📦 STOK MAP (' ' KORUNUR) + ======================================================= */ + const apiStockMap = {} + for (const x of data) { + const key = + x.item_dim1_code === null || x.item_dim1_code === '' + ? ' ' + : String(x.item_dim1_code) + apiStockMap[key] = Number(x.kullanilabilir_envanter ?? 0) + } + + const finalStockMap = {} + for (const lbl of form.bedenLabels) { + finalStockMap[lbl] = apiStockMap[lbl] ?? 0 + } + + stockMap.value = { ...finalStockMap } + bedenStock.value = Object.entries(stockMap.value).map( + ([beden, stok]) => ({ beden, stok }) + ) + + /* ======================================================= + 💾 CACHE + ======================================================= */ + sizeCache.value[cacheKey] = { + labels: [...form.bedenLabels], + stockArray: [...bedenStock.value], + stockMap: { ...stockMap.value } + } + + console.log(`✅ loadProductSizes FINAL v4.2 → ${grpKey}`) + } catch (err) { + console.error('❌ loadProductSizes hata:', err) + $q?.notify?.({ type: 'negative', message: 'Beden / stok alınamadı' }) + } finally { + store._uiBusy = prevBusy + store.preventPersist = prevPrevent + console.log('🧩 Editor beden hydrate', { + grpKey: form.grpKey, + labels: form.bedenLabels, + values: form.bedenler + }) + + } + } + + + + + , + + // ======================================================= +// 🔸 TOPLAM HESAPLAMA (store içi) — X3 SAFE +// ------------------------------------------------------- +// ✔ f.adet / f.tutar hesaplanır +// ✔ store.totalAmount ASLA set edilmez +// ✔ gerçek toplam → header.TotalAmount +// ======================================================= + updateTotals(f) { + // 1️⃣ Satır adet + f.adet = (f.bedenler || []).reduce( + (a, b) => a + Number(b || 0), + 0 + ) + + // 2️⃣ Satır tutar + const fiyat = Number(f.fiyat) || 0 + f.tutar = Number((f.adet * fiyat).toFixed(2)) + + // 3️⃣ Header toplam (tek gerçek state) + if (this.header) { + const total = (this.summaryRows || []).reduce( + (sum, r) => sum + Number(r?.tutar || 0), + 0 + ) + + this.header.TotalAmount = Number(total.toFixed(2)) + } + + return f + } + , + + // ======================================================= + // 🔸 GRUP ANAHTARI TESPİTİ + // ======================================================= + activeGroupKeyForRow(row) { + const g = (row?.urunAnaGrubu || '').toUpperCase() + if (g.includes('TAKIM')) return 'tak' + if (g.includes('PANTOLON')) return 'pan' + if (g.includes('GÖMLEK')) return 'gom' + if (g.includes('AYAKKABI')) return 'ayk' + if (g.includes('YAŞ')) return 'yas' + return 'tak' + }, + /* ======================================================= + 🔹 MODE YÖNETİMİ — new / edit arası geçiş +======================================================= */ + setMode(mode) { + if (!['new', 'edit', 'view'].includes(mode)) { + console.warn('⚠️ Geçersiz mode:', mode) + return + } + + this.mode = mode + console.log(`🧭 Order mode set edildi → ${mode}`) + } + , + /* =========================================================== + 🟦 submitAllReal (v12.1c — FINAL / CLEAN + PRE-VALIDATE) + ----------------------------------------------------------- + ✔ NEW → INSERT, EDIT → UPDATE (tek karar noktası) + ✔ Controlled submit → route guard SUSAR + ✔ Snapshot temizliği route öncesi + ✔ Kaydet → edit replace → backend reload + ✔ Listeye giderken guard popup 1 kez bypass + ✔ ✅ PRE-VALIDATE → prItemVariant olmayan kombinasyonlar kaydı DURDURUR + =========================================================== */ + async submitAllReal($q, router, form, summaryRows, productCache) { + let serverOrderId = null + let serverOrderNo = null + + try { + this.loading = true + + // 🔒 Kontrollü submit → route leave guard susar + this.isControlledSubmit = true + + const isNew = this.mode === 'new' + const { header, lines } = this.buildFinalOrderJson() + + // ======================================================= + // 🧾 DEBUG — FRONTEND → BACKEND GİDEN PAYLOAD + // ======================================================= + console.groupCollapsed( + `%c📤 ORDER PAYLOAD (${this.mode})`, + 'color:#c9a873;font-weight:bold' + ) + + console.log('HEADER:', JSON.parse(JSON.stringify(header))) + + lines.forEach((l, i) => { + console.log(`LINE[${i}]`, { + OrderLineID: l.OrderLineID, + ClientKey: l.ClientKey, + ItemCode: l.ItemCode, + ColorCode: l.ColorCode, + ItemDim1Code: l.ItemDim1Code, + ItemDim2Code: l.ItemDim2Code, + ItemDim3Code: l.ItemDim3Code, + Qty1: l.Qty1, + ComboKey: l.ComboKey + }) + }) + + console.groupEnd() + + // ======================================================= + // 🧾 DEBUG (opsiyonel helper) + // ======================================================= + this.debugOrderPayload?.(header, lines, 'PRE-VALIDATE') + + // ======================================================= + // 🧪 PRE-VALIDATE — prItemVariant ön kontrol + // - invalid varsa CREATE/UPDATE ÇALIŞMAZ + // ======================================================= + const v = await api.post('/order/validate', { header, lines }) + const invalid = v?.data?.invalid || [] + + if (invalid.length > 0) { + await this.showInvalidVariantDialog?.($q, invalid) + return // ❌ create / update ÇALIŞMAZ + } + + console.log('📤 submitAllReal payload', { + mode: this.mode, + lines: lines.length, + deletes: lines.filter(l => l._deleteSignal).length + }) + + /* ======================================================= + 🚀 API CALL — TEK NOKTA + ======================================================= */ + const resp = await api.post( + isNew ? '/order/create' : '/order/update', + { header, lines } + ) + + const data = resp?.data || {} + + serverOrderId = + data.orderID || + data.orderHeaderID || + data.id || + header?.OrderHeaderID + + serverOrderNo = + data.orderNumber || + data.orderNo || + header?.OrderNumber + + if (!serverOrderId) { + throw new Error('OrderHeaderID backend’den dönmedi') + } + + /* ======================================================= + 🔁 MODE SWITCH → EDIT + ======================================================= */ + this.setMode('edit') + + // Header patch (ID / No) + this.header = { + ...this.header, + OrderHeaderID: serverOrderId, + OrderNumber: serverOrderNo + } + + /* ======================================================= + 🧹 KRİTİK: Snapshot + Dirty temizliği + ❗ ROUTE değişmeden ÖNCE + ======================================================= */ + this.updateHeaderTotals?.() + this.markAsSaved?.() + + /* ======================================================= + 🧹 KRİTİK: NEW → EDIT geçişinde TÜM SNAPSHOT TEMİZLENİR + ======================================================= */ + this.clearAllOrderSnapshots() + + $q.notify({ + type: 'positive', + message: `Sipariş kaydedildi: ${serverOrderNo || ''}`.trim() + }) + + /* ======================================================= + 🔀 ROUTE REPLACE (EDIT MODE) + - aynı sayfa → param değişti + - guard 1 kez bypass + ======================================================= */ + this.allowRouteLeaveOnce = true + + await router.replace({ + name: 'order-entry', + params: { orderHeaderID: serverOrderId }, + query: { mode: 'edit', source: 'backend' } + }) + + /* ======================================================= + 🔄 BACKEND RELOAD (TEK GERÇEK KAYNAK) + ======================================================= */ + await this.openExistingForEdit(serverOrderId, { + $q, + form, + summaryRowsRef: summaryRows, + productCache + }) + + /* ======================================================= + ❓ USER NEXT STEP + ======================================================= */ + const choice = await new Promise(resolve => { + $q.dialog({ + title: 'Sipariş Kaydedildi', + options: { + type: 'radio', + model: 'continue', + items: [ + { label: '✏️ Düzenlemeye Devam', value: 'continue' }, + { label: '🖨 Yazdır', value: 'print' }, + { label: '📋 Listeye Dön', value: 'list' } + ] + }, + ok: { label: 'Seç' }, + cancel: { label: 'Kapat' } + }) + .onOk(v => resolve(v)) + .onCancel(() => resolve('continue')) + }) + + /* ======================================================= + 🧭 USER ROUTING + ======================================================= */ + if (choice === 'print') { + const id = this.header?.OrderHeaderID || serverOrderId + if (id) await this.downloadOrderPdf(id) + return + } + + if (choice === 'list') { + this.allowRouteLeaveOnce = true + await router.push({ name: 'order-list' }) + return + } + + // continue → sayfada kal (hiçbir şey yapma) + + } catch (err) { + console.error('❌ submitAllReal:', err) + + $q.notify({ + type: 'negative', + message: + err?.response?.data?.message || + err?.message || + 'Kayıt sırasında hata' + }) + + } finally { + // 🔓 Guard’lar normale dönsün + this.isControlledSubmit = false + this.loading = false + } + } + + , + + /* ======================================================= + 🧪 SUBMIT ALL TEST + ======================================================= */ + async submitAllTest($q = null) { + try { + const { header, lines } = this.buildFinalOrderJson() + + console.log('🧾 TEST HEADER', Object.keys(header).length, 'alan') + console.log(JSON.stringify(header, null, 2)) + + console.log('🧾 TEST LINES', lines.length, 'satır') + console.log(JSON.stringify(lines, null, 2)) + + $q?.notify?.({ + type: 'info', + message: `Header (${Object.keys(header).length}) + Lines (${lines.length}) gösterildi`, + position: 'top' + }) + } catch (err) { + console.error('❌ submitAllTest hata:', err) + $q?.notify?.({ + type: 'negative', + message: 'Gösterimde hata oluştu ❌', + position: 'top' + }) + } + }, + + + /* ======================================================= + 🧹 KAYIT SONRASI TEMİZLİK + ======================================================= */ + afterSubmit(opts = { + keepLocalStorage: true, + backendPayload: null, + resetMode: true // 🔑 yeni + }) { + try { + console.log('🧹 afterSubmit başlatıldı', opts) + + if (opts?.backendPayload?.header?.OrderHeaderID) { + this.mergeAndPersistBackendOrder( + opts.backendPayload.header.OrderHeaderID, + opts.backendPayload + ) + } + + if (!opts?.keepLocalStorage) { + this.clearStorage() + this.clearTransaction() + } else { + this.saveSnapshot() + } + + this.orders = [] + this.header = {} + this.editingKey = null + this.currentOrderId = null + + // 🔐 MODE RESET OPSİYONEL + if (opts.resetMode === true) { + this.mode = 'new' + } + + console.log('✅ afterSubmit tamamlandı.') + } catch (err) { + console.error('❌ afterSubmit hata:', err) + } + } + + , + + + /* =========================================================== + 🟦 BUILD FINAL ORDER JSON — SAFE v26.1 (FINAL) + ----------------------------------------------------------- + ✔ ComboKey TEK OTORİTE → buildComboKey (bedenKey ile) + ✔ UI/Map placeholder: '_' (bedenKey) + ✔ DB/payload: '' (bedenPayload) → "_" ASLA GİTMEZ + ✔ payload içinde aynı ComboKey TEK satır + ✔ backend duplicate guard %100 uyumlu (ComboKey stabil) + ✔ Final assert: payload’da "_" yakalanırsa patlatır + =========================================================== */ + buildFinalOrderJson () { + const auth = useAuthStore() + const u = auth?.user || {} + const now = dayjs() + + /* ========================= + HELPERS + ========================== */ + const toNum = v => Number(v) || 0 + const safeStr = v => (v == null ? '' : String(v).trim()) + + const formatDateOnly = v => (v ? dayjs(v).format('YYYY-MM-DD') : null) + const formatTimeOnly = v => dayjs(v).format('HH:mm:ss') + const formatDateTime = v => (v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : null) + + // ✅ Payload beden normalize: "_" / "-" / "" => '' + const normBeden = (v) => { + const s = safeStr(v) + if (s === '' || s === '_' || s === '-') return '' // payload empty + return s + } + + /* ========================= + USER META + ========================== */ + const group = safeStr(u?.v3usergroup) + const v3name = safeStr(u?.v3_username) + const who = (group && v3name) ? `${group} ${v3name}` : (v3name || 'BSS') + + const PCT_CODE_ZERO = '%0' + const VAT_CODE_ZERO = '%0' + + /* ========================= + HEADER + ========================== */ + const headerId = this.header?.OrderHeaderID || crypto.randomUUID() + const docCurrency = safeStr(this.header?.DocCurrencyCode) || 'TRY' + const exRate = toNum(this.header?.ExchangeRate) || 1 + + const avgDueSource = + this.header?.AverageDueDate || + dayjs(this.header?.OrderDate || now).add(14, 'day') + + const header = { + ...this.header, + + OrderHeaderID: headerId, + OrderDate: formatDateOnly(this.header?.OrderDate || now), + OrderTime: formatTimeOnly(now), + AverageDueDate: formatDateOnly(avgDueSource), + + DocCurrencyCode: docCurrency, + LocalCurrencyCode: safeStr(this.header?.LocalCurrencyCode) || 'TRY', + ExchangeRate: exRate, + + CreatedUserName: + this.mode === 'edit' + ? (this.header?.CreatedUserName || who) + : who, + + CreatedDate: + this.mode === 'edit' + ? formatDateTime(this.header?.CreatedDate || now) + : formatDateTime(now), + + LastUpdatedUserName: who, + LastUpdatedDate: formatDateTime(now) + } + + /* ======================================================= + LINES — COMBOKEY AGGREGATE (TEK MAP) + ======================================================= */ + const lines = [] + const lineByCombo = new Map() // 🔒 KEY = ComboKey + + const pushOrMerge = (row, ctx) => { + const { + grpKey, + bedenKey, // ✅ sadece ComboKey / Map için ('_' olabilir) + bedenPayload, // ✅ DB için ('' / 'S' / 'M' ...) + qty, + orderLineId, + isDeleteSignal + } = ctx + + if (qty <= 0 && !isDeleteSignal) return + + // ComboKey stabil kalsın diye bedenKey kullan + const comboKey = buildComboKey(row, bedenKey) + + const makeLine = () => ({ + OrderLineID: orderLineId || '', + ClientKey: makeLineClientKey(row, grpKey, bedenKey), + ComboKey: comboKey, + + SortOrder: 0, + ItemTypeCode: 1, + + ItemCode: safeStr(row.model), + ColorCode: safeStr(row.renk), + + // ✅ PAYLOAD: "_" ASLA YOK + ItemDim1Code: bedenPayload, + + ItemDim2Code: safeStr(row.renk2), + ItemDim3Code: '', + + Qty1: isDeleteSignal ? 0 : qty, + Qty2: 0, + + CancelQty1: 0, + CancelQty2: 0, + + DeliveryDate: row.terminTarihi + ? formatDateTime(row.terminTarihi) + : null, + + PlannedDateOfLading: row.terminTarihi + ? formatDateOnly(row.terminTarihi) + : null, + + LineDescription: safeStr(row.aciklama), + UsedBarcode: '', + CostCenterCode: '', + + VatCode: VAT_CODE_ZERO, + VatRate: toNum(row.vatRate ?? row.VatRate ?? 0), + + PCTCode: PCT_CODE_ZERO, + PCTRate: 0, + + LDisRate1: 0, + LDisRate2: 0, + LDisRate3: 0, + LDisRate4: 0, + LDisRate5: 0, + + DocCurrencyCode: header.DocCurrencyCode, + PriceCurrencyCode: header.DocCurrencyCode, + PriceExchangeRate: toNum(header.ExchangeRate), + Price: toNum(row.fiyat), + + BaseProcessCode: 'WS', + BaseOrderNumber: header.OrderNumber, + BaseCustomerTypeCode: 0, + BaseCustomerCode: header.CurrAccCode, + BaseSubCurrAccID: null, + BaseStoreCode: '', + + OrderHeaderID: headerId, + + CreatedUserName: who, + CreatedDate: formatDateTime(row.CreatedDate || now), + LastUpdatedUserName: who, + LastUpdatedDate: formatDateTime(now), + + SurplusOrderQtyToleranceRate: 0, + WithHoldingTaxTypeCode: '', + DOVCode: '' + }) + + const existing = lineByCombo.get(comboKey) + + if (!existing) { + const ln = makeLine() + lineByCombo.set(comboKey, ln) + lines.push(ln) + return + } + + /* DELETE */ + if (isDeleteSignal) { + if (orderLineId && !existing.OrderLineID) { + existing.OrderLineID = orderLineId + } + existing.Qty1 = 0 + return + } + + /* MERGE */ + existing.Qty1 += qty + + if (this.mode === 'edit' && orderLineId && !existing.OrderLineID) { + existing.OrderLineID = orderLineId + } + + existing.Price = toNum(row.fiyat) + } + + /* ======================================================= + ORDER ROW LOOP + ======================================================= */ + for (const row of this.orders || []) { + if (row?.isClosed === true) continue + + const grpKey = + row.grpKey || + Object.keys(row.bedenMap || {})[0] || + 'GENEL' + + const lineIdMap = row.lineIdMap || {} + + const grouped = row.bedenMap?.[grpKey] + const flat = + (row.bedenMap && typeof row.bedenMap === 'object' && !grouped) + ? row.bedenMap + : null + + const map = grouped || flat + const hasAnyBeden = + map && typeof map === 'object' && Object.keys(map).length > 0 + + /* 🔹 BEDENSİZ / AKSBİR */ + if (!hasAnyBeden) { + const qty = toNum(row.qty ?? row.Qty1 ?? row.miktar ?? 0) + + // ✅ ComboKey stabil: bedenKey = '_' + const bedenKey = '_' + // ✅ Payload: boş string + const bedenPayload = '' + + let orderLineId = '' + if (this.mode === 'edit') { + // lineIdMap burada '_' ile tutuluyorsa onu da oku + orderLineId = + safeStr(lineIdMap?.[bedenKey]) || + safeStr(lineIdMap?.[bedenPayload]) || + safeStr(row.OrderLineID) + } + + pushOrMerge(row, { + grpKey, + bedenKey, + bedenPayload, + qty, + orderLineId, + isDeleteSignal: row._deleteSignal === true && !!orderLineId + }) + continue + } + + /* 🔹 BEDENLİ */ + for (const [bedenRaw, qtyRaw] of Object.entries(map)) { + const qty = toNum(qtyRaw) + + // ✅ payload beden: '' / 'S' / 'M' ... + const bedenPayload = normBeden(bedenRaw) + // ✅ combokey beden: boşsa '_' ile stabil kalsın + const bedenKey = bedenPayload || '_' + + let orderLineId = '' + if (this.mode === 'edit') { + // lineIdMap anahtarı sizde hangi bedenle tutuluyorsa ikisini de dene + orderLineId = + safeStr(lineIdMap?.[bedenKey]) || + safeStr(lineIdMap?.[bedenPayload]) || + (Object.keys(map).length === 1 + ? safeStr(row.OrderLineID) + : '') + } + + pushOrMerge(row, { + grpKey, + bedenKey, + bedenPayload, + qty, + orderLineId, + isDeleteSignal: row._deleteSignal === true && !!orderLineId + }) + } + } + + /* ======================================================= + FINAL SORT + ======================================================= */ + lines.forEach((ln, i) => { ln.SortOrder = i + 1 }) + + /* ======================================================= + ASSERT — payload’da "_" OLAMAZ + ======================================================= */ + if (lines.some(l => (l.ItemDim1Code || '') === '_' )) { + console.error('❌ Payload’da "_" yakalandı', lines.filter(l => l.ItemDim1Code === '_')) + throw new Error('Payload ItemDim1Code "_" olamaz') + } + /* ======================================================= + 🔍 DEBUG — BUILD FINAL ORDER JSON OUTPUT + ======================================================= */ + console.groupCollapsed('%c📦 BUILD FINAL ORDER JSON', 'color:#c9a873;font-weight:bold') + + console.log('🧾 HEADER:', header) + + console.table( + lines.map((l, i) => ({ + i: i + 1, + OrderLineID: l.OrderLineID, + ClientKey: l.ClientKey, + ComboKey: l.ComboKey, + ItemCode: l.ItemCode, + ColorCode: l.ColorCode, + ItemDim1Code: JSON.stringify(l.ItemDim1Code), // <-- kritik + ItemDim2Code: l.ItemDim2Code, + Qty1: l.Qty1, + Price: l.Price + })) + ) + + console.groupEnd() + + return { header, lines } + } + + + + + + + + ,/* =========================================================== + ✅ STORE ACTIONS — FIXED HELPERS + - setRowErrorByClientKey + - clearRowErrorByClientKey + - applyTerminToRowsIfEmpty +=========================================================== */ + setRowErrorByClientKey(clientKey, payload) { + if (!clientKey) return + if (!Array.isArray(this.summaryRows)) return + + const row = this.summaryRows.find(r => r?.clientKey === clientKey) + if (!row) return + + row._error = { + code: payload?.code, + message: payload?.message + } + }, + + clearRowErrorByClientKey(clientKey) { + if (!clientKey) return + if (!Array.isArray(this.summaryRows)) return + + const row = this.summaryRows.find(r => r?.clientKey === clientKey) + if (!row) return + + if (row._error) { + delete row._error + } + }, + + applyTerminToRowsIfEmpty(dateStr) { + if (!dateStr) return + if (!Array.isArray(this.summaryRows)) return + + // ❗ reassign YOK — patch/mutate + for (const r of this.summaryRows) { + if (!r?.terminTarihi || r.terminTarihi === '') { + r.terminTarihi = dateStr + } + } + + // opsiyonel ama genelde doğru: + this.persistLocalStorage?.() + } + + + + + + + + + + + + + + } // actions sonu +}) // defineStore sonu + + +/* =========================================================== + 🔹 BEDEN LABEL NORMALİZASYONU (exported helper) + =========================================================== */ +export function normalizeBedenLabel(v) { + if (v === null || v === undefined) return ' ' + let s = String(v).trim() + if (s === '') return ' ' + // 44R, 50L vb. son ekleri at + s = s.replace(/(^\d+)\s*[A-Z]?$/i, '$1') + s = s.toUpperCase() + + // harfli bedenlerin normalizasyonu + const map = { + 'XS': 'XS', 'S': 'S', 'M': 'M', 'L': 'L', 'XL': 'XL', + 'XXL': '2XL', '2XL': '2XL', '3XL': '3XL', '4XL': '4XL', + '5XL': '5XL', '6XL': '6XL', '7XL': '7XL', 'STD': 'STD' + } + if (map[s]) return map[s] + + // tamamen sayıysa string olarak döndür + if (/^\d+$/.test(s)) return s + + // virgüllü değer geldiyse ilkini al + if (s.includes(',')) return s.split(',')[0].trim() + return s +} + +/* =========================================================== + 🔹 BEDEN GRUBU ALGILAMA HELPER’I + ----------------------------------------------------------- + Gelen beden listesini, ürün grubu/kategori bilgisine göre + doğru grup anahtarına dönüştürür (ayk, yas, pan, gom, tak, aksbir). + ----------------------------------------------------------- +=========================================================== */ +export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') { + const list = Array.isArray(bedenList) && bedenList.length > 0 + ? bedenList.map(v => (v || '').toString().trim().toUpperCase()) + : [' '] + + const ana = (urunAnaGrubu || '') + .toUpperCase() + .trim() + .replace(/\(.*?\)/g, '') + .replace(/[^A-ZÇĞİÖŞÜ0-9\s]/g, '') + .replace(/\s+/g, ' ') + + const kat = (urunKategori || '').toUpperCase().trim() +// 🔸 Aksesuar ise "aksbir" + const aksesuarGruplari = [ + 'AKSESUAR','KRAVAT','PAPYON','KEMER','CORAP','ÇORAP', + 'FULAR','MENDIL','MENDİL','KASKOL','ASKI', + 'YAKA','KOL DUGMESI','KOL DÜĞMESİ' + ] + const giyimGruplari = ['GÖMLEK','CEKET','PANTOLON','MONT','YELEK','TAKIM','TSHIRT','TİŞÖRT'] + // 🔸 Pantolon özel durumu + if ( + aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) && + !giyimGruplari.some(g => ana.includes(g)) + ) return 'aksbir' + + if (ana.includes('PANTOLON') && kat.includes('YETİŞKİN')) return 'pan' + // 🔸 Tamamen numerik (örneğin 39-44 arası) → ayakkabı + const allNumeric = list.every(v => /^\d+$/.test(v)) + if (allNumeric) { + const nums = list.map(v => parseInt(v, 10)).filter(Boolean) + const diffs = nums.slice(1).map((v, i) => v - nums[i]) + if (diffs.every(d => d === 1) && nums[0] >= 35 && nums[0] <= 46) return 'ayk' + } + + // 🔸 Yaş grubu (çocuk/garson) + if (kat.includes('GARSON') || kat.includes('ÇOCUK')) return 'yas' + + // 🔸 Harfli beden varsa doğrudan "gom" (gömlek, üst giyim) + const harfliBedenler = ['XS','S','M','L','XL','XXL','3XL','4XL'] + if (list.some(b => harfliBedenler.includes(b))) return 'gom' + + + + + // 🔸 Varsayılan: takım elbise + return 'tak' +} + +export function toSummaryRowFromForm(form) { + if (!form) return null + + const grpKey = form.grpKey || 'tak' + const bedenMap = {} + + const labels = Array.isArray(form.bedenLabels) ? form.bedenLabels : [] + const values = Array.isArray(form.bedenler) ? form.bedenler : [] + + for (let i = 0; i < labels.length; i++) { + const rawLbl = labels[i] + const lbl = + rawLbl == null || String(rawLbl).trim() === '' + ? ' ' + : String(rawLbl).trim() + + const val = Number(values[i] || 0) + if (val > 0) { + bedenMap[lbl] = val + } + } + + return { + id: form.id || newGuid(), + OrderLineID: form.OrderLineID || null, + + model: form.model || '', + renk: form.renk || '', + renk2: form.renk2 || '', + + urunAnaGrubu: form.urunAnaGrubu || '', + urunAltGrubu: form.urunAltGrubu || '', + aciklama: form.aciklama || '', + + fiyat: Number(form.fiyat || 0), + pb: form.pb || 'USD', + + adet: Number(form.adet || 0), + tutar: Number(form.tutar || 0), + + grpKey, + bedenMap: { + [grpKey]: { ...bedenMap } + }, + + terminTarihi: (form.terminTarihi || '').substring(0, 10) + } +} + + + + +/* =========================================================== + 🔹 TOPLAM HESAPLAMA (EXPORT) + ----------------------------------------------------------- + Hem store içinde hem de component tarafında kullanılabilir. +=========================================================== */ +export function updateTotals(f) { + f.adet = (f.bedenler || []).reduce((a, b) => a + Number(b || 0), 0) + const fiyat = Number(f.fiyat) || 0 + f.tutar = (f.adet * fiyat).toFixed(2) + return f +} + +/* =========================================================== + 🔹 EXPORT SET — Tek Merkezli Dışa Aktarımlar + =========================================================== */ + +/** + * 🧩 Shared Reactive Refs + * ----------------------------------------------------------- + * import { sharedOrderEntryRefs } from 'src/stores/orderentryStore' + * const { stockMap, bedenStock, sizeCache } = sharedOrderEntryRefs + */ +export const sharedOrderEntryRefs = { + stockMap, + bedenStock, + sizeCache, + + +} + diff --git a/ui/src/stores/permissionStore.js b/ui/src/stores/permissionStore.js new file mode 100644 index 0000000..6fb2fb4 --- /dev/null +++ b/ui/src/stores/permissionStore.js @@ -0,0 +1,182 @@ +import { defineStore } from 'pinia' +import api from 'src/services/api' +import { useAuthStore } from 'stores/authStore' + + +export const usePermissionStore = defineStore('permission', { + + state: () => ({ + + // API route yetkileri + routes: [], + + // module+action matrix + matrix: [], + + loaded: false + }), + + + getters: { + + /* ================= ADMIN ================= */ + + isAdmin () { + const auth = useAuthStore() + return auth.isAdmin === true + }, + + + /* ================= API ROUTE ================= */ + + hasApiPermission: (state) => (apiPathOrPerm) => { + + const auth = useAuthStore() + + if (auth.isAdmin) return true + if (!state.loaded) return false + if (!apiPathOrPerm) return true + + + // ============================ + // 1️⃣ MODULE:ACTION GELDİYSE + // ============================ + + if (apiPathOrPerm.includes(':')) { + + const [module, action] = apiPathOrPerm.split(':') + + return state.matrix.some(p => + p.module === module && + p.action === action && + p.allowed === true + ) + } + + + // ============================ + // 2️⃣ API PATH GELDİYSE + // =========================== + + + const apiPath = apiPathOrPerm + + + // exact match + if (state.routes.some(p => + p.route === apiPath && p.can_access + )) { + return true + } + + + // /{id} normalize + const normalized = apiPath + .replace(/\/\d+/g, '/{id}') + + + if (state.routes.some(p => + p.route === normalized && p.can_access + )) { + return true + } + + + // prefix + return state.routes.some(p => + p.can_access && apiPath.startsWith(p.route) + ) + }, + + + /* ================= MODULE ================= */ + + hasModule: (state) => (module) => { + + const auth = useAuthStore() + + if (auth.isAdmin) return true + if (!state.loaded) return false + + return state.matrix.some(p => + p.module === module && + p.allowed === true + ) + }, + + + /* ================= ACTION ================= */ + + hasPermission: (state) => (module, action) => { + + const auth = useAuthStore() + + if (auth.isAdmin) return true + if (!state.loaded) return false + + return state.matrix.some(p => + p.module === module && + p.action === action && + p.allowed === true + ) + } + }, + + + actions: { + + async fetchPermissions () { + + const auth = useAuthStore() + + if (auth.isAdmin) { + + this.routes = [] + this.matrix = [] + this.loaded = true + + return + } + + + try { + + // API ROUTES + const routesRes = await api.get('/permissions/routes') + this.routes = routesRes.data || [] + + + // EFFECTIVE MATRIX + const effRes = await api.get('/permissions/effective') + this.matrix = effRes.data || [] + console.group('🔐 PERMISSION DEBUG') + + console.log('API ROUTES:', this.routes) + console.log('EFFECTIVE MATRIX:', this.matrix) + + console.groupEnd() + + + } catch (err) { + + console.error('❌ Permission load failed', err) + + this.routes = [] + this.matrix = [] + + + } finally { + + this.loaded = true + } + }, + + + clear () { + + this.routes = [] + this.matrix = [] + this.loaded = false + } + } +}) diff --git a/ui/src/stores/statementdetailStore.js b/ui/src/stores/statementdetailStore.js new file mode 100644 index 0000000..056cfa8 --- /dev/null +++ b/ui/src/stores/statementdetailStore.js @@ -0,0 +1,66 @@ +// src/stores/statementdetailStore.js +import { defineStore } from 'pinia' +import api from 'src/services/api' + +export const useStatementdetailStore = defineStore('statementdetail', { + state: () => ({ + details: [], + loading: false, + error: null + }), + + actions: { + async loadDetails ({ accountCode, startDate, endDate, parislemler }) { + if (!accountCode) { + this.error = 'Geçerli bir cari kod seçilmedi.' + return + } + + this.loading = true + this.error = null + + try { + // ✅ Params (arrayFormat=repeat global) + const params = { + startdate: startDate, + enddate: endDate + } + + if (Array.isArray(parislemler) && parislemler.length > 0) { + params.parislemler = parislemler.filter( + p => p !== undefined && p !== null && p !== '' + ) + } + + // 🔐 TOKEN + SERIALIZER + ERROR HANDLING OTOMATİK + const res = await api.get( + `/statements/${accountCode}/details`, + { params } + ) + + this.details = res.data || [] + + } catch (err) { + console.error('❌ Details yüklenemedi:', err) + this.error = + err?.data?.message || + err?.message || + 'Detaylar yüklenemedi' + } finally { + this.loading = false + } + }, + + getDetailsByBelge (belgeNo) { + return this.details.filter( + d => d.belge_ref_numarasi === belgeNo + ) + }, + + reset () { + this.details = [] + this.loading = false + this.error = null + } + } +}) diff --git a/ui/src/stores/statementheaderStore.js b/ui/src/stores/statementheaderStore.js new file mode 100644 index 0000000..609256c --- /dev/null +++ b/ui/src/stores/statementheaderStore.js @@ -0,0 +1,163 @@ +// src/stores/statementheaderStore.js +import { defineStore } from 'pinia' +import api from 'src/services/api' +import qs from 'qs' +import dayjs from 'src/boot/dayjs' + +export const useStatementheaderStore = defineStore('statementheader', { + state: () => ({ + headers: [], // Ana tablo verileri + details: {}, // Alt tablolar (belge bazlı) + loading: false, // Yükleme durumu + groupOpen: {} // Para birimi bazlı aç/kapa durumu + }), + + getters: { + // 🔹 Benzersiz para birimleri listesi + currencies(state) { + const set = new Set() + for (const r of state.headers) { + set.add(r.para_birimi || '—') + } + return Array.from(set).sort() + }, + + // 🔹 Her para birimi için toplam borç / alacak / bakiye + totalsByCurrency(state) { + const out = {} + + for (const r of state.headers) { + const k = r.para_birimi || '—' + if (!out[k]) { + out[k] = { borc: 0, alacak: 0, bakiye: 0, count: 0 } + } + + out[k].borc += Number(r.borc) || 0 + out[k].alacak += Number(r.alacak) || 0 + out[k].bakiye += Number(r.bakiye) || 0 + out[k].count += 1 + } + + return out + }, + + // 🔹 QTable için satırlar (group + data) + groupedRows: (state) => { + const grouped = {} + + for (const row of state.headers) { + const k = row.para_birimi || '—' + if (!grouped[k]) grouped[k] = [] + grouped[k].push(row) + } + + const output = [] + + for (const [currency, rows] of Object.entries(grouped)) { + if (!rows.length) continue + + // 📅 Tarihe göre sırala + const sorted = [...rows].sort( + (a, b) => new Date(a.belge_tarihi) - new Date(b.belge_tarihi) + ) + + const lastRow = sorted.at(-1) + const lastBalance = + lastRow && lastRow.bakiye != null + ? Number(lastRow.bakiye) + : 0 + + // 🔹 Grup satırı + output.push({ + _type: 'group', + para_birimi: currency, + sonBakiye: lastBalance + }) + + // 🔹 Alt satırlar + if (state.groupOpen[currency] !== false) { + sorted.forEach(r => { + output.push({ ...r, _type: 'data' }) + }) + } + } + + return output + } + }, + + actions: { + /* ========================================================== + 🔄 ANA STATEMENT LİSTESİ + ========================================================== */ + async loadStatements(params = {}) { + this.loading = true + + try { + const { data } = await api.get( + '/statements', + { + params, + paramsSerializer: p => + qs.stringify(p, { arrayFormat: 'repeat' }) + } + ) + + this.headers = Array.isArray(data) ? data : [] + + + // 🔹 Yeni gelen para birimleri default açık + for (const k of this.currencies) { + if (!(k in this.groupOpen)) { + this.groupOpen[k] = true + } + } + } catch (err) { + console.error('❌ Statements yüklenemedi:', err) + this.headers = [] + } finally { + this.loading = false + } + }, + + /* ========================================================== + 📄 BELGE DETAYLARI + ========================================================== */ + async loadDetails(belgeNo) { + if (!belgeNo || this.details[belgeNo]) return + + try { + const { data } = await api.get( + `/statements/${belgeNo}/details` + ) + + this.details[belgeNo] = Array.isArray(data) ? data : [] + + + } catch (err) { + console.error('❌ Details yüklenemedi:', err) + this.details[belgeNo] = [] + } + }, + + /* ========================================================== + 🔘 GRUP AÇ / KAPA + ========================================================== */ + toggleGroup(currency) { + const key = currency || '—' + this.groupOpen[key] = !this.groupOpen[key] + }, + + openAllGroups() { + for (const k of this.currencies) { + this.groupOpen[k] = true + } + }, + + closeAllGroups() { + for (const k of this.currencies) { + this.groupOpen[k] = false + } + } + } +}) diff --git a/ui/src/stores/store-flag.d.ts b/ui/src/stores/store-flag.d.ts new file mode 100644 index 0000000..5a3d510 --- /dev/null +++ b/ui/src/stores/store-flag.d.ts @@ -0,0 +1,13 @@ +/* + WARNING: DO NOT MODIFY OR DELETE + This file is auto-generated by Quasar CLI + It's recommended to NOT .gitignore it + You don't have to use TypeScript in your project, don't worry +*/ +import "quasar/dist/types/feature-flag.d.ts"; + +declare module "quasar/dist/types/feature-flag.d.ts" { + interface QuasarFeatureFlags { + store: true; + } +} diff --git a/ui/src/stores/userPermissionStore.js b/ui/src/stores/userPermissionStore.js new file mode 100644 index 0000000..eadaec0 --- /dev/null +++ b/ui/src/stores/userPermissionStore.js @@ -0,0 +1,31 @@ +import { defineStore } from 'pinia' +import api from 'src/services/api' + +export const useUserPermissionStore = defineStore('userPerm', { + + state: () => ({ + rows: [], + loading: false, + saving: false + }), + + actions: { + + async fetch (id) { + this.loading = true + + const res = await api.get(`/users/${id}/permissions`) + this.rows = res.data || [] + + this.loading = false + }, + + async save (id) { + this.saving = true + + await api.post(`/users/${id}/permissions`, this.rows) + + this.saving = false + } + } +}) diff --git a/ui/src/stores/userSyncStore.js b/ui/src/stores/userSyncStore.js new file mode 100644 index 0000000..c968651 --- /dev/null +++ b/ui/src/stores/userSyncStore.js @@ -0,0 +1,53 @@ +// src/stores/userSyncStore.js +import { defineStore } from 'pinia' + +export const useUserSyncStore = defineStore('userSync', { + state: () => ({ + pgUsers: [], // Postgre kullanıcıları + msUsers: [], // MSSQL kullanıcıları + loading: false, + selectedPg: null + }), + + actions: { + // 🧩 Dummy veri ile başlat + async loadDummy() { + this.loading = true + await new Promise(r => setTimeout(r, 600)) // küçük gecikme + this.pgUsers = [ + { id: 1, code: 'mehmetk', full_name: 'Mehmet Keçeci', email: 'm@b.com', mssql_username: 'MKECECI', sync_status: 'synced', is_active: true }, + { id: 2, code: 'ayse', full_name: 'Ayşe Yılmaz', email: 'a@y.com', mssql_username: null, sync_status: 'pending', is_active: true }, + { id: 3, code: 'ali', full_name: 'Ali Demir', email: 'a@d.com', mssql_username: 'ALI.D', sync_status: 'blocked', is_active: false } + ] + this.msUsers = [ + { username: 'MKECECI', first_name: 'Mehmet', last_name: 'Keçeci', email: 'm@b.com', is_blocked: 0 }, + { username: 'ALI.D', first_name: 'Ali', last_name: 'Demir', email: 'a@d.com', is_blocked: 1 }, + { username: 'YENIUSR', first_name: 'Yeni', last_name: 'Kullanıcı', email: 'y@b.com', is_blocked: 0 } + ] + this.loading = false + }, + + async syncNow() { + this.loading = true + console.log('🔄 Dummy sync tetiklendi...') + await new Promise(r => setTimeout(r, 800)) + this.loading = false + }, + + async map(pgId, msUsername) { + const user = this.pgUsers.find(u => u.id === pgId) + if (user) { + user.mssql_username = msUsername + user.sync_status = 'manual' + } + }, + + async unmap(pgId) { + const user = this.pgUsers.find(u => u.id === pgId) + if (user) { + user.mssql_username = null + user.sync_status = 'pending' + } + } + } +}) diff --git a/ui/src/utils/flatten.js b/ui/src/utils/flatten.js new file mode 100644 index 0000000..5d4c280 --- /dev/null +++ b/ui/src/utils/flatten.js @@ -0,0 +1,54 @@ +// src/utils/flatten.js + +/** + * 🔹 sql.Null* türlerini sadeleştirir. + * Backend'den gelen objelerde genellikle { String, Int32, Float64, Time, Bool, Valid } alanları vardır. + * Bu yardımcı, bunları otomatik olarak primitive değerlere dönüştürür. + * Örnek: + * { String: "ABC", Valid: true } → "ABC" + * { Float64: 10.5, Valid: true } → 10.5 + * { Bool: false, Valid: true } → false + * { Valid: false } → null + */ +export function flat(value) { + if (value === null || value === undefined) return null + + // Eğer sql.Null* objesi geldiyse + if (typeof value === 'object' && 'Valid' in value) { + if (!value.Valid) return null + + // Farklı tiplerde gelen alanları sırayla dene + if ('String' in value) return value.String + if ('Float64' in value) return value.Float64 + if ('Int32' in value) return value.Int32 + if ('Int16' in value) return value.Int16 + if ('Bool' in value) return value.Bool + if ('Time' in value) { + try { + // Tarih objesi veya ISO string olabilir + const t = value.Time + return typeof t === 'string' ? t : new Date(t).toISOString() + } catch { + return null + } + } + return null + } + + // Düz değer (primitive) + return value +} + +/** + * 🔹 Objeyi tamamen düzleştirir (tüm alanlarda flat uygular) + * Örnek: + * flattenObject({ A: { String: "x", Valid: true }, B: 5 }) + * → { A: "x", B: 5 } + */ +export function flattenObject(obj = {}) { + const result = {} + for (const [key, val] of Object.entries(obj)) { + result[key] = flat(val) + } + return result +} diff --git a/ui/src/utils/formatters.js b/ui/src/utils/formatters.js new file mode 100644 index 0000000..126bf76 --- /dev/null +++ b/ui/src/utils/formatters.js @@ -0,0 +1,86 @@ +/* =========================================================== + 📦 src/utils/formatters.js + Tarih, para, sayı, yüzde formatlama yardımcıları +=========================================================== */ + +/** + * 📅 formatDateInput(value) + * SQL veya ISO datetime'i "yyyy-MM-dd" formatına çevirir. + * için uygundur. + * Örnek: "2025-11-07 00:00:00" → "2025-11-07" + */ +export function formatDateInput(value) { + if (!value) return '' + try { + if (typeof value === 'string') { + // "2025-11-07 00:00:00" → "2025-11-07" + if (value.includes(' ')) return value.split(' ')[0] + // "2025-11-07T00:00:00Z" → "2025-11-07" + if (value.includes('T')) return value.split('T')[0] + // zaten yyyy-MM-dd ise aynen dön + if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value + } + if (value instanceof Date) { + return value.toISOString().split('T')[0] + } + return '' + } catch { + return '' + } +} + +/** + * 📅 formatDateDisplay(value) + * "2025-10-31" veya "2025-10-31 00:00:00" → "31.10.2025" + * Sadece ekranda gösterim için. + */ +export function formatDateDisplay(value) { + if (!value) return '' + try { + const d = new Date(value) + if (isNaN(d)) return '' + const dd = String(d.getDate()).padStart(2, '0') + const mm = String(d.getMonth() + 1).padStart(2, '0') + const yyyy = d.getFullYear() + return `${dd}.${mm}.${yyyy}` + } catch { + return '' + } +} + +/** + * 💰 formatMoney(value, currency) + * Sayıyı belirtilen para birimiyle biçimlendirir. + * Varsayılan: TRY + * Örnek: formatMoney(12500, 'USD') + */ +export function formatMoney(value, currency = 'TRY') { + const n = Number(value || 0) + return new Intl.NumberFormat('tr-TR', { + style: 'currency', + currency, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(n) +} + +/** + * 🔢 formatNumber(value, fraction) + * Genel sayı biçimlendirme (küsuratlı veya küsuratsız) + */ +export function formatNumber(value, fraction = 2) { + const n = Number(value || 0) + return n.toLocaleString('tr-TR', { + minimumFractionDigits: fraction, + maximumFractionDigits: fraction, + }) +} + +/** + * 💬 formatPercent(value) + * 0.1 → "%10.00" şeklinde gösterir + */ +export function formatPercent(value) { + const n = Number(value || 0) + return `${(n * 100).toFixed(2)}%` +}