Compare commits

..
110 Commits
Author SHA1 Message Date
M_Kececi db13574e9d Fix product performance grouped JSON build 2026-08-19 01:10:20 +03:00
M_Kececi ba9a744aef Fix product performance grouped JSON build 2026-08-19 01:06:55 +03:00
M_Kececi 82ad3783b3 Fix product performance grouped JSON build 2026-08-19 01:00:03 +03:00
M_Kececi 322bed753f Fix product performance grouped JSON build 2026-08-19 00:53:51 +03:00
M_Kececi bdc9459330 Fix product performance grouped JSON build 2026-08-19 00:50:40 +03:00
M_Kececi 2d85a33623 Fix product performance grouped JSON build 2026-08-19 00:45:58 +03:00
M_Kececi 5f1c37859b Fix product performance grouped JSON build 2026-08-19 00:08:19 +03:00
M_Kececi 31a71ab566 Fix product performance grouped JSON build 2026-08-18 23:38:24 +03:00
M_Kececi 25ef023df5 Fix product performance grouped JSON build 2026-08-18 23:32:39 +03:00
M_Kececi 0c51729f7a Fix product performance grouped JSON build 2026-08-18 22:41:08 +03:00
M_Kececi 20f5fea2e8 Fix product performance grouped JSON build 2026-08-18 22:35:20 +03:00
M_Kececi 65e8aa2b62 Fix product performance grouped JSON build 2026-08-18 22:23:41 +03:00
M_Kececi 1f8355b77c Fix product performance grouped JSON build 2026-08-18 22:17:28 +03:00
M_Kececi 11048f1555 Fix product performance grouped JSON build 2026-08-18 22:11:13 +03:00
M_Kececi b83a3d22ce Fix product performance grouped JSON build 2026-08-18 22:07:33 +03:00
M_Kececi 77af9ef533 Fix product performance grouped JSON build 2026-08-18 22:02:44 +03:00
M_Kececi 4395d6bae2 Fix product performance grouped JSON build 2026-08-18 21:57:46 +03:00
M_Kececi 6bc19e7a43 Fix product performance grouped JSON build 2026-08-18 21:54:16 +03:00
M_Kececi 797d069e59 Fix product performance grouped JSON build 2026-08-18 21:47:27 +03:00
M_Kececi 36e0ec9ff5 Fix product performance grouped JSON build 2026-08-18 21:39:53 +03:00
M_Kececi f14a96a024 Fix product performance grouped JSON build 2026-08-18 21:35:10 +03:00
M_Kececi f877b611b7 Fix product performance grouped JSON build 2026-08-18 21:13:38 +03:00
M_Kececi dbb3f3f380 Fix product performance grouped JSON build 2026-08-18 21:07:52 +03:00
M_Kececi 03287d4850 Fix product performance grouped JSON build 2026-08-18 20:58:29 +03:00
M_Kececi 12ce3a3eca Fix product performance grouped JSON build 2026-08-18 20:54:45 +03:00
M_Kececi 52420bc5ff Fix product performance grouped JSON build 2026-08-18 20:40:39 +03:00
M_Kececi d5aa43ccb5 Fix product performance grouped JSON build 2026-08-18 20:30:44 +03:00
M_Kececi 04461acb58 Fix product performance grouped JSON build 2026-08-18 20:23:55 +03:00
M_Kececi cd101811c4 Fix product performance grouped JSON build 2026-08-18 20:19:24 +03:00
M_Kececi c4028010c0 Fix product performance grouped JSON build 2026-08-18 20:13:21 +03:00
M_Kececi c2d010751f Fix product performance grouped JSON build 2026-08-18 20:08:09 +03:00
M_Kececi a9ab9fd9b0 Fix product performance grouped JSON build 2026-08-18 20:03:12 +03:00
M_Kececi 1945fd4456 Fix product performance grouped JSON build 2026-08-18 19:57:24 +03:00
M_Kececi 7b8f8905e5 Fix product performance grouped JSON build 2026-08-18 19:44:36 +03:00
M_Kececi 89df3d2d3d Fix product performance grouped JSON build 2026-08-18 18:44:26 +03:00
M_Kececi 474904481f Fix product performance grouped JSON build 2026-08-18 13:38:32 +03:00
M_Kececi 6eaf0e3ed3 Fix product performance grouped JSON build 2026-08-18 13:23:10 +03:00
M_Kececi 8249761415 Fix product performance grouped JSON build 2026-08-18 13:10:11 +03:00
M_Kececi dbd84f7399 Fix product performance grouped JSON build 2026-08-18 13:04:50 +03:00
M_Kececi 9edc806345 Fix product performance grouped JSON build 2026-08-18 12:45:11 +03:00
M_Kececi 7a7008cc1b Fix product performance grouped JSON build 2026-07-14 00:18:23 +03:00
M_Kececi 3e0253f6be Fix product performance grouped JSON build 2026-07-13 22:43:51 +03:00
M_Kececi b80b5024f4 Fix product performance grouped JSON build 2026-07-13 21:55:17 +03:00
M_Kececi 5eaf8be9e6 Fix product performance grouped JSON build 2026-07-12 16:15:41 +03:00
M_Kececi cc2930caf2 Fix product performance grouped JSON build 2026-07-09 16:41:41 +03:00
M_Kececi 26b4a63c45 Fix product performance grouped JSON build 2026-07-07 23:51:29 +03:00
M_Kececi c6a3d1552e Fix product performance grouped JSON build 2026-07-07 12:59:19 +03:00
M_Kececi 005d2eafac Fix product performance grouped JSON build 2026-07-07 01:21:07 +03:00
M_Kececi 289f204752 Fix product performance grouped JSON build 2026-07-06 18:01:34 +03:00
M_Kececi 1448dab9d7 Fix product performance grouped JSON build 2026-07-06 17:01:39 +03:00
M_Kececi 5d4216d42e Fix product performance grouped JSON build 2026-07-06 16:54:36 +03:00
M_Kececi 3993c363cb Fix product performance grouped JSON build 2026-07-06 01:28:44 +03:00
M_Kececi a0da3f2bed Fix product performance grouped JSON build 2026-07-06 00:53:30 +03:00
M_Kececi 19a42e0551 Merge remote-tracking branch 'origin/master' 2026-07-05 23:43:20 +03:00
M_Kececi 53cdd1a4a1 Fix product performance grouped JSON build 2026-07-05 23:43:17 +03:00
M_Kececi fe29e763c5 Fix product performance grouped JSON build 2026-07-05 23:41:26 +03:00
M_Kececi 9e1ecb9b3f Fix product performance grouped JSON build 2026-07-05 22:06:43 +03:00
M_Kececi fbdde54eee Fix product performance grouped JSON build 2026-07-05 20:31:27 +03:00
M_Kececi b2113a8d49 Fix product performance grouped JSON build 2026-07-05 18:31:40 +03:00
M_Kececi 28e33d5e58 Fix product performance grouped JSON build 2026-07-05 17:02:28 +03:00
M_Kececi fbdc4934ff Merge remote-tracking branch 'origin/master' 2026-07-05 15:30:29 +03:00
M_Kececi 5b512fd30a Fix product performance grouped JSON build 2026-07-05 15:30:25 +03:00
M_Kececi 7879172c54 Fix product performance grouped JSON build 2026-07-05 15:29:59 +03:00
M_Kececi c602e1e119 Fix product performance grouped JSON build 2026-07-05 13:54:52 +03:00
M_Kececi 27934bccdf Fix product performance grouped JSON build 2026-07-05 05:16:31 +03:00
M_Kececi 48bde09c2c Fix product performance grouped JSON build 2026-07-05 02:58:06 +03:00
M_Kececi 480cad0d6f Fix product performance grouped JSON build 2026-07-05 01:02:04 +03:00
M_Kececi 9f6e288d04 Fix product performance grouped JSON build 2026-07-05 00:38:44 +03:00
M_Kececi d0717900a6 Fix product performance grouped JSON build 2026-07-04 23:31:45 +03:00
M_Kececi 25bc9b2d02 Fix product performance grouped JSON build 2026-07-04 14:11:19 +03:00
M_Kececi 4c56062ef4 Fix product performance grouped JSON build 2026-07-04 13:45:43 +03:00
M_Kececi 60b2f76537 Fix product performance grouped JSON build 2026-07-04 13:26:10 +03:00
M_Kececi 81384426ff Fix product performance grouped JSON build 2026-07-04 13:10:34 +03:00
M_Kececi 3697e35064 Fix product performance grouped JSON build 2026-07-03 21:35:14 +03:00
M_Kececi 4ae6475875 Merge remote-tracking branch 'origin/master' 2026-07-03 20:53:53 +03:00
M_Kececi 5f006b8be0 Fix product performance grouped JSON build 2026-07-03 20:53:48 +03:00
M_Kececi 38c168f7bd Fix product performance grouped JSON build 2026-07-03 20:52:48 +03:00
M_Kececi be125a6880 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	svc/queries/product_performance_sql.go
#	ui/src/pages/ProductPerformanceProfitability.vue
2026-07-03 20:46:31 +03:00
M_Kececi 3b2bcda462 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	svc/queries/product_performance_sql.go
#	ui/src/pages/ProductPerformanceProfitability.vue
2026-07-03 20:40:35 +03:00
M_Kececi 9e22cc5ec5 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	svc/queries/product_performance_sql.go
#	ui/src/pages/ProductPerformanceProfitability.vue
2026-07-03 20:10:05 +03:00
M_Kececi 5e2a85b98e product performance grouped kpi improvements 2026-07-03 20:09:39 +03:00
M_Kececi a45478d15f product performance grouped kpi improvements 2026-07-03 19:53:16 +03:00
M_Kececi b133d43ebe product performance grouped kpi improvements 2026-07-03 17:43:13 +03:00
M_Kececi 9fee8c8ab2 product performance grouped kpi improvements 2026-07-03 17:12:30 +03:00
M_Kececi 95d72ec74b product performance grouped kpi improvements 2026-07-03 15:39:39 +03:00
M_Kececi 4cd1b118d0 product performance grouped kpi improvements 2026-07-03 14:49:50 +03:00
M_Kececi 2f885a4841 product performance grouped kpi improvements 2026-07-03 14:34:30 +03:00
M_Kececi 4aebb36ff2 product performance grouped kpi improvements 2026-07-03 06:01:35 +03:00
M_Kececi fd086d668f product performance grouped kpi improvements 2026-07-03 05:25:05 +03:00
M_Kececi 5288f19976 product performance grouped kpi improvements 2026-07-03 04:27:42 +03:00
M_Kececi 60eef45769 product performance grouped kpi improvements 2026-07-03 03:58:15 +03:00
M_Kececi a05e6bf1e2 product performance grouped kpi improvements 2026-07-03 03:43:17 +03:00
M_Kececi 94c83c3825 product performance grouped kpi improvements 2026-07-03 03:34:55 +03:00
M_Kececi a0dd832d2b product performance grouped kpi improvements 2026-07-03 03:29:31 +03:00
M_Kececi c2d27b2d77 product performance grouped kpi improvements 2026-07-03 03:14:52 +03:00
M_Kececi 9f45160648 product performance grouped kpi improvements 2026-07-03 02:55:58 +03:00
M_Kececi c7ff2328fb product performance grouped kpi improvements 2026-07-03 02:37:58 +03:00
M_Kececi e7a4c58934 product performance grouped kpi improvements 2026-07-03 02:24:24 +03:00
M_Kececi 5d5a38647d product performance grouped kpi improvements 2026-07-03 02:12:55 +03:00
M_Kececi 75e5136ab7 fix sales breakdown ambiguous product fields 2026-07-03 01:56:16 +03:00
M_Kececi 64105815a0 Merge remote-tracking branch 'origin/master' 2026-07-03 01:53:23 +03:00
M_Kececi 98e3587608 product performance grouped kpi improvements 2026-07-03 01:53:19 +03:00
M_Kececi a0d06b5fcf product performance grouped kpi improvements 2026-07-03 01:53:00 +03:00
M_Kececi be6feb8aa8 ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-03 00:30:54 +03:00
M_Kececi c536b85a4c ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-03 00:01:42 +03:00
M_Kececi d419f52e3c ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 23:08:02 +03:00
M_Kececi f3d4661cff ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 21:45:53 +03:00
M_Kececi f07ea2359d Merge remote-tracking branch 'origin/master' 2026-07-02 21:42:14 +03:00
M_Kececi 1ff7a03a81 ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 21:42:10 +03:00
M_Kececi 05e192e30b ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 21:38:08 +03:00
28 changed files with 14125 additions and 1193 deletions
+1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4"> <module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" /> <component name="Go" enabled="true" />
<component name="GoModuleSettings" enabled="true" />
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
+2
View File
@@ -3,6 +3,8 @@ bssapp
# Env # Env
.env .env
.env.local
.env.argebaggi.local
mail.env mail.env
# Runtime fonts # Runtime fonts
+2 -1
View File
@@ -66,10 +66,11 @@ func main() {
log.Fatalf("refresh failed: %v", err) log.Fatalf("refresh failed: %v", err)
} }
fmt.Printf("product performance refresh done: sales=%d stock=%d kpi=%d\n", fmt.Printf("product performance refresh done: sales=%d stock=%d kpi=%d snapshots=%d\n",
result.SalesRows, result.SalesRows,
result.StockRows, result.StockRows,
result.KpiRows, result.KpiRows,
result.SnapshotRows,
) )
} }
+39
View File
@@ -14,6 +14,7 @@ import (
var MssqlDB *sql.DB var MssqlDB *sql.DB
var UretimDB *sql.DB var UretimDB *sql.DB
var ArgeBaggiDB *sql.DB
func envInt(name string, fallback int) int { func envInt(name string, fallback int) int {
raw := strings.TrimSpace(os.Getenv(name)) raw := strings.TrimSpace(os.Getenv(name))
@@ -158,3 +159,41 @@ func ConnectMSSQLUretim() error {
func GetUretimDB() *sql.DB { func GetUretimDB() *sql.DB {
return UretimDB return UretimDB
} }
// ConnectMSSQLArgeBaggi initializes the optional ArgeBAGGI connection.
// Credentials must be supplied through ARGEBAGGI_MSSQL_CONN; never keep them
// in source control.
func ConnectMSSQLArgeBaggi() error {
connString := strings.TrimSpace(os.Getenv("ARGEBAGGI_MSSQL_CONN"))
if connString == "" {
return fmt.Errorf("ARGEBAGGI_MSSQL_CONN tanimli degil")
}
connectionTimeoutSec := envInt("ARGEBAGGI_MSSQL_CONNECTION_TIMEOUT_SEC", 10)
dialTimeoutSec := envInt("ARGEBAGGI_MSSQL_DIAL_TIMEOUT_SEC", connectionTimeoutSec)
connString = ensureMSSQLTimeouts(connString, connectionTimeoutSec, dialTimeoutSec)
var err error
ArgeBaggiDB, err = sql.Open("sqlserver", connString)
if err != nil {
return fmt.Errorf("ArgeBAGGI MSSQL baglanti hatasi: %w", err)
}
ArgeBaggiDB.SetMaxOpenConns(envInt("ARGEBAGGI_MSSQL_MAX_OPEN_CONNS", 10))
ArgeBaggiDB.SetMaxIdleConns(envInt("ARGEBAGGI_MSSQL_MAX_IDLE_CONNS", 5))
ArgeBaggiDB.SetConnMaxLifetime(time.Duration(envInt("ARGEBAGGI_MSSQL_CONN_MAX_LIFETIME_MIN", 30)) * time.Minute)
ArgeBaggiDB.SetConnMaxIdleTime(time.Duration(envInt("ARGEBAGGI_MSSQL_CONN_MAX_IDLE_MIN", 10)) * time.Minute)
if err = ArgeBaggiDB.Ping(); err != nil {
_ = ArgeBaggiDB.Close()
ArgeBaggiDB = nil
return fmt.Errorf("ArgeBAGGI MSSQL erisilemiyor: %w", err)
}
fmt.Printf("ArgeBAGGI MSSQL baglantisi basarili (connection timeout=%ds, dial timeout=%ds)\n", connectionTimeoutSec, dialTimeoutSec)
return nil
}
func GetArgeBaggiDB() *sql.DB {
return ArgeBaggiDB
}
+61
View File
@@ -227,6 +227,7 @@ InitRoutes — FULL V3 (Method-aware) PERMISSION EDITION
func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router { func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {
r := mux.NewRouter() r := mux.NewRouter()
mountUploads(r)
mountSPA(r) mountSPA(r)
/* /*
@@ -829,6 +830,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"order", "view", "order", "view",
wrapV3(http.HandlerFunc(routes.GetProductImagesHandler(pgDB))), wrapV3(http.HandlerFunc(routes.GetProductImagesHandler(pgDB))),
) )
bindV3(r, pgDB,
"/api/product-images/batch", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductImagesBatchHandler(pgDB))),
)
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/product-images/{id}/content", "GET", "/api/product-images/{id}/content", "GET",
"order", "view", "order", "view",
@@ -934,6 +940,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"pricing", "view", "pricing", "view",
wrapV3(routes.GetProductPerformanceHandler(pgDB)), wrapV3(routes.GetProductPerformanceHandler(pgDB)),
) )
bindV3(r, pgDB,
"/api/pricing/product-performance/export-excel", "POST",
"pricing", "view",
wrapV3(routes.ExportProductPerformanceExcelHandler(pgDB)),
)
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/pricing/product-performance/summary", "GET", "/api/pricing/product-performance/summary", "GET",
"pricing", "view", "pricing", "view",
@@ -979,6 +990,26 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"pricing", "view", "pricing", "view",
wrapV3(routes.GetProductPerformanceCustomersHandler(pgDB)), wrapV3(routes.GetProductPerformanceCustomersHandler(pgDB)),
) )
bindV3(r, pgDB,
"/api/pricing/product-performance/sales-breakdown", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceSalesBreakdownHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/grouped", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceGroupedHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/grouped", "POST",
"pricing", "view",
wrapV3(routes.GetProductPerformanceGroupedHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/grouped/filter-options", "POST",
"pricing", "view",
wrapV3(routes.GetProductPerformanceGroupedFilterOptionsHandler(pgDB)),
)
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/pricing/product-performance/sales-details", "GET", "/api/pricing/product-performance/sales-details", "GET",
"pricing", "view", "pricing", "view",
@@ -1359,6 +1390,7 @@ func main() {
// (e.g. SSH-tunnel PostgreSQL on 127.0.0.1:15432). // (e.g. SSH-tunnel PostgreSQL on 127.0.0.1:15432).
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
_ = godotenv.Overload(".env.local") _ = godotenv.Overload(".env.local")
_ = godotenv.Overload(".env.argebaggi.local")
} }
jwtSecret := os.Getenv("JWT_SECRET") jwtSecret := os.Getenv("JWT_SECRET")
@@ -1386,6 +1418,15 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
} }
if err := db.ConnectMSSQLArgeBaggi(); err != nil {
if strings.Contains(err.Error(), "ARGEBAGGI_MSSQL_CONN tanimli degil") {
log.Println("ArgeBAGGI DB baglantisi atlandi: ARGEBAGGI_MSSQL_CONN tanimli degil")
} else {
// This source enriches one screen; a temporary outage must not stop
// the whole application from starting.
log.Println("ArgeBAGGI DB baglantisi basarisiz:", err)
}
}
pgDB, err := db.ConnectPostgres() pgDB, err := db.ConnectPostgres()
if err != nil { if err != nil {
@@ -1487,6 +1528,26 @@ func main() {
} }
func mountUploads(r *mux.Router) {
root := strings.TrimSpace(os.Getenv("BLOB_ROOT"))
if root == "" {
return
}
uploadsRoot := filepath.Join(root, "uploads")
if fi, err := os.Stat(uploadsRoot); err != nil || !fi.IsDir() {
return
}
fileServer := http.StripPrefix("/uploads/", http.FileServer(http.Dir(uploadsRoot)))
r.PathPrefix("/uploads/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Cache-Control", "public, max-age=3600")
fileServer.ServeHTTP(w, r)
})).Methods(http.MethodGet, http.MethodHead, http.MethodOptions)
}
func mountSPA(r *mux.Router) { func mountSPA(r *mux.Router) {
r.NotFoundHandler = http.HandlerFunc(spaIndex) r.NotFoundHandler = http.HandlerFunc(spaIndex)
r.HandleFunc("/", spaIndex).Methods(http.MethodGet) r.HandleFunc("/", spaIndex).Methods(http.MethodGet)
+1
View File
@@ -66,6 +66,7 @@ var routeMetaCache sync.Map
var routeMetaFallback = map[string]routeMeta{ var routeMetaFallback = map[string]routeMeta{
"GET /api/product-images": {module: "order", action: "view"}, "GET /api/product-images": {module: "order", action: "view"},
"POST /api/product-images/batch": {module: "order", action: "view"},
"GET /api/product-images/{id}/content": {module: "order", action: "view"}, "GET /api/product-images/{id}/content": {module: "order", action: "view"},
"GET /api/product-stock-query-by-attributes": {module: "order", action: "view"}, "GET /api/product-stock-query-by-attributes": {module: "order", action: "view"},
} }
+140
View File
@@ -4,6 +4,7 @@ type ProductPerformanceRow struct {
KpiDate string `json:"kpi_date"` KpiDate string `json:"kpi_date"`
ProductCode string `json:"product_code"` ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"` ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"` YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"` ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"` Kategori string `json:"kategori"`
@@ -20,14 +21,28 @@ type ProductPerformanceRow struct {
SalesQty180 float64 `json:"sales_qty_180d"` SalesQty180 float64 `json:"sales_qty_180d"`
SalesQty365 float64 `json:"sales_qty_365d"` SalesQty365 float64 `json:"sales_qty_365d"`
SalesQty730 float64 `json:"sales_qty_730d"` SalesQty730 float64 `json:"sales_qty_730d"`
SalesQtyTotal float64 `json:"sales_qty_total"`
SalesUSD30 float64 `json:"sales_usd_30d"` SalesUSD30 float64 `json:"sales_usd_30d"`
SalesUSD90 float64 `json:"sales_usd_90d"` SalesUSD90 float64 `json:"sales_usd_90d"`
SalesUSD180 float64 `json:"sales_usd_180d"` SalesUSD180 float64 `json:"sales_usd_180d"`
SalesUSD365 float64 `json:"sales_usd_365d"` SalesUSD365 float64 `json:"sales_usd_365d"`
SalesUSDTotal float64 `json:"sales_usd_total"`
AvgDailySales90 float64 `json:"avg_daily_sales_90d"` AvgDailySales90 float64 `json:"avg_daily_sales_90d"`
AvgDailySales180 float64 `json:"avg_daily_sales_180d"` AvgDailySales180 float64 `json:"avg_daily_sales_180d"`
AvgDailySales365 float64 `json:"avg_daily_sales_365d"`
AvgDailySalesTotal float64 `json:"avg_daily_sales_total"`
AvgStock90 float64 `json:"avg_stock_90d"`
AvgStock180 float64 `json:"avg_stock_180d"`
AvgStock365 float64 `json:"avg_stock_365d"`
AvgStockTotal float64 `json:"avg_stock_total"`
StockDays90 float64 `json:"stock_days_90d"` StockDays90 float64 `json:"stock_days_90d"`
StockDays180 float64 `json:"stock_days_180d"` StockDays180 float64 `json:"stock_days_180d"`
StockDays365 float64 `json:"stock_days_365d"`
StockDaysTotal float64 `json:"stock_days_total"`
StockTurnover90 float64 `json:"stock_turnover_90d"`
StockTurnover180 float64 `json:"stock_turnover_180d"`
StockTurnover365 float64 `json:"stock_turnover_365d"`
StockTurnoverTotal float64 `json:"stock_turnover_total"`
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"` AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"` AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
CostPriceUSD float64 `json:"cost_price_usd"` CostPriceUSD float64 `json:"cost_price_usd"`
@@ -42,10 +57,23 @@ type ProductPerformanceRow struct {
UnitProfitBase90 float64 `json:"unit_profit_base_90d"` UnitProfitBase90 float64 `json:"unit_profit_base_90d"`
UnitProfitBase180 float64 `json:"unit_profit_base_180d"` UnitProfitBase180 float64 `json:"unit_profit_base_180d"`
MarketCount90 int `json:"market_count_90d"` MarketCount90 int `json:"market_count_90d"`
MarketCount180 int `json:"market_count_180d"`
MarketCount365 int `json:"market_count_365d"`
MarketCountTotal int `json:"market_count_total"`
CustomerCount90 int `json:"customer_count_90d"` CustomerCount90 int `json:"customer_count_90d"`
CustomerCount180 int `json:"customer_count_180d"`
CustomerCount365 int `json:"customer_count_365d"`
CustomerCountTotal int `json:"customer_count_total"`
SalesIndex90 float64 `json:"sales_index_90d"` SalesIndex90 float64 `json:"sales_index_90d"`
SalesIndex180 float64 `json:"sales_index_180d"`
SalesIndex365 float64 `json:"sales_index_365d"`
SalesIndexTotal float64 `json:"sales_index_total"`
PriceIndex90 float64 `json:"price_index_90d"` PriceIndex90 float64 `json:"price_index_90d"`
MarginIndex90 float64 `json:"margin_index_90d"` MarginIndex90 float64 `json:"margin_index_90d"`
PerformanceScore90 float64 `json:"performance_score_90d"`
PerformanceScore180 float64 `json:"performance_score_180d"`
PerformanceScore365 float64 `json:"performance_score_365d"`
PerformanceScoreTotal float64 `json:"performance_score_total"`
PerformanceScore float64 `json:"performance_score"` PerformanceScore float64 `json:"performance_score"`
PerformanceBucket string `json:"performance_bucket"` PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"` Recommendation string `json:"recommendation"`
@@ -76,6 +104,7 @@ type ProductPerformanceGeneralRow struct {
PeriodEnd string `json:"period_end"` PeriodEnd string `json:"period_end"`
ProductCode string `json:"product_code"` ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"` ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"` YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"` ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"` Kategori string `json:"kategori"`
@@ -113,6 +142,7 @@ type ProductPerformanceGeneralRow struct {
type ProductPerformanceOrderAnalysisRow struct { type ProductPerformanceOrderAnalysisRow struct {
ProductCode string `json:"product_code"` ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"` ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"` YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"` ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"` Kategori string `json:"kategori"`
@@ -177,8 +207,14 @@ type ProductPerformanceOrderGroupRow struct {
type ProductPerformanceOrderProductCustomerRow struct { type ProductPerformanceOrderProductCustomerRow struct {
ProductCode string `json:"product_code"` ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"` ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"` YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"` ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
MarketKey string `json:"market_key"` MarketKey string `json:"market_key"`
CustomerCode string `json:"customer_code"` CustomerCode string `json:"customer_code"`
CustomerName string `json:"customer_name"` CustomerName string `json:"customer_name"`
@@ -209,8 +245,14 @@ type ProductPerformanceOrderMarketDetailRow struct {
DueDate string `json:"due_date"` DueDate string `json:"due_date"`
ProductCode string `json:"product_code"` ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"` ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"` YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"` ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
OrderQty float64 `json:"order_qty"` OrderQty float64 `json:"order_qty"`
OrderUSD float64 `json:"order_usd"` OrderUSD float64 `json:"order_usd"`
AvgOrderPriceUSD float64 `json:"avg_order_price_usd"` AvgOrderPriceUSD float64 `json:"avg_order_price_usd"`
@@ -282,6 +324,104 @@ type ProductPerformanceCustomerRow struct {
LastSaleDate string `json:"last_sale_date"` LastSaleDate string `json:"last_sale_date"`
} }
type ProductPerformanceSalesBreakdownRow struct {
Breakdown string `json:"breakdown"`
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
MarketKey string `json:"market_key"`
Country string `json:"country"`
CustomerSegment string `json:"customer_segment"`
CustomerCode string `json:"customer_code"`
CustomerName string `json:"customer_name"`
ProductCount int `json:"product_count"`
ProductGroupCount90 int `json:"product_group_count_90d"`
ProductGroupCount180 int `json:"product_group_count_180d"`
ProductGroupCount365 int `json:"product_group_count_365d"`
ProductGroupCountTotal int `json:"product_group_count_total"`
MarketCount90 int `json:"market_count_90d"`
MarketCount180 int `json:"market_count_180d"`
MarketCount365 int `json:"market_count_365d"`
MarketCountTotal int `json:"market_count_total"`
CustomerCount90 int `json:"customer_count_90d"`
InvoiceCount90 int `json:"invoice_count_90d"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
BasePriceUSD90 float64 `json:"base_price_usd_90d"`
CostPriceUSD90 float64 `json:"cost_price_usd_90d"`
GrossProfitBase90 float64 `json:"gross_profit_base_usd_90d"`
GrossProfitCost90 float64 `json:"gross_profit_cost_usd_90d"`
GrossMarginBase90 float64 `json:"gross_margin_base_90d"`
GrossMarginCost90 float64 `json:"gross_margin_cost_90d"`
CustomerCount180 int `json:"customer_count_180d"`
InvoiceCount180 int `json:"invoice_count_180d"`
SalesQty180 float64 `json:"sales_qty_180d"`
SalesUSD180 float64 `json:"sales_usd_180d"`
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
BasePriceUSD180 float64 `json:"base_price_usd_180d"`
CostPriceUSD180 float64 `json:"cost_price_usd_180d"`
GrossProfitBase180 float64 `json:"gross_profit_base_usd_180d"`
GrossProfitCost180 float64 `json:"gross_profit_cost_usd_180d"`
GrossMarginBase180 float64 `json:"gross_margin_base_180d"`
GrossMarginCost180 float64 `json:"gross_margin_cost_180d"`
SalesQty365 float64 `json:"sales_qty_365d"`
SalesUSD365 float64 `json:"sales_usd_365d"`
AvgPriceUSD365 float64 `json:"avg_price_usd_365d"`
BasePriceUSD365 float64 `json:"base_price_usd_365d"`
CostPriceUSD365 float64 `json:"cost_price_usd_365d"`
GrossProfitBase365 float64 `json:"gross_profit_base_usd_365d"`
GrossProfitCost365 float64 `json:"gross_profit_cost_usd_365d"`
GrossMarginBase365 float64 `json:"gross_margin_base_365d"`
GrossMarginCost365 float64 `json:"gross_margin_cost_365d"`
CustomerCount365 int `json:"customer_count_365d"`
InvoiceCount365 int `json:"invoice_count_365d"`
CustomerCountTotal int `json:"customer_count_total"`
InvoiceCountTotal int `json:"invoice_count_total"`
SalesQtyTotal float64 `json:"sales_qty_total"`
SalesUSDTotal float64 `json:"sales_usd_total"`
AvgPriceUSDTotal float64 `json:"avg_price_usd_total"`
BasePriceUSDTotal float64 `json:"base_price_usd_total"`
CostPriceUSDTotal float64 `json:"cost_price_usd_total"`
GrossProfitBaseTotal float64 `json:"gross_profit_base_usd_total"`
GrossProfitCostTotal float64 `json:"gross_profit_cost_usd_total"`
GrossMarginBaseTotal float64 `json:"gross_margin_base_total"`
GrossMarginCostTotal float64 `json:"gross_margin_cost_total"`
StockQty float64 `json:"stock_qty"`
AvgStock90 float64 `json:"avg_stock_90d"`
AvgStock180 float64 `json:"avg_stock_180d"`
AvgStock365 float64 `json:"avg_stock_365d"`
AvgStockTotal float64 `json:"avg_stock_total"`
StockTurnover90 float64 `json:"stock_turnover_90d"`
StockTurnover180 float64 `json:"stock_turnover_180d"`
StockTurnover365 float64 `json:"stock_turnover_365d"`
StockTurnoverTotal float64 `json:"stock_turnover_total"`
HasCost bool `json:"has_cost"`
SalesIndex90 float64 `json:"sales_index_90d"`
SalesIndex180 float64 `json:"sales_index_180d"`
SalesIndex365 float64 `json:"sales_index_365d"`
SalesIndexTotal float64 `json:"sales_index_total"`
CustomerScore90 float64 `json:"customer_score_90d"`
CustomerScore180 float64 `json:"customer_score_180d"`
CustomerScore365 float64 `json:"customer_score_365d"`
CustomerScoreTotal float64 `json:"customer_score_total"`
PerformanceScore90 float64 `json:"performance_score_90d"`
PerformanceScore180 float64 `json:"performance_score_180d"`
PerformanceScore365 float64 `json:"performance_score_365d"`
PerformanceScoreTotal float64 `json:"performance_score_total"`
PerformanceScore float64 `json:"performance_score"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
LastSaleDate string `json:"last_sale_date"`
}
type ProductPerformanceSalesDetailRow struct { type ProductPerformanceSalesDetailRow struct {
SalesDate string `json:"sales_date"` SalesDate string `json:"sales_date"`
RefNumber string `json:"ref_number"` RefNumber string `json:"ref_number"`
+7
View File
@@ -12,11 +12,18 @@ type ProductionNoCostProductRow struct {
SKullaniciAdi string `json:"sKullaniciAdi"` SKullaniciAdi string `json:"sKullaniciAdi"`
SKullaniciGunc string `json:"sKullaniciAdiGunc"` SKullaniciGunc string `json:"sKullaniciAdiGunc"`
MMiktarG float64 `json:"lMMiktar_G"` MMiktarG float64 `json:"lMMiktar_G"`
CeketDepoGiris float64 `json:"ceketDepoGiris"`
PantolonDepoGiris float64 `json:"pantolonDepoGiris"`
YelekDepoGiris float64 `json:"yelekDepoGiris"`
MModelKodu string `json:"sMModelKodu"` MModelKodu string `json:"sMModelKodu"`
} }
type ProductionHasCostProductRow struct { type ProductionHasCostProductRow struct {
UretimSekli string `json:"UretimSekli"` UretimSekli string `json:"UretimSekli"`
UrtSiparisNo string `json:"nUrtSiparisNo"`
CeketDepoGiris float64 `json:"ceketDepoGiris"`
PantolonDepoGiris float64 `json:"pantolonDepoGiris"`
YelekDepoGiris float64 `json:"yelekDepoGiris"`
NOnMLNo string `json:"nOnMLNo"` NOnMLNo string `json:"nOnMLNo"`
UrunKodu string `json:"UrunKodu"` UrunKodu string `json:"UrunKodu"`
UrunAdi string `json:"UrunAdi"` UrunAdi string `json:"UrunAdi"`
+94 -2
View File
@@ -26,6 +26,12 @@ func startProductPerformanceScheduler(pgDB *sql.DB) {
deltaHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_DELTA_HHMM", "02:00") deltaHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_DELTA_HHMM", "02:00")
deltaTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_DELTA_TIMEOUT_HOURS", 6, 1, 24) deltaTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_DELTA_TIMEOUT_HOURS", 6, 1, 24)
runOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_DELTA_RUN_ON_STARTUP", false) runOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_DELTA_RUN_ON_STARTUP", false)
fullEnabled := productPerformanceEnvBool("PRODUCT_PERFORMANCE_FULL_ENABLED", false)
fullWeekday := productPerformanceEnvWeekday("PRODUCT_PERFORMANCE_FULL_WEEKDAY", time.Saturday)
fullHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_FULL_HHMM", "16:00")
fullStartDate := productPerformanceEnvDate("PRODUCT_PERFORMANCE_FULL_START_DATE", time.Date(2022, 1, 1, 0, 0, 0, 0, time.Local))
fullTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_FULL_TIMEOUT_HOURS", 18, 1, 72)
fullRunOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_FULL_RUN_ON_STARTUP", false)
var running int32 var running int32
runDelta := func(reason string) { runDelta := func(reason string) {
@@ -49,12 +55,40 @@ func startProductPerformanceScheduler(pgDB *sql.DB) {
log.Printf("[ProductPerformanceJob] error (%s): %v", reason, err) log.Printf("[ProductPerformanceJob] error (%s): %v", reason, err)
return return
} }
log.Printf("[ProductPerformanceJob] ok (%s): mode=%s stage=%s start=%s end=%s sales=%d stock=%d kpi=%d duration_ms=%d", log.Printf("[ProductPerformanceJob] ok (%s): mode=%s stage=%s start=%s end=%s sales=%d stock=%d kpi=%d snapshots=%d duration_ms=%d",
reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.DurationMS) reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.SnapshotRows, result.DurationMS)
}
runFull := func(reason string) {
if !atomic.CompareAndSwapInt32(&running, 0, 1) {
log.Printf("[ProductPerformanceJob] skip (%s): already running", reason)
return
}
defer atomic.StoreInt32(&running, 0)
endDate := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(fullTimeoutHours)*time.Hour)
defer cancel()
result, err := queries.RefreshProductPerformance(ctx, pgDB, queries.ProductPerformanceRefreshRequest{
Mode: "full",
Stage: "all",
StartDate: fullStartDate,
EndDate: endDate,
})
if err != nil {
log.Printf("[ProductPerformanceJob] error (%s): %v", reason, err)
return
}
log.Printf("[ProductPerformanceJob] ok (%s): mode=%s stage=%s start=%s end=%s sales=%d stock=%d kpi=%d snapshots=%d duration_ms=%d",
reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.SnapshotRows, result.DurationMS)
} }
log.Printf("[ProductPerformanceJob] scheduled daily_delta=%s lookback_days=%d run_delta_on_startup=%t", log.Printf("[ProductPerformanceJob] scheduled daily_delta=%s lookback_days=%d run_delta_on_startup=%t",
deltaHHMM, deltaDays, runOnStartup) deltaHHMM, deltaDays, runOnStartup)
if fullEnabled {
log.Printf("[ProductPerformanceJob] scheduled weekly_full=%s %s start_date=%s timeout_hours=%d run_full_on_startup=%t",
fullWeekday.String(), fullHHMM, fullStartDate.Format("2006-01-02"), fullTimeoutHours, fullRunOnStartup)
}
go func() { go func() {
if runOnStartup { if runOnStartup {
@@ -69,6 +103,21 @@ func startProductPerformanceScheduler(pgDB *sql.DB) {
runDelta("daily-delta") runDelta("daily-delta")
} }
}() }()
if fullEnabled {
go func() {
if fullRunOnStartup {
time.Sleep(30 * time.Second)
runFull("startup-full")
}
for {
next := productPerformanceNextWeekly(time.Now(), fullWeekday, fullHHMM)
log.Printf("[ProductPerformanceJob] weekly full next_at=%s in=%s", next.Format(time.RFC3339), time.Until(next).Round(time.Second))
time.Sleep(time.Until(next))
runFull("weekly-full")
}
}()
}
} }
func productPerformanceNextDaily(now time.Time, hhmm string) time.Time { func productPerformanceNextDaily(now time.Time, hhmm string) time.Time {
@@ -80,6 +129,16 @@ func productPerformanceNextDaily(now time.Time, hhmm string) time.Time {
return next return next
} }
func productPerformanceNextWeekly(now time.Time, weekday time.Weekday, hhmm string) time.Time {
hour, minute := productPerformanceParseHHMM(hhmm, 16, 0)
daysUntil := (int(weekday) - int(now.Weekday()) + 7) % 7
next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location()).AddDate(0, 0, daysUntil)
if !next.After(now) {
next = next.AddDate(0, 0, 7)
}
return next
}
func productPerformanceParseHHMM(raw string, fallbackHour, fallbackMinute int) (int, int) { func productPerformanceParseHHMM(raw string, fallbackHour, fallbackMinute int) (int, int) {
parts := strings.Split(strings.TrimSpace(raw), ":") parts := strings.Split(strings.TrimSpace(raw), ":")
if len(parts) != 2 { if len(parts) != 2 {
@@ -120,3 +179,36 @@ func productPerformanceEnvBool(name string, fallback bool) bool {
} }
return raw == "1" || raw == "true" || raw == "on" || raw == "yes" return raw == "1" || raw == "true" || raw == "on" || raw == "yes"
} }
func productPerformanceEnvDate(name string, fallback time.Time) time.Time {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
parsed, err := time.ParseInLocation("2006-01-02", raw, time.Local)
if err != nil {
return fallback
}
return parsed
}
func productPerformanceEnvWeekday(name string, fallback time.Weekday) time.Weekday {
switch strings.ToLower(strings.TrimSpace(os.Getenv(name))) {
case "sunday", "sun", "pazar", "0":
return time.Sunday
case "monday", "mon", "pazartesi", "1":
return time.Monday
case "tuesday", "tue", "sali", "salı", "2":
return time.Tuesday
case "wednesday", "wed", "carsamba", "çarşamba", "3":
return time.Wednesday
case "thursday", "thu", "persembe", "perşembe", "4":
return time.Thursday
case "friday", "fri", "cuma", "5":
return time.Friday
case "saturday", "sat", "cumartesi", "6":
return time.Saturday
default:
return fallback
}
}
File diff suppressed because it is too large Load Diff
+272 -168
View File
@@ -8,9 +8,9 @@ func productPerformanceSalesSQL() string {
SalesDate = CAST(I.InvoiceDate AS date), SalesDate = CAST(I.InvoiceDate AS date),
I.InvoiceHeaderID, I.InvoiceHeaderID,
I.InvoiceNumber, I.InvoiceNumber,
ProductCode = LTRIM(RTRIM(I.ItemCode)), ProductCode = UPPER(LTRIM(RTRIM(I.ItemCode))),
ColorCode = LTRIM(RTRIM(ISNULL(I.ColorCode, ''))), ColorCode = UPPER(LTRIM(RTRIM(ISNULL(I.ColorCode, '')))),
YakaKodu = LTRIM(RTRIM(ISNULL(I.ItemDim2Code, ''))), YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(I.ItemDim2Code, '')))),
ItemDescription = dbo.HG_Temizlik(ISNULL(( ItemDescription = dbo.HG_Temizlik(ISNULL((
SELECT ItemDescription SELECT ItemDescription
FROM cdItemDesc WITH(NOLOCK) FROM cdItemDesc WITH(NOLOCK)
@@ -18,67 +18,11 @@ func productPerformanceSalesSQL() string {
AND cdItemDesc.ItemCode = I.ItemCode AND cdItemDesc.ItemCode = I.ItemCode
AND cdItemDesc.LangCode = 'TR' AND cdItemDesc.LangCode = 'TR'
), SPACE(0))), ), SPACE(0))),
UrunAnaGrubu = dbo.HG_Temizlik(ISNULL(( UrunAnaGrubu = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt01, SPACE(0))),
SELECT AttributeDescription UrunAltGrubu = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt02, SPACE(0))),
FROM cdItemAttributeDesc WITH(NOLOCK) Kategori = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt44, SPACE(0))),
WHERE cdItemAttributeDesc.ItemTypeCode = I.ItemTypeCode UrunIlkGrubu = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt42, SPACE(0))),
AND cdItemAttributeDesc.AttributeTypeCode = 1 AskiliYan = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt45, SPACE(0))),
AND cdItemAttributeDesc.AttributeCode = (
SELECT TOP 1 AttributeCode
FROM prItemAttribute WITH(NOLOCK)
WHERE AttributeTypeCode = 1
AND prItemAttribute.ItemTypeCode = I.ItemTypeCode
AND prItemAttribute.ItemCode = I.ItemCode
)
AND cdItemAttributeDesc.LangCode = 'TR'
), SPACE(0))),
UrunAltGrubu = dbo.HG_Temizlik(ISNULL((
SELECT AttributeDescription
FROM cdItemAttributeDesc WITH(NOLOCK)
WHERE cdItemAttributeDesc.ItemTypeCode = I.ItemTypeCode
AND cdItemAttributeDesc.AttributeTypeCode = 2
AND cdItemAttributeDesc.AttributeCode = (
SELECT TOP 1 AttributeCode
FROM prItemAttribute WITH(NOLOCK)
WHERE AttributeTypeCode = 2
AND prItemAttribute.ItemTypeCode = I.ItemTypeCode
AND prItemAttribute.ItemCode = I.ItemCode
)
AND cdItemAttributeDesc.LangCode = 'TR'
), SPACE(0))),
Kategori = dbo.HG_Temizlik(ISNULL((
SELECT AttributeDescription
FROM cdItemAttributeDesc WITH(NOLOCK)
WHERE cdItemAttributeDesc.ItemTypeCode = I.ItemTypeCode
AND cdItemAttributeDesc.AttributeTypeCode = 42
AND cdItemAttributeDesc.AttributeCode = (
SELECT TOP 1 AttributeCode
FROM prItemAttribute WITH(NOLOCK)
WHERE AttributeTypeCode = 42
AND prItemAttribute.ItemTypeCode = I.ItemTypeCode
AND prItemAttribute.ItemCode = I.ItemCode
)
AND cdItemAttributeDesc.LangCode = 'TR'
), SPACE(0))),
UrunIlkGrubu = dbo.HG_Temizlik(ISNULL((
SELECT AttributeDescription
FROM cdItemAttributeDesc WITH(NOLOCK)
WHERE cdItemAttributeDesc.ItemTypeCode = I.ItemTypeCode
AND cdItemAttributeDesc.AttributeTypeCode = 44
AND cdItemAttributeDesc.AttributeCode = (
SELECT TOP 1 AttributeCode
FROM prItemAttribute WITH(NOLOCK)
WHERE AttributeTypeCode = 44
AND prItemAttribute.ItemTypeCode = I.ItemTypeCode
AND prItemAttribute.ItemCode = I.ItemCode
)
AND cdItemAttributeDesc.LangCode = 'TR'
), SPACE(0))),
AskiliYan = dbo.HG_Temizlik(ISNULL((
SELECT TOP 1 ProductAtt45
FROM ProductAttributesFilter WITH(NOLOCK)
WHERE ProductAttributesFilter.ItemCode = I.ItemCode
), SPACE(0))),
ChannelCode = CASE ChannelCode = CASE
WHEN I.ProcessCode IN ('R') THEN dbo.HG_Temizlik(ISNULL(( WHEN I.ProcessCode IN ('R') THEN dbo.HG_Temizlik(ISNULL((
SELECT OfficeDescription SELECT OfficeDescription
@@ -132,16 +76,19 @@ func productPerformanceSalesSQL() string {
), SPACE(0))) ), SPACE(0)))
END, END,
Qty1 = ISNULL(I.Qty1, 0), Qty1 = ISNULL(I.Qty1, 0),
TLAmount = ISNULL(I.Doc_Amount, 0) * ISNULL(I.Loc_ExchangeRate, 0), TLAmount = CASE
USDAmount = (ISNULL(I.Doc_Amount, 0) * ISNULL(I.Loc_ExchangeRate, 0)) / ISNULL(( WHEN Amount.DocCurrencyCode = 'TRY' THEN Amount.DocNetAmount
SELECT TOP 1 Rate WHEN CurRate.Rate > 0 THEN Amount.DocNetAmount * CurRate.Rate
FROM AllExchangeRates WITH(NOLOCK) WHEN ISNULL(I.Loc_ExchangeRate, 0) > 0 THEN Amount.DocNetAmount * I.Loc_ExchangeRate
WHERE CurrencyCode = 'USD' ELSE 0
AND RelationCurrencyCode = 'TRY' END,
AND ExchangeTypeCode = 6 USDAmount = CASE
AND Rate > 1 WHEN Amount.DocCurrencyCode = 'USD' THEN Amount.DocNetAmount
ORDER BY ABS(DATEDIFF(DAY, AllExchangeRates.Date, I.InvoiceDate)) ASC WHEN Amount.DocCurrencyCode = 'TRY' AND UsdRate.Rate > 0 THEN Amount.DocNetAmount / UsdRate.Rate
), 1), WHEN CurRate.Rate > 0 AND UsdRate.Rate > 0 THEN (Amount.DocNetAmount * CurRate.Rate) / UsdRate.Rate
WHEN ISNULL(I.Loc_ExchangeRate, 0) > 0 AND UsdRate.Rate > 0 THEN (Amount.DocNetAmount * I.Loc_ExchangeRate) / UsdRate.Rate
ELSE 0
END,
RN = ROW_NUMBER() OVER ( RN = ROW_NUMBER() OVER (
PARTITION BY I.InvoiceLineID PARTITION BY I.InvoiceLineID
ORDER BY I.InvoiceDate DESC, I.InvoiceNumber ORDER BY I.InvoiceDate DESC, I.InvoiceNumber
@@ -150,21 +97,46 @@ func productPerformanceSalesSQL() string {
LEFT OUTER JOIN CustomerAttributesFilter CAF WITH(NOLOCK) LEFT OUTER JOIN CustomerAttributesFilter CAF WITH(NOLOCK)
ON CAF.CurrAccTypeCode = I.CurrAccTypeCode ON CAF.CurrAccTypeCode = I.CurrAccTypeCode
AND CAF.CurrAccCode = I.CurrAccCode AND CAF.CurrAccCode = I.CurrAccCode
LEFT OUTER JOIN ProductAttributesFilter PAF WITH(NOLOCK)
ON PAF.ItemCode = I.ItemCode
LEFT OUTER JOIN dbo.trInvoiceLineCurrency ILC WITH(NOLOCK)
ON ILC.InvoiceLineID = I.InvoiceLineID
AND UPPER(LTRIM(RTRIM(ILC.CurrencyCode))) = UPPER(LTRIM(RTRIM(ISNULL(NULLIF(LTRIM(RTRIM(I.Doc_CurrencyCode)), ''), 'TRY'))))
OUTER APPLY (
SELECT
DocCurrencyCode = UPPER(LTRIM(RTRIM(ISNULL(NULLIF(LTRIM(RTRIM(I.Doc_CurrencyCode)), ''), ISNULL(NULLIF(LTRIM(RTRIM(ILC.CurrencyCode)), ''), 'TRY'))))),
DocNetAmount = COALESCE(
NULLIF(ISNULL(ILC.NetAmount, 0), 0),
NULLIF(ISNULL(I.Doc_Amount, 0), 0),
NULLIF(ISNULL(I.Doc_Price, 0) * ISNULL(I.Qty1, 0), 0),
0
)
) Amount
OUTER APPLY (
SELECT TOP 1 Rate
FROM AllExchangeRates WITH(NOLOCK)
WHERE CurrencyCode = 'USD'
AND RelationCurrencyCode = 'TRY'
AND ExchangeTypeCode = 6
AND Rate > 0
AND Date <= CAST(I.InvoiceDate AS date)
ORDER BY Date DESC
) UsdRate
OUTER APPLY (
SELECT TOP 1 Rate
FROM AllExchangeRates WITH(NOLOCK)
WHERE CurrencyCode = Amount.DocCurrencyCode
AND RelationCurrencyCode = 'TRY'
AND ExchangeTypeCode = 6
AND Rate > 0
AND Date <= CAST(I.InvoiceDate AS date)
ORDER BY Date DESC
) CurRate
WHERE I.InvoiceDate >= @p1 WHERE I.InvoiceDate >= @p1
AND I.InvoiceDate < DATEADD(DAY, 1, @p2) AND I.InvoiceDate < DATEADD(DAY, 1, @p2)
AND I.ItemTypeCode = 1 AND I.ItemTypeCode = 1
AND ( AND LEN(LTRIM(RTRIM(I.ItemCode))) = 13
LEN(LTRIM(RTRIM(I.ItemCode))) = 13 AND (@p3 = '' OR UPPER(LTRIM(RTRIM(I.ItemCode))) LIKE @p3 + '%')
AND (
I.ItemCode LIKE 'S%'
OR I.ItemCode LIKE 'O%'
OR I.ItemCode LIKE 'N%'
OR I.ItemCode LIKE 'X%'
OR I.ItemCode LIKE 'I%'
OR I.ItemCode LIKE 'A%'
)
)
AND (@p3 = '' OR I.ItemCode LIKE @p3 + '%')
AND ( AND (
(I.CompanyCode = 1 AND I.ATAtt01 IN (1,2) AND I.ProcessCode IN ('WS','R')) (I.CompanyCode = 1 AND I.ATAtt01 IN (1,2) AND I.ProcessCode IN ('WS','R'))
OR (I.CompanyCode = 4 AND I.ProcessCode = 'R') OR (I.CompanyCode = 4 AND I.ProcessCode = 'R')
@@ -185,13 +157,13 @@ SELECT
YakaKodu, YakaKodu,
ItemDescription = MAX(ItemDescription), ItemDescription = MAX(ItemDescription),
Kategori = MAX(Kategori), Kategori = MAX(Kategori),
Seri = MAX(Kategori), Seri = '',
YasGrubu = MAX(UrunIlkGrubu), YasGrubu = '',
AskiliYan = MAX(AskiliYan), AskiliYan = ISNULL(NULLIF(MAX(AskiliYan), '-'), ''),
UrunIlkGrubu = MAX(UrunIlkGrubu), UrunIlkGrubu = MAX(UrunIlkGrubu),
UrunAnaGrubu = MAX(UrunAnaGrubu), UrunAnaGrubu = MAX(UrunAnaGrubu),
UrunAltGrubu = MAX(UrunAltGrubu), UrunAltGrubu = MAX(UrunAltGrubu),
MarketKey = CONCAT(MAX(Kategori), '|', MAX(UrunIlkGrubu), '|', MAX(AskiliYan), '|', MAX(UrunAnaGrubu), '|', MAX(UrunAltGrubu), '|', ChannelCode), MarketKey = CONCAT(MAX(Kategori), '|', MAX(UrunIlkGrubu), '|', ISNULL(NULLIF(MAX(AskiliYan), '-'), ''), '|', MAX(UrunAnaGrubu), '|', MAX(UrunAltGrubu), '|', ChannelCode),
ChannelCode, ChannelCode,
CustomerCountry = ISNULL(NULLIF(CustomerCountry, ''), '-'), CustomerCountry = ISNULL(NULLIF(CustomerCountry, ''), '-'),
CustomerSegment = ISNULL(NULLIF(CustomerSegment, ''), '-'), CustomerSegment = ISNULL(NULLIF(CustomerSegment, ''), '-'),
@@ -211,7 +183,7 @@ GROUP BY SalesDate, ProductCode, ColorCode, YakaKodu, ChannelCode, CustomerCount
` `
} }
func productPerformanceStockSQL() string { func productPerformanceStockSQLUnused() string {
return ` return `
;WITH ActiveWarehouses AS ( ;WITH ActiveWarehouses AS (
SELECT WarehouseCode SELECT WarehouseCode
@@ -225,9 +197,9 @@ func productPerformanceStockSQL() string {
), ),
Raw AS ( Raw AS (
SELECT SELECT
ProductCode = LTRIM(RTRIM(S.ItemCode)), ProductCode = UPPER(LTRIM(RTRIM(S.ItemCode))),
ColorCode = LTRIM(RTRIM(ISNULL(S.ColorCode, ''))), ColorCode = UPPER(LTRIM(RTRIM(ISNULL(S.ColorCode, '')))),
YakaKodu = LTRIM(RTRIM(ISNULL(S.ItemDim2Code, ''))), YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(S.ItemDim2Code, '')))),
MovementDate = CAST(S.DocumentDate AS date), MovementDate = CAST(S.DocumentDate AS date),
ProcessCode = CASE ProcessCode = CASE
WHEN LTRIM(RTRIM(ISNULL(S.ProcessCode, ''))) <> '' THEN LTRIM(RTRIM(S.ProcessCode)) WHEN LTRIM(RTRIM(ISNULL(S.ProcessCode, ''))) <> '' THEN LTRIM(RTRIM(S.ProcessCode))
@@ -240,18 +212,8 @@ Raw AS (
INNER JOIN ActiveWarehouses W INNER JOIN ActiveWarehouses W
ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode)) ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode))
WHERE S.ItemTypeCode = 1 WHERE S.ItemTypeCode = 1
AND ( AND LEN(LTRIM(RTRIM(S.ItemCode))) = 13
LEN(LTRIM(RTRIM(S.ItemCode))) = 13 AND (@p3 = '' OR UPPER(LTRIM(RTRIM(S.ItemCode))) LIKE @p3 + '%')
AND (
S.ItemCode LIKE 'S%'
OR S.ItemCode LIKE 'O%'
OR S.ItemCode LIKE 'N%'
OR S.ItemCode LIKE 'X%'
OR S.ItemCode LIKE 'I%'
OR S.ItemCode LIKE 'A%'
)
)
AND (@p3 = '' OR S.ItemCode LIKE @p3 + '%')
AND S.DocumentDate < DATEADD(DAY, 1, @p2) AND S.DocumentDate < DATEADD(DAY, 1, @p2)
GROUP BY GROUP BY
S.ItemCode, S.ItemCode,
@@ -373,37 +335,23 @@ ORDER BY StockDate, ProductCode, ColorCode, YakaKodu
func productPerformancePriceSQL() string { func productPerformancePriceSQL() string {
return ` return `
;WITH ProductCodes AS ( ;WITH ProductCodes AS (
SELECT DISTINCT ItemCode
FROM cdItem WITH(NOLOCK)
WHERE ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND (@p1 = '' OR UPPER(LTRIM(RTRIM(ItemCode))) LIKE @p1 + '%')
UNION
SELECT DISTINCT ItemCode SELECT DISTINCT ItemCode
FROM trPriceListLine WITH(NOLOCK) FROM trPriceListLine WITH(NOLOCK)
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND ( AND LEN(LTRIM(RTRIM(ItemCode))) = 13
LEN(LTRIM(RTRIM(ItemCode))) = 13 AND (@p1 = '' OR UPPER(LTRIM(RTRIM(ItemCode))) LIKE @p1 + '%')
AND (
ItemCode LIKE 'S%'
OR ItemCode LIKE 'O%'
OR ItemCode LIKE 'N%'
OR ItemCode LIKE 'X%'
OR ItemCode LIKE 'I%'
OR ItemCode LIKE 'A%'
)
)
AND (@p1 = '' OR ItemCode LIKE @p1 + '%')
UNION UNION
SELECT DISTINCT ItemCode SELECT DISTINCT ItemCode
FROM prItemBasePrice WITH(NOLOCK) FROM prItemBasePrice WITH(NOLOCK)
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND ( AND LEN(LTRIM(RTRIM(ItemCode))) = 13
LEN(LTRIM(RTRIM(ItemCode))) = 13 AND (@p1 = '' OR UPPER(LTRIM(RTRIM(ItemCode))) LIKE @p1 + '%')
AND (
ItemCode LIKE 'S%'
OR ItemCode LIKE 'O%'
OR ItemCode LIKE 'N%'
OR ItemCode LIKE 'X%'
OR ItemCode LIKE 'I%'
OR ItemCode LIKE 'A%'
)
)
AND (@p1 = '' OR ItemCode LIKE @p1 + '%')
), ),
LatestPrice AS ( LatestPrice AS (
SELECT SELECT
@@ -450,12 +398,26 @@ Cost AS (
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD' AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
) )
SELECT SELECT
pc.ItemCode, UPPER(LTRIM(RTRIM(pc.ItemCode))) AS ItemCode,
ItemDescription = dbo.HG_Temizlik(ISNULL((
SELECT ItemDescription
FROM cdItemDesc WITH(NOLOCK)
WHERE cdItemDesc.ItemTypeCode = 1
AND cdItemDesc.ItemCode = pc.ItemCode
AND cdItemDesc.LangCode = 'TR'
), SPACE(0))),
Kategori = dbo.HG_Temizlik(ISNULL(paf.ProductAtt44, SPACE(0))),
AskiliYan = dbo.HG_Temizlik(ISNULL(paf.ProductAtt45, SPACE(0))),
UrunIlkGrubu = dbo.HG_Temizlik(ISNULL(paf.ProductAtt42, SPACE(0))),
UrunAnaGrubu = dbo.HG_Temizlik(ISNULL(paf.ProductAtt01, SPACE(0))),
UrunAltGrubu = dbo.HG_Temizlik(ISNULL(paf.ProductAtt02, SPACE(0))),
CostPriceUsd = ISNULL(c.CostPriceUsd, 0), CostPriceUsd = ISNULL(c.CostPriceUsd, 0),
BasePriceUsd = ISNULL(bp.BasePriceUsd, 0), BasePriceUsd = ISNULL(bp.BasePriceUsd, 0),
BasePriceTry = ISNULL(bp.BasePriceTry, 0), BasePriceTry = ISNULL(bp.BasePriceTry, 0),
c.LastPricingDate c.LastPricingDate
FROM ProductCodes pc FROM ProductCodes pc
LEFT JOIN ProductAttributesFilter paf WITH(NOLOCK)
ON paf.ItemCode = pc.ItemCode
LEFT JOIN BasePrice bp ON bp.ItemCode = pc.ItemCode LEFT JOIN BasePrice bp ON bp.ItemCode = pc.ItemCode
LEFT JOIN Cost c ON c.ItemCode = pc.ItemCode AND c.rn = 1 LEFT JOIN Cost c ON c.ItemCode = pc.ItemCode AND c.rn = 1
` `
@@ -470,6 +432,34 @@ WITH LatestStock AS (
WHERE stock_date <= $1 WHERE stock_date <= $1
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
), ),
Stock90Start AS (
SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
product_code, color_code, yaka_kodu, stock_qty
FROM mk_product_performance_stock_daily
WHERE stock_date <= $1::date - INTERVAL '89 days'
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
Stock180Start AS (
SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
product_code, color_code, yaka_kodu, stock_qty
FROM mk_product_performance_stock_daily
WHERE stock_date <= $1::date - INTERVAL '179 days'
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
Stock365Start AS (
SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
product_code, color_code, yaka_kodu, stock_qty
FROM mk_product_performance_stock_daily
WHERE stock_date <= $1::date - INTERVAL '359 days'
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
StockTotalStart AS (
SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
product_code, color_code, yaka_kodu, stock_qty
FROM mk_product_performance_stock_daily
WHERE stock_date <= DATE '2022-01-01'
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
SalesAgg AS ( SalesAgg AS (
SELECT SELECT
product_code, product_code,
@@ -480,34 +470,46 @@ SalesAgg AS (
MAX(kategori) AS kategori, MAX(kategori) AS kategori,
MAX(seri) AS seri, MAX(seri) AS seri,
MAX(yas_grubu) AS yas_grubu, MAX(yas_grubu) AS yas_grubu,
MAX(askili_yan) AS askili_yan, COALESCE(MAX(NULLIF(askili_yan, '-')), '') AS askili_yan,
MAX(urun_ilk_grubu) AS urun_ilk_grubu, MAX(urun_ilk_grubu) AS urun_ilk_grubu,
MAX(urun_ana_grubu) AS urun_ana_grubu, MAX(urun_ana_grubu) AS urun_ana_grubu,
MAX(urun_alt_grubu) AS urun_alt_grubu, MAX(urun_alt_grubu) AS urun_alt_grubu,
SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '29 days') AS sales_qty_30d, SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '29 days') AS sales_qty_30d,
SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS sales_qty_90d, SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS sales_qty_90d,
SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days') AS sales_qty_180d, SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days') AS sales_qty_180d,
SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '364 days') AS sales_qty_365d, SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS sales_qty_365d,
SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '729 days') AS sales_qty_730d, SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '729 days') AS sales_qty_730d,
SUM(sales_qty) FILTER (WHERE sales_date >= DATE '2022-01-01') AS sales_qty_total,
SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '29 days') AS sales_usd_30d, SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '29 days') AS sales_usd_30d,
SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS sales_usd_90d, SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS sales_usd_90d,
SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days') AS sales_usd_180d, SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days') AS sales_usd_180d,
SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '364 days') AS sales_usd_365d, SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS sales_usd_365d,
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS customer_count_90d, SUM(sales_usd) FILTER (WHERE sales_date >= DATE '2022-01-01') AS sales_usd_total,
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_90d,
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_180d,
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_365d,
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(sales_usd, 0) > 0) AS customer_count_total,
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS first_sale_date_90d,
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days') AS first_sale_date_180d,
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS first_sale_date_365d,
MIN(sales_date) FILTER (WHERE sales_date >= DATE '2022-01-01') AS first_sale_date_total,
MAX(sales_date) AS last_sale_date, MAX(sales_date) AS last_sale_date,
MAX(last_ref_number) AS last_ref_number MAX(last_ref_number) AS last_ref_number
FROM mk_product_performance_sales_daily FROM mk_product_performance_sales_daily
WHERE sales_date >= $1::date - INTERVAL '729 days' WHERE sales_date >= DATE '2022-01-01'
AND sales_date <= $1::date AND sales_date <= $1::date
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
GROUP BY product_code, color_code, yaka_kodu, market_key GROUP BY product_code, color_code, yaka_kodu, market_key
), ),
Scope AS ( Scope AS (
SELECT SELECT
product_code, color_code, yaka_kodu, market_key, product_code, color_code, yaka_kodu, market_key,
item_description, kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, item_description, kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu,
sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total,
sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, sales_usd_total,
customer_count_90d, last_sale_date, last_ref_number customer_count_90d, customer_count_180d, customer_count_365d, customer_count_total,
first_sale_date_90d, first_sale_date_180d, first_sale_date_365d, first_sale_date_total,
last_sale_date, last_ref_number
FROM SalesAgg FROM SalesAgg
UNION ALL UNION ALL
SELECT SELECT
@@ -520,7 +522,7 @@ Scope AS (
'' AS seri, '' AS seri,
'' AS yas_grubu, '' AS yas_grubu,
'' AS askili_yan, '' AS askili_yan,
'' AS urun_ilk_grubu, COALESCE(pd.urun_ilk_grubu, '') AS urun_ilk_grubu,
'' AS urun_ana_grubu, '' AS urun_ana_grubu,
'' AS urun_alt_grubu, '' AS urun_alt_grubu,
0 AS sales_qty_30d, 0 AS sales_qty_30d,
@@ -528,15 +530,26 @@ Scope AS (
0 AS sales_qty_180d, 0 AS sales_qty_180d,
0 AS sales_qty_365d, 0 AS sales_qty_365d,
0 AS sales_qty_730d, 0 AS sales_qty_730d,
0 AS sales_qty_total,
0 AS sales_usd_30d, 0 AS sales_usd_30d,
0 AS sales_usd_90d, 0 AS sales_usd_90d,
0 AS sales_usd_180d, 0 AS sales_usd_180d,
0 AS sales_usd_365d, 0 AS sales_usd_365d,
0 AS sales_usd_total,
0 AS customer_count_90d, 0 AS customer_count_90d,
0 AS customer_count_180d,
0 AS customer_count_365d,
0 AS customer_count_total,
NULL::date AS first_sale_date_90d,
NULL::date AS first_sale_date_180d,
NULL::date AS first_sale_date_365d,
NULL::date AS first_sale_date_total,
NULL::date AS last_sale_date, NULL::date AS last_sale_date,
'' AS last_ref_number '' AS last_ref_number
FROM LatestStock ls FROM LatestStock ls
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code=ls.product_code
WHERE COALESCE(ls.stock_qty, 0) <> 0 WHERE COALESCE(ls.stock_qty, 0) <> 0
AND upper(translate(btrim(COALESCE(pd.urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 SELECT 1
FROM SalesAgg sa FROM SalesAgg sa
@@ -545,6 +558,23 @@ Scope AS (
AND sa.yaka_kodu = ls.yaka_kodu AND sa.yaka_kodu = ls.yaka_kodu
) )
), ),
PriceDim AS (
SELECT
product_code,
COALESCE(urun_ilk_grubu, '') AS urun_ilk_grubu,
COALESCE(urun_ana_grubu, '') AS urun_ana_grubu,
COALESCE(urun_alt_grubu, '') AS urun_alt_grubu,
CASE
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd > base_price_usd THEN base_price_usd
ELSE cost_price_usd
END AS cost_price_usd,
CASE
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd > base_price_usd THEN cost_price_usd
ELSE base_price_usd
END AS base_price_usd,
base_price_try
FROM mk_product_performance_price_dim
),
Base AS ( Base AS (
SELECT SELECT
s.*, s.*,
@@ -554,6 +584,12 @@ Base AS (
COALESCE(pd.base_price_try, 0) AS base_price_try, COALESCE(pd.base_price_try, 0) AS base_price_try,
COALESCE(s.sales_qty_90d, 0) / 90.0 AS avg_daily_sales_90d, COALESCE(s.sales_qty_90d, 0) / 90.0 AS avg_daily_sales_90d,
COALESCE(s.sales_qty_180d, 0) / 180.0 AS avg_daily_sales_180d, COALESCE(s.sales_qty_180d, 0) / 180.0 AS avg_daily_sales_180d,
COALESCE(s.sales_qty_365d, 0) / 360.0 AS avg_daily_sales_365d,
COALESCE(s.sales_qty_total, 0) / GREATEST(1, ($1::date - DATE '2022-01-01') + 1) AS avg_daily_sales_total,
(COALESCE(s90.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_90d,
(COALESCE(s180.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_180d,
(COALESCE(s365.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_365d,
(COALESCE(stotal.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_total,
CASE WHEN COALESCE(s.sales_qty_90d, 0) = 0 THEN 0 ELSE COALESCE(s.sales_usd_90d, 0) / NULLIF(s.sales_qty_90d, 0) END AS avg_price_usd_90d, CASE WHEN COALESCE(s.sales_qty_90d, 0) = 0 THEN 0 ELSE COALESCE(s.sales_usd_90d, 0) / NULLIF(s.sales_qty_90d, 0) END AS avg_price_usd_90d,
CASE WHEN COALESCE(s.sales_qty_180d, 0) = 0 THEN 0 ELSE COALESCE(s.sales_usd_180d, 0) / NULLIF(s.sales_qty_180d, 0) END AS avg_price_usd_180d, CASE WHEN COALESCE(s.sales_qty_180d, 0) = 0 THEN 0 ELSE COALESCE(s.sales_usd_180d, 0) / NULLIF(s.sales_qty_180d, 0) END AS avg_price_usd_180d,
COALESCE(s.sales_usd_90d, 0) - (COALESCE(s.sales_qty_90d, 0) * COALESCE(pd.cost_price_usd, 0)) AS gross_profit_usd_90d, COALESCE(s.sales_usd_90d, 0) - (COALESCE(s.sales_qty_90d, 0) * COALESCE(pd.cost_price_usd, 0)) AS gross_profit_usd_90d,
@@ -570,22 +606,68 @@ Base AS (
CASE WHEN COALESCE(s.sales_qty_180d, 0) = 0 THEN 0 ELSE (COALESCE(s.sales_usd_180d, 0) / NULLIF(s.sales_qty_180d, 0)) - COALESCE(pd.base_price_usd, 0) END AS unit_profit_base_180d CASE WHEN COALESCE(s.sales_qty_180d, 0) = 0 THEN 0 ELSE (COALESCE(s.sales_usd_180d, 0) / NULLIF(s.sales_qty_180d, 0)) - COALESCE(pd.base_price_usd, 0) END AS unit_profit_base_180d
FROM Scope s FROM Scope s
LEFT JOIN LatestStock ls ON ls.product_code=s.product_code AND ls.color_code=s.color_code AND ls.yaka_kodu=s.yaka_kodu LEFT JOIN LatestStock ls ON ls.product_code=s.product_code AND ls.color_code=s.color_code AND ls.yaka_kodu=s.yaka_kodu
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code=s.product_code LEFT JOIN PriceDim pd ON pd.product_code=s.product_code
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code=s.product_code
AND x.color_code=s.color_code
AND x.yaka_kodu=s.yaka_kodu
AND x.stock_date <= COALESCE(s.first_sale_date_90d, $1::date - INTERVAL '89 days')
ORDER BY x.stock_date DESC
LIMIT 1
) s90 ON TRUE
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code=s.product_code
AND x.color_code=s.color_code
AND x.yaka_kodu=s.yaka_kodu
AND x.stock_date <= COALESCE(s.first_sale_date_180d, $1::date - INTERVAL '179 days')
ORDER BY x.stock_date DESC
LIMIT 1
) s180 ON TRUE
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code=s.product_code
AND x.color_code=s.color_code
AND x.yaka_kodu=s.yaka_kodu
AND x.stock_date <= COALESCE(s.first_sale_date_365d, $1::date - INTERVAL '359 days')
ORDER BY x.stock_date DESC
LIMIT 1
) s365 ON TRUE
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code=s.product_code
AND x.color_code=s.color_code
AND x.yaka_kodu=s.yaka_kodu
AND x.stock_date <= COALESCE(s.first_sale_date_total, DATE '2022-01-01')
ORDER BY x.stock_date DESC
LIMIT 1
) stotal ON TRUE
WHERE upper(translate(btrim(COALESCE(NULLIF(s.urun_ilk_grubu,''), pd.urun_ilk_grubu, '')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
), ),
Spread AS ( Spread AS (
SELECT SELECT
product_code, product_code,
color_code, color_code,
yaka_kodu, yaka_kodu,
COUNT(DISTINCT market_key) AS market_count_90d COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_90d, 0) > 0) AS market_count_90d,
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_180d, 0) > 0) AS market_count_180d,
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_365d, 0) > 0) AS market_count_365d,
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_total, 0) > 0) AS market_count_total
FROM SalesAgg FROM SalesAgg
WHERE COALESCE(sales_qty_90d, 0) > 0
GROUP BY product_code, color_code, yaka_kodu GROUP BY product_code, color_code, yaka_kodu
), ),
Market AS ( Market AS (
SELECT SELECT
market_key, market_key,
AVG(NULLIF(sales_qty_90d, 0)) AS market_avg_sales_qty_90d, AVG(NULLIF(sales_qty_90d, 0)) AS market_avg_sales_qty_90d,
AVG(NULLIF(sales_qty_180d, 0)) AS market_avg_sales_qty_180d,
AVG(NULLIF(sales_qty_365d, 0)) AS market_avg_sales_qty_365d,
AVG(NULLIF(sales_qty_total, 0)) AS market_avg_sales_qty_total,
AVG(NULLIF(avg_price_usd_90d, 0)) AS market_avg_price_usd_90d, AVG(NULLIF(avg_price_usd_90d, 0)) AS market_avg_price_usd_90d,
AVG(NULLIF(gross_margin_90d, 0)) AS market_avg_margin_90d AVG(NULLIF(gross_margin_90d, 0)) AS market_avg_margin_90d
FROM Base FROM Base
@@ -595,9 +677,21 @@ Scored AS (
SELECT SELECT
b.*, b.*,
COALESCE(sp.market_count_90d, 0) AS market_count_90d, COALESCE(sp.market_count_90d, 0) AS market_count_90d,
CASE WHEN b.avg_daily_sales_90d = 0 THEN 0 ELSE b.stock_qty / NULLIF(b.avg_daily_sales_90d, 0) END AS stock_days_90d, COALESCE(sp.market_count_180d, 0) AS market_count_180d,
CASE WHEN b.avg_daily_sales_180d = 0 THEN 0 ELSE b.stock_qty / NULLIF(b.avg_daily_sales_180d, 0) END AS stock_days_180d, COALESCE(sp.market_count_365d, 0) AS market_count_365d,
COALESCE(sp.market_count_total, 0) AS market_count_total,
CASE WHEN b.avg_daily_sales_90d = 0 THEN 0 ELSE b.avg_stock_90d / NULLIF(b.avg_daily_sales_90d, 0) END AS stock_days_90d,
CASE WHEN b.avg_daily_sales_180d = 0 THEN 0 ELSE b.avg_stock_180d / NULLIF(b.avg_daily_sales_180d, 0) END AS stock_days_180d,
CASE WHEN b.avg_daily_sales_365d = 0 THEN 0 ELSE b.avg_stock_365d / NULLIF(b.avg_daily_sales_365d, 0) END AS stock_days_365d,
CASE WHEN b.avg_daily_sales_total = 0 THEN 0 ELSE b.avg_stock_total / NULLIF(b.avg_daily_sales_total, 0) END AS stock_days_total,
CASE WHEN b.avg_stock_90d = 0 THEN 0 ELSE b.sales_qty_90d / NULLIF(b.avg_stock_90d, 0) * 4.0 END AS stock_turnover_90d,
CASE WHEN b.avg_stock_180d = 0 THEN 0 ELSE b.sales_qty_180d / NULLIF(b.avg_stock_180d, 0) * 2.0 END AS stock_turnover_180d,
CASE WHEN b.avg_stock_365d = 0 THEN 0 ELSE b.sales_qty_365d / NULLIF(b.avg_stock_365d, 0) END AS stock_turnover_365d,
CASE WHEN b.avg_stock_total = 0 THEN 0 ELSE b.sales_qty_total / NULLIF(b.avg_stock_total, 0) * 360.0 / GREATEST(1, ($1::date - DATE '2022-01-01') + 1) END AS stock_turnover_total,
CASE WHEN COALESCE(m.market_avg_sales_qty_90d, 0) = 0 THEN 0 ELSE b.sales_qty_90d / NULLIF(m.market_avg_sales_qty_90d, 0) END AS sales_index_90d, CASE WHEN COALESCE(m.market_avg_sales_qty_90d, 0) = 0 THEN 0 ELSE b.sales_qty_90d / NULLIF(m.market_avg_sales_qty_90d, 0) END AS sales_index_90d,
CASE WHEN COALESCE(m.market_avg_sales_qty_180d, 0) = 0 THEN 0 ELSE b.sales_qty_180d / NULLIF(m.market_avg_sales_qty_180d, 0) END AS sales_index_180d,
CASE WHEN COALESCE(m.market_avg_sales_qty_365d, 0) = 0 THEN 0 ELSE b.sales_qty_365d / NULLIF(m.market_avg_sales_qty_365d, 0) END AS sales_index_365d,
CASE WHEN COALESCE(m.market_avg_sales_qty_total, 0) = 0 THEN 0 ELSE b.sales_qty_total / NULLIF(m.market_avg_sales_qty_total, 0) END AS sales_index_total,
CASE WHEN COALESCE(m.market_avg_price_usd_90d, 0) = 0 THEN 0 ELSE b.avg_price_usd_90d / NULLIF(m.market_avg_price_usd_90d, 0) END AS price_index_90d, CASE WHEN COALESCE(m.market_avg_price_usd_90d, 0) = 0 THEN 0 ELSE b.avg_price_usd_90d / NULLIF(m.market_avg_price_usd_90d, 0) END AS price_index_90d,
CASE WHEN COALESCE(m.market_avg_margin_90d, 0) = 0 THEN 0 ELSE b.gross_margin_90d / NULLIF(m.market_avg_margin_90d, 0) END AS margin_index_90d CASE WHEN COALESCE(m.market_avg_margin_90d, 0) = 0 THEN 0 ELSE b.gross_margin_90d / NULLIF(m.market_avg_margin_90d, 0) END AS margin_index_90d
FROM Base b FROM Base b
@@ -607,33 +701,43 @@ Scored AS (
INSERT INTO mk_product_performance_kpi_daily ( INSERT INTO mk_product_performance_kpi_daily (
kpi_date, product_code, color_code, yaka_kodu, item_description, kpi_date, product_code, color_code, yaka_kodu, item_description,
kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty, kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty,
sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total,
sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, avg_daily_sales_90d, avg_daily_sales_180d, sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, sales_usd_total, avg_daily_sales_90d, avg_daily_sales_180d, avg_daily_sales_365d, avg_daily_sales_total,
stock_days_90d, stock_days_180d, avg_price_usd_90d, avg_price_usd_180d, cost_price_usd, base_price_usd, base_price_try, avg_stock_90d, avg_stock_180d, avg_stock_365d, avg_stock_total,
stock_days_90d, stock_days_180d, stock_days_365d, stock_days_total, stock_turnover_90d, stock_turnover_180d, stock_turnover_365d, stock_turnover_total,
avg_price_usd_90d, avg_price_usd_180d, cost_price_usd, base_price_usd, base_price_try,
gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d, gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d,
unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d, market_count_90d, customer_count_90d, unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d,
sales_index_90d, price_index_90d, margin_index_90d, performance_score, market_count_90d, market_count_180d, market_count_365d, market_count_total,
customer_count_90d, customer_count_180d, customer_count_365d, customer_count_total,
sales_index_90d, sales_index_180d, sales_index_365d, sales_index_total, price_index_90d, margin_index_90d, performance_score,
performance_bucket, recommendation, last_sale_date, last_ref_number, updated_at performance_bucket, recommendation, last_sale_date, last_ref_number, updated_at
) )
SELECT SELECT
$1::date, $1::date,
product_code, color_code, yaka_kodu, item_description, product_code, color_code, yaka_kodu, item_description,
kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty, kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty,
COALESCE(sales_qty_30d, 0), COALESCE(sales_qty_90d, 0), COALESCE(sales_qty_180d, 0), COALESCE(sales_qty_365d, 0), COALESCE(sales_qty_730d, 0), COALESCE(sales_qty_30d, 0), COALESCE(sales_qty_90d, 0), COALESCE(sales_qty_180d, 0), COALESCE(sales_qty_365d, 0), COALESCE(sales_qty_730d, 0), COALESCE(sales_qty_total, 0),
COALESCE(sales_usd_30d, 0), COALESCE(sales_usd_90d, 0), COALESCE(sales_usd_180d, 0), COALESCE(sales_usd_365d, 0), COALESCE(avg_daily_sales_90d, 0), COALESCE(avg_daily_sales_180d, 0), COALESCE(sales_usd_30d, 0), COALESCE(sales_usd_90d, 0), COALESCE(sales_usd_180d, 0), COALESCE(sales_usd_365d, 0), COALESCE(sales_usd_total, 0), COALESCE(avg_daily_sales_90d, 0), COALESCE(avg_daily_sales_180d, 0), COALESCE(avg_daily_sales_365d, 0), COALESCE(avg_daily_sales_total, 0),
COALESCE(stock_days_90d, 0), COALESCE(stock_days_180d, 0), COALESCE(avg_price_usd_90d, 0), COALESCE(avg_price_usd_180d, 0), COALESCE(cost_price_usd, 0), COALESCE(base_price_usd, 0), COALESCE(base_price_try, 0), COALESCE(avg_stock_90d, 0), COALESCE(avg_stock_180d, 0), COALESCE(avg_stock_365d, 0), COALESCE(avg_stock_total, 0),
COALESCE(stock_days_90d, 0), COALESCE(stock_days_180d, 0), COALESCE(stock_days_365d, 0), COALESCE(stock_days_total, 0), COALESCE(stock_turnover_90d, 0), COALESCE(stock_turnover_180d, 0), COALESCE(stock_turnover_365d, 0), COALESCE(stock_turnover_total, 0),
COALESCE(avg_price_usd_90d, 0), COALESCE(avg_price_usd_180d, 0), COALESCE(cost_price_usd, 0), COALESCE(base_price_usd, 0), COALESCE(base_price_try, 0),
COALESCE(gross_profit_usd_90d, 0), COALESCE(gross_profit_usd_180d, 0), COALESCE(gross_margin_90d, 0), COALESCE(gross_margin_180d, 0), COALESCE(gross_profit_usd_90d, 0), COALESCE(gross_profit_usd_180d, 0), COALESCE(gross_margin_90d, 0), COALESCE(gross_margin_180d, 0),
COALESCE(unit_profit_cost_90d, 0), COALESCE(unit_profit_cost_180d, 0), COALESCE(unit_profit_base_90d, 0), COALESCE(unit_profit_base_180d, 0), COALESCE(market_count_90d, 0), COALESCE(customer_count_90d, 0), COALESCE(unit_profit_cost_90d, 0), COALESCE(unit_profit_cost_180d, 0), COALESCE(unit_profit_base_90d, 0), COALESCE(unit_profit_base_180d, 0),
COALESCE(sales_index_90d, 0), COALESCE(price_index_90d, 0), COALESCE(margin_index_90d, 0), COALESCE(market_count_90d, 0), COALESCE(market_count_180d, 0), COALESCE(market_count_365d, 0), COALESCE(market_count_total, 0),
COALESCE(customer_count_90d, 0), COALESCE(customer_count_180d, 0), COALESCE(customer_count_365d, 0), COALESCE(customer_count_total, 0),
COALESCE(sales_index_90d, 0), COALESCE(sales_index_180d, 0), COALESCE(sales_index_365d, 0), COALESCE(sales_index_total, 0), COALESCE(price_index_90d, 0), COALESCE(margin_index_90d, 0),
CASE WHEN COALESCE(sales_usd_90d, 0) <= 0 THEN 1 ELSE
ROUND( ROUND(
LEAST(35, COALESCE(sales_index_90d, 0) * 18) LEAST(30, GREATEST(COALESCE(gross_margin_90d, 0), 0) / 0.45 * 30)
+ LEAST(25, GREATEST(COALESCE(gross_margin_90d, 0), 0) * 50) + LEAST(20, COALESCE(stock_turnover_90d, 0) / 4.0 * 20)
+ LEAST(20, COALESCE(customer_count_90d, 0) * 2) + LEAST(20, COALESCE(sales_usd_90d, 0) / 25000.0 * 20)
+ LEAST(10, COALESCE(market_count_90d, 0) * 3) + LEAST(5, COALESCE(market_count_90d, 0) / 3.0 * 5)
+ CASE WHEN COALESCE(stock_days_90d, 0) BETWEEN 10 AND 90 THEN 10 WHEN COALESCE(stock_days_90d, 0) > 180 THEN -10 ELSE 0 END + LEAST(25, COALESCE(customer_count_90d, 0) / 8.0 * 25)
, 4) AS performance_score, , 4)
END AS performance_score,
CASE CASE
WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.25 THEN 'YILDIZ_URUN' WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.45 THEN 'YILDIZ_URUN'
WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'STOKSUZ_TALEP' WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'STOKSUZ_TALEP'
WHEN COALESCE(stock_days_90d,0) > 180 AND COALESCE(sales_index_90d,0) < 0.75 THEN 'STOK_RISKI' WHEN COALESCE(stock_days_90d,0) > 180 AND COALESCE(sales_index_90d,0) < 0.75 THEN 'STOK_RISKI'
WHEN COALESCE(price_index_90d,0) > 1.10 AND COALESCE(sales_index_90d,0) < 0.80 THEN 'FIYAT_BASKISI' WHEN COALESCE(price_index_90d,0) > 1.10 AND COALESCE(sales_index_90d,0) < 0.80 THEN 'FIYAT_BASKISI'
@@ -641,12 +745,12 @@ SELECT
ELSE 'TAKIP' ELSE 'TAKIP'
END AS performance_bucket, END AS performance_bucket,
CASE CASE
WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.25 THEN 'Piyasa üstü satış ve iyi marj: stok ve fiyat gücü takip edilmeli.' WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.45 THEN 'Piyasa ustu satis ve iyi marj: stok ve fiyat gucu takip edilmeli.'
WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'Talep var, stok yok: üretim/satın alma planına alınmalı.' WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'Talep var, stok yok: uretim/satin alma planina alinmali.'
WHEN COALESCE(stock_days_90d,0) > 180 AND COALESCE(sales_index_90d,0) < 0.75 THEN 'Stok yüksek, satış piyasa altı: kampanya veya fiyat kontrolü gerekli.' WHEN COALESCE(stock_days_90d,0) > 180 AND COALESCE(sales_index_90d,0) < 0.75 THEN 'Stok yuksek, satis piyasa alti: kampanya veya fiyat kontrolu gerekli.'
WHEN COALESCE(price_index_90d,0) > 1.10 AND COALESCE(sales_index_90d,0) < 0.80 THEN 'Fiyat piyasa üstünde ve satış zayıf: fiyat revizyonu değerlendirilmeli.' WHEN COALESCE(price_index_90d,0) > 1.10 AND COALESCE(sales_index_90d,0) < 0.80 THEN 'Fiyat piyasa ustunde ve satis zayif: fiyat revizyonu degerlendirilmeli.'
WHEN COALESCE(price_index_90d,0) < 0.90 AND COALESCE(sales_index_90d,0) > 1.00 THEN 'Satış güçlü, fiyat piyasa altı: taban fiyat artışı değerlendirilebilir.' WHEN COALESCE(price_index_90d,0) < 0.90 AND COALESCE(sales_index_90d,0) > 1.00 THEN 'Satis guclu, fiyat piyasa alti: taban fiyat artisi degerlendirilebilir.'
ELSE 'Düzenli takip.' ELSE 'Duzenli takip.'
END AS recommendation, END AS recommendation,
last_sale_date, last_sale_date,
COALESCE(last_ref_number, ''), COALESCE(last_ref_number, ''),
+92 -5
View File
@@ -793,6 +793,82 @@ ORDER BY SonIsEmri.dteIslemTarihi DESC, LTRIM(RTRIM(R.sMModelKodu)) ASC
return uretimDB.QueryContext(ctx, sqlText, fromDate, search) return uretimDB.QueryContext(ctx, sqlText, fromDate, search)
} }
type ArgeBaggiDepotEntryTotals struct {
Ceket float64
Pantolon float64
Yelek float64
}
// GetArgeBaggiDepotEntryTotals returns operation 985/986/987 quantities keyed
// by production work order. The requested work orders are queried in one batch
// to avoid an ArgeBAGGI round trip for every list row.
func GetArgeBaggiDepotEntryTotals(ctx context.Context, argeDB *sql.DB, workOrders []string) (map[string]ArgeBaggiDepotEntryTotals, error) {
out := make(map[string]ArgeBaggiDepotEntryTotals)
if argeDB == nil || len(workOrders) == 0 {
return out, nil
}
seen := make(map[string]struct{}, len(workOrders))
values := make([]string, 0, len(workOrders))
for _, raw := range workOrders {
value := strings.TrimSpace(raw)
if value == "" || value == "0" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
values = append(values, value)
}
if len(values) == 0 {
return out, nil
}
placeholders := make([]string, 0, len(values))
args := make([]any, 0, len(values))
for i, value := range values {
placeholders = append(placeholders, fmt.Sprintf("@p%d", i+1))
args = append(args, value)
}
sqlText := fmt.Sprintf(`
SELECT
LTRIM(RTRIM(CONVERT(VARCHAR(64), MN.model_adi))) AS IsEmriNo,
SUM(CASE WHEN O.oper_num = 985 THEN CONVERT(float, B.adet) ELSE 0 END) AS CeketDepoGiris,
SUM(CASE WHEN O.oper_num = 986 THEN CONVERT(float, B.adet) ELSE 0 END) AS PantolonDepoGiris,
SUM(CASE WHEN O.oper_num = 987 THEN CONVERT(float, B.adet) ELSE 0 END) AS YelekDepoGiris
FROM dbo.is_acma_bar B
INNER JOIN dbo.is_acma I ON I.is_num = B.is_num
INNER JOIN dbo.model_def MD ON I.imdef_num = MD.mdef_num
INNER JOIN dbo.oper_urun_no O ON B.oper_num = O.oper_num
INNER JOIN dbo.model_num MN ON I.model_num = MN.model_num
INNER JOIN dbo.bant_no BN ON I.bant_num = BN.bant_no
INNER JOIN dbo.model_oper MO ON O.oper_num = MO.oper_num AND MD.mdef_num = MO.model_num
INNER JOIN dbo.model_oper_tkp_def TD ON MO.tkp_cik = TD.tkp_cik
WHERE B.tarih >= '2025-01-01'
AND TD.tkp_cik IN (1,2,3,4,5)
AND O.oper_num IN (985,986,987)
AND LTRIM(RTRIM(CONVERT(VARCHAR(64), MN.model_adi))) IN (%s)
GROUP BY LTRIM(RTRIM(CONVERT(VARCHAR(64), MN.model_adi)))
`, strings.Join(placeholders, ","))
rows, err := argeDB.QueryContext(ctx, sqlText, args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var workOrder string
var totals ArgeBaggiDepotEntryTotals
if err := rows.Scan(&workOrder, &totals.Ceket, &totals.Pantolon, &totals.Yelek); err != nil {
return nil, err
}
out[strings.TrimSpace(workOrder)] = totals
}
return out, rows.Err()
}
func GetProductionHasCostProducts( func GetProductionHasCostProducts(
ctx context.Context, ctx context.Context,
uretimDB *sql.DB, uretimDB *sql.DB,
@@ -816,12 +892,22 @@ WITH SonOnMaliyet AS (
FROM dbo.spUrtOnMLMas M FROM dbo.spUrtOnMLMas M
WHERE LTRIM(RTRIM(M.UrunKodu)) <> '' WHERE LTRIM(RTRIM(M.UrunKodu)) <> ''
), ),
SonSiparis AS ( SonSiparisSirali AS (
SELECT SELECT
LTRIM(RTRIM(sMModelKodu)) AS UrunKodu, LTRIM(RTRIM(SD.sMModelKodu)) AS UrunKodu,
MAX(dteIslemTarihi) AS SonSiparisTarihi SD.dteIslemTarihi AS SonSiparisTarihi,
FROM dbo.spUrtSiparisDet SM.nUrtSiparisNo,
GROUP BY LTRIM(RTRIM(sMModelKodu)) ROW_NUMBER() OVER (
PARTITION BY LTRIM(RTRIM(SD.sMModelKodu))
ORDER BY SD.dteIslemTarihi DESC, SD.nUrtSiparisID DESC
) AS rn
FROM dbo.spUrtSiparisDet SD
INNER JOIN dbo.spUrtSiparis SM ON SM.nUrtSiparisID = SD.nUrtSiparisID
),
SonSiparis AS (
SELECT UrunKodu, SonSiparisTarihi, nUrtSiparisNo
FROM SonSiparisSirali
WHERE rn = 1
) )
SELECT SELECT
CASE CASE
@@ -831,6 +917,7 @@ SELECT
THEN N'FASON ICIN URETILEN' THEN N'FASON ICIN URETILEN'
ELSE N'Tanimsiz' ELSE N'Tanimsiz'
END AS UretimSekli, END AS UretimSekli,
RTRIM(CONVERT(VARCHAR(32), ISNULL(SS.nUrtSiparisNo, 0))) AS nUrtSiparisNo,
RTRIM(CONVERT(VARCHAR(32), ISNULL(OM.nOnMLNo, 0))) AS nOnMLNo, RTRIM(CONVERT(VARCHAR(32), ISNULL(OM.nOnMLNo, 0))) AS nOnMLNo,
LTRIM(RTRIM(ISNULL(OM.UrunKodu, ''))) AS UrunKodu, LTRIM(RTRIM(ISNULL(OM.UrunKodu, ''))) AS UrunKodu,
ISNULL(OM.UrunAdi, '') AS UrunAdi, ISNULL(OM.UrunAdi, '') AS UrunAdi,
+23 -23
View File
@@ -3,7 +3,7 @@ package queries
// GetProductStockQuery: // GetProductStockQuery:
// Urun kodu bazli, STOCK/PICK/RESERVE/DISP ayrik CTE ile optimize sorgu. // Urun kodu bazli, STOCK/PICK/RESERVE/DISP ayrik CTE ile optimize sorgu.
const GetProductStockQuery = ` const GetProductStockQuery = `
DECLARE @ProductCode NVARCHAR(50) = @p1; DECLARE @ProductCode NVARCHAR(50) = UPPER(LTRIM(RTRIM(@p1)));
;WITH STOCK AS ;WITH STOCK AS
( (
@@ -14,7 +14,7 @@ DECLARE @ProductCode NVARCHAR(50) = @p1;
StoreCode, StoreCode,
WarehouseCode, WarehouseCode,
ItemTypeCode, ItemTypeCode,
ItemCode, UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode,
ColorCode, ColorCode,
ItemDim1Code, ItemDim1Code,
ItemDim2Code, ItemDim2Code,
@@ -22,12 +22,12 @@ DECLARE @ProductCode NVARCHAR(50) = @p1;
SUM(In_Qty1 - Out_Qty1) AS InventoryQty1 SUM(In_Qty1 - Out_Qty1) AS InventoryQty1
FROM trStock FROM trStock
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(ItemCode) = 13 AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
GROUP BY GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code ItemDim1Code, ItemDim2Code, ItemDim3Code
), ),
@@ -35,17 +35,17 @@ PICK AS
( (
SELECT SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code, ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS PickingQty1 SUM(Qty1) AS PickingQty1
FROM PickingStates FROM PickingStates
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(ItemCode) = 13 AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
GROUP BY GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code ItemDim1Code, ItemDim2Code, ItemDim3Code
), ),
@@ -53,17 +53,17 @@ RESERVE AS
( (
SELECT SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code, ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS ReserveQty1 SUM(Qty1) AS ReserveQty1
FROM ReserveStates FROM ReserveStates
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(ItemCode) = 13 AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
GROUP BY GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code ItemDim1Code, ItemDim2Code, ItemDim3Code
), ),
@@ -71,17 +71,17 @@ DISP AS
( (
SELECT SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code, ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS DispOrderQty1 SUM(Qty1) AS DispOrderQty1
FROM DispOrderStates FROM DispOrderStates
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(ItemCode) = 13 AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
GROUP BY GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code ItemDim1Code, ItemDim2Code, ItemDim3Code
) )
@@ -185,14 +185,14 @@ AND DP.ItemDim2Code=S.ItemDim2Code
AND DP.ItemDim3Code=S.ItemDim3Code AND DP.ItemDim3Code=S.ItemDim3Code
JOIN cdItem JOIN cdItem
ON S.ItemCode = cdItem.ItemCode ON UPPER(LTRIM(RTRIM(S.ItemCode))) = UPPER(LTRIM(RTRIM(cdItem.ItemCode)))
AND S.ItemTypeCode = cdItem.ItemTypeCode AND S.ItemTypeCode = cdItem.ItemTypeCode
LEFT JOIN cdUnitOfMeasure LEFT JOIN cdUnitOfMeasure
ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode
LEFT JOIN ProductFilterWithDescription('TR') P LEFT JOIN ProductFilterWithDescription('TR') P
ON P.ProductCode = S.ItemCode ON UPPER(LTRIM(RTRIM(P.ProductCode))) = UPPER(LTRIM(RTRIM(S.ItemCode)))
LEFT JOIN bsItemTypeDesc LEFT JOIN bsItemTypeDesc
ON bsItemTypeDesc.ItemTypeCode = S.ItemTypeCode ON bsItemTypeDesc.ItemTypeCode = S.ItemTypeCode
@@ -214,14 +214,14 @@ LEFT JOIN (
ROW_NUMBER() OVER (PARTITION BY ItemCode, ItemTypeCode ORDER BY PriceDate DESC) AS RowNum ROW_NUMBER() OVER (PARTITION BY ItemCode, ItemTypeCode ORDER BY PriceDate DESC) AS RowNum
FROM prItemBasePrice FROM prItemBasePrice
) prFilteredBasePrice ) prFilteredBasePrice
ON prFilteredBasePrice.ItemCode = S.ItemCode ON UPPER(LTRIM(RTRIM(prFilteredBasePrice.ItemCode))) = UPPER(LTRIM(RTRIM(S.ItemCode)))
AND prFilteredBasePrice.ItemTypeCode = S.ItemTypeCode AND prFilteredBasePrice.ItemTypeCode = S.ItemTypeCode
AND prFilteredBasePrice.RowNum = 1 AND prFilteredBasePrice.RowNum = 1
WHERE WHERE
S.ItemTypeCode IN (1) S.ItemTypeCode IN (1)
AND S.ItemCode = @ProductCode AND UPPER(LTRIM(RTRIM(S.ItemCode))) = @ProductCode
AND LEN(S.ItemCode) = 13 AND LEN(LTRIM(RTRIM(S.ItemCode))) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
AND ( AND (
S.InventoryQty1 S.InventoryQty1
@@ -234,6 +234,6 @@ WHERE
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
); );
` `
+45 -45
View File
@@ -28,7 +28,7 @@ IF OBJECT_ID('dbo.ProductFilterTRCache','U') IS NOT NULL
BEGIN BEGIN
INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal) INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal)
SELECT SELECT
ProductCode, UPPER(LTRIM(RTRIM(ProductCode))) AS ProductCode,
Kategori = LTRIM(RTRIM(ProductAtt44Desc)), Kategori = LTRIM(RTRIM(ProductAtt44Desc)),
UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)), UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)),
UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)), UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)),
@@ -48,7 +48,7 @@ ELSE
BEGIN BEGIN
INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal) INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal)
SELECT SELECT
ProductCode, UPPER(LTRIM(RTRIM(ProductCode))) AS ProductCode,
Kategori = LTRIM(RTRIM(ProductAtt44Desc)), Kategori = LTRIM(RTRIM(ProductAtt44Desc)),
UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)), UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)),
UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)), UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)),
@@ -101,40 +101,40 @@ CREATE NONCLUSTERED INDEX IX_AttrBase_Filter ON #AttrBase(Kategori, UrunAnaGrubu
FROM FROM
( (
SELECT SELECT
P.ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, UPPER(LTRIM(RTRIM(P.ItemCode))) AS ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code,
P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1 P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1
FROM PickingStates P FROM PickingStates P
INNER JOIN #AttrBase AB ON AB.ProductCode = P.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(P.ItemCode)))
WHERE P.ItemTypeCode = 1 WHERE P.ItemTypeCode = 1
AND LEN(P.ItemCode) = 13 AND LEN(LTRIM(RTRIM(P.ItemCode))) = 13
UNION ALL UNION ALL
SELECT SELECT
R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, UPPER(LTRIM(RTRIM(R.ItemCode))) AS ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code,
0, R.Qty1, 0, 0 0, R.Qty1, 0, 0
FROM ReserveStates R FROM ReserveStates R
INNER JOIN #AttrBase AB ON AB.ProductCode = R.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(R.ItemCode)))
WHERE R.ItemTypeCode = 1 WHERE R.ItemTypeCode = 1
AND LEN(R.ItemCode) = 13 AND LEN(LTRIM(RTRIM(R.ItemCode))) = 13
UNION ALL UNION ALL
SELECT SELECT
D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, UPPER(LTRIM(RTRIM(D.ItemCode))) AS ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code,
0, 0, D.Qty1, 0 0, 0, D.Qty1, 0
FROM DispOrderStates D FROM DispOrderStates D
INNER JOIN #AttrBase AB ON AB.ProductCode = D.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(D.ItemCode)))
WHERE D.ItemTypeCode = 1 WHERE D.ItemTypeCode = 1
AND LEN(D.ItemCode) = 13 AND LEN(LTRIM(RTRIM(D.ItemCode))) = 13
UNION ALL UNION ALL
SELECT SELECT
T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, UPPER(LTRIM(RTRIM(T.ItemCode))) AS ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code,
0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1) 0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1)
FROM trStock T FROM trStock T
INNER JOIN #AttrBase AB ON AB.ProductCode = T.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(T.ItemCode)))
WHERE T.ItemTypeCode = 1 WHERE T.ItemTypeCode = 1
AND LEN(T.ItemCode) = 13 AND LEN(LTRIM(RTRIM(T.ItemCode))) = 13
GROUP BY T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code GROUP BY UPPER(LTRIM(RTRIM(T.ItemCode))), T.ColorCode, T.ItemDim1Code, T.ItemDim2Code
) X ) X
GROUP BY X.ItemCode, X.ColorCode, X.ItemDim1Code, X.ItemDim2Code GROUP BY X.ItemCode, X.ColorCode, X.ItemDim1Code, X.ItemDim2Code
), ),
@@ -180,7 +180,7 @@ SELECT 'renk', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END SELECT DISTINCT FieldValue = CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END
FROM Avail A FROM Avail A
INNER JOIN #AttrBase AB ON AB.ProductCode = A.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(A.ItemCode)))
WHERE (CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END) <> '' WHERE (CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END) <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -196,7 +196,7 @@ SELECT 'renk2', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = A.Renk2 SELECT DISTINCT FieldValue = A.Renk2
FROM Avail A FROM Avail A
INNER JOIN #AttrBase AB ON AB.ProductCode = A.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(A.ItemCode)))
WHERE A.Renk2 <> '' WHERE A.Renk2 <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -245,7 +245,7 @@ SELECT 'beden', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = A.Beden SELECT DISTINCT FieldValue = A.Beden
FROM Avail A FROM Avail A
INNER JOIN #AttrBase AB ON AB.ProductCode = A.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(A.ItemCode)))
WHERE A.Beden <> '' WHERE A.Beden <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -346,7 +346,7 @@ BEGIN
ProductAtt46Desc ProductAtt46Desc
) )
SELECT SELECT
C.ProductCode, UPPER(LTRIM(RTRIM(C.ProductCode))) AS ProductCode,
C.ProductDescription, C.ProductDescription,
C.ProductAtt01Desc, C.ProductAtt01Desc,
C.ProductAtt02Desc, C.ProductAtt02Desc,
@@ -379,7 +379,7 @@ BEGIN
ISNULL(PF.ProductAtt45Desc, '') AS ProductAtt45Desc, ISNULL(PF.ProductAtt45Desc, '') AS ProductAtt45Desc,
ISNULL(PF.ProductAtt46Desc, '') AS ProductAtt46Desc ISNULL(PF.ProductAtt46Desc, '') AS ProductAtt46Desc
FROM dbo.ProductFilterTRCache C FROM dbo.ProductFilterTRCache C
LEFT JOIN ProductFilterWithDescription('TR') PF ON PF.ProductCode = C.ProductCode LEFT JOIN ProductFilterWithDescription('TR') PF ON UPPER(LTRIM(RTRIM(PF.ProductCode))) = UPPER(LTRIM(RTRIM(C.ProductCode)))
WHERE LEN(C.ProductCode) = 13 WHERE LEN(C.ProductCode) = 13
AND (@Kategori IS NULL OR C.ProductAtt44Desc = @Kategori) AND (@Kategori IS NULL OR C.ProductAtt44Desc = @Kategori)
AND (@UrunAnaGrubu IS NULL OR C.ProductAtt01Desc = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR C.ProductAtt01Desc = @UrunAnaGrubu)
@@ -426,7 +426,7 @@ BEGIN
ProductAtt46Desc ProductAtt46Desc
) )
SELECT SELECT
ProductCode, UPPER(LTRIM(RTRIM(ProductCode))) AS ProductCode,
ProductDescription, ProductDescription,
ProductAtt01Desc, ProductAtt01Desc,
ProductAtt02Desc, ProductAtt02Desc,
@@ -493,69 +493,69 @@ INV AS
( (
SELECT SELECT
P.CompanyCode, P.OfficeCode, P.StoreTypeCode, P.StoreCode, P.WarehouseCode, P.CompanyCode, P.OfficeCode, P.StoreTypeCode, P.StoreCode, P.WarehouseCode,
P.ItemTypeCode, P.ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, P.ItemDim3Code, P.ItemTypeCode, UPPER(LTRIM(RTRIM(P.ItemCode))) AS ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, P.ItemDim3Code,
P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1 P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1
FROM PickingStates P FROM PickingStates P
INNER JOIN #AttrFiltered AF ON AF.ProductCode = P.ItemCode INNER JOIN #AttrFiltered AF ON AF.ProductCode = UPPER(LTRIM(RTRIM(P.ItemCode)))
WHERE P.ItemTypeCode = 1 WHERE P.ItemTypeCode = 1
AND LEN(P.ItemCode) = 13 AND LEN(LTRIM(RTRIM(P.ItemCode))) = 13
AND P.WarehouseCode IN AND P.WarehouseCode IN
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
) )
UNION ALL UNION ALL
SELECT SELECT
R.CompanyCode, R.OfficeCode, R.StoreTypeCode, R.StoreCode, R.WarehouseCode, R.CompanyCode, R.OfficeCode, R.StoreTypeCode, R.StoreCode, R.WarehouseCode,
R.ItemTypeCode, R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, R.ItemDim3Code, R.ItemTypeCode, UPPER(LTRIM(RTRIM(R.ItemCode))) AS ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, R.ItemDim3Code,
0, R.Qty1, 0, 0 0, R.Qty1, 0, 0
FROM ReserveStates R FROM ReserveStates R
INNER JOIN #AttrFiltered AF ON AF.ProductCode = R.ItemCode INNER JOIN #AttrFiltered AF ON AF.ProductCode = UPPER(LTRIM(RTRIM(R.ItemCode)))
WHERE R.ItemTypeCode = 1 WHERE R.ItemTypeCode = 1
AND LEN(R.ItemCode) = 13 AND LEN(LTRIM(RTRIM(R.ItemCode))) = 13
AND R.WarehouseCode IN AND R.WarehouseCode IN
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
) )
UNION ALL UNION ALL
SELECT SELECT
D.CompanyCode, D.OfficeCode, D.StoreTypeCode, D.StoreCode, D.WarehouseCode, D.CompanyCode, D.OfficeCode, D.StoreTypeCode, D.StoreCode, D.WarehouseCode,
D.ItemTypeCode, D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, D.ItemDim3Code, D.ItemTypeCode, UPPER(LTRIM(RTRIM(D.ItemCode))) AS ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, D.ItemDim3Code,
0, 0, D.Qty1, 0 0, 0, D.Qty1, 0
FROM DispOrderStates D FROM DispOrderStates D
INNER JOIN #AttrFiltered AF ON AF.ProductCode = D.ItemCode INNER JOIN #AttrFiltered AF ON AF.ProductCode = UPPER(LTRIM(RTRIM(D.ItemCode)))
WHERE D.ItemTypeCode = 1 WHERE D.ItemTypeCode = 1
AND LEN(D.ItemCode) = 13 AND LEN(LTRIM(RTRIM(D.ItemCode))) = 13
AND D.WarehouseCode IN AND D.WarehouseCode IN
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
) )
UNION ALL UNION ALL
SELECT SELECT
T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode, T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode,
T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code, T.ItemTypeCode, UPPER(LTRIM(RTRIM(T.ItemCode))) AS ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code,
0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1) 0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1)
FROM trStock T FROM trStock T
INNER JOIN #AttrFiltered AF ON AF.ProductCode = T.ItemCode INNER JOIN #AttrFiltered AF ON AF.ProductCode = UPPER(LTRIM(RTRIM(T.ItemCode)))
WHERE T.ItemTypeCode = 1 WHERE T.ItemTypeCode = 1
AND LEN(T.ItemCode) = 13 AND LEN(LTRIM(RTRIM(T.ItemCode))) = 13
AND T.WarehouseCode IN AND T.WarehouseCode IN
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
) )
GROUP BY GROUP BY
T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode, T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode,
T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code T.ItemTypeCode, UPPER(LTRIM(RTRIM(T.ItemCode))), T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code
) X ) X
GROUP BY GROUP BY
X.CompanyCode, X.OfficeCode, X.StoreTypeCode, X.StoreCode, X.WarehouseCode, X.CompanyCode, X.OfficeCode, X.StoreTypeCode, X.StoreCode, X.WarehouseCode,
@@ -580,21 +580,21 @@ Avail AS
FROM INV I FROM INV I
JOIN cdItem CI JOIN cdItem CI
ON CI.ItemTypeCode = I.ItemTypeCode ON CI.ItemTypeCode = I.ItemTypeCode
AND CI.ItemCode = I.ItemCode AND UPPER(LTRIM(RTRIM(CI.ItemCode))) = I.ItemCode
LEFT JOIN cdUnitOfMeasure U LEFT JOIN cdUnitOfMeasure U
ON U.UnitOfMeasureCode = CI.UnitOfMeasureCode1 ON U.UnitOfMeasureCode = CI.UnitOfMeasureCode1
LEFT JOIN cdColorDesc C LEFT JOIN cdColorDesc C
ON C.ColorCode = I.ColorCode ON C.ColorCode = I.ColorCode
AND C.LangCode = 'TR' AND C.LangCode = 'TR'
WHERE I.ItemTypeCode = 1 WHERE I.ItemTypeCode = 1
AND LEN(I.ItemCode) = 13 AND LEN(LTRIM(RTRIM(I.ItemCode))) = 13
AND (I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1) > 0 AND (I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1) > 0
AND CI.IsBlocked = 0 AND CI.IsBlocked = 0
AND I.WarehouseCode IN AND I.WarehouseCode IN
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
) )
), ),
Grouped AS Grouped AS
@@ -604,7 +604,7 @@ Grouped AS
A.ColorCode, A.ColorCode,
A.ItemDim2Code A.ItemDim2Code
FROM Avail A FROM Avail A
INNER JOIN #AttrFiltered AF ON AF.ProductCode = A.ItemCode INNER JOIN #AttrFiltered AF ON AF.ProductCode = UPPER(LTRIM(RTRIM(A.ItemCode)))
WHERE (@RenkList IS NULL OR CHARINDEX(NCHAR(31) + ISNULL((CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.ColorCode END),'') + NCHAR(31), NCHAR(31) + @RenkList + NCHAR(31)) > 0) WHERE (@RenkList IS NULL OR CHARINDEX(NCHAR(31) + ISNULL((CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.ColorCode END),'') + NCHAR(31), NCHAR(31) + @RenkList + NCHAR(31)) > 0)
AND (@Renk2List IS NULL OR CHARINDEX(NCHAR(31) + ISNULL(A.ItemDim2Code,'') + NCHAR(31), NCHAR(31) + @Renk2List + NCHAR(31)) > 0) AND (@Renk2List IS NULL OR CHARINDEX(NCHAR(31) + ISNULL(A.ItemDim2Code,'') + NCHAR(31), NCHAR(31) + @Renk2List + NCHAR(31)) > 0)
AND ( AND (
@@ -669,7 +669,7 @@ INNER JOIN Grouped G
AND G.ColorCode = A.ColorCode AND G.ColorCode = A.ColorCode
AND ISNULL(G.ItemDim2Code, '') = ISNULL(A.ItemDim2Code, '') AND ISNULL(G.ItemDim2Code, '') = ISNULL(A.ItemDim2Code, '')
INNER JOIN #AttrFiltered AF INNER JOIN #AttrFiltered AF
ON AF.ProductCode = A.ItemCode ON AF.ProductCode = UPPER(LTRIM(RTRIM(A.ItemCode)))
LEFT JOIN cdWarehouseDesc W LEFT JOIN cdWarehouseDesc W
ON W.WarehouseCode = A.WarehouseCode ON W.WarehouseCode = A.WarehouseCode
AND W.LangCode = 'TR' AND W.LangCode = 'TR'
@@ -680,8 +680,8 @@ OUTER APPLY (
SELECT TOP 1 Price SELECT TOP 1 Price
FROM prItemBasePrice PB FROM prItemBasePrice PB
WHERE PB.ItemTypeCode = 1 WHERE PB.ItemTypeCode = 1
AND PB.ItemCode = A.ItemCode AND UPPER(LTRIM(RTRIM(PB.ItemCode))) = UPPER(LTRIM(RTRIM(A.ItemCode)))
AND LEN(PB.ItemCode) = 13 AND LEN(LTRIM(RTRIM(PB.ItemCode))) = 13
ORDER BY PB.PriceDate DESC ORDER BY PB.PriceDate DESC
) P ) P
OPTION (RECOMPILE); OPTION (RECOMPILE);
+269 -9
View File
@@ -25,6 +25,25 @@ type ProductImageItem struct {
UUID string `json:"uuid,omitempty"` UUID string `json:"uuid,omitempty"`
ThumbURL string `json:"thumb_url,omitempty"` ThumbURL string `json:"thumb_url,omitempty"`
FullURL string `json:"full_url,omitempty"` FullURL string `json:"full_url,omitempty"`
StoredInDB bool `json:"stored_in_db,omitempty"`
HasDBContent bool `json:"-"`
}
type ProductImageBatchItem struct {
Key string `json:"key"`
Code string `json:"code"`
Dim1 string `json:"dim1"`
Dim3 string `json:"dim3"`
Limit int `json:"limit,omitempty"`
}
type ProductImageBatchRequest struct {
Items []ProductImageBatchItem `json:"items"`
}
type ProductImageBatchResponseItem struct {
Key string `json:"key"`
Images []ProductImageItem `json:"images"`
} }
var uuidPattern = regexp.MustCompile(`(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) var uuidPattern = regexp.MustCompile(`(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
@@ -119,6 +138,206 @@ func extractImageUUID(storagePath, fileName string) string {
return "" return ""
} }
func enrichProductImageItem(it *ProductImageItem) {
if it == nil {
return
}
if u := extractImageUUID(it.Storage, it.FileName); u != "" {
it.UUID = u
it.ThumbURL = productImagePublicURLIfExists("uploads/image/t300/" + u + ".jpg")
it.FullURL = productImagePublicURLIfExists("uploads/image/" + u + ".jpg")
}
if it.FullURL == "" {
it.FullURL = productImagePublicURLIfExists(it.Storage)
}
if it.FullURL == "" {
it.FullURL = productImagePublicURLIfExists(it.FileName)
}
if it.ID > 0 && (it.HasDBContent || productImageStoredFileExists(it.Storage, it.FileName)) {
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
}
}
func productImageItemHasURL(it *ProductImageItem) bool {
return it != nil && (it.ThumbURL != "" || it.FullURL != "" || it.ContentURL != "")
}
func productImageStoredFileExists(storagePath, fileName string) bool {
if resolved, _ := resolveStoragePath(storagePath); resolved != "" {
return true
}
if resolved, _ := resolveStoragePath(fileName); resolved != "" {
return true
}
return false
}
func productImagePublicURLIfExists(storagePath string) string {
raw := strings.TrimSpace(storagePath)
if raw == "" {
return ""
}
resolved, _ := resolveStoragePath(raw)
if resolved == "" {
return ""
}
normalized := strings.ReplaceAll(raw, "\\", "/")
lower := strings.ToLower(normalized)
if idx := strings.Index(lower, "/uploads/"); idx >= 0 {
return normalized[idx:]
}
if strings.HasPrefix(lower, "uploads/") {
return "/" + normalized
}
root := strings.TrimSpace(os.Getenv("BLOB_ROOT"))
if root == "" {
return ""
}
rel, err := filepath.Rel(root, resolved)
if err != nil || rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
return ""
}
rel = filepath.ToSlash(rel)
if strings.HasPrefix(strings.ToLower(rel), "uploads/") {
return "/" + rel
}
return ""
}
// POST /api/product-images/batch
func PostProductImagesBatchHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
reqID := strings.TrimSpace(r.Header.Get("X-Request-ID"))
if reqID == "" {
reqID = uuid.NewString()
}
w.Header().Set("X-Request-ID", reqID)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
var req ProductImageBatchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz gorsel batch istegi: "+err.Error(), http.StatusBadRequest)
return
}
if len(req.Items) > 300 {
req.Items = req.Items[:300]
}
cleanItems := make([]ProductImageBatchItem, 0, len(req.Items))
seen := map[string]bool{}
for _, item := range req.Items {
item.Key = strings.TrimSpace(item.Key)
item.Code = strings.TrimSpace(item.Code)
item.Dim1 = normalizeDimParam(item.Dim1)
item.Dim3 = normalizeDimParam(item.Dim3)
if item.Key == "" {
item.Key = item.Code + "|" + item.Dim1 + "|" + item.Dim3
}
if item.Code == "" || seen[item.Key] {
continue
}
seen[item.Key] = true
cleanItems = append(cleanItems, item)
}
if len(cleanItems) == 0 {
_ = json.NewEncoder(w).Encode([]ProductImageBatchResponseItem{})
return
}
payload, err := json.Marshal(cleanItems)
if err != nil {
http.Error(w, "Gorsel batch hazirlanamadi: "+err.Error(), http.StatusInternalServerError)
return
}
rows, err := pg.Query(`
WITH req AS (
SELECT *
FROM jsonb_to_recordset($1::jsonb) AS x(key text, code text, dim1 text, dim3 text)
),
matched_item AS (
SELECT DISTINCT ON (r.key)
r.key,
r.code,
r.dim1,
r.dim3,
m.id AS mmitem_id
FROM req r
JOIN mmitem m ON
UPPER(REPLACE(COALESCE(m.code,''), ' ', '')) = UPPER(REPLACE(COALESCE(r.code,''), ' ', ''))
OR UPPER(REPLACE(REGEXP_REPLACE(COALESCE(m.code,''), '^.*-', ''), ' ', '')) =
UPPER(REPLACE(REGEXP_REPLACE(COALESCE(r.code,''), '^.*-', ''), ' ', ''))
ORDER BY r.key, m.id
),
ranked AS (
SELECT
mi.key,
b.id,
COALESCE(b.file_name,'') AS file_name,
COALESCE(b.file_size,0) AS file_size,
COALESCE(b.storage_path,'') AS storage_path,
COALESCE(b.stored_in_db,false) AS stored_in_db,
(COALESCE(octet_length(b.bin),0) > 0) AS has_db_content,
ROW_NUMBER() OVER (
PARTITION BY mi.key
ORDER BY
CASE
WHEN mi.dim1 <> '' AND mi.dim3 <> '' AND COALESCE(b.dimval1::text,'') = mi.dim1 AND COALESCE(b.dimval3::text,'') = mi.dim3 THEN 0
WHEN mi.dim1 <> '' AND COALESCE(b.dimval1::text,'') = mi.dim1 AND COALESCE(b.dimval3::text,'') = '' THEN 1
WHEN COALESCE(b.dimval1::text,'') = '' AND COALESCE(b.dimval3::text,'') = '' THEN 2
ELSE 3
END,
COALESCE(b.sort_order,999999),
b.id
) AS rn
FROM matched_item mi
JOIN dfblob b ON b.src_table='mmitem'
AND b.typ='img'
AND b.src_id=mi.mmitem_id
)
SELECT key, id, file_name, file_size, storage_path, stored_in_db, has_db_content
FROM ranked
WHERE rn <= 5
ORDER BY key, rn
`, string(payload))
if err != nil {
slog.Error("product_images.batch.query_failed", "req_id", reqID, "err", err.Error())
http.Error(w, "Gorsel batch sorgu hatasi: "+err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
grouped := map[string][]ProductImageItem{}
for rows.Next() {
var key string
var it ProductImageItem
if err := rows.Scan(&key, &it.ID, &it.FileName, &it.FileSize, &it.Storage, &it.StoredInDB, &it.HasDBContent); err != nil {
continue
}
enrichProductImageItem(&it)
if !productImageItemHasURL(&it) {
continue
}
grouped[key] = append(grouped[key], it)
}
if err := rows.Err(); err != nil {
http.Error(w, "Gorsel batch okuma hatasi: "+err.Error(), http.StatusInternalServerError)
return
}
out := make([]ProductImageBatchResponseItem, 0, len(cleanItems))
for _, item := range cleanItems {
out = append(out, ProductImageBatchResponseItem{
Key: item.Key,
Images: grouped[item.Key],
})
}
_ = json.NewEncoder(w).Encode(out)
}
}
// GET /api/product-images?code=...&dim1=...&dim3=... // GET /api/product-images?code=...&dim1=...&dim3=...
func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc { func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -190,7 +409,9 @@ SELECT
id, id,
COALESCE(file_name,'') AS file_name, COALESCE(file_name,'') AS file_name,
COALESCE(file_size,0) AS file_size, COALESCE(file_size,0) AS file_size,
COALESCE(storage_path,'') AS storage_path COALESCE(storage_path,'') AS storage_path,
COALESCE(stored_in_db,false) AS stored_in_db,
(COALESCE(octet_length(bin),0) > 0) AS has_db_content
FROM dfblob FROM dfblob
WHERE typ='img' WHERE typ='img'
AND src_table='mmitem' AND src_table='mmitem'
@@ -221,17 +442,14 @@ ORDER BY
items := make([]ProductImageItem, 0, 16) items := make([]ProductImageItem, 0, 16)
for rows.Next() { for rows.Next() {
var it ProductImageItem var it ProductImageItem
if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage); err != nil { if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage, &it.StoredInDB, &it.HasDBContent); err != nil {
continue continue
} }
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID) enrichProductImageItem(&it)
if u := extractImageUUID(it.Storage, it.FileName); u != "" { if productImageItemHasURL(&it) {
it.UUID = u
it.ThumbURL = "/uploads/image/t300/" + u + ".jpg"
it.FullURL = "/uploads/image/" + u + ".jpg"
}
items = append(items, it) items = append(items, it)
} }
}
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
return nil, err return nil, err
} }
@@ -372,7 +590,8 @@ WHERE id = $1
return return
} }
if storedInDB && len(binData) > 0 { _ = storedInDB
if len(binData) > 0 {
w.Header().Set("Content-Type", http.DetectContentType(binData)) w.Header().Set("Content-Type", http.DetectContentType(binData))
w.Header().Set("Cache-Control", "public, max-age=3600") w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(binData) _, _ = w.Write(binData)
@@ -380,6 +599,9 @@ WHERE id = $1
} }
resolved, _ := resolveStoragePath(storagePath) resolved, _ := resolveStoragePath(storagePath)
if resolved == "" {
resolved, _ = resolveStoragePath(fileName)
}
if resolved == "" { if resolved == "" {
http.NotFound(w, r) http.NotFound(w, r)
return return
@@ -413,14 +635,42 @@ func resolveStoragePath(storagePath string) (string, []string) {
raw = filepath.ToSlash(filepath.Clean(raw)) raw = filepath.ToSlash(filepath.Clean(raw))
relUploads := filepath.FromSlash(filepath.Join("uploads", raw)) relUploads := filepath.FromSlash(filepath.Join("uploads", raw))
baseName := filepath.Base(raw)
relImage := filepath.FromSlash(filepath.Join("uploads", "image", baseName))
relImageThumb := filepath.FromSlash(filepath.Join("uploads", "image", "t300", baseName))
candidates := []string{ candidates := []string{
filepath.Clean(storagePath), filepath.Clean(storagePath),
filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))), filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))),
filepath.FromSlash(filepath.Clean(raw)), filepath.FromSlash(filepath.Clean(raw)),
relUploads, relUploads,
relImage,
relImageThumb,
filepath.Join(".", relUploads), filepath.Join(".", relUploads),
filepath.Join(".", relImage),
filepath.Join(".", relImageThumb),
filepath.Join("..", relUploads), filepath.Join("..", relUploads),
filepath.Join("..", relImage),
filepath.Join("..", relImageThumb),
filepath.Join("..", "..", relUploads), filepath.Join("..", "..", relUploads),
filepath.Join("..", "..", relImage),
filepath.Join("..", "..", relImageThumb),
}
if u := extractImageUUID(raw, baseName); u != "" {
uFull := filepath.FromSlash(filepath.Join("uploads", "image", u+".jpg"))
uThumb := filepath.FromSlash(filepath.Join("uploads", "image", "t300", u+".jpg"))
candidates = append(candidates,
filepath.FromSlash(filepath.Join("image", u+".jpg")),
filepath.FromSlash(filepath.Join("image", "t300", u+".jpg")),
uFull,
uThumb,
filepath.Join(".", uFull),
filepath.Join(".", uThumb),
filepath.Join("..", uFull),
filepath.Join("..", uThumb),
filepath.Join("..", "..", uFull),
filepath.Join("..", "..", uThumb),
)
} }
if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" { if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" {
@@ -428,7 +678,17 @@ func resolveStoragePath(storagePath string) (string, []string) {
filepath.Join(root, raw), filepath.Join(root, raw),
filepath.Join(root, relUploads), filepath.Join(root, relUploads),
filepath.Join(root, "uploads", raw), filepath.Join(root, "uploads", raw),
filepath.Join(root, relImage),
filepath.Join(root, relImageThumb),
) )
if u := extractImageUUID(raw, baseName); u != "" {
candidates = append(candidates,
filepath.Join(root, "uploads", "image", u+".jpg"),
filepath.Join(root, "uploads", "image", "t300", u+".jpg"),
filepath.Join(root, "image", u+".jpg"),
filepath.Join(root, "image", "t300", u+".jpg"),
)
}
} }
for _, p := range candidates { for _, p := range candidates {
+208 -19
View File
@@ -6,6 +6,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"log"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@@ -16,7 +17,7 @@ func GetProductPerformanceHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second) ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel() defer cancel()
f := queries.ProductPerformanceFilters{ f := queries.ProductPerformanceFilters{
@@ -28,7 +29,7 @@ func GetProductPerformanceHandler(pg *sql.DB) http.HandlerFunc {
Bucket: strings.TrimSpace(r.URL.Query().Get("bucket")), Bucket: strings.TrimSpace(r.URL.Query().Get("bucket")),
SortBy: strings.TrimSpace(r.URL.Query().Get("sort_by")), SortBy: strings.TrimSpace(r.URL.Query().Get("sort_by")),
Descending: boolQuery(r, "descending", true), Descending: boolQuery(r, "descending", true),
Limit: intQuery(r, "limit", 100), Limit: intQuery(r, "limit", 50000),
Page: intQuery(r, "page", 1), Page: intQuery(r, "page", 1),
} }
rows, total, err := queries.ListProductPerformance(ctx, pg, f) rows, total, err := queries.ListProductPerformance(ctx, pg, f)
@@ -90,7 +91,7 @@ func GetProductPerformanceSummaryHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second) ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel() defer cancel()
summary, err := queries.GetProductPerformanceSummary(ctx, pg) summary, err := queries.GetProductPerformanceSummary(ctx, pg)
@@ -106,10 +107,10 @@ func GetProductPerformanceGeneralHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second) ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel() defer cancel()
rows, err := queries.ListProductPerformanceGeneral(ctx, pg, intQuery(r, "limit", 500)) rows, err := queries.ListProductPerformanceGeneral(ctx, pg, intQuery(r, "limit", 50000))
if err != nil { if err != nil {
http.Error(w, "genel urun performans KPI alinamadi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "genel urun performans KPI alinamadi: "+err.Error(), http.StatusInternalServerError)
return return
@@ -122,10 +123,10 @@ func GetProductPerformanceOrdersHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second) ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel() defer cancel()
rows, err := queries.ListProductPerformanceOrderAnalysis(ctx, pg, intQuery(r, "limit", 500)) rows, err := queries.ListProductPerformanceOrderAnalysis(ctx, pg, intQuery(r, "limit", 50000))
if err != nil { if err != nil {
http.Error(w, "siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return return
@@ -138,14 +139,14 @@ func GetProductPerformanceOrderGroupsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second) ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel() defer cancel()
rows, err := queries.ListProductPerformanceOrderGroups( rows, err := queries.ListProductPerformanceOrderGroups(
ctx, ctx,
pg, pg,
strings.TrimSpace(r.URL.Query().Get("breakdown")), strings.TrimSpace(r.URL.Query().Get("breakdown")),
intQuery(r, "limit", 500), intQuery(r, "limit", 50000),
) )
if err != nil { if err != nil {
http.Error(w, "siparis grup analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "siparis grup analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
@@ -159,10 +160,10 @@ func GetProductPerformanceOrderProductCustomersHandler(pg *sql.DB) http.HandlerF
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second) ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel() defer cancel()
rows, err := queries.ListProductPerformanceOrderProductCustomers(ctx, pg, intQuery(r, "limit", 500)) rows, err := queries.ListProductPerformanceOrderProductCustomers(ctx, pg, intQuery(r, "limit", 50000))
if err != nil { if err != nil {
http.Error(w, "urun musteri siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "urun musteri siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return return
@@ -175,10 +176,10 @@ func GetProductPerformanceOrderMarketDetailsHandler(pg *sql.DB) http.HandlerFunc
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second) ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel() defer cancel()
rows, err := queries.ListProductPerformanceOrderMarketDetails(ctx, pg, intQuery(r, "limit", 800)) rows, err := queries.ListProductPerformanceOrderMarketDetails(ctx, pg, intQuery(r, "limit", 50000))
if err != nil { if err != nil {
http.Error(w, "piyasa siparis detay analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "piyasa siparis detay analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return return
@@ -191,10 +192,10 @@ func GetProductPerformanceMarketsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second) ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel() defer cancel()
rows, err := queries.ListProductPerformanceMarkets(ctx, pg, intQuery(r, "limit", 100)) rows, err := queries.ListProductPerformanceMarkets(ctx, pg, intQuery(r, "limit", 50000))
if err != nil { if err != nil {
http.Error(w, "piyasa performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "piyasa performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError)
return return
@@ -207,10 +208,10 @@ func GetProductPerformanceCountriesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second) ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel() defer cancel()
rows, err := queries.ListProductPerformanceCountries(ctx, pg, intQuery(r, "limit", 100)) rows, err := queries.ListProductPerformanceCountries(ctx, pg, intQuery(r, "limit", 50000))
if err != nil { if err != nil {
http.Error(w, "ulke performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "ulke performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError)
return return
@@ -223,14 +224,14 @@ func GetProductPerformanceCustomersHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r) traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 30*time.Second) ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel() defer cancel()
rows, err := queries.ListProductPerformanceCustomers( rows, err := queries.ListProductPerformanceCustomers(
ctx, ctx,
pg, pg,
strings.TrimSpace(r.URL.Query().Get("breakdown")), strings.TrimSpace(r.URL.Query().Get("breakdown")),
intQuery(r, "limit", 100), intQuery(r, "limit", 50000),
) )
if err != nil { if err != nil {
http.Error(w, "musteri performans kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "musteri performans kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError)
@@ -240,6 +241,194 @@ func GetProductPerformanceCustomersHandler(pg *sql.DB) http.HandlerFunc {
} }
} }
func GetProductPerformanceSalesBreakdownHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceSalesBreakdown(
ctx,
pg,
strings.TrimSpace(r.URL.Query().Get("mode")),
intQuery(r, "limit", 50000),
)
if err != nil {
http.Error(w, "satis kpi kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
mode := strings.TrimSpace(r.URL.Query().Get("mode"))
mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu"))
sortBy := strings.TrimSpace(r.URL.Query().Get("sort_by"))
descending := boolQuery(r, "descending", true)
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
limit := intQuery(r, "limit", 50000)
expanded := map[string]bool{}
filters := map[string][]string{}
expandThroughLevel := -1
if rawExpandLevel := strings.TrimSpace(r.URL.Query().Get("expand_through_level")); rawExpandLevel != "" {
if parsed, err := strconv.Atoi(rawExpandLevel); err == nil {
expandThroughLevel = parsed
}
}
const maxExpandedGroupKeys = 1000
addExpandedKey := func(key string) {
key = strings.TrimSpace(key)
if key != "" && len(expanded) < maxExpandedGroupKeys {
expanded[key] = true
}
}
if r.Method == http.MethodPost {
var body struct {
Mode string `json:"mode"`
GroupLevels []string `json:"group_levels"`
ExpandedKeys []string `json:"expanded_keys"`
ExpandThroughLevel *int `json:"expand_through_level"`
Limit int `json:"limit"`
MainGroup string `json:"urun_ana_grubu"`
Filters map[string][]string `json:"filters"`
SortBy string `json:"sort_by"`
Descending *bool `json:"descending"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "gecersiz grup istegi: "+err.Error(), http.StatusBadRequest)
return
}
if strings.TrimSpace(body.Mode) != "" {
mode = strings.TrimSpace(body.Mode)
}
if strings.TrimSpace(body.MainGroup) != "" {
mainGroup = strings.TrimSpace(body.MainGroup)
}
if len(body.GroupLevels) > 0 {
groupLevels = body.GroupLevels
}
if body.Limit > 0 {
limit = body.Limit
}
for _, key := range body.ExpandedKeys {
addExpandedKey(key)
}
if body.ExpandThroughLevel != nil {
expandThroughLevel = *body.ExpandThroughLevel
}
if len(body.Filters) > 0 {
filters = body.Filters
}
if strings.TrimSpace(body.SortBy) != "" {
sortBy = strings.TrimSpace(body.SortBy)
}
if body.Descending != nil {
descending = *body.Descending
}
} else {
for _, key := range strings.Split(r.URL.Query().Get("expanded_keys"), ",") {
addExpandedKey(key)
}
}
rows, err := queries.ListProductPerformanceGrouped(ctx, pg, queries.ProductPerformanceGroupedRequest{
Mode: mode,
GroupLevels: groupLevels,
ExpandedKeys: expanded,
ExpandThroughLevel: expandThroughLevel,
Limit: limit,
MainGroup: mainGroup,
Filters: filters,
SortBy: sortBy,
Descending: descending,
})
if err != nil {
log.Printf("[ProductPerformance][grouped] failed trace_id=%s mode=%s levels=%v expand_through_level=%d filters=%d: %v", traceID, mode, groupLevels, expandThroughLevel, len(filters), err)
http.Error(w, "urun performans grup verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceGroupedFilterOptionsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second)
defer cancel()
mode := strings.TrimSpace(r.URL.Query().Get("mode"))
mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu"))
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
fields := splitCSVQuery(r.URL.Query().Get("fields"))
limit := intQuery(r, "limit", 50000)
if r.Method == http.MethodPost {
var body struct {
Mode string `json:"mode"`
GroupLevels []string `json:"group_levels"`
MainGroup string `json:"urun_ana_grubu"`
Fields []string `json:"fields"`
Limit int `json:"limit"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "gecersiz filtre secenek istegi: "+err.Error(), http.StatusBadRequest)
return
}
if strings.TrimSpace(body.Mode) != "" {
mode = strings.TrimSpace(body.Mode)
}
if strings.TrimSpace(body.MainGroup) != "" {
mainGroup = strings.TrimSpace(body.MainGroup)
}
if len(body.GroupLevels) > 0 {
groupLevels = body.GroupLevels
}
if len(body.Fields) > 0 {
fields = body.Fields
}
if body.Limit > 0 {
limit = body.Limit
}
}
options, err := queries.ListProductPerformanceGroupedFilterOptions(ctx, pg, queries.ProductPerformanceGroupedFilterOptionsRequest{
Mode: mode,
GroupLevels: groupLevels,
MainGroup: mainGroup,
Fields: fields,
Limit: limit,
})
if err != nil {
log.Printf("[ProductPerformance][grouped-filter-options] failed trace_id=%s mode=%s levels=%v fields=%d: %v", traceID, mode, groupLevels, len(fields), err)
http.Error(w, "urun performans filtre secenekleri alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(options)
}
}
func splitCSVQuery(value string) []string {
parts := strings.Split(value, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
out = append(out, part)
}
}
return out
}
func intQuery(r *http.Request, key string, fallback int) int { func intQuery(r *http.Request, key string, fallback int) int {
raw := strings.TrimSpace(r.URL.Query().Get(key)) raw := strings.TrimSpace(r.URL.Query().Get(key))
if raw == "" { if raw == "" {
+1140
View File
@@ -0,0 +1,1140 @@
package routes
import (
"bssapp-backend/models"
"bssapp-backend/queries"
"bssapp-backend/utils"
"context"
"database/sql"
"encoding/json"
"fmt"
"math"
"net/http"
"sort"
"strings"
"time"
"github.com/xuri/excelize/v2"
)
type productPerformanceExcelExportRequest struct {
Filters map[string][]string `json:"filters"`
SortBy string `json:"sort_by"`
Descending *bool `json:"descending"`
}
type productPerformanceExcelColumn struct {
Header string
Kind string
Value func(*productPerformanceExcelVariant, productPerformanceExcelStats) any
}
type productPerformanceExcelStats struct {
AvgSalesUSD90 float64
AvgSalesUSD180 float64
AvgSalesUSD365 float64
AvgSalesUSDTotal float64
}
type productPerformanceExcelVariant struct {
KpiDate string
ProductCode string
ColorCode string
ColorDescription string
YakaKodu string
ItemDescription string
Kategori string
Seri string
YasGrubu string
AskiliYan string
UrunIlkGrubu string
UrunAnaGrubu string
UrunAltGrubu string
StockQty float64
AvgStock90 float64
AvgStock180 float64
AvgStock365 float64
AvgStockTotal float64
BasePriceUSD float64
CostPriceUSD float64
SalesQty90 float64
SalesQty180 float64
SalesQty365 float64
SalesUSD90 float64
SalesUSD180 float64
SalesUSD365 float64
SalesQtyTotalProduct float64
SalesUSDTotalProduct float64
SalesQtyTotalGeneral float64
SalesUSDTotalGeneral float64
HasGeneralTotal bool
MarketCount90 int
CustomerCount90 int
MarketCountTotal int
CustomerCountTotal int
Markets90 map[string]bool
MarketsTotal map[string]bool
MarketFiltered bool
PerformanceBucket string
Recommendation string
LastSaleDate string
LastRefNumber string
UpdatedAt string
}
func ExportProductPerformanceExcelHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
var req productPerformanceExcelExportRequest
if r.Method == http.MethodPost {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "gecersiz excel istegi: "+err.Error(), http.StatusBadRequest)
return
}
}
productRows, _, err := queries.ListProductPerformance(ctx, pg, queries.ProductPerformanceFilters{
Limit: 50000,
Page: 1,
SortBy: "product_code",
Descending: false,
})
if err != nil {
http.Error(w, "urun performans excel listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
generalRows, err := queries.ListProductPerformanceGeneral(ctx, pg, 50000)
if err != nil {
http.Error(w, "urun performans genel excel listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
rows := buildProductPerformanceExcelVariants(productRows, generalRows, req.Filters)
sortProductPerformanceExcelVariants(rows, req.SortBy, req.Descending == nil || *req.Descending)
stats := productPerformanceExcelStatsFor(rows)
file, err := buildProductPerformanceExcelFile(rows, stats)
if err != nil {
http.Error(w, "urun performans excel dosyasi hazirlanamadi: "+err.Error(), http.StatusInternalServerError)
return
}
defer func() { _ = file.Close() }()
buf, err := file.WriteToBuffer()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
filename := fmt.Sprintf("product_performance_renk_yaka_%s.xlsx", time.Now().Format("20060102_150405"))
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
w.Header().Set("Content-Length", fmt.Sprint(len(buf.Bytes())))
w.WriteHeader(http.StatusOK)
_, _ = w.Write(buf.Bytes())
}
}
func buildProductPerformanceExcelVariants(productRows []models.ProductPerformanceRow, generalRows []models.ProductPerformanceGeneralRow, filters map[string][]string) []*productPerformanceExcelVariant {
byKey := map[string]*productPerformanceExcelVariant{}
marketFiltered := productPerformanceExcelHasFilter(filters, "market_key")
for _, row := range productRows {
if !productPerformanceExcelProductRowMatchesFilters(row, filters) {
continue
}
key := productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)
v := byKey[key]
if v == nil {
v = &productPerformanceExcelVariant{Markets90: map[string]bool{}, MarketsTotal: map[string]bool{}, MarketFiltered: marketFiltered}
byKey[key] = v
}
v.addProductRow(row)
}
for _, row := range generalRows {
if !productPerformanceExcelGeneralRowMatchesFilters(row, filters) {
continue
}
key := productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)
v := byKey[key]
if v == nil {
v = &productPerformanceExcelVariant{Markets90: map[string]bool{}, MarketsTotal: map[string]bool{}, MarketFiltered: marketFiltered}
byKey[key] = v
}
v.addGeneralRow(row)
}
out := make([]*productPerformanceExcelVariant, 0, len(byKey))
for _, row := range byKey {
if productPerformanceExcelVariantMatchesPostFilters(row, filters) {
out = append(out, row)
}
}
return out
}
func (v *productPerformanceExcelVariant) addProductRow(row models.ProductPerformanceRow) {
v.fillDimensions(
row.KpiDate, row.ProductCode, row.ColorCode, row.ColorDescription, row.YakaKodu, row.ItemDescription,
row.Kategori, row.Seri, row.YasGrubu, row.AskiliYan, row.UrunIlkGrubu, row.UrunAnaGrubu, row.UrunAltGrubu,
row.BasePriceUSD, row.CostPriceUSD, row.StockQty, row.LastSaleDate, row.LastRefNumber, row.UpdatedAt,
)
v.SalesQty90 += row.SalesQty90
v.SalesQty180 += row.SalesQty180
v.SalesQty365 += row.SalesQty365
v.SalesUSD90 += row.SalesUSD90
v.SalesUSD180 += row.SalesUSD180
v.SalesUSD365 += row.SalesUSD365
v.AvgStock90 = productPerformanceExcelFirstNonZero(v.AvgStock90, row.AvgStock90)
v.AvgStock180 = productPerformanceExcelFirstNonZero(v.AvgStock180, row.AvgStock180)
v.AvgStock365 = productPerformanceExcelFirstNonZero(v.AvgStock365, row.AvgStock365)
v.AvgStockTotal = productPerformanceExcelFirstNonZero(v.AvgStockTotal, row.AvgStockTotal)
v.SalesQtyTotalProduct += row.SalesQtyTotal
v.SalesUSDTotalProduct += row.SalesUSDTotal
v.MarketCount90 = productPerformanceExcelMaxInt(v.MarketCount90, row.MarketCount90)
if row.SalesUSD90 > 0 {
v.CustomerCount90 += row.CustomerCount90
}
v.PerformanceBucket = productPerformanceExcelFirstNonEmpty(v.PerformanceBucket, row.PerformanceBucket)
v.Recommendation = productPerformanceExcelFirstNonEmpty(v.Recommendation, row.Recommendation)
v.addMarket(row.MarketKey, row.SalesQty90, row.SalesUSD90, row.SalesQtyTotal, row.SalesUSDTotal)
}
func (v *productPerformanceExcelVariant) addGeneralRow(row models.ProductPerformanceGeneralRow) {
v.fillDimensions(
productPerformanceExcelFirstNonEmpty(row.PeriodEnd, row.PeriodStart), row.ProductCode, row.ColorCode, row.ColorDescription, row.YakaKodu, row.ItemDescription,
row.Kategori, row.Seri, row.YasGrubu, row.AskiliYan, row.UrunIlkGrubu, row.UrunAnaGrubu, row.UrunAltGrubu,
row.BasePriceUSD, row.CostPriceUSD, row.StockQty, row.LastSaleDate, row.LastRefNumber, "",
)
v.SalesQtyTotalGeneral += row.SalesQtyTotal
v.SalesUSDTotalGeneral += row.SalesUSDTotal
v.HasGeneralTotal = true
v.MarketCountTotal = productPerformanceExcelMaxInt(v.MarketCountTotal, row.MarketCountTotal)
if row.SalesUSDTotal > 0 {
v.CustomerCountTotal += row.CustomerCountTotal
}
v.PerformanceBucket = productPerformanceExcelFirstNonEmpty(v.PerformanceBucket, row.PerformanceBucket)
v.Recommendation = productPerformanceExcelFirstNonEmpty(v.Recommendation, row.Recommendation)
v.addMarket(row.MarketKey, 0, 0, row.SalesQtyTotal, row.SalesUSDTotal)
}
func (v *productPerformanceExcelVariant) fillDimensions(kpiDate, productCode, colorCode, colorDescription, yakaKodu, itemDescription, kategori, seri, yasGrubu, askiliYan, urunIlkGrubu, urunAnaGrubu, urunAltGrubu string, basePrice, costPrice, stockQty float64, lastSaleDate, lastRefNumber, updatedAt string) {
v.KpiDate = productPerformanceExcelFirstNonEmpty(v.KpiDate, kpiDate)
v.ProductCode = productPerformanceExcelFirstNonEmpty(v.ProductCode, productCode)
v.ColorCode = productPerformanceExcelFirstNonEmpty(v.ColorCode, colorCode)
v.ColorDescription = productPerformanceExcelFirstNonEmpty(v.ColorDescription, colorDescription)
v.YakaKodu = productPerformanceExcelFirstNonEmpty(v.YakaKodu, yakaKodu)
v.ItemDescription = productPerformanceExcelFirstNonEmpty(v.ItemDescription, itemDescription)
v.Kategori = productPerformanceExcelFirstNonEmpty(v.Kategori, kategori)
v.Seri = productPerformanceExcelFirstNonEmpty(v.Seri, seri)
v.YasGrubu = productPerformanceExcelFirstNonEmpty(v.YasGrubu, yasGrubu)
v.AskiliYan = productPerformanceExcelFirstNonEmpty(v.AskiliYan, productPerformanceExcelCleanOptional(askiliYan))
v.UrunIlkGrubu = productPerformanceExcelFirstNonEmpty(v.UrunIlkGrubu, productPerformanceExcelCleanOptional(urunIlkGrubu))
v.UrunAnaGrubu = productPerformanceExcelFirstNonEmpty(v.UrunAnaGrubu, urunAnaGrubu)
v.UrunAltGrubu = productPerformanceExcelFirstNonEmpty(v.UrunAltGrubu, urunAltGrubu)
v.BasePriceUSD = productPerformanceExcelFirstNonZero(v.BasePriceUSD, basePrice)
v.CostPriceUSD = productPerformanceExcelFirstNonZero(v.CostPriceUSD, costPrice)
v.CostPriceUSD, v.BasePriceUSD = productPerformanceExcelNormalizeCostPair(v.CostPriceUSD, v.BasePriceUSD)
if stockQty > v.StockQty {
v.StockQty = stockQty
}
if lastSaleDate > v.LastSaleDate {
v.LastSaleDate = lastSaleDate
v.LastRefNumber = lastRefNumber
}
if updatedAt > v.UpdatedAt {
v.UpdatedAt = updatedAt
}
}
func (v *productPerformanceExcelVariant) addMarket(marketKey string, salesQty90, salesUSD90, salesQtyTotal, salesUSDTotal float64) {
market := productPerformanceExcelMarketName(marketKey)
if market == "" || market == "STOK" {
return
}
if salesUSD90 > 0 {
v.Markets90[market] = true
}
if salesUSDTotal > 0 {
v.MarketsTotal[market] = true
}
}
func (v *productPerformanceExcelVariant) totalQty() float64 {
if v.HasGeneralTotal {
return v.SalesQtyTotalGeneral
}
return v.SalesQtyTotalProduct
}
func (v *productPerformanceExcelVariant) totalUSD() float64 {
if v.HasGeneralTotal {
return v.SalesUSDTotalGeneral
}
return v.SalesUSDTotalProduct
}
func (v *productPerformanceExcelVariant) marketCount90() int {
if v.MarketFiltered {
return len(v.Markets90)
}
return productPerformanceExcelMaxInt(v.MarketCount90, len(v.Markets90))
}
func (v *productPerformanceExcelVariant) marketCountTotal() int {
if v.MarketFiltered {
return len(v.MarketsTotal)
}
return productPerformanceExcelMaxInt(v.MarketCountTotal, len(v.MarketsTotal))
}
func (v *productPerformanceExcelVariant) marketsLabel() string {
return productPerformanceExcelSortedSetLabel(v.MarketsTotal)
}
func buildProductPerformanceExcelFile(rows []*productPerformanceExcelVariant, stats productPerformanceExcelStats) (*excelize.File, error) {
f := excelize.NewFile()
sheet := "RenkYaka"
f.SetSheetName("Sheet1", sheet)
columns := productPerformanceExcelColumns()
headerStyle, _ := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "FFFFFF"},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"1F4E78"}},
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
})
textStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 49})
intStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 3})
decimalStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 4})
percentStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 10})
for i, col := range columns {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
_ = f.SetCellStr(sheet, cell, col.Header)
}
if len(columns) > 0 {
lastHeader, _ := excelize.CoordinatesToCellName(len(columns), 1)
_ = f.SetCellStyle(sheet, "A1", lastHeader, headerStyle)
}
for rowIndex, row := range rows {
excelRow := rowIndex + 2
for colIndex, col := range columns {
cell, _ := excelize.CoordinatesToCellName(colIndex+1, excelRow)
value := col.Value(row, stats)
if col.Kind == "text" {
_ = f.SetCellStr(sheet, cell, strings.TrimSpace(fmt.Sprint(value)))
} else {
_ = f.SetCellValue(sheet, cell, productPerformanceExcelFloat(value))
}
}
}
lastRow := productPerformanceExcelMaxInt(len(rows)+1, 2)
if len(columns) > 0 {
lastFilterCell, _ := excelize.CoordinatesToCellName(len(columns), lastRow)
_ = f.AutoFilter(sheet, "A1:"+lastFilterCell, []excelize.AutoFilterOptions{})
}
for i, col := range columns {
colName, _ := excelize.ColumnNumberToName(i + 1)
_ = f.SetColWidth(sheet, colName, colName, productPerformanceExcelColumnWidth(col.Header, col.Kind))
style := decimalStyle
switch col.Kind {
case "text":
style = textStyle
case "int":
style = intStyle
case "percent":
style = percentStyle
}
_ = f.SetCellStyle(sheet, fmt.Sprintf("%s2", colName), fmt.Sprintf("%s%d", colName, lastRow), style)
}
_ = f.SetPanes(sheet, &excelize.Panes{Freeze: true, YSplit: 1, TopLeftCell: "A2", ActivePane: "bottomLeft"})
return f, nil
}
func productPerformanceExcelColumns() []productPerformanceExcelColumn {
base := []productPerformanceExcelColumn{
textExcelColumn("KPI Tarihi", func(v *productPerformanceExcelVariant) any { return v.KpiDate }),
textExcelColumn("Ürün İlk Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunIlkGrubu }),
textExcelColumn("Askılı/Yan", func(v *productPerformanceExcelVariant) any { return v.AskiliYan }),
textExcelColumn("Kategori", func(v *productPerformanceExcelVariant) any { return v.Kategori }),
textExcelColumn("Ürün Ana Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunAnaGrubu }),
textExcelColumn("Ürün Alt Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunAltGrubu }),
textExcelColumn("Ürün", func(v *productPerformanceExcelVariant) any { return v.ProductCode }),
textExcelColumn("Açıklama", func(v *productPerformanceExcelVariant) any { return v.ItemDescription }),
textExcelColumn("Renk", func(v *productPerformanceExcelVariant) any { return v.ColorCode }),
textExcelColumn("Renk Açıklama", func(v *productPerformanceExcelVariant) any { return v.ColorDescription }),
textExcelColumn("Yaka", func(v *productPerformanceExcelVariant) any { return v.YakaKodu }),
textExcelColumn("Renk/Yaka", func(v *productPerformanceExcelVariant) any {
return productPerformanceExcelColorYaka(v.ColorCode, v.ColorDescription, v.YakaKodu)
}),
textExcelColumn("Piyasalar", func(v *productPerformanceExcelVariant) any { return v.marketsLabel() }),
numberExcelColumn("Toplam Stok", "int", func(v *productPerformanceExcelVariant) any { return v.StockQty }),
}
base = append(base, productPerformanceExcelPeriodColumns("90G", "90d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) {
return v.SalesQty90, v.SalesUSD90, v.marketCount90(), v.CustomerCount90
})...)
base = append(base, productPerformanceExcelPeriodColumns("180G", "180d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) {
return v.SalesQty180, v.SalesUSD180, v.marketCount90(), v.CustomerCount90
})...)
base = append(base, productPerformanceExcelPeriodColumns("360G", "365d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) {
return v.SalesQty365, v.SalesUSD365, v.marketCount90(), v.CustomerCount90
})...)
base = append(base, productPerformanceExcelPeriodColumns("Genel", "total", func(v *productPerformanceExcelVariant) (float64, float64, int, int) {
return v.totalQty(), v.totalUSD(), v.marketCountTotal(), v.CustomerCountTotal
})...)
base = append(base,
textExcelColumn("Durum", func(v *productPerformanceExcelVariant) any { return productPerformanceExcelStatus(v) }),
textExcelColumn("Öneri", func(v *productPerformanceExcelVariant) any { return productPerformanceExcelRecommendation(v) }),
textExcelColumn("Son Satış", func(v *productPerformanceExcelVariant) any { return v.LastSaleDate }),
textExcelColumn("Son Ref", func(v *productPerformanceExcelVariant) any { return v.LastRefNumber }),
textExcelColumn("Güncelleme", func(v *productPerformanceExcelVariant) any { return v.UpdatedAt }),
)
return base
}
func productPerformanceExcelPeriodColumns(label, suffix string, values func(*productPerformanceExcelVariant) (float64, float64, int, int)) []productPerformanceExcelColumn {
return []productPerformanceExcelColumn{
numberExcelColumn(label+" Toplam Adet", "int", func(v *productPerformanceExcelVariant) any {
qty, _, _, _ := values(v)
return qty
}),
numberExcelColumn(label+" Toplam Ciro USD", "number", func(v *productPerformanceExcelVariant) any {
_, usd, _, _ := values(v)
return usd
}),
numberExcelColumn(label+" Ort. Satış Fiyatı USD", "number", func(v *productPerformanceExcelVariant) any {
qty, usd, _, _ := values(v)
return productPerformanceExcelAvgPrice(usd, qty)
}),
numberExcelColumn(label+" Yıllık Stok Devir", "number", func(v *productPerformanceExcelVariant) any {
qty, _, _, _ := values(v)
return productPerformanceExcelStockTurnover(qty, productPerformanceExcelAvgStock(v, suffix), productPerformanceExcelPeriodDays(v, suffix))
}),
numberExcelColumn(label+" Taban Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.BasePriceUSD }),
numberExcelColumn(label+" Çıplak Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.CostPriceUSD }),
numberExcelColumn(label+" Taban Toplam K/Z USD", "number", func(v *productPerformanceExcelVariant) any {
qty, usd, _, _ := values(v)
return productPerformanceExcelGrossProfit(usd, qty, v.BasePriceUSD)
}),
numberExcelColumn(label+" Çıplak Toplam K/Z USD", "number", func(v *productPerformanceExcelVariant) any {
qty, usd, _, _ := values(v)
return productPerformanceExcelGrossProfit(usd, qty, v.CostPriceUSD)
}),
numberExcelColumn(label+" Taban Marj", "percent", func(v *productPerformanceExcelVariant) any {
qty, usd, _, _ := values(v)
return productPerformanceExcelMargin(usd, qty, v.BasePriceUSD)
}),
numberExcelColumn(label+" Çıplak Marj", "percent", func(v *productPerformanceExcelVariant) any {
qty, usd, _, _ := values(v)
return productPerformanceExcelMargin(usd, qty, v.CostPriceUSD)
}),
numberExcelColumn(label+" Tekil Piyasa", "int", func(v *productPerformanceExcelVariant) any {
_, _, markets, _ := values(v)
return markets
}),
numberExcelColumn(label+" Tekil Müşteri", "int", func(v *productPerformanceExcelVariant) any {
_, _, _, customers := values(v)
return customers
}),
numberExcelColumn(label+" Ürün Skor", "number", func(v *productPerformanceExcelVariant) any {
qty, usd, markets, customers := values(v)
days := productPerformanceExcelPeriodDays(v, suffix)
return productPerformanceExcelProductScore(suffix, usd, productPerformanceExcelStockTurnover(qty, productPerformanceExcelAvgStock(v, suffix), days), float64(markets), float64(customers), productPerformanceExcelMargin(usd, qty, v.CostPriceUSD), days)
}),
}
}
func textExcelColumn(header string, value func(*productPerformanceExcelVariant) any) productPerformanceExcelColumn {
return productPerformanceExcelColumn{Header: header, Kind: "text", Value: func(v *productPerformanceExcelVariant, _ productPerformanceExcelStats) any { return value(v) }}
}
func numberExcelColumn(header, kind string, value func(*productPerformanceExcelVariant) any) productPerformanceExcelColumn {
return productPerformanceExcelColumn{Header: header, Kind: kind, Value: func(v *productPerformanceExcelVariant, _ productPerformanceExcelStats) any { return value(v) }}
}
func productPerformanceExcelProductRowMatchesFilters(row models.ProductPerformanceRow, filters map[string][]string) bool {
for field, selected := range productPerformanceExcelFilterSets(filters) {
if field == "performance_bucket" {
continue
}
if !productPerformanceExcelMatchesAny(selected, productPerformanceExcelProductFilterCandidates(row, field)) {
return false
}
}
return true
}
func productPerformanceExcelGeneralRowMatchesFilters(row models.ProductPerformanceGeneralRow, filters map[string][]string) bool {
for field, selected := range productPerformanceExcelFilterSets(filters) {
if field == "performance_bucket" {
continue
}
if !productPerformanceExcelMatchesAny(selected, productPerformanceExcelGeneralFilterCandidates(row, field)) {
return false
}
}
return true
}
func productPerformanceExcelVariantMatchesPostFilters(row *productPerformanceExcelVariant, filters map[string][]string) bool {
selected := productPerformanceExcelFilterSets(filters)["performance_bucket"]
if len(selected) == 0 {
return true
}
return productPerformanceExcelMatchesAny(selected, []string{row.PerformanceBucket, productPerformanceExcelStatus(row)})
}
func productPerformanceExcelFilterSets(filters map[string][]string) map[string]map[string]bool {
out := map[string]map[string]bool{}
for field, values := range filters {
field = strings.TrimSpace(field)
for _, value := range values {
value = productPerformanceExcelNormalize(value)
if value == "" {
continue
}
if out[field] == nil {
out[field] = map[string]bool{}
}
out[field][value] = true
}
}
return out
}
func productPerformanceExcelHasFilter(filters map[string][]string, field string) bool {
for _, value := range filters[strings.TrimSpace(field)] {
if strings.TrimSpace(value) != "" {
return true
}
}
return false
}
func productPerformanceExcelProductFilterCandidates(row models.ProductPerformanceRow, field string) []string {
switch strings.TrimSpace(field) {
case "product_code":
return []string{row.ProductCode}
case "item_description":
return []string{row.ItemDescription}
case "color_code":
return []string{row.ColorCode}
case "yaka_kodu":
return []string{row.YakaKodu}
case "color_yaka":
return []string{productPerformanceExcelColorYaka(row.ColorCode, row.ColorDescription, row.YakaKodu), row.ColorCode + "/" + row.YakaKodu}
case "kategori":
return []string{row.Kategori}
case "askili_yan":
return []string{productPerformanceExcelCleanOptional(row.AskiliYan)}
case "urun_ilk_grubu":
return []string{productPerformanceExcelCleanOptional(row.UrunIlkGrubu)}
case "urun_ana_grubu":
return []string{row.UrunAnaGrubu}
case "urun_alt_grubu":
return []string{row.UrunAltGrubu}
case "market_key":
return []string{row.MarketKey, productPerformanceExcelMarketName(row.MarketKey)}
default:
return nil
}
}
func productPerformanceExcelGeneralFilterCandidates(row models.ProductPerformanceGeneralRow, field string) []string {
switch strings.TrimSpace(field) {
case "product_code":
return []string{row.ProductCode}
case "item_description":
return []string{row.ItemDescription}
case "color_code":
return []string{row.ColorCode}
case "yaka_kodu":
return []string{row.YakaKodu}
case "color_yaka":
return []string{productPerformanceExcelColorYaka(row.ColorCode, row.ColorDescription, row.YakaKodu), row.ColorCode + "/" + row.YakaKodu}
case "kategori":
return []string{row.Kategori}
case "askili_yan":
return []string{productPerformanceExcelCleanOptional(row.AskiliYan)}
case "urun_ilk_grubu":
return []string{productPerformanceExcelCleanOptional(row.UrunIlkGrubu)}
case "urun_ana_grubu":
return []string{row.UrunAnaGrubu}
case "urun_alt_grubu":
return []string{row.UrunAltGrubu}
case "market_key":
return []string{row.MarketKey, productPerformanceExcelMarketName(row.MarketKey)}
default:
return nil
}
}
func productPerformanceExcelMatchesAny(selected map[string]bool, candidates []string) bool {
if len(selected) == 0 {
return true
}
for _, candidate := range candidates {
if selected[productPerformanceExcelNormalize(candidate)] {
return true
}
}
return false
}
func sortProductPerformanceExcelVariants(rows []*productPerformanceExcelVariant, sortBy string, desc bool) {
sortBy = strings.TrimSpace(sortBy)
if sortBy == "" || sortBy == "image" {
sortBy = "product_code"
desc = false
}
sort.SliceStable(rows, func(i, j int) bool {
cmp := productPerformanceExcelCompare(rows[i], rows[j], sortBy)
if cmp == 0 {
cmp = strings.Compare(productPerformanceExcelHierarchySortKey(rows[i]), productPerformanceExcelHierarchySortKey(rows[j]))
}
if desc {
return cmp > 0
}
return cmp < 0
})
}
func productPerformanceExcelCompare(a, b *productPerformanceExcelVariant, sortBy string) int {
if productPerformanceExcelIsNumericSort(sortBy) {
av := productPerformanceExcelSortNumber(a, sortBy)
bv := productPerformanceExcelSortNumber(b, sortBy)
if av < bv {
return -1
}
if av > bv {
return 1
}
return 0
}
return strings.Compare(productPerformanceExcelNormalize(productPerformanceExcelSortText(a, sortBy)), productPerformanceExcelNormalize(productPerformanceExcelSortText(b, sortBy)))
}
func productPerformanceExcelIsNumericSort(sortBy string) bool {
switch sortBy {
case "stock_qty", "sales_qty_90d", "sales_qty_180d", "sales_qty_365d", "sales_qty_total",
"sales_usd_90d", "sales_usd_180d", "sales_usd_365d", "sales_usd_total",
"avg_price_usd_90d", "avg_price_usd_180d", "avg_price_usd_365d", "avg_price_usd_total",
"stock_turnover_90d", "stock_turnover_180d", "stock_turnover_365d", "stock_turnover_total",
"base_price_usd", "cost_price_usd", "market_count_90d", "customer_count_90d", "market_count_total", "customer_count_total",
"performance_score", "performance_score_90d", "performance_score_total":
return true
default:
return false
}
}
func productPerformanceExcelSortNumber(row *productPerformanceExcelVariant, sortBy string) float64 {
switch sortBy {
case "stock_qty":
return row.StockQty
case "sales_qty_90d":
return row.SalesQty90
case "sales_qty_180d":
return row.SalesQty180
case "sales_qty_365d":
return row.SalesQty365
case "sales_qty_total":
return row.totalQty()
case "sales_usd_90d":
return row.SalesUSD90
case "sales_usd_180d":
return row.SalesUSD180
case "sales_usd_365d":
return row.SalesUSD365
case "sales_usd_total":
return row.totalUSD()
case "avg_price_usd_90d":
return productPerformanceExcelAvgPrice(row.SalesUSD90, row.SalesQty90)
case "avg_price_usd_180d":
return productPerformanceExcelAvgPrice(row.SalesUSD180, row.SalesQty180)
case "avg_price_usd_365d":
return productPerformanceExcelAvgPrice(row.SalesUSD365, row.SalesQty365)
case "avg_price_usd_total":
return productPerformanceExcelAvgPrice(row.totalUSD(), row.totalQty())
case "stock_turnover_90d":
return productPerformanceExcelStockTurnover(row.SalesQty90, productPerformanceExcelAvgStock(row, "90d"), productPerformanceExcelPeriodDays(row, "90d"))
case "stock_turnover_180d":
return productPerformanceExcelStockTurnover(row.SalesQty180, productPerformanceExcelAvgStock(row, "180d"), productPerformanceExcelPeriodDays(row, "180d"))
case "stock_turnover_365d":
return productPerformanceExcelStockTurnover(row.SalesQty365, productPerformanceExcelAvgStock(row, "365d"), productPerformanceExcelPeriodDays(row, "365d"))
case "stock_turnover_total":
return productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), productPerformanceExcelPeriodDays(row, "total"))
case "base_price_usd":
return row.BasePriceUSD
case "cost_price_usd":
return row.CostPriceUSD
case "market_count_90d":
return float64(row.marketCount90())
case "customer_count_90d":
return float64(row.CustomerCount90)
case "market_count_total":
return float64(row.marketCountTotal())
case "customer_count_total":
return float64(row.CustomerCountTotal)
case "performance_score", "performance_score_90d":
days := productPerformanceExcelPeriodDays(row, "90d")
return productPerformanceExcelProductScore("90d", row.SalesUSD90, productPerformanceExcelStockTurnover(row.SalesQty90, productPerformanceExcelAvgStock(row, "90d"), days), float64(row.marketCount90()), float64(row.CustomerCount90), productPerformanceExcelMargin(row.SalesUSD90, row.SalesQty90, row.CostPriceUSD), days)
case "performance_score_total":
days := productPerformanceExcelPeriodDays(row, "total")
return productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), days), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD), days)
default:
return 0
}
}
func productPerformanceExcelSortText(row *productPerformanceExcelVariant, sortBy string) string {
switch sortBy {
case "product_code":
return row.ProductCode
case "color_yaka":
return productPerformanceExcelColorYaka(row.ColorCode, row.ColorDescription, row.YakaKodu)
case "color_code":
return row.ColorCode
case "yaka_kodu":
return row.YakaKodu
case "item_description":
return row.ItemDescription
case "kategori":
return row.Kategori
case "askili_yan":
return row.AskiliYan
case "urun_ilk_grubu":
return row.UrunIlkGrubu
case "urun_ana_grubu":
return row.UrunAnaGrubu
case "urun_alt_grubu":
return row.UrunAltGrubu
case "market_key":
return row.marketsLabel()
case "performance_bucket":
return productPerformanceExcelStatus(row)
default:
return productPerformanceExcelHierarchySortKey(row)
}
}
func productPerformanceExcelStatsFor(rows []*productPerformanceExcelVariant) productPerformanceExcelStats {
var stats productPerformanceExcelStats
var c90, c180, c365, cTotal float64
for _, row := range rows {
if row.SalesUSD90 > 0 {
stats.AvgSalesUSD90 += row.SalesUSD90
c90++
}
if row.SalesUSD180 > 0 {
stats.AvgSalesUSD180 += row.SalesUSD180
c180++
}
if row.SalesUSD365 > 0 {
stats.AvgSalesUSD365 += row.SalesUSD365
c365++
}
if row.totalUSD() > 0 {
stats.AvgSalesUSDTotal += row.totalUSD()
cTotal++
}
}
if c90 > 0 {
stats.AvgSalesUSD90 /= c90
}
if c180 > 0 {
stats.AvgSalesUSD180 /= c180
}
if c365 > 0 {
stats.AvgSalesUSD365 /= c365
}
if cTotal > 0 {
stats.AvgSalesUSDTotal /= cTotal
}
return stats
}
func productPerformanceExcelVariantKey(productCode, colorCode, yakaKodu string) string {
return strings.ToUpper(strings.TrimSpace(productCode)) + "|" + strings.ToUpper(strings.TrimSpace(colorCode)) + "|" + strings.ToUpper(strings.TrimSpace(yakaKodu))
}
func productPerformanceExcelHierarchySortKey(row *productPerformanceExcelVariant) string {
return strings.Join([]string{row.UrunIlkGrubu, row.AskiliYan, row.Kategori, row.UrunAnaGrubu, row.UrunAltGrubu, row.ProductCode, row.ColorCode, row.YakaKodu}, "|")
}
func productPerformanceExcelColorYaka(colorCode, colorDescription, yakaKodu string) string {
color := strings.TrimSpace(colorCode)
if desc := strings.TrimSpace(colorDescription); color != "" && desc != "" {
color = color + "-" + strings.ToUpper(desc)
}
yaka := strings.TrimSpace(yakaKodu)
parts := make([]string, 0, 2)
if color != "" && color != "-" {
parts = append(parts, color)
}
if yaka != "" && yaka != "-" {
parts = append(parts, yaka)
}
return strings.Join(parts, "/")
}
func productPerformanceExcelMarketName(value string) string {
parts := strings.Split(value, "|")
for i := len(parts) - 1; i >= 0; i-- {
part := strings.TrimSpace(parts[i])
if part != "" {
return part
}
}
return ""
}
func productPerformanceExcelStatus(row *productPerformanceExcelVariant) string {
switch {
case row.totalQty() > 0 && row.totalUSD() <= 0:
return "Ciro/Fiyat Kontrol"
case row.totalQty() <= 0 && row.StockQty > 0:
return "Stok Riski"
case productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD) < 0:
return "Fiyat Baskısı"
case productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), productPerformanceExcelPeriodDays(row, "total")), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD), productPerformanceExcelPeriodDays(row, "total")) >= 70:
return "Yıldız Ürün"
default:
return productPerformanceExcelBucketLabel(row.PerformanceBucket)
}
}
func productPerformanceExcelRecommendation(row *productPerformanceExcelVariant) string {
switch {
case row.totalQty() > 0 && row.totalUSD() <= 0:
return "Satış adedi var ama USD ciro/fiyat yok. Satış tutarı aktarımı ve döviz dönüşümü kontrol edilmeli."
case row.totalQty() <= 0 && row.StockQty > 0:
return "Satış yok, stok duruyor. Piyasa/fiyat aksiyonu gerekli."
case row.Recommendation != "":
return row.Recommendation
default:
return "Düzenli takip."
}
}
func productPerformanceExcelBucketLabel(value string) string {
switch strings.TrimSpace(value) {
case "YILDIZ_URUN":
return "Yıldız Ürün"
case "STOKSUZ_TALEP":
return "Stoksuz Talep"
case "STOK_RISKI":
return "Stok Riski"
case "FIYAT_BASKISI":
return "Fiyat Baskısı"
case "FIYAT_FIRSATI":
return "Fiyat Fırsatı"
case "MALIYET_YOK":
return "Maliyet Yok"
case "TAKIP":
return "Takip"
default:
return strings.TrimSpace(value)
}
}
func productPerformanceExcelCleanOptional(value string) string {
value = strings.TrimSpace(value)
if value == "-" {
return ""
}
return value
}
func productPerformanceExcelNormalize(value string) string {
return strings.ToUpper(strings.TrimSpace(value))
}
func productPerformanceExcelFirstNonEmpty(values ...string) string {
for _, value := range values {
value = strings.TrimSpace(value)
if value != "" {
return value
}
}
return ""
}
func productPerformanceExcelFirstNonZero(values ...float64) float64 {
for _, value := range values {
if value != 0 && !math.IsNaN(value) && !math.IsInf(value, 0) {
return value
}
}
return 0
}
func productPerformanceExcelNormalizeCostPair(costPriceUSD, basePriceUSD float64) (float64, float64) {
if costPriceUSD > 0 && basePriceUSD > 0 && costPriceUSD > basePriceUSD {
return basePriceUSD, costPriceUSD
}
return costPriceUSD, basePriceUSD
}
func productPerformanceExcelAvgPrice(salesUSD, qty float64) float64 {
if qty <= 0 {
return 0
}
return salesUSD / qty
}
const productPerformanceExcelStockTurnoverYearDays = 360.0
func productPerformanceExcelAvgStock(row *productPerformanceExcelVariant, suffix string) float64 {
switch suffix {
case "90d":
return productPerformanceExcelFirstNonZero(row.AvgStock90, row.StockQty)
case "180d":
return productPerformanceExcelFirstNonZero(row.AvgStock180, row.StockQty)
case "365d":
return productPerformanceExcelFirstNonZero(row.AvgStock365, row.StockQty)
case "total":
return productPerformanceExcelFirstNonZero(row.AvgStockTotal, row.StockQty)
default:
return row.StockQty
}
}
func productPerformanceExcelPeriodDays(row *productPerformanceExcelVariant, suffix string) float64 {
switch suffix {
case "90d":
return 90
case "180d":
return 180
case "365d":
return 360
case "total":
end, err := time.Parse("2006-01-02", strings.TrimSpace(row.KpiDate))
if err != nil {
return 0
}
start := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
if end.Before(start) {
return 0
}
return end.Sub(start).Hours()/24 + 1
default:
return 0
}
}
func productPerformanceExcelStockTurnover(salesQty, avgStock, periodDays float64) float64 {
if salesQty <= 0 || avgStock <= 0 {
return 0
}
raw := salesQty / avgStock
if periodDays <= 0 {
return raw
}
return raw * productPerformanceExcelStockTurnoverYearDays / periodDays
}
func productPerformanceExcelGrossProfit(salesUSD, qty, unitCost float64) float64 {
if qty <= 0 {
return 0
}
return salesUSD - qty*unitCost
}
func productPerformanceExcelMargin(salesUSD, qty, unitCost float64) float64 {
if salesUSD <= 0 {
return 0
}
return productPerformanceExcelGrossProfit(salesUSD, qty, unitCost) / salesUSD
}
func productPerformanceExcelProductScore(suffix string, salesUSD, stockTurnover, marketCount, customerCount, margin float64, periodDays ...float64) float64 {
if salesUSD <= 0 {
return 1
}
days := productPerformanceExcelScorePeriodDays(suffix, periodDays...)
revenueScore := productPerformanceExcelRatioScore(salesUSD, productPerformanceExcelProductRevenueTargetForDays(suffix, days))
score := 0.30*productPerformanceExcelMarginScore(margin) +
0.20*productPerformanceExcelRatioScore(stockTurnover, 4) +
0.20*revenueScore +
0.05*productPerformanceExcelRatioScore(marketCount, productPerformanceExcelMarketSpreadTarget(suffix)) +
0.25*productPerformanceExcelRatioScore(customerCount, productPerformanceExcelCustomerSpreadTargetForDays(suffix, days))
return productPerformanceExcelRoundScore(score)
}
func productPerformanceExcelMarketSpreadTarget(suffix string) float64 {
switch suffix {
case "90d":
return 3
case "180d":
return 3
case "365d":
return 6
default:
return 6
}
}
func productPerformanceExcelCustomerSpreadTarget(suffix string) float64 {
return productPerformanceExcelCustomerSpreadTargetForDays(suffix, productPerformanceExcelScorePeriodDays(suffix))
}
func productPerformanceExcelCustomerSpreadTargetForDays(suffix string, periodDays float64) float64 {
switch suffix {
case "90d":
return 8
case "180d":
return 20
case "365d":
return 50
default:
return 20 * productPerformanceExcelTotalPeriodMultiplier(periodDays)
}
}
func productPerformanceExcelProductRevenueTarget(suffix string) float64 {
return productPerformanceExcelProductRevenueTargetForDays(suffix, productPerformanceExcelScorePeriodDays(suffix))
}
func productPerformanceExcelProductRevenueTargetForDays(suffix string, periodDays float64) float64 {
switch suffix {
case "180d":
return 50000
case "365d":
return 100000
case "total":
return 50000 * productPerformanceExcelTotalPeriodMultiplier(periodDays)
default:
return 25000
}
}
func productPerformanceExcelScorePeriodDays(suffix string, periodDays ...float64) float64 {
if len(periodDays) > 0 && periodDays[0] > 0 {
return periodDays[0]
}
switch suffix {
case "90d":
return 90
case "180d":
return 180
case "365d":
return 360
case "total":
start := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
now := time.Now()
if now.Before(start) {
return 0
}
return now.Sub(start).Hours()/24 + 1
default:
return 0
}
}
func productPerformanceExcelTotalPeriodMultiplier(periodDays float64) float64 {
if periodDays <= 0 {
periodDays = 180
}
return math.Max(1, periodDays/180.0)
}
func productPerformanceExcelMarginScore(margin float64) float64 {
if margin < 0 {
margin = 0
}
return productPerformanceExcelRatioScore(margin, 0.45)
}
func productPerformanceExcelRatioScore(value, target float64) float64 {
if target <= 0 || value <= 0 {
return 0
}
score := value * 100 / target
if score > 100 {
return 100
}
if score < 0 {
return 0
}
return score
}
func productPerformanceExcelRoundScore(score float64) float64 {
if score < 0 {
score = 0
}
if score > 100 {
score = 100
}
return math.Round(score*10000) / 10000
}
func productPerformanceExcelFloat(value any) float64 {
switch v := value.(type) {
case float64:
if math.IsNaN(v) || math.IsInf(v, 0) {
return 0
}
return v
case float32:
return productPerformanceExcelFloat(float64(v))
case int:
return float64(v)
case int64:
return float64(v)
case int32:
return float64(v)
case uint:
return float64(v)
case uint64:
return float64(v)
case uint32:
return float64(v)
default:
return 0
}
}
func productPerformanceExcelSortedSetLabel(values map[string]bool) string {
out := make([]string, 0, len(values))
for value := range values {
if strings.TrimSpace(value) != "" {
out = append(out, value)
}
}
sort.Strings(out)
return strings.Join(out, ", ")
}
func productPerformanceExcelColumnWidth(header, kind string) float64 {
if kind == "text" {
if strings.Contains(header, "Piyasalar") || strings.Contains(header, "Öneri") {
return 34
}
if len([]rune(header)) > 14 {
return 24
}
return 16
}
if strings.Contains(header, "Marj") || strings.Contains(header, "Skor") {
return 14
}
return 16
}
func productPerformanceExcelMaxInt(a, b int) int {
if a > b {
return a
}
return b
}
+47
View File
@@ -158,6 +158,26 @@ func GetProductionNoCostProductsHandler(w http.ResponseWriter, r *http.Request)
return return
} }
if argeDB := db.GetArgeBaggiDB(); argeDB != nil && len(list) > 0 {
workOrders := make([]string, 0, len(list))
for i := range list {
workOrders = append(workOrders, list[i].UrtSiparisNo)
}
argeCtx, cancelArge := context.WithTimeout(r.Context(), 15*time.Second)
totalsByWorkOrder, argeErr := queries.GetArgeBaggiDepotEntryTotals(argeCtx, argeDB, workOrders)
cancelArge()
if argeErr != nil {
log.Printf("[ProductionNoCost] ArgeBAGGI enrichment error: %v", argeErr)
} else {
for i := range list {
totals := totalsByWorkOrder[strings.TrimSpace(list[i].UrtSiparisNo)]
list[i].CeketDepoGiris = totals.Ceket
list[i].PantolonDepoGiris = totals.Pantolon
list[i].YelekDepoGiris = totals.Yelek
}
}
}
_ = json.NewEncoder(w).Encode(list) _ = json.NewEncoder(w).Encode(list)
} }
@@ -191,6 +211,7 @@ func GetProductionHasCostProductsHandler(w http.ResponseWriter, r *http.Request)
var item models.ProductionHasCostProductRow var item models.ProductionHasCostProductRow
if err := rows.Scan( if err := rows.Scan(
&item.UretimSekli, &item.UretimSekli,
&item.UrtSiparisNo,
&item.NOnMLNo, &item.NOnMLNo,
&item.UrunKodu, &item.UrunKodu,
&item.UrunAdi, &item.UrunAdi,
@@ -219,6 +240,26 @@ func GetProductionHasCostProductsHandler(w http.ResponseWriter, r *http.Request)
return return
} }
if argeDB := db.GetArgeBaggiDB(); argeDB != nil && len(list) > 0 {
workOrders := make([]string, 0, len(list))
for i := range list {
workOrders = append(workOrders, list[i].UrtSiparisNo)
}
argeCtx, cancelArge := context.WithTimeout(r.Context(), 15*time.Second)
totalsByWorkOrder, argeErr := queries.GetArgeBaggiDepotEntryTotals(argeCtx, argeDB, workOrders)
cancelArge()
if argeErr != nil {
log.Printf("[ProductionHasCost] ArgeBAGGI enrichment error: %v", argeErr)
} else {
for i := range list {
totals := totalsByWorkOrder[strings.TrimSpace(list[i].UrtSiparisNo)]
list[i].CeketDepoGiris = totals.Ceket
list[i].PantolonDepoGiris = totals.Pantolon
list[i].YelekDepoGiris = totals.Yelek
}
}
}
_ = json.NewEncoder(w).Encode(list) _ = json.NewEncoder(w).Encode(list)
} }
@@ -1427,6 +1468,12 @@ func postProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
totalUSD := 0.0 totalUSD := 0.0
totalEUR := 0.0 totalEUR := 0.0
for _, r := range req.Detail.Upserts { for _, r := range req.Detail.Upserts {
// The top header in the detail screen only includes rows marked
// "Maliyete Dahil". Persist that exact business total to the master
// record as well; excluded detail rows must not inflate the cost.
if r.MaliyeteDahil != 1 {
continue
}
qty := r.LMiktar qty := r.LMiktar
if qty < 0 { if qty < 0 {
qty = 0 qty = 0
+15 -1
View File
@@ -1,8 +1,20 @@
// quasar.config.js // quasar.config.js
import { defineConfig } from '#q-app/wrappers' import { defineConfig } from '#q-app/wrappers'
import { execSync } from 'node:child_process'
function readGitBuildInfo () {
try {
const commitCount = execSync('git rev-list --count HEAD', { encoding: 'utf8' }).trim()
const commitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim()
return { version: `v1.0.${commitCount}`, commit: commitHash }
} catch {
return { version: 'v1.0.0', commit: 'local' }
}
}
export default defineConfig(() => { export default defineConfig(() => {
const apiBaseUrl = (process.env.VITE_API_BASE_URL || '/api').trim() const apiBaseUrl = (process.env.VITE_API_BASE_URL || '/api').trim()
const gitBuildInfo = readGitBuildInfo()
return { return {
@@ -36,7 +48,9 @@ export default defineConfig(() => {
build: { build: {
vueRouterMode: 'hash', vueRouterMode: 'hash',
env: { env: {
VITE_API_BASE_URL: apiBaseUrl VITE_API_BASE_URL: apiBaseUrl,
APP_VERSION: gitBuildInfo.version,
APP_COMMIT: gitBuildInfo.commit
}, },
esbuildTarget: { esbuildTarget: {
+21
View File
@@ -74,6 +74,10 @@
</q-form> </q-form>
</q-card> </q-card>
<div class="login-version" :title="`Commit: ${appCommit}`">
Baggi BSS {{ appVersion }}
</div>
<!-- 🔐 FORGOT PASSWORD --> <!-- 🔐 FORGOT PASSWORD -->
<q-dialog v-model="forgotOpen" persistent> <q-dialog v-model="forgotOpen" persistent>
<q-card style="width:420px; max-width:90vw"> <q-card style="width:420px; max-width:90vw">
@@ -129,6 +133,8 @@ import api from 'src/services/api'
const router = useRouter() const router = useRouter()
const auth = useAuthStore() const auth = useAuthStore()
const $q = useQuasar() const $q = useQuasar()
const appVersion = process.env.APP_VERSION || 'v1.0.0'
const appCommit = process.env.APP_COMMIT || 'local'
const username = ref('') const username = ref('')
const password = ref('') const password = ref('')
@@ -266,6 +272,21 @@ async function sendResetMail () {
.full-width { .full-width {
width: 100%; width: 100%;
} }
.login-version {
position: absolute;
left: 16px;
bottom: 12px;
z-index: 2;
padding: 4px 8px;
border-radius: 6px;
color: rgba(45, 45, 45, 0.75);
background: rgba(255, 255, 255, 0.68);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.2px;
user-select: text;
}
/* =============================== /* ===============================
LOGIN INPUT TEXT COLOR (GOLD) LOGIN INPUT TEXT COLOR (GOLD)
=============================== */ =============================== */
+39 -32
View File
@@ -1222,49 +1222,58 @@ async function reloadData ({ page = 1 } = {}) {
} }
async function loadImagesForRows (list) { async function loadImagesForRows (list) {
const targets = [] const items = []
const rowByKey = new Map()
const seen = new Set() const seen = new Set()
for (const row of list) { for (const row of list) {
const key = `${row.productCode}|${row.dim1 || 0}|${row.dim3 || 0}` const key = `${row.productCode}|${row.dim1 || 0}|${row.dim3 || 0}`
if (!row.productCode || seen.has(key)) continue if (!row.productCode || seen.has(key)) continue
seen.add(key) seen.add(key)
targets.push({ row, key })
}
const concurrency = 12
let cursor = 0
let loaded = 0
const workers = Array.from({ length: Math.min(concurrency, targets.length) }, async () => {
for (;;) {
const target = targets[cursor]
cursor += 1
if (!target) return
const { row, key } = target
if (imageCache.has(key)) { if (imageCache.has(key)) {
row.imageUrl = imageCache.get(key) row.imageUrl = imageCache.get(key)
continue continue
} }
try { rowByKey.set(key, row)
const res = await api.get('/product-images', { items.push({
params: { key,
code: row.productCode, code: row.productCode,
dim1_id: row.dim1 || '', dim1: row.dim1 || '',
dim3_id: row.dim3 || '' dim3: row.dim3 || ''
},
timeout: 15000
}) })
const first = Array.isArray(res?.data) ? res.data[0] : null }
const url = resolveProductImageUrl(first) if (!items.length) {
rows.value = [...rows.value]
return
}
const chunks = []
for (let i = 0; i < items.length; i += 250) chunks.push(items.slice(i, i + 250))
for (const chunk of chunks) {
try {
const res = await api.post('/product-images/batch', { items: chunk }, { timeout: 30000 })
const returned = new Set()
for (const item of Array.isArray(res?.data) ? res.data : []) {
const key = toText(item?.key)
if (!key) continue
returned.add(key)
const list = Array.isArray(item.images) ? item.images : []
const url = resolveProductImageUrl(list[0])
imageCache.set(key, url) imageCache.set(key, url)
row.imageUrl = url imageListCache.set(key, list)
imageListCache.set(key, Array.isArray(res?.data) ? res.data : []) const row = rowByKey.get(key)
if (row) row.imageUrl = url
}
for (const item of chunk) {
if (!returned.has(item.key)) {
imageCache.set(item.key, '')
imageListCache.set(item.key, [])
}
}
} catch { } catch {
imageCache.set(key, '') for (const item of chunk) imageCache.set(item.key, '')
} }
loaded += 1 rows.value = [...rows.value]
if (loaded % 12 === 0) rows.value = [...rows.value]
} }
})
await Promise.all(workers)
rows.value = [...rows.value] rows.value = [...rows.value]
} }
@@ -1280,14 +1289,12 @@ function normalizeUploadsPath (storagePath) {
function resolveProductImageUrl (item) { function resolveProductImageUrl (item) {
if (!item || typeof item !== 'object') return '' if (!item || typeof item !== 'object') return ''
const imageId = Number(item.id || item.ID || 0) const contentUrl = toText(item.content_url || item.ContentURL)
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content` if (contentUrl) return contentUrl.startsWith('/api/') ? contentUrl : contentUrl
const thumbUrl = toText(item.thumb_url || item.thumbUrl) const thumbUrl = toText(item.thumb_url || item.thumbUrl)
if (thumbUrl) return thumbUrl if (thumbUrl) return thumbUrl
const fullUrl = toText(item.full_url || item.fullUrl) const fullUrl = toText(item.full_url || item.fullUrl)
if (fullUrl) return fullUrl if (fullUrl) return fullUrl
const contentUrl = toText(item.content_url || item.ContentURL)
if (contentUrl) return contentUrl.startsWith('/api/') ? contentUrl : contentUrl
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage) const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage)
if (uploadsPath) return uploadsPath if (uploadsPath) return uploadsPath
const fileName = toText(item.file_name || item.FileName) const fileName = toText(item.file_name || item.FileName)
File diff suppressed because it is too large Load Diff
+72 -32
View File
@@ -423,7 +423,6 @@ const productImageListByCode = ref({})
const productImageListLoading = ref({}) const productImageListLoading = ref({})
const productImageFallbackByKey = ref({}) const productImageFallbackByKey = ref({})
const productImageContentLoading = ref({}) const productImageContentLoading = ref({})
const productImageBlobUrls = ref([])
const productImageListBlockedUntil = ref(0) const productImageListBlockedUntil = ref(0)
const productCardDialog = ref(false) const productCardDialog = ref(false)
const productCardData = ref({}) const productCardData = ref({})
@@ -444,6 +443,8 @@ const IMAGE_LIST_CONCURRENCY = 8
const FILTER_OPTIONS_CACHE_TTL_MS = 60 * 1000 const FILTER_OPTIONS_CACHE_TTL_MS = 60 * 1000
const FILTER_OPTIONS_DEBOUNCE_MS = 250 const FILTER_OPTIONS_DEBOUNCE_MS = 250
let imageListActiveRequests = 0 let imageListActiveRequests = 0
let productImageBatchTimer = null
const productImageBatchQueue = new Map()
let filterOptionsDebounceTimer = null let filterOptionsDebounceTimer = null
let filterOptionsRequestSeq = 0 let filterOptionsRequestSeq = 0
const imageListWaitQueue = [] const imageListWaitQueue = []
@@ -635,14 +636,9 @@ function resolveProductImageUrl(item) {
} }
let contentUrl = '' let contentUrl = ''
const imageId = Number(item.id || item.ID || 0)
if (Number.isFinite(imageId) && imageId > 0) {
contentUrl = `/api/product-images/${imageId}/content`
} else {
const contentURL = String(item.content_url || item.ContentURL || '').trim() const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) contentUrl = contentURL if (contentURL.startsWith('/api/')) contentUrl = contentURL
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}` else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
}
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '') const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
let publicUrl = '' let publicUrl = ''
@@ -706,34 +702,82 @@ function clearGalleryQueryIndex() {
async function resolveProductImageUrlForCarousel(item) { async function resolveProductImageUrlForCarousel(item) {
const resolved = resolveProductImageUrl(item) const resolved = resolveProductImageUrl(item)
const contentUrl = String(resolved.contentUrl || '').trim() const contentUrl = String(resolved.contentUrl || '').trim()
if (contentUrl) { if (contentUrl) return contentUrl
try {
const blobRes = await api.get(contentUrl, { baseURL: '', responseType: 'blob' }) const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
const blob = blobRes?.data if (directUrl) return directUrl
if (blob instanceof Blob) {
const objectUrl = URL.createObjectURL(blob) return ''
productImageBlobUrls.value.push(objectUrl)
return objectUrl
}
} catch {
// fall through to public url
}
}
const fullUrl = String(resolved.fullUrl || '').trim()
if (fullUrl) return fullUrl
const publicUrl = String(resolved.publicUrl || '').trim()
return String(publicUrl || fullUrl || contentUrl || '').trim()
} }
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') { function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id) const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const existing = productImageCache.value[key] const existing = productImageCache.value[key]
if (existing !== undefined) return existing || '' if (existing !== undefined) return existing || ''
queueProductImageBatch(code, color, secondColor, dim1Id, dim3Id)
void ensureProductImage(code, color, secondColor, dim1Id, dim3Id)
return '' return ''
} }
function queueProductImageBatch(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const codeTrim = String(code || '').trim().toUpperCase()
if (!codeTrim || productImageCache.value[key] !== undefined || productImageLoading.value[key]) return
productImageLoading.value[key] = true
productImageBatchQueue.set(key, {
key,
code: codeTrim,
dim1: String(dim1Id || color || '').trim().toUpperCase(),
dim3: String(dim3Id || secondColor || '').trim().toUpperCase(),
listKey: buildImageKey(
codeTrim,
String(color || '').trim().toUpperCase(),
String(secondColor || '').trim().toUpperCase(),
String(dim1Id || '').trim().toUpperCase(),
String(dim3Id || '').trim().toUpperCase()
)
})
if (productImageBatchTimer) window.clearTimeout(productImageBatchTimer)
productImageBatchTimer = window.setTimeout(flushProductImageBatch, 80)
}
async function flushProductImageBatch() {
const batch = Array.from(productImageBatchQueue.values()).slice(0, 250)
for (const item of batch) productImageBatchQueue.delete(item.key)
if (!batch.length) return
try {
const res = await api.post('/product-images/batch', {
items: batch.map(item => ({ key: item.key, code: item.code, dim1: item.dim1, dim3: item.dim3 }))
}, { timeout: 30000 })
const returned = new Set()
for (const item of Array.isArray(res?.data) ? res.data : []) {
const key = String(item?.key || '').trim()
if (!key) continue
returned.add(key)
const queued = batch.find(entry => entry.key === key)
const list = Array.isArray(item.images) ? item.images : []
const first = list[0] || null
const resolved = resolveProductImageUrl(first)
const url = resolved.contentUrl || resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
productImageCache.value[key] = String(url || '').trim()
productImageFallbackByKey.value[key] = resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
if (queued) productImageListByCode.value[queued.listKey] = list
delete productImageLoading.value[key]
}
for (const item of batch) {
if (!returned.has(item.key)) {
productImageCache.value[item.key] = ''
productImageListByCode.value[item.listKey] = []
delete productImageLoading.value[item.key]
}
}
} catch {
for (const item of batch) {
delete productImageLoading.value[item.key]
void ensureProductImage(item.code, item.dim1, item.dim3)
}
}
}
async function onProductImageError(code, color, secondColor = '', dim1Id = '', dim3Id = '') { async function onProductImageError(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id) const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
productImageCache.value[key] = String(productImageFallbackByKey.value[key] || '').trim() productImageCache.value[key] = String(productImageFallbackByKey.value[key] || '').trim()
@@ -804,11 +848,11 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di
productImageCache.value[key] = String( productImageCache.value[key] = String(
preferredCardUrl || preferredCardUrl ||
primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || primaryResolved.contentUrl || primaryResolved.contentUrl || primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl ||
secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || secondaryResolved.contentUrl || secondaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl ||
'' ''
).trim() ).trim()
productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.contentUrl || '' productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || ''
} catch (err) { } catch (err) {
console.warn('[ProductStockByAttributes] product image fetch failed', { code, color, err }) console.warn('[ProductStockByAttributes] product image fetch failed', { code, color, err })
productImageCache.value[key] = '' productImageCache.value[key] = ''
@@ -1520,10 +1564,6 @@ onUnmounted(() => {
clearTimeout(filterOptionsDebounceTimer) clearTimeout(filterOptionsDebounceTimer)
filterOptionsDebounceTimer = null filterOptionsDebounceTimer = null
} }
for (const url of productImageBlobUrls.value) {
try { URL.revokeObjectURL(url) } catch {}
}
productImageBlobUrls.value = []
}) })
</script> </script>
+72 -31
View File
@@ -390,7 +390,6 @@ const productImageListByCode = ref({})
const productImageListLoading = ref({}) const productImageListLoading = ref({})
const productImageFallbackByKey = ref({}) const productImageFallbackByKey = ref({})
const productImageContentLoading = ref({}) const productImageContentLoading = ref({})
const productImageBlobUrls = ref([])
const productImageListBlockedUntil = ref(0) const productImageListBlockedUntil = ref(0)
const productCardDialog = ref(false) const productCardDialog = ref(false)
const productCardData = ref({}) const productCardData = ref({})
@@ -409,6 +408,8 @@ const productImageFullscreenDragOriginX = ref(0)
const productImageFullscreenDragOriginY = ref(0) const productImageFullscreenDragOriginY = ref(0)
const IMAGE_LIST_CONCURRENCY = 8 const IMAGE_LIST_CONCURRENCY = 8
let imageListActiveRequests = 0 let imageListActiveRequests = 0
let productImageBatchTimer = null
const productImageBatchQueue = new Map()
const imageListWaitQueue = [] const imageListWaitQueue = []
const activeSchema = ref(storeSchemaByKey.tak) const activeSchema = ref(storeSchemaByKey.tak)
const activeGrpKey = ref('tak') const activeGrpKey = ref('tak')
@@ -616,14 +617,9 @@ function resolveProductImageUrl(item) {
} }
let contentUrl = '' let contentUrl = ''
const imageId = Number(item.id || item.ID || 0)
if (Number.isFinite(imageId) && imageId > 0) {
contentUrl = `/api/product-images/${imageId}/content`
} else {
const contentURL = String(item.content_url || item.ContentURL || '').trim() const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) contentUrl = contentURL if (contentURL.startsWith('/api/')) contentUrl = contentURL
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}` else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
}
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '') const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
let publicUrl = '' let publicUrl = ''
@@ -685,33 +681,82 @@ function clearGalleryQueryIndex() {
async function resolveProductImageUrlForCarousel(item) { async function resolveProductImageUrlForCarousel(item) {
const resolved = resolveProductImageUrl(item) const resolved = resolveProductImageUrl(item)
const contentUrl = String(resolved.contentUrl || '').trim() const contentUrl = String(resolved.contentUrl || '').trim()
if (contentUrl) { if (contentUrl) return contentUrl
try {
const blobRes = await api.get(contentUrl, { baseURL: '', responseType: 'blob' }) const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
const blob = blobRes?.data if (directUrl) return directUrl
if (blob instanceof Blob) {
const objectUrl = URL.createObjectURL(blob) return ''
productImageBlobUrls.value.push(objectUrl)
return objectUrl
}
} catch {
// fall through to public url
}
}
const fullUrl = String(resolved.fullUrl || '').trim()
if (fullUrl) return fullUrl
const publicUrl = String(resolved.publicUrl || '').trim()
return String(publicUrl || fullUrl || contentUrl || '').trim()
} }
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') { function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id) const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const existing = productImageCache.value[key] const existing = productImageCache.value[key]
if (existing !== undefined) return existing || '' if (existing !== undefined) return existing || ''
void ensureProductImage(code, color, secondColor, dim1Id, dim3Id) queueProductImageBatch(code, color, secondColor, dim1Id, dim3Id)
return '' return ''
} }
function queueProductImageBatch(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const codeTrim = String(code || '').trim().toUpperCase()
if (!codeTrim || productImageCache.value[key] !== undefined || productImageLoading.value[key]) return
productImageLoading.value[key] = true
productImageBatchQueue.set(key, {
key,
code: codeTrim,
dim1: String(dim1Id || color || '').trim().toUpperCase(),
dim3: String(dim3Id || secondColor || '').trim().toUpperCase(),
listKey: buildImageKey(
codeTrim,
String(color || '').trim().toUpperCase(),
String(secondColor || '').trim().toUpperCase(),
String(dim1Id || '').trim().toUpperCase(),
String(dim3Id || '').trim().toUpperCase()
)
})
if (productImageBatchTimer) window.clearTimeout(productImageBatchTimer)
productImageBatchTimer = window.setTimeout(flushProductImageBatch, 80)
}
async function flushProductImageBatch() {
const batch = Array.from(productImageBatchQueue.values()).slice(0, 250)
for (const item of batch) productImageBatchQueue.delete(item.key)
if (!batch.length) return
try {
const res = await api.post('/product-images/batch', {
items: batch.map(item => ({ key: item.key, code: item.code, dim1: item.dim1, dim3: item.dim3 }))
}, { timeout: 30000 })
const returned = new Set()
for (const item of Array.isArray(res?.data) ? res.data : []) {
const key = String(item?.key || '').trim()
if (!key) continue
returned.add(key)
const queued = batch.find(entry => entry.key === key)
const list = Array.isArray(item.images) ? item.images : []
const first = list[0] || null
const resolved = resolveProductImageUrl(first)
const url = resolved.contentUrl || resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
productImageCache.value[key] = String(url || '').trim()
productImageFallbackByKey.value[key] = resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
if (queued) productImageListByCode.value[queued.listKey] = list
delete productImageLoading.value[key]
}
for (const item of batch) {
if (!returned.has(item.key)) {
productImageCache.value[item.key] = ''
productImageListByCode.value[item.listKey] = []
delete productImageLoading.value[item.key]
}
}
} catch {
for (const item of batch) {
delete productImageLoading.value[item.key]
void ensureProductImage(item.code, item.dim1, item.dim3)
}
}
}
async function onProductImageError(code, color, secondColor = '', dim1Id = '', dim3Id = '') { async function onProductImageError(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id) const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
productImageCache.value[key] = String(productImageFallbackByKey.value[key] || '').trim() productImageCache.value[key] = String(productImageFallbackByKey.value[key] || '').trim()
@@ -779,11 +824,11 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di
const secondaryResolved = resolveProductImageUrl(secondaryItem) const secondaryResolved = resolveProductImageUrl(secondaryItem)
productImageCache.value[key] = String( productImageCache.value[key] = String(
preferredCardUrl || preferredCardUrl ||
primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || primaryResolved.contentUrl || primaryResolved.contentUrl || primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl ||
secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || secondaryResolved.contentUrl || secondaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl ||
'' ''
).trim() ).trim()
productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.contentUrl || '' productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || ''
} catch (err) { } catch (err) {
console.warn('[ProductStockQuery] product image fetch failed', { code, color, err }) console.warn('[ProductStockQuery] product image fetch failed', { code, color, err })
productImageCache.value[key] = '' productImageCache.value[key] = ''
@@ -1317,10 +1362,6 @@ function resetForm() {
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('mousemove', onFullscreenMouseMove) window.removeEventListener('mousemove', onFullscreenMouseMove)
window.removeEventListener('mouseup', onFullscreenMouseUp) window.removeEventListener('mouseup', onFullscreenMouseUp)
for (const url of productImageBlobUrls.value) {
try { URL.revokeObjectURL(url) } catch {}
}
productImageBlobUrls.value = []
}) })
onMounted(async () => { onMounted(async () => {
@@ -40,6 +40,14 @@
:loading="loading" :loading="loading"
@click="fetchRows" @click="fetchRows"
/> />
<q-btn
label="Excel'e Aktar"
icon="download"
color="primary"
outline
:disable="loading || rows.length === 0"
@click="exportVisibleRows"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -141,7 +149,7 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
import { get, extractApiErrorDetail } from 'src/services/api' import { get, extractApiErrorDetail } from 'src/services/api'
@@ -168,6 +176,10 @@ const dateColumns = new Set(['Tarihi', 'dteKayitTarihi', 'dteGuncellemeTarihi',
const columns = [ const columns = [
{ name: 'open', label: '', field: 'open', align: 'center', sortable: false, style: 'width:3%', headerStyle: 'width:3%' }, { name: 'open', label: '', field: 'open', align: 'center', sortable: false, style: 'width:3%', headerStyle: 'width:3%' },
{ name: 'UretimSekli', label: 'Uretim Sekli', field: 'UretimSekli', align: 'left', sortable: true, style: 'width:9%', headerStyle: 'width:9%' }, { name: 'UretimSekli', label: 'Uretim Sekli', field: 'UretimSekli', align: 'left', sortable: true, style: 'width:9%', headerStyle: 'width:9%' },
{ name: 'nUrtSiparisNo', label: 'Uretim Siparis No', field: 'nUrtSiparisNo', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'ceketDepoGiris', label: 'C-Ceket Depo Giris', field: 'ceketDepoGiris', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'pantolonDepoGiris', label: 'P-Pantolon Depo Giris', field: 'pantolonDepoGiris', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'yelekDepoGiris', label: 'Y-Yelek Depo Giris', field: 'yelekDepoGiris', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'nOnMLNo', label: 'nOnMLNo', field: 'nOnMLNo', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' }, { name: 'nOnMLNo', label: 'nOnMLNo', field: 'nOnMLNo', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'UrunKodu', label: 'UrunKodu', field: 'UrunKodu', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' }, { name: 'UrunKodu', label: 'UrunKodu', field: 'UrunKodu', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
{ name: 'UrunAdi', label: 'UrunAdi', field: 'UrunAdi', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' }, { name: 'UrunAdi', label: 'UrunAdi', field: 'UrunAdi', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
@@ -186,6 +198,30 @@ const columns = [
] ]
const columnFilters = reactive({}) const columnFilters = reactive({})
const LIST_STATE_STORAGE_KEY = 'bssapp:production-product-costing:has-cost-list-state:v1'
function persistListState () {
try {
sessionStorage.setItem(LIST_STATE_STORAGE_KEY, JSON.stringify({
filters: { search: String(filters.search || '') },
columnFilters: JSON.parse(JSON.stringify(columnFilters)),
pagination: { ...tablePagination.value }
}))
} catch {}
}
function restoreListState () {
try {
const raw = sessionStorage.getItem(LIST_STATE_STORAGE_KEY)
if (!raw) return
const saved = JSON.parse(raw)
filters.search = String(saved?.filters?.search || '')
Object.assign(columnFilters, saved?.columnFilters && typeof saved.columnFilters === 'object' ? saved.columnFilters : {})
if (saved?.pagination && typeof saved.pagination === 'object') {
tablePagination.value = { ...tablePagination.value, ...saved.pagination }
}
} catch {}
}
function getColumnFilter (name) { function getColumnFilter (name) {
if (!columnFilters[name]) { if (!columnFilters[name]) {
@@ -293,9 +329,11 @@ function clearAllColumnFilters () {
} }
let searchTimer = null let searchTimer = null
let filterWatchEnabled = false
watch( watch(
() => filters.search, () => filters.search,
() => { () => {
if (!filterWatchEnabled) return
clearTimeout(searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => { searchTimer = setTimeout(() => {
fetchRows() fetchRows()
@@ -348,19 +386,66 @@ function clearFilters () {
fetchRows() fetchRows()
} }
function escapeExcelCsvCell (value) {
let text = String(value ?? '')
// Prevent spreadsheet applications from evaluating data as a formula.
if (/^[=+\-@]/.test(text)) text = `'${text}`
return `"${text.replace(/"/g, '""')}"`
}
const excelNumericColumns = new Set(['ceketDepoGiris', 'pantolonDepoGiris', 'yelekDepoGiris', 'lTutarTL', 'lTutarUSD', 'lTutarEURO'])
function formatExcelCsvCell (col, rawValue, displayValue) {
if (excelNumericColumns.has(col?.name)) {
const numericValue = Number(rawValue)
if (Number.isFinite(numericValue)) return String(numericValue).replace('.', ',')
}
return escapeExcelCsvCell(displayValue)
}
function exportVisibleRows () {
const visibleRows = Array.isArray(rows.value) ? rows.value : []
if (visibleRows.length === 0) return
const exportColumns = columns.filter(col => col.name !== 'open')
const csvLines = [
exportColumns.map(col => escapeExcelCsvCell(col.label)).join(';'),
...visibleRows.map(row => exportColumns.map(col => {
const rawValue = typeof col.field === 'function' ? col.field(row) : row?.[col.field]
const displayValue = typeof col.format === 'function' ? col.format(rawValue, row) : rawValue
return formatExcelCsvCell(col, rawValue, displayValue)
}).join(';'))
]
const blob = new Blob([`\uFEFF${csvLines.join('\r\n')}`], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `mevcut_maliyetli_urunler_${new Date().toISOString().slice(0, 10)}.csv`
document.body.appendChild(link)
link.click()
link.remove()
URL.revokeObjectURL(url)
}
function openRow (row) { function openRow (row) {
const urunKodu = String(row?.UrunKodu || '').trim() const urunKodu = String(row?.UrunKodu || '').trim()
if (!urunKodu) return if (!urunKodu) return
persistListState()
router.push({ router.push({
name: 'production-product-costing-has-cost-history', name: 'production-product-costing-has-cost-history',
query: { urun_kodu: urunKodu } query: { urun_kodu: urunKodu }
}) })
} }
onMounted(() => { onMounted(async () => {
if (!canReadOrder.value) return if (!canReadOrder.value) return
fetchRows() restoreListState()
await nextTick()
filterWatchEnabled = true
await fetchRows()
}) })
</script> </script>
@@ -457,7 +457,7 @@
<div v-else class="column q-gutter-md pcd-content-body"> <div v-else class="column q-gutter-md pcd-content-body">
<div <div
v-for="(grp, gi) in detailGroups" v-for="(grp, gi) in detailGroupsWithTotals"
:key="groupKey(grp, gi)" :key="groupKey(grp, gi)"
class="pcd-group-card" class="pcd-group-card"
> >
@@ -469,7 +469,11 @@
</span> </span>
</div> </div>
<div class="sub-right pcd-sub-right-clickable" @click="toggleGroup(grp, gi)"> <div class="sub-right pcd-sub-right-clickable" @click="toggleGroup(grp, gi)">
Grup Toplami TRY: {{ formatBarMoney(resolveGroupTRYTutar(grp)) }} | USD: {{ formatBarMoney(resolveGroupUSDTutar(grp)) }} <span class="pcd-group-total-text" data-no-runtime-i18n="true">
Grup Toplamı TRY: {{ formatBarMoney(grp.tryTotal) }}
<span class="pcd-group-total-divider">|</span>
USD: {{ formatBarMoney(grp.usdTotal) }}
</span>
<q-icon <q-icon
:name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'" :name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'"
size="18px" size="18px"
@@ -1557,8 +1561,14 @@ function ensureBeforeUnloadGuard (enabled) {
return '' return ''
} }
} }
function shouldIncludeRowInCostingTotal (row) {
// The explicit "Maliyete Dahil" selection is authoritative for the top
// header and the amount persisted to spUrtOnMLMas, including CM rows.
return normalizeBooleanFlag(row?.maliyeteDahil ?? row?.maliyete_dahil ?? row?.Maliyete_dahil)
}
const toolbarSummary = computed(() => flatDetailRows.value.reduce((acc, row) => { const toolbarSummary = computed(() => flatDetailRows.value.reduce((acc, row) => {
if (!row?.maliyeteDahil) return acc if (!shouldIncludeRowInCostingTotal(row)) return acc
acc.tryTotal += resolveRowTRYTutar(row) acc.tryTotal += resolveRowTRYTutar(row)
acc.usdTotal += resolveRowUSDTutar(row) acc.usdTotal += resolveRowUSDTutar(row)
acc.eurTotal += resolveRowEURTutar(row) acc.eurTotal += resolveRowEURTutar(row)
@@ -2694,7 +2704,9 @@ function normalizeDetailRows (items, groupName = '') {
// Use fiyat_girilen which is the unit price in original currency. // Use fiyat_girilen which is the unit price in original currency.
inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen), inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen),
inputPricePrBr: originalCurrency, inputPricePrBr: originalCurrency,
maliyeteDahil: x?.maliyeteDahil ?? normalizeBooleanFlag(x?.maliyete_dahil ?? x?.Maliyete_dahil), maliyeteDahil: normalizeBooleanFlag(x?.maliyeteDahil ?? x?.maliyete_dahil ?? x?.Maliyete_dahil),
__lastValidTRYAmount: parseMoneyInput(x?.lTutar ?? x?.lTutarTL),
__lastValidUSDAmount: parseMoneyInput(x?.usdTutar ?? x?.lTutarUSD),
cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3), cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3),
draftChanged: Boolean(x?.draftChanged), draftChanged: Boolean(x?.draftChanged),
priceUpdateState: String(x?.priceUpdateState || '').trim() priceUpdateState: String(x?.priceUpdateState || '').trim()
@@ -2796,6 +2808,13 @@ function recalculateDetailRow (row, options = {}) {
row.lTutarGBP = row.gbpTutar row.lTutarGBP = row.gbpTutar
row.lTutarTL = row.lTutar row.lTutarTL = row.lTutar
if (row.lTutar !== 0 || options.markChanged) {
row.__lastValidTRYAmount = row.lTutar
}
if (row.usdTutar !== 0 || options.markChanged) {
row.__lastValidUSDAmount = row.usdTutar
}
if (!options.preserveInputs) { if (!options.preserveInputs) {
row.miktarInput = normalizeQuantityInput(quantity) row.miktarInput = normalizeQuantityInput(quantity)
row.inputPrice = normalizeInputPrice(inputPrice) row.inputPrice = normalizeInputPrice(inputPrice)
@@ -2817,58 +2836,124 @@ function recalculateDetailRow (row, options = {}) {
} }
function recalculateAllDetailRows () { function recalculateAllDetailRows () {
detailGroups.value = detailGroups.value.map(grp => ({ // Preserve row identity. QTable editors and the computed group totals must
...grp, // observe the exact same reactive row objects. Replacing rows with
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => recalculateDetailRow({ ...row }, { preserveInputs: true })) // `{ ...row }` after the async exchange-rate request left QTable editing an
})) // older object while totals watched the replacement object.
detailGroups.value.forEach(grp => {
const items = Array.isArray(grp?.items) ? grp.items : []
items.forEach(row => {
recalculateDetailRow(row, { preserveInputs: true })
})
})
} }
function resolveRowTRYTutar (row) { function resolveRowTRYTutar (row) {
const tryAmount = Number(row?.lTutar || 0) const quantity = resolveNumericRowQuantity(row)
return Number.isFinite(tryAmount) ? tryAmount : 0 const inputPrice = resolveNumericRowInputPrice(row)
const inputCurrency = resolveInputCurrency(row)
const liveAmount = resolveTRYUnitPriceByInput(
inputPrice,
inputCurrency,
resolveExchangeRateValue('USD'),
resolveExchangeRateValue('EUR'),
resolveExchangeRateValue('GBP')
) * quantity
if (Number.isFinite(liveAmount) && inputPrice > 0) return liveAmount
return parseMoneyInput(row?.lTutar)
} }
function resolveRowEURTutar (row) { function resolveRowEURTutar (row) {
const eurAmount = Number(row?.eurTutar || 0) return parseMoneyInput(row?.eurTutar)
return Number.isFinite(eurAmount) ? eurAmount : 0
} }
function resolveRowGBPTutar (row) { function resolveRowGBPTutar (row) {
const gbpAmount = Number(row?.gbpTutar || 0) return parseMoneyInput(row?.gbpTutar)
return Number.isFinite(gbpAmount) ? gbpAmount : 0
} }
function resolveRowUSDTutar (row) { function resolveRowUSDTutar (row) {
const usdAmount = Number(row?.usdTutar || 0) const quantity = resolveNumericRowQuantity(row)
if (Number.isFinite(usdAmount)) return usdAmount const inputPrice = resolveNumericRowInputPrice(row)
const inputCurrency = resolveInputCurrency(row)
const liveAmount = resolveUSDUnitPriceByInput(
inputPrice,
inputCurrency,
resolveExchangeRateValue('USD'),
resolveExchangeRateValue('EUR'),
resolveExchangeRateValue('GBP')
) * quantity
const miktar = resolveNumericRowQuantity(row) if (Number.isFinite(liveAmount) && inputPrice > 0) return liveAmount
const dovizFiyati = Number(row?.lDovizFiyati || 0)
const calc = miktar * dovizFiyati const rawUSDAmount = row?.usdTutar
if (rawUSDAmount !== undefined && rawUSDAmount !== null && String(rawUSDAmount).trim() !== '') {
return parseMoneyInput(rawUSDAmount)
}
const dovizFiyati = parseMoneyInput(row?.lDovizFiyati)
const calc = quantity * dovizFiyati
return Number.isFinite(calc) ? calc : 0 return Number.isFinite(calc) ? calc : 0
} }
function shouldIgnoreGroupMaliyeteDahil (grp) {
return isCMGroupName(grp?.sAciklama3)
}
function shouldIncludeRowInGroupTotal (grp, row) { function shouldIncludeRowInGroupTotal (grp, row) {
return shouldIgnoreGroupMaliyeteDahil(grp) || normalizeBooleanFlag(row?.maliyeteDahil) if (!row) return false
// CM1/CM2 sub-headers always show their full group value. For every other
// group, the explicit "Maliyete Dahil" selection controls the subtotal.
if (isCMGroupName(grp?.sAciklama3 || row?.sAciklama3)) return true
const inclusionValue = row?.maliyeteDahil ?? row?.maliyete_dahil ?? row?.Maliyete_dahil
if (inclusionValue === false || inclusionValue === 0) return false
const normalizedValue = String(inclusionValue ?? '').trim().toLowerCase()
return normalizedValue !== '0' && normalizedValue !== 'false' && normalizedValue !== 'hayir'
} }
function resolveGroupTRYTutar (grp) { function resolveGroupRowTRYAmount (row) {
const items = Array.isArray(grp?.items) ? grp.items : [] const columnAmount = parseMoneyInput(row?.lTutar)
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowTRYTutar(row) : 0), 0) if (columnAmount !== 0) return columnAmount
const liveAmount = resolveRowTRYTutar(row)
if (liveAmount !== 0) return liveAmount
return parseMoneyInput(row?.__lastValidTRYAmount ?? row?.lTutarTL)
} }
function resolveGroupUSDTutar (grp) { function resolveGroupRowUSDAmount (row) {
const items = Array.isArray(grp?.items) ? grp.items : [] const columnAmount = parseMoneyInput(row?.usdTutar ?? row?.lTutarUSD)
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowUSDTutar(row) : 0), 0) if (columnAmount !== 0) return columnAmount
const liveAmount = resolveRowUSDTutar(row)
if (liveAmount !== 0) return liveAmount
return parseMoneyInput(row?.__lastValidUSDAmount)
} }
// OrderEntry groupedRows ile ayni desen: sub-header degerleri ayri bir
// state/cache icinde tutulmaz; iki gorunen tutar kolonu satirlardan reaktif
// olarak her degisiklikte yeniden toplanir.
const detailGroupsWithTotals = computed(() => (
detailGroups.value.map(group => {
const items = Array.isArray(group?.items) ? group.items : []
const totals = items.reduce((acc, row) => {
if (!shouldIncludeRowInGroupTotal(group, row)) return acc
acc.tryTotal += resolveGroupRowTRYAmount(row)
acc.usdTotal += resolveGroupRowUSDAmount(row)
return acc
}, { tryTotal: 0, usdTotal: 0 })
return {
...group,
tryTotal: totals.tryTotal,
usdTotal: totals.usdTotal
}
})
))
function resolveGroupQuantity (grp) { function resolveGroupQuantity (grp) {
const items = Array.isArray(grp?.items) ? grp.items : [] return (Array.isArray(grp?.items) ? grp.items : []).reduce((total, row) => (
return items.reduce((acc, row) => acc + (Number(row?.lMiktar || 0) || 0), 0) total + resolveNumericRowQuantity(row)
), 0)
} }
function groupKey (grp, gi) { function groupKey (grp, gi) {
@@ -3244,7 +3329,7 @@ function onRowQuantityInput (row, value) {
} }
function triggerUIUpdate () { function triggerUIUpdate () {
detailGroups.value = [...detailGroups.value] // detailGroupsWithTotals is computed from the reactive row fields.
} }
function normalizeRowQuantityDisplay (row) { function normalizeRowQuantityDisplay (row) {
@@ -3342,25 +3427,26 @@ async function fetchBulkItemPrices () {
} }
let appliedCount = 0 let appliedCount = 0
detailGroups.value = detailGroups.value.map(grp => ({ detailGroups.value.forEach(grp => {
...grp, const items = Array.isArray(grp?.items) ? grp.items : []
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => { items.forEach(row => {
const match = updates.find(update => rowMatchesBulkUpdate(row, update)) const match = updates.find(update => rowMatchesBulkUpdate(row, update))
if (!match) return row if (!match) return
appliedCount += 1 appliedCount += 1
return recalculateDetailRow({ row.inputPrice = match.inputPrice
...row, row.fiyat_girilen = match.fiyat_girilen
inputPrice: match.inputPrice, row.inputPricePrBr = match.inputPricePrBr
fiyat_girilen: match.fiyat_girilen, row.fiyat_doviz = match.fiyat_doviz
inputPricePrBr: match.inputPricePrBr, recalculateDetailRow(row, {
fiyat_doviz: match.fiyat_doviz
}, {
priceType: match.priceType || 'SAF', priceType: match.priceType || 'SAF',
updateState: 'bulk', updateState: 'bulk',
markChanged: true markChanged: true
}) })
}) })
})) })
// Update sub-headers immediately after the bulk response is applied. Do
// not wait for the optional previous-cost autofill request below.
triggerUIUpdate()
$q.notify({ $q.notify({
type: appliedCount > 0 ? 'positive' : 'warning', type: appliedCount > 0 ? 'positive' : 'warning',
@@ -3400,14 +3486,14 @@ async function fetchBulkItemPrices () {
}) })
let filled = 0 let filled = 0
detailGroups.value = detailGroups.value.map(grp => ({ detailGroups.value.forEach(grp => {
...grp, const items = Array.isArray(grp?.items) ? grp.items : []
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => { items.forEach(row => {
if (!row?.requiredPlaceholder) return row if (!row?.requiredPlaceholder) return
if (isCMGroupName(row?.sAciklama3) || normalizeGroupName(row?.sAciklama3) === 'FABRIC') return row if (isCMGroupName(row?.sAciklama3) || normalizeGroupName(row?.sAciklama3) === 'FABRIC') return
const no = parseInt(String(row?.nHammaddeTuruNo || '').trim() || '0', 10) || 0 const no = parseInt(String(row?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
const hit = byNo[no] const hit = byNo[no]
if (!hit) return row if (!hit) return
const next = { ...row } const next = { ...row }
const hasCode = String(next.sKodu || '').trim() !== '' const hasCode = String(next.sKodu || '').trim() !== ''
@@ -3427,20 +3513,20 @@ async function fetchBulkItemPrices () {
// Only mark if something changed // Only mark if something changed
const changed = (next.sKodu !== row.sKodu) || (next.sAciklama !== row.sAciklama) || (next.inputPrice !== row.inputPrice) || (next.inputPricePrBr !== row.inputPricePrBr) const changed = (next.sKodu !== row.sKodu) || (next.sAciklama !== row.sAciklama) || (next.inputPrice !== row.inputPrice) || (next.inputPricePrBr !== row.inputPricePrBr)
if (!changed) return row if (!changed) return
filled += 1 filled += 1
return recalculateDetailRow({ Object.assign(row, next, {
...next, __autoFilledFromPrev: true,
__autoFilledFromPrev: true
,
__autoFilledFromPrevSameFirma: Boolean(hit?.is_same_firma || hit?.isSameFirma) __autoFilledFromPrevSameFirma: Boolean(hit?.is_same_firma || hit?.isSameFirma)
}, { })
recalculateDetailRow(row, {
priceType: 'PREV', priceType: 'PREV',
updateState: 'autofill', updateState: 'autofill',
markChanged: true markChanged: true
}) })
}) })
})) })
triggerUIUpdate()
if (filled > 0) { if (filled > 0) {
$q.notify({ $q.notify({
@@ -3457,6 +3543,11 @@ async function fetchBulkItemPrices () {
error: String(e?.message || e || '') error: String(e?.message || e || '')
}) })
} }
// Bulk price and previous-cost autofill paths can update many rows at once.
// Recalculate once more as a whole so group sub-header totals react immediately.
recalculateAllDetailRows()
schedulePersistLocalDraft()
triggerUIUpdate()
} catch (err) { } catch (err) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
@@ -3928,10 +4019,14 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
const groupName = normalizeGroupName(meta?.groupName || '') const groupName = normalizeGroupName(meta?.groupName || '')
const hammaddeAdi = String(meta?.hammaddeAdi || '').trim() const hammaddeAdi = String(meta?.hammaddeAdi || '').trim()
const effectiveGroupName = groupName || 'TANIMSIZ' const effectiveGroupName = groupName || 'TANIMSIZ'
const mtBolumID = (meta?.mtBolumID > 0 ? meta.mtBolumID : mappingMtBolumID) || 0 // The product-type mapping is authoritative for the required part. A raw
const metaParcaCandidate = normalizeGroupName((meta?.parcaAdi || '').trim()) // material type can be required for multiple parts, while its master
// Defensive: if backend/lookup accidentally returns group label (DT/TP/...) as part name, ignore it. // record only carries one default MT section. Using that default here
const desiredParcaAdi = normalizeGroupName((metaParcaCandidate && !isKnownGroupName(metaParcaCandidate)) ? metaParcaCandidate : mappingParcaAdi) // incorrectly made an existing recipe row satisfy every mapped part.
const mtBolumID = mappingMtBolumID > 0 ? mappingMtBolumID : (meta?.mtBolumID || 0)
const desiredParcaAdi = mappingMtBolumID > 0
? mappingParcaAdi
: normalizeGroupName(meta?.parcaAdi || mappingParcaAdi)
const anyMatch = flatDetailRows.value.find(r => { const anyMatch = flatDetailRows.value.find(r => {
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
@@ -5062,13 +5157,31 @@ async function saveChanges () {
// If we created a new OnML (no-cost), switch to has-cost detail mode. // If we created a new OnML (no-cost), switch to has-cost detail mode.
if (isNoCostDetail.value && newOnMLNo > 0) { if (isNoCostDetail.value && newOnMLNo > 0) {
router.replace({ await router.replace({
name: 'production-product-costing-has-cost-detail', name: 'production-product-costing-has-cost-detail',
query: { query: {
n_onml_no: String(newOnMLNo), n_onml_no: String(newOnMLNo),
urun_kodu: String(header?.UrunKodu || productCode.value || '').trim() urun_kodu: String(header?.UrunKodu || productCode.value || '').trim()
} }
}) })
$q.dialog({
title: 'Maliyet Kaydedildi',
message: 'Maliyeti olmayan urunler listesine donup kaldiginiz yerden devam etmek ister misiniz?',
ok: {
label: 'Listeye Don',
color: 'primary',
icon: 'arrow_back'
},
cancel: {
label: 'Bu Sayfada Devam Et',
color: 'grey-7',
flat: true
},
persistent: true
}).onOk(() => {
router.push({ name: 'production-product-costing-no-cost' })
})
return return
} }
@@ -5078,6 +5191,24 @@ async function saveChanges () {
window.setTimeout(() => { window.setTimeout(() => {
try { refreshLast10Warnings() } catch {} try { refreshLast10Warnings() } catch {}
}, 1200) }, 1200)
$q.dialog({
title: 'Maliyet Guncellendi',
message: 'Mevcut maliyeti olan urunler listesine donup kaldiginiz yerden devam etmek ister misiniz?',
ok: {
label: 'Listeye Don',
color: 'primary',
icon: 'arrow_back'
},
cancel: {
label: 'Bu Sayfada Devam Et',
color: 'grey-7',
flat: true
},
persistent: true
}).onOk(() => {
router.push({ name: 'production-product-costing-has-cost' })
})
} catch (e) { } catch (e) {
// Surface backend message (http.Error text) when available. // Surface backend message (http.Error text) when available.
const msg = String( const msg = String(
@@ -5526,8 +5657,8 @@ watch(
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
padding: 8px 10px; padding: 8px 10px;
min-height: 42px; min-height: 46px;
height: 42px; height: auto;
border-top: 1px solid #d6c06a; border-top: 1px solid #d6c06a;
border-bottom: 1px solid #d6c06a; border-bottom: 1px solid #d6c06a;
background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%); background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%);
@@ -5549,7 +5680,9 @@ watch(
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
flex-wrap: nowrap; flex-wrap: wrap;
row-gap: 4px;
max-width: 70%;
} }
.pcd-sub-header .sub-left { .pcd-sub-header .sub-left {
display: flex; display: flex;
@@ -5566,8 +5699,31 @@ watch(
user-select: none; user-select: none;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 6px 10px;
flex-wrap: nowrap; flex-wrap: wrap;
}
.pcd-group-total-text {
color: #2f2408;
font-size: 13px;
font-weight: 900;
white-space: nowrap;
}
.pcd-group-total-divider {
margin: 0 4px;
color: #8d6d13;
}
.pcd-group-debug {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px 7px;
border: 1px solid rgba(180, 35, 24, 0.35);
border-radius: 5px;
color: #8f1d14;
background: rgba(255, 255, 255, 0.78);
font-size: 11px;
font-weight: 800;
text-transform: none;
white-space: nowrap; white-space: nowrap;
} }
.pcd-sub-mt-qty { .pcd-sub-mt-qty {
@@ -5577,6 +5733,18 @@ watch(
font-weight: 900; font-weight: 900;
} }
@media (max-width: 900px) {
.pcd-sub-header {
align-items: flex-start;
}
.pcd-sub-header .sub-right {
max-width: 78%;
}
.pcd-group-debug {
flex-basis: auto;
}
}
.pcd-detail-table :deep(.q-table__middle) { .pcd-detail-table :deep(.q-table__middle) {
overflow: visible !important; overflow: visible !important;
} }
@@ -5717,12 +5885,69 @@ watch(
} }
.pcd-history-table :deep(.q-table thead th) { .pcd-history-table :deep(.q-table thead th) {
font-size: 12px; font-size: 9.5px;
line-height: 1.1;
white-space: normal;
padding: 3px 2px;
} }
.pcd-history-table :deep(.q-table tbody td) { .pcd-history-table :deep(.q-table tbody td) {
font-size: 12px; font-size: 9.5px;
vertical-align: top; vertical-align: top;
padding: 2px;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
.pcd-history-table :deep(.q-table__middle) {
width: 100%;
max-width: 100%;
overflow-x: hidden;
}
.pcd-history-table :deep(.q-table) {
table-layout: fixed;
width: 100%;
min-width: 0;
}
.pcd-history-table :deep(.q-table th),
.pcd-history-table :deep(.q-table td) {
min-width: 0 !important;
}
.pcd-history-table :deep(.q-table th:nth-child(1)), .pcd-history-table :deep(.q-table td:nth-child(1)) { width: 5% !important; }
.pcd-history-table :deep(.q-table th:nth-child(2)), .pcd-history-table :deep(.q-table td:nth-child(2)) { width: 6% !important; }
.pcd-history-table :deep(.q-table th:nth-child(3)), .pcd-history-table :deep(.q-table td:nth-child(3)) { width: 7% !important; }
.pcd-history-table :deep(.q-table th:nth-child(4)), .pcd-history-table :deep(.q-table td:nth-child(4)) { width: 6% !important; }
.pcd-history-table :deep(.q-table th:nth-child(5)), .pcd-history-table :deep(.q-table td:nth-child(5)) { width: 8% !important; }
.pcd-history-table :deep(.q-table th:nth-child(6)), .pcd-history-table :deep(.q-table td:nth-child(6)) { width: 7% !important; }
.pcd-history-table :deep(.q-table th:nth-child(7)), .pcd-history-table :deep(.q-table td:nth-child(7)) { width: 10% !important; }
.pcd-history-table :deep(.q-table th:nth-child(8)), .pcd-history-table :deep(.q-table td:nth-child(8)) { width: 5% !important; }
.pcd-history-table :deep(.q-table th:nth-child(9)), .pcd-history-table :deep(.q-table td:nth-child(9)) { width: 7% !important; }
.pcd-history-table :deep(.q-table th:nth-child(10)), .pcd-history-table :deep(.q-table td:nth-child(10)) { width: 5% !important; }
.pcd-history-table :deep(.q-table th:nth-child(11)), .pcd-history-table :deep(.q-table td:nth-child(11)) { width: 7% !important; }
.pcd-history-table :deep(.q-table th:nth-child(12)), .pcd-history-table :deep(.q-table td:nth-child(12)) { width: 5% !important; }
.pcd-history-table :deep(.q-table th:nth-child(13)), .pcd-history-table :deep(.q-table td:nth-child(13)) { width: 3% !important; }
.pcd-history-table :deep(.q-table th:nth-child(14)), .pcd-history-table :deep(.q-table td:nth-child(14)) { width: 5% !important; }
.pcd-history-table :deep(.q-table th:nth-child(15)), .pcd-history-table :deep(.q-table td:nth-child(15)) { width: 6% !important; }
.pcd-history-table :deep(.q-table th:nth-child(16)), .pcd-history-table :deep(.q-table td:nth-child(16)) { width: 4% !important; }
.pcd-history-table :deep(.q-table th:nth-child(17)), .pcd-history-table :deep(.q-table td:nth-child(17)) { width: 4% !important; }
.pcd-history-table :deep(.pcd-history-source-chip) {
min-width: 0;
max-width: 100%;
padding: 2px 4px;
font-size: 9px;
}
.pcd-history-table :deep(.q-btn) {
min-width: 0;
padding-left: 5px;
padding-right: 5px;
font-size: 9px;
} }
.pcd-history-table :deep(.pcd-history-row-purchase td) { .pcd-history-table :deep(.pcd-history-row-purchase td) {
+102 -4
View File
@@ -49,6 +49,14 @@
:loading="loading" :loading="loading"
@click="fetchRows" @click="fetchRows"
/> />
<q-btn
label="Excel'e Aktar"
icon="download"
color="primary"
outline
:disable="loading || rows.length === 0"
@click="exportVisibleRows"
/>
<div class="npc-missing-count text-caption text-grey-7"> <div class="npc-missing-count text-caption text-grey-7">
Maliyeti Girilmemis Satir Sayisi: <b>{{ missingCostRowCount }}</b> Maliyeti Girilmemis Satir Sayisi: <b>{{ missingCostRowCount }}</b>
</div> </div>
@@ -176,7 +184,7 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
@@ -244,6 +252,9 @@ const columns = [
style: 'width:7%', style: 'width:7%',
headerStyle: 'width:7%' headerStyle: 'width:7%'
}, },
{ name: 'ceketDepoGiris', label: 'C-Ceket Depo Giris', field: 'ceketDepoGiris', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width:7%', headerStyle: 'width:7%' },
{ name: 'pantolonDepoGiris', label: 'P-Pantolon Depo Giris', field: 'pantolonDepoGiris', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width:7%', headerStyle: 'width:7%' },
{ name: 'yelekDepoGiris', label: 'Y-Yelek Depo Giris', field: 'yelekDepoGiris', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width:7%', headerStyle: 'width:7%' },
{ {
name: 'sMModelKodu', name: 'sMModelKodu',
label: 'Model Kodu', label: 'Model Kodu',
@@ -262,6 +273,34 @@ const columns = [
] ]
const columnFilters = reactive({}) const columnFilters = reactive({})
const LIST_STATE_STORAGE_KEY = 'bssapp:production-product-costing:no-cost-list-state:v1'
function persistListState () {
try {
sessionStorage.setItem(LIST_STATE_STORAGE_KEY, JSON.stringify({
filters: {
search: String(filters.search || ''),
fromDate: String(filters.fromDate || '')
},
columnFilters: JSON.parse(JSON.stringify(columnFilters)),
scrollY: Number(window.scrollY || 0)
}))
} catch {}
}
function restoreListState () {
try {
const raw = sessionStorage.getItem(LIST_STATE_STORAGE_KEY)
if (!raw) return 0
const saved = JSON.parse(raw)
filters.search = String(saved?.filters?.search || '')
filters.fromDate = String(saved?.filters?.fromDate || '2025-06-01')
Object.assign(columnFilters, saved?.columnFilters && typeof saved.columnFilters === 'object' ? saved.columnFilters : {})
return Number(saved?.scrollY || 0)
} catch {
return 0
}
}
function getColumnFilter (name) { function getColumnFilter (name) {
if (!columnFilters[name]) { if (!columnFilters[name]) {
@@ -276,7 +315,11 @@ function getColumnFilter (name) {
} }
function isNumericColumn (name) { function isNumericColumn (name) {
return name === 'lMMiktar_G' return ['lMMiktar_G', 'ceketDepoGiris', 'pantolonDepoGiris', 'yelekDepoGiris'].includes(name)
}
function formatQuantity (value) {
return Number(value || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
} }
function parseNumberFilter (value) { function parseNumberFilter (value) {
@@ -384,9 +427,11 @@ function clearAllColumnFilters () {
} }
let searchTimer = null let searchTimer = null
let filterWatchEnabled = false
watch( watch(
() => filters.search, () => filters.search,
() => { () => {
if (!filterWatchEnabled) return
clearTimeout(searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => { searchTimer = setTimeout(() => {
fetchRows() fetchRows()
@@ -397,6 +442,7 @@ watch(
watch( watch(
() => filters.fromDate, () => filters.fromDate,
() => { () => {
if (!filterWatchEnabled) return
fetchRows() fetchRows()
} }
) )
@@ -430,6 +476,49 @@ function clearFilters () {
fetchRows() fetchRows()
} }
function escapeExcelCsvCell (value) {
let text = String(value ?? '')
// Prevent spreadsheet applications from evaluating data as a formula.
if (/^[=+\-@]/.test(text)) text = `'${text}`
return `"${text.replace(/"/g, '""')}"`
}
function formatExcelCsvCell (col, rawValue, displayValue) {
if (isNumericColumn(col?.name)) {
const numericValue = Number(rawValue)
if (Number.isFinite(numericValue)) return String(numericValue).replace('.', ',')
}
return escapeExcelCsvCell(displayValue)
}
function exportVisibleRows () {
const visibleRows = Array.isArray(rows.value) ? rows.value : []
if (visibleRows.length === 0) {
$q.notify({ type: 'warning', message: 'Excel aktarimi icin ekranda satir bulunamadi.', position: 'top-right' })
return
}
const exportColumns = columns.filter(col => col.name !== 'open')
const csvLines = [
exportColumns.map(col => escapeExcelCsvCell(col.label)).join(';'),
...visibleRows.map(row => exportColumns.map(col => {
const rawValue = typeof col.field === 'function' ? col.field(row) : row?.[col.field]
const displayValue = typeof col.format === 'function' ? col.format(rawValue, row) : rawValue
return formatExcelCsvCell(col, rawValue, displayValue)
}).join(';'))
]
const blob = new Blob([`\uFEFF${csvLines.join('\r\n')}`], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `maliyeti_olmayan_urunler_${new Date().toISOString().slice(0, 10)}.csv`
document.body.appendChild(link)
link.click()
link.remove()
URL.revokeObjectURL(url)
}
function openRow (row) { function openRow (row) {
const productCode = String(row?.sMModelKodu || '').trim() const productCode = String(row?.sMModelKodu || '').trim()
const recipeCode = String(row?.sKodu || '').trim() const recipeCode = String(row?.sKodu || '').trim()
@@ -449,6 +538,8 @@ function openRow (row) {
recipe_code: recipeCode recipe_code: recipeCode
}) })
persistListState()
router.push({ router.push({
name: 'production-product-costing-has-cost-detail', name: 'production-product-costing-has-cost-detail',
query: { query: {
@@ -460,9 +551,16 @@ function openRow (row) {
}) })
} }
onMounted(() => { onMounted(async () => {
if (!canReadOrder.value) return if (!canReadOrder.value) return
fetchRows() const savedScrollY = restoreListState()
await nextTick()
filterWatchEnabled = true
await fetchRows()
if (savedScrollY > 0) {
await nextTick()
window.scrollTo({ top: savedScrollY, behavior: 'auto' })
}
}) })
</script> </script>
+264
View File
@@ -0,0 +1,264 @@
import { defineStore } from 'pinia'
import api from 'src/services/api'
const GROUPED_TTL_MS = 2 * 60 * 1000
const IMAGE_TTL_MS = 15 * 60 * 1000
function resolveProductImageUrl (item) {
return resolveProductImageUrls(item)[0] || ''
}
function resolveProductImageUrls (item) {
if (!item || typeof item !== 'object') return []
const urls = []
const addURL = value => {
const url = String(value || '').trim()
if (url && !urls.includes(url)) urls.push(url)
}
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
addURL(thumbURL)
const fullURL = String(item.full_url || item.fullUrl || '').trim()
addURL(fullURL)
const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) addURL(contentURL)
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
return urls
}
function normalizedFilterMap (filters = {}) {
return Object.fromEntries(Object.entries(filters || {})
.map(([key, values]) => [
String(key || '').trim(),
normalizedList(values, true)
])
.filter(([key, values]) => key && values.length)
.sort((a, b) => a[0].localeCompare(b[0], 'tr')))
}
function normalizedList (value, sort = false) {
if (!Array.isArray(value)) return []
const out = value.map(x => String(x || '').trim()).filter(Boolean)
return sort ? out.sort() : out
}
export const useProductPerformanceStore = defineStore('product-performance-store', {
state: () => ({
groupedRowsByKey: {},
groupedLoadedAtByKey: {},
groupedLoadingByKey: {},
groupedInFlightByKey: {},
groupedFilterOptionsByKey: {},
groupedFilterOptionsLoadedAtByKey: {},
groupedFilterOptionsInFlightByKey: {},
imageUrlByKey: {},
imageListByKey: {},
imageLoadedAtByKey: {}
}),
getters: {
groupedRows: state => key => state.groupedRowsByKey[key] || [],
hasImageKey: state => key => Object.prototype.hasOwnProperty.call(state.imageListByKey, key),
imageUrl: state => key => state.imageUrlByKey[key] || '',
imageList: state => key => state.imageListByKey[key] || []
},
actions: {
groupedCacheKey (params = {}) {
return JSON.stringify({
mode: String(params.mode || ''),
mainGroup: String(params.urunAnaGrubu || params.urun_ana_grubu || ''),
groupLevels: normalizedList(params.groupLevels),
expandedKeys: normalizedList(params.expandedKeys, true),
expandThroughLevel: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1),
filters: normalizedFilterMap(params.filters),
sortBy: String(params.sortBy || params.sort_by || ''),
descending: params.descending !== false,
limit: Number(params.limit || 0)
})
},
groupedFilterOptionsCacheKey (params = {}) {
return JSON.stringify({
mode: String(params.mode || ''),
mainGroup: String(params.urunAnaGrubu || params.urun_ana_grubu || ''),
groupLevels: normalizedList(params.groupLevels),
fields: normalizedList(params.fields, true),
limit: Number(params.limit || 0)
})
},
async fetchGroupedRows (params = {}, options = {}) {
const key = this.groupedCacheKey(params)
const now = Date.now()
const loadedAt = Number(this.groupedLoadedAtByKey[key] || 0)
const cached = this.groupedRowsByKey[key]
const logPrefix = '[ProductPerformance][grouped]'
if (!options.force && Array.isArray(cached) && now - loadedAt < GROUPED_TTL_MS) {
console.info(`${logPrefix} cache hit`, {
mode: params.mode,
rows: cached.length,
ageMs: now - loadedAt
})
return cached
}
if (this.groupedInFlightByKey[key]) {
console.info(`${logPrefix} in-flight reuse`, {
mode: params.mode,
groupLevels: normalizedList(params.groupLevels),
expandedKeys: normalizedList(params.expandedKeys, true).length,
expandThroughLevel: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1)
})
return this.groupedInFlightByKey[key]
}
this.groupedLoadingByKey = { ...this.groupedLoadingByKey, [key]: true }
const startedAt = performance.now()
console.info(`${logPrefix} request start`, {
mode: params.mode,
groupLevels: normalizedList(params.groupLevels),
expandedKeys: normalizedList(params.expandedKeys, true).length,
expandThroughLevel: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1),
limit: params.limit
})
const request = api.post('/pricing/product-performance/grouped', {
mode: params.mode,
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
expanded_keys: Array.isArray(params.expandedKeys) ? params.expandedKeys : String(params.expandedKeys || '').split(',').filter(Boolean),
expand_through_level: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1),
filters: normalizedFilterMap(params.filters),
sort_by: String(params.sortBy || params.sort_by || ''),
descending: params.descending !== false,
limit: params.limit
}, {
params: {
mode: params.mode,
limit: params.limit,
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
expand_through_level: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1),
sort_by: String(params.sortBy || params.sort_by || ''),
descending: params.descending !== false
},
timeout: 90000
}).then(resp => {
const rows = Array.isArray(resp?.data) ? resp.data : []
this.groupedRowsByKey = { ...this.groupedRowsByKey, [key]: rows }
this.groupedLoadedAtByKey = { ...this.groupedLoadedAtByKey, [key]: Date.now() }
console.info(`${logPrefix} request done`, {
mode: params.mode,
rows: rows.length,
elapsedSec: Number(((performance.now() - startedAt) / 1000).toFixed(2))
})
return rows
}).catch(err => {
const message = err?.response?.data || err?.message || String(err || '')
console.warn(`${logPrefix} request failed: ${message}`, {
mode: params.mode,
elapsedSec: Number(((performance.now() - startedAt) / 1000).toFixed(2)),
message
})
throw err
}).finally(() => {
const nextLoading = { ...this.groupedLoadingByKey }
const nextInFlight = { ...this.groupedInFlightByKey }
delete nextLoading[key]
delete nextInFlight[key]
this.groupedLoadingByKey = nextLoading
this.groupedInFlightByKey = nextInFlight
})
this.groupedInFlightByKey = { ...this.groupedInFlightByKey, [key]: request }
return request
},
async fetchGroupedFilterOptions (params = {}, options = {}) {
const key = this.groupedFilterOptionsCacheKey(params)
const now = Date.now()
const loadedAt = Number(this.groupedFilterOptionsLoadedAtByKey[key] || 0)
const cached = this.groupedFilterOptionsByKey[key]
if (!options.force && cached && now - loadedAt < GROUPED_TTL_MS) return cached
if (this.groupedFilterOptionsInFlightByKey[key]) return this.groupedFilterOptionsInFlightByKey[key]
const request = api.post('/pricing/product-performance/grouped/filter-options', {
mode: params.mode,
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
fields: Array.isArray(params.fields) ? params.fields : String(params.fields || '').split(',').filter(Boolean),
limit: params.limit || 50000
}, { timeout: 60000 }).then(resp => {
const data = resp?.data && typeof resp.data === 'object' ? resp.data : {}
this.groupedFilterOptionsByKey = { ...this.groupedFilterOptionsByKey, [key]: data }
this.groupedFilterOptionsLoadedAtByKey = { ...this.groupedFilterOptionsLoadedAtByKey, [key]: Date.now() }
return data
}).finally(() => {
const nextInFlight = { ...this.groupedFilterOptionsInFlightByKey }
delete nextInFlight[key]
this.groupedFilterOptionsInFlightByKey = nextInFlight
})
this.groupedFilterOptionsInFlightByKey = { ...this.groupedFilterOptionsInFlightByKey, [key]: request }
return request
},
setImageCache (key, urls = []) {
const normalizedKey = String(key || '').trim()
if (!normalizedKey) return
const list = Array.isArray(urls) ? urls.filter(Boolean) : []
this.imageListByKey = { ...this.imageListByKey, [normalizedKey]: list }
this.imageUrlByKey = { ...this.imageUrlByKey, [normalizedKey]: list[0] || '' }
this.imageLoadedAtByKey = { ...this.imageLoadedAtByKey, [normalizedKey]: Date.now() }
},
async fetchImageBatch (items = [], options = {}) {
const now = Date.now()
const pending = []
const seen = new Set()
for (const item of items) {
const key = String(item?.key || '').trim()
if (!key || seen.has(key)) continue
seen.add(key)
const loadedAt = Number(this.imageLoadedAtByKey[key] || 0)
if (!options.force && Object.prototype.hasOwnProperty.call(this.imageListByKey, key) && now - loadedAt < IMAGE_TTL_MS) continue
pending.push({
key,
code: String(item?.code || '').trim(),
dim1: String(item?.dim1 || '').trim(),
dim3: String(item?.dim3 || '').trim()
})
}
if (!pending.length) {
const keys = Array.from(seen.values())
return {
lists: Object.fromEntries(keys.map(key => [key, this.imageListByKey[key] || []])),
urls: Object.fromEntries(keys.map(key => [key, this.imageUrlByKey[key] || '']))
}
}
const resp = await api.post('/product-images/batch', { items: pending }, { timeout: 60000 })
const returned = new Set()
const lists = {}
const urlsByKey = {}
for (const item of Array.isArray(resp?.data) ? resp.data : []) {
const key = String(item?.key || '').trim()
if (!key) continue
returned.add(key)
const urls = (Array.isArray(item.images) ? item.images : []).flatMap(resolveProductImageUrls).filter(Boolean)
this.setImageCache(key, urls)
lists[key] = urls
urlsByKey[key] = urls[0] || ''
}
for (const item of pending) {
if (returned.has(item.key)) continue
this.setImageCache(item.key, [])
lists[item.key] = []
urlsByKey[item.key] = ''
}
return { lists, urls: urlsByKey }
}
}
})