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"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="GoModuleSettings" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
+2
View File
@@ -3,6 +3,8 @@ bssapp
# Env
.env
.env.local
.env.argebaggi.local
mail.env
# Runtime fonts
+2 -1
View File
@@ -66,10 +66,11 @@ func main() {
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.StockRows,
result.KpiRows,
result.SnapshotRows,
)
}
+39
View File
@@ -14,6 +14,7 @@ import (
var MssqlDB *sql.DB
var UretimDB *sql.DB
var ArgeBaggiDB *sql.DB
func envInt(name string, fallback int) int {
raw := strings.TrimSpace(os.Getenv(name))
@@ -158,3 +159,41 @@ func ConnectMSSQLUretim() error {
func GetUretimDB() *sql.DB {
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 {
r := mux.NewRouter()
mountUploads(r)
mountSPA(r)
/*
@@ -829,6 +830,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"order", "view",
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,
"/api/product-images/{id}/content", "GET",
"order", "view",
@@ -934,6 +940,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"pricing", "view",
wrapV3(routes.GetProductPerformanceHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/export-excel", "POST",
"pricing", "view",
wrapV3(routes.ExportProductPerformanceExcelHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/summary", "GET",
"pricing", "view",
@@ -979,6 +990,26 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"pricing", "view",
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,
"/api/pricing/product-performance/sales-details", "GET",
"pricing", "view",
@@ -1359,6 +1390,7 @@ func main() {
// (e.g. SSH-tunnel PostgreSQL on 127.0.0.1:15432).
if runtime.GOOS == "windows" {
_ = godotenv.Overload(".env.local")
_ = godotenv.Overload(".env.argebaggi.local")
}
jwtSecret := os.Getenv("JWT_SECRET")
@@ -1386,6 +1418,15 @@ func main() {
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()
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) {
r.NotFoundHandler = http.HandlerFunc(spaIndex)
r.HandleFunc("/", spaIndex).Methods(http.MethodGet)
+1
View File
@@ -66,6 +66,7 @@ var routeMetaCache sync.Map
var routeMetaFallback = map[string]routeMeta{
"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-stock-query-by-attributes": {module: "order", action: "view"},
}
+191 -51
View File
@@ -1,57 +1,85 @@
package models
type ProductPerformanceRow struct {
KpiDate string `json:"kpi_date"`
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
Seri string `json:"seri"`
YasGrubu string `json:"yas_grubu"`
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"`
StockQty float64 `json:"stock_qty"`
SalesQty30 float64 `json:"sales_qty_30d"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesQty180 float64 `json:"sales_qty_180d"`
SalesQty365 float64 `json:"sales_qty_365d"`
SalesQty730 float64 `json:"sales_qty_730d"`
SalesUSD30 float64 `json:"sales_usd_30d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
SalesUSD180 float64 `json:"sales_usd_180d"`
SalesUSD365 float64 `json:"sales_usd_365d"`
AvgDailySales90 float64 `json:"avg_daily_sales_90d"`
AvgDailySales180 float64 `json:"avg_daily_sales_180d"`
StockDays90 float64 `json:"stock_days_90d"`
StockDays180 float64 `json:"stock_days_180d"`
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
CostPriceUSD float64 `json:"cost_price_usd"`
BasePriceUSD float64 `json:"base_price_usd"`
BasePriceTRY float64 `json:"base_price_try"`
GrossProfitUSD90 float64 `json:"gross_profit_usd_90d"`
GrossProfitUSD180 float64 `json:"gross_profit_usd_180d"`
GrossMargin90 float64 `json:"gross_margin_90d"`
GrossMargin180 float64 `json:"gross_margin_180d"`
UnitProfitCost90 float64 `json:"unit_profit_cost_90d"`
UnitProfitCost180 float64 `json:"unit_profit_cost_180d"`
UnitProfitBase90 float64 `json:"unit_profit_base_90d"`
UnitProfitBase180 float64 `json:"unit_profit_base_180d"`
MarketCount90 int `json:"market_count_90d"`
CustomerCount90 int `json:"customer_count_90d"`
SalesIndex90 float64 `json:"sales_index_90d"`
PriceIndex90 float64 `json:"price_index_90d"`
MarginIndex90 float64 `json:"margin_index_90d"`
PerformanceScore float64 `json:"performance_score"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
LastSaleDate string `json:"last_sale_date"`
LastRefNumber string `json:"last_ref_number"`
UpdatedAt string `json:"updated_at"`
KpiDate string `json:"kpi_date"`
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"`
Seri string `json:"seri"`
YasGrubu string `json:"yas_grubu"`
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"`
StockQty float64 `json:"stock_qty"`
SalesQty30 float64 `json:"sales_qty_30d"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesQty180 float64 `json:"sales_qty_180d"`
SalesQty365 float64 `json:"sales_qty_365d"`
SalesQty730 float64 `json:"sales_qty_730d"`
SalesQtyTotal float64 `json:"sales_qty_total"`
SalesUSD30 float64 `json:"sales_usd_30d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
SalesUSD180 float64 `json:"sales_usd_180d"`
SalesUSD365 float64 `json:"sales_usd_365d"`
SalesUSDTotal float64 `json:"sales_usd_total"`
AvgDailySales90 float64 `json:"avg_daily_sales_90d"`
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"`
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"`
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
CostPriceUSD float64 `json:"cost_price_usd"`
BasePriceUSD float64 `json:"base_price_usd"`
BasePriceTRY float64 `json:"base_price_try"`
GrossProfitUSD90 float64 `json:"gross_profit_usd_90d"`
GrossProfitUSD180 float64 `json:"gross_profit_usd_180d"`
GrossMargin90 float64 `json:"gross_margin_90d"`
GrossMargin180 float64 `json:"gross_margin_180d"`
UnitProfitCost90 float64 `json:"unit_profit_cost_90d"`
UnitProfitCost180 float64 `json:"unit_profit_cost_180d"`
UnitProfitBase90 float64 `json:"unit_profit_base_90d"`
UnitProfitBase180 float64 `json:"unit_profit_base_180d"`
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"`
CustomerCount180 int `json:"customer_count_180d"`
CustomerCount365 int `json:"customer_count_365d"`
CustomerCountTotal int `json:"customer_count_total"`
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"`
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"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
LastSaleDate string `json:"last_sale_date"`
LastRefNumber string `json:"last_ref_number"`
UpdatedAt string `json:"updated_at"`
}
type ProductPerformanceSummary struct {
@@ -76,6 +104,7 @@ type ProductPerformanceGeneralRow struct {
PeriodEnd string `json:"period_end"`
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"`
@@ -113,6 +142,7 @@ type ProductPerformanceGeneralRow struct {
type ProductPerformanceOrderAnalysisRow struct {
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"`
@@ -177,8 +207,14 @@ type ProductPerformanceOrderGroupRow struct {
type ProductPerformanceOrderProductCustomerRow struct {
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"`
CustomerCode string `json:"customer_code"`
CustomerName string `json:"customer_name"`
@@ -209,8 +245,14 @@ type ProductPerformanceOrderMarketDetailRow struct {
DueDate string `json:"due_date"`
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"`
OrderQty float64 `json:"order_qty"`
OrderUSD float64 `json:"order_usd"`
AvgOrderPriceUSD float64 `json:"avg_order_price_usd"`
@@ -282,6 +324,104 @@ type ProductPerformanceCustomerRow struct {
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 {
SalesDate string `json:"sales_date"`
RefNumber string `json:"ref_number"`
+19 -12
View File
@@ -1,22 +1,29 @@
package models
type ProductionNoCostProductRow struct {
UretimSekli string `json:"UretimSekli"`
UrtSiparisNo string `json:"nUrtSiparisNo"`
IslemTarihi string `json:"dteIslemTarihi"`
FirmaKodu string `json:"FirmaKodu"`
FirmaAdi string `json:"FirmaAdi"`
SonIsEmriVeren string `json:"SonIsEmriVeren"`
ModelAdi string `json:"sAdi"`
Kodu string `json:"sKodu"`
SKullaniciAdi string `json:"sKullaniciAdi"`
SKullaniciGunc string `json:"sKullaniciAdiGunc"`
MMiktarG float64 `json:"lMMiktar_G"`
MModelKodu string `json:"sMModelKodu"`
UretimSekli string `json:"UretimSekli"`
UrtSiparisNo string `json:"nUrtSiparisNo"`
IslemTarihi string `json:"dteIslemTarihi"`
FirmaKodu string `json:"FirmaKodu"`
FirmaAdi string `json:"FirmaAdi"`
SonIsEmriVeren string `json:"SonIsEmriVeren"`
ModelAdi string `json:"sAdi"`
Kodu string `json:"sKodu"`
SKullaniciAdi string `json:"sKullaniciAdi"`
SKullaniciGunc string `json:"sKullaniciAdiGunc"`
MMiktarG float64 `json:"lMMiktar_G"`
CeketDepoGiris float64 `json:"ceketDepoGiris"`
PantolonDepoGiris float64 `json:"pantolonDepoGiris"`
YelekDepoGiris float64 `json:"yelekDepoGiris"`
MModelKodu string `json:"sMModelKodu"`
}
type ProductionHasCostProductRow struct {
UretimSekli string `json:"UretimSekli"`
UrtSiparisNo string `json:"nUrtSiparisNo"`
CeketDepoGiris float64 `json:"ceketDepoGiris"`
PantolonDepoGiris float64 `json:"pantolonDepoGiris"`
YelekDepoGiris float64 `json:"yelekDepoGiris"`
NOnMLNo string `json:"nOnMLNo"`
UrunKodu string `json:"UrunKodu"`
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")
deltaTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_DELTA_TIMEOUT_HOURS", 6, 1, 24)
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
runDelta := func(reason string) {
@@ -49,12 +55,40 @@ func startProductPerformanceScheduler(pgDB *sql.DB) {
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 duration_ms=%d",
reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.DurationMS)
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)
}
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",
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() {
if runOnStartup {
@@ -69,6 +103,21 @@ func startProductPerformanceScheduler(pgDB *sql.DB) {
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 {
@@ -80,6 +129,16 @@ func productPerformanceNextDaily(now time.Time, hhmm string) time.Time {
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) {
parts := strings.Split(strings.TrimSpace(raw), ":")
if len(parts) != 2 {
@@ -120,3 +179,36 @@ func productPerformanceEnvBool(name string, fallback bool) bool {
}
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
+273 -169
View File
@@ -8,9 +8,9 @@ func productPerformanceSalesSQL() string {
SalesDate = CAST(I.InvoiceDate AS date),
I.InvoiceHeaderID,
I.InvoiceNumber,
ProductCode = LTRIM(RTRIM(I.ItemCode)),
ColorCode = LTRIM(RTRIM(ISNULL(I.ColorCode, ''))),
YakaKodu = LTRIM(RTRIM(ISNULL(I.ItemDim2Code, ''))),
ProductCode = UPPER(LTRIM(RTRIM(I.ItemCode))),
ColorCode = UPPER(LTRIM(RTRIM(ISNULL(I.ColorCode, '')))),
YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(I.ItemDim2Code, '')))),
ItemDescription = dbo.HG_Temizlik(ISNULL((
SELECT ItemDescription
FROM cdItemDesc WITH(NOLOCK)
@@ -18,67 +18,11 @@ func productPerformanceSalesSQL() string {
AND cdItemDesc.ItemCode = I.ItemCode
AND cdItemDesc.LangCode = 'TR'
), SPACE(0))),
UrunAnaGrubu = dbo.HG_Temizlik(ISNULL((
SELECT AttributeDescription
FROM cdItemAttributeDesc WITH(NOLOCK)
WHERE cdItemAttributeDesc.ItemTypeCode = I.ItemTypeCode
AND cdItemAttributeDesc.AttributeTypeCode = 1
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))),
UrunAnaGrubu = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt01, SPACE(0))),
UrunAltGrubu = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt02, SPACE(0))),
Kategori = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt44, SPACE(0))),
UrunIlkGrubu = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt42, SPACE(0))),
AskiliYan = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt45, SPACE(0))),
ChannelCode = CASE
WHEN I.ProcessCode IN ('R') THEN dbo.HG_Temizlik(ISNULL((
SELECT OfficeDescription
@@ -132,16 +76,19 @@ func productPerformanceSalesSQL() string {
), SPACE(0)))
END,
Qty1 = ISNULL(I.Qty1, 0),
TLAmount = ISNULL(I.Doc_Amount, 0) * ISNULL(I.Loc_ExchangeRate, 0),
USDAmount = (ISNULL(I.Doc_Amount, 0) * ISNULL(I.Loc_ExchangeRate, 0)) / ISNULL((
SELECT TOP 1 Rate
FROM AllExchangeRates WITH(NOLOCK)
WHERE CurrencyCode = 'USD'
AND RelationCurrencyCode = 'TRY'
AND ExchangeTypeCode = 6
AND Rate > 1
ORDER BY ABS(DATEDIFF(DAY, AllExchangeRates.Date, I.InvoiceDate)) ASC
), 1),
TLAmount = CASE
WHEN Amount.DocCurrencyCode = 'TRY' THEN Amount.DocNetAmount
WHEN CurRate.Rate > 0 THEN Amount.DocNetAmount * CurRate.Rate
WHEN ISNULL(I.Loc_ExchangeRate, 0) > 0 THEN Amount.DocNetAmount * I.Loc_ExchangeRate
ELSE 0
END,
USDAmount = CASE
WHEN Amount.DocCurrencyCode = 'USD' THEN Amount.DocNetAmount
WHEN Amount.DocCurrencyCode = 'TRY' AND UsdRate.Rate > 0 THEN Amount.DocNetAmount / UsdRate.Rate
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 (
PARTITION BY I.InvoiceLineID
ORDER BY I.InvoiceDate DESC, I.InvoiceNumber
@@ -150,21 +97,46 @@ func productPerformanceSalesSQL() string {
LEFT OUTER JOIN CustomerAttributesFilter CAF WITH(NOLOCK)
ON CAF.CurrAccTypeCode = I.CurrAccTypeCode
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
AND I.InvoiceDate < DATEADD(DAY, 1, @p2)
AND I.ItemTypeCode = 1
AND (
LEN(LTRIM(RTRIM(I.ItemCode))) = 13
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 LEN(LTRIM(RTRIM(I.ItemCode))) = 13
AND (@p3 = '' OR UPPER(LTRIM(RTRIM(I.ItemCode))) LIKE @p3 + '%')
AND (
(I.CompanyCode = 1 AND I.ATAtt01 IN (1,2) AND I.ProcessCode IN ('WS','R'))
OR (I.CompanyCode = 4 AND I.ProcessCode = 'R')
@@ -185,13 +157,13 @@ SELECT
YakaKodu,
ItemDescription = MAX(ItemDescription),
Kategori = MAX(Kategori),
Seri = MAX(Kategori),
YasGrubu = MAX(UrunIlkGrubu),
AskiliYan = MAX(AskiliYan),
Seri = '',
YasGrubu = '',
AskiliYan = ISNULL(NULLIF(MAX(AskiliYan), '-'), ''),
UrunIlkGrubu = MAX(UrunIlkGrubu),
UrunAnaGrubu = MAX(UrunAnaGrubu),
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,
CustomerCountry = ISNULL(NULLIF(CustomerCountry, ''), '-'),
CustomerSegment = ISNULL(NULLIF(CustomerSegment, ''), '-'),
@@ -211,7 +183,7 @@ GROUP BY SalesDate, ProductCode, ColorCode, YakaKodu, ChannelCode, CustomerCount
`
}
func productPerformanceStockSQL() string {
func productPerformanceStockSQLUnused() string {
return `
;WITH ActiveWarehouses AS (
SELECT WarehouseCode
@@ -225,9 +197,9 @@ func productPerformanceStockSQL() string {
),
Raw AS (
SELECT
ProductCode = LTRIM(RTRIM(S.ItemCode)),
ColorCode = LTRIM(RTRIM(ISNULL(S.ColorCode, ''))),
YakaKodu = LTRIM(RTRIM(ISNULL(S.ItemDim2Code, ''))),
ProductCode = UPPER(LTRIM(RTRIM(S.ItemCode))),
ColorCode = UPPER(LTRIM(RTRIM(ISNULL(S.ColorCode, '')))),
YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(S.ItemDim2Code, '')))),
MovementDate = CAST(S.DocumentDate AS date),
ProcessCode = CASE
WHEN LTRIM(RTRIM(ISNULL(S.ProcessCode, ''))) <> '' THEN LTRIM(RTRIM(S.ProcessCode))
@@ -240,18 +212,8 @@ Raw AS (
INNER JOIN ActiveWarehouses W
ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode))
WHERE S.ItemTypeCode = 1
AND (
LEN(LTRIM(RTRIM(S.ItemCode))) = 13
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 LEN(LTRIM(RTRIM(S.ItemCode))) = 13
AND (@p3 = '' OR UPPER(LTRIM(RTRIM(S.ItemCode))) LIKE @p3 + '%')
AND S.DocumentDate < DATEADD(DAY, 1, @p2)
GROUP BY
S.ItemCode,
@@ -373,37 +335,23 @@ ORDER BY StockDate, ProductCode, ColorCode, YakaKodu
func productPerformancePriceSQL() string {
return `
;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
FROM trPriceListLine WITH(NOLOCK)
WHERE ItemTypeCode = 1
AND (
LEN(LTRIM(RTRIM(ItemCode))) = 13
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 + '%')
AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND (@p1 = '' OR UPPER(LTRIM(RTRIM(ItemCode))) LIKE @p1 + '%')
UNION
SELECT DISTINCT ItemCode
FROM prItemBasePrice WITH(NOLOCK)
WHERE ItemTypeCode = 1
AND (
LEN(LTRIM(RTRIM(ItemCode))) = 13
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 + '%')
AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND (@p1 = '' OR UPPER(LTRIM(RTRIM(ItemCode))) LIKE @p1 + '%')
),
LatestPrice AS (
SELECT
@@ -450,12 +398,26 @@ Cost AS (
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
)
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),
BasePriceUsd = ISNULL(bp.BasePriceUsd, 0),
BasePriceTry = ISNULL(bp.BasePriceTry, 0),
c.LastPricingDate
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 Cost c ON c.ItemCode = pc.ItemCode AND c.rn = 1
`
@@ -470,6 +432,34 @@ WITH LatestStock AS (
WHERE stock_date <= $1
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 (
SELECT
product_code,
@@ -480,34 +470,46 @@ SalesAgg AS (
MAX(kategori) AS kategori,
MAX(seri) AS seri,
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_ana_grubu) AS urun_ana_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 '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 '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 >= 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 '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 '364 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 >= $1::date - INTERVAL '359 days') AS sales_usd_365d,
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(last_ref_number) AS last_ref_number
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 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
),
Scope AS (
SELECT
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,
sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d,
sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d,
customer_count_90d, last_sale_date, last_ref_number
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_total,
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
UNION ALL
SELECT
@@ -520,7 +522,7 @@ Scope AS (
'' AS seri,
'' AS yas_grubu,
'' AS askili_yan,
'' AS urun_ilk_grubu,
COALESCE(pd.urun_ilk_grubu, '') AS urun_ilk_grubu,
'' AS urun_ana_grubu,
'' AS urun_alt_grubu,
0 AS sales_qty_30d,
@@ -528,15 +530,26 @@ Scope AS (
0 AS sales_qty_180d,
0 AS sales_qty_365d,
0 AS sales_qty_730d,
0 AS sales_qty_total,
0 AS sales_usd_30d,
0 AS sales_usd_90d,
0 AS sales_usd_180d,
0 AS sales_usd_365d,
0 AS sales_usd_total,
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,
'' AS last_ref_number
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
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 (
SELECT 1
FROM SalesAgg sa
@@ -545,6 +558,23 @@ Scope AS (
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 (
SELECT
s.*,
@@ -554,6 +584,12 @@ Base AS (
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_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_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,
@@ -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
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 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 (
SELECT
product_code,
color_code,
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
WHERE COALESCE(sales_qty_90d, 0) > 0
GROUP BY product_code, color_code, yaka_kodu
),
Market AS (
SELECT
market_key,
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(gross_margin_90d, 0)) AS market_avg_margin_90d
FROM Base
@@ -595,9 +677,21 @@ Scored AS (
SELECT
b.*,
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,
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_180d, 0) AS market_count_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_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_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
@@ -607,33 +701,43 @@ Scored AS (
INSERT INTO mk_product_performance_kpi_daily (
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,
sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d,
sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, avg_daily_sales_90d, avg_daily_sales_180d,
stock_days_90d, stock_days_180d, avg_price_usd_90d, avg_price_usd_180d, cost_price_usd, base_price_usd, base_price_try,
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_total, avg_daily_sales_90d, avg_daily_sales_180d, avg_daily_sales_365d, avg_daily_sales_total,
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,
unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d, market_count_90d, customer_count_90d,
sales_index_90d, price_index_90d, margin_index_90d, performance_score,
unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d,
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
)
SELECT
$1::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,
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_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(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(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(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(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(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(sales_index_90d, 0), COALESCE(price_index_90d, 0), COALESCE(margin_index_90d, 0),
ROUND(
LEAST(35, COALESCE(sales_index_90d, 0) * 18)
+ LEAST(25, GREATEST(COALESCE(gross_margin_90d, 0), 0) * 50)
+ LEAST(20, COALESCE(customer_count_90d, 0) * 2)
+ LEAST(10, COALESCE(market_count_90d, 0) * 3)
+ 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
, 4) AS performance_score,
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(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(
LEAST(30, GREATEST(COALESCE(gross_margin_90d, 0), 0) / 0.45 * 30)
+ LEAST(20, COALESCE(stock_turnover_90d, 0) / 4.0 * 20)
+ LEAST(20, COALESCE(sales_usd_90d, 0) / 25000.0 * 20)
+ LEAST(5, COALESCE(market_count_90d, 0) / 3.0 * 5)
+ LEAST(25, COALESCE(customer_count_90d, 0) / 8.0 * 25)
, 4)
END AS performance_score,
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_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'
@@ -641,12 +745,12 @@ SELECT
ELSE 'TAKIP'
END AS performance_bucket,
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(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_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(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) < 0.90 AND COALESCE(sales_index_90d,0) > 1.00 THEN 'Satış güçlü, fiyat piyasa altı: taban fiyat artışı değerlendirilebilir.'
ELSE 'Düzenli takip.'
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: uretim/satin alma planina alinmali.'
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 ustunde ve satis zayif: fiyat revizyonu degerlendirilmeli.'
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 'Duzenli takip.'
END AS recommendation,
last_sale_date,
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)
}
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(
ctx context.Context,
uretimDB *sql.DB,
@@ -816,12 +892,22 @@ WITH SonOnMaliyet AS (
FROM dbo.spUrtOnMLMas M
WHERE LTRIM(RTRIM(M.UrunKodu)) <> ''
),
SonSiparis AS (
SonSiparisSirali AS (
SELECT
LTRIM(RTRIM(sMModelKodu)) AS UrunKodu,
MAX(dteIslemTarihi) AS SonSiparisTarihi
FROM dbo.spUrtSiparisDet
GROUP BY LTRIM(RTRIM(sMModelKodu))
LTRIM(RTRIM(SD.sMModelKodu)) AS UrunKodu,
SD.dteIslemTarihi AS SonSiparisTarihi,
SM.nUrtSiparisNo,
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
CASE
@@ -831,6 +917,7 @@ SELECT
THEN N'FASON ICIN URETILEN'
ELSE N'Tanimsiz'
END AS UretimSekli,
RTRIM(CONVERT(VARCHAR(32), ISNULL(SS.nUrtSiparisNo, 0))) AS nUrtSiparisNo,
RTRIM(CONVERT(VARCHAR(32), ISNULL(OM.nOnMLNo, 0))) AS nOnMLNo,
LTRIM(RTRIM(ISNULL(OM.UrunKodu, ''))) AS UrunKodu,
ISNULL(OM.UrunAdi, '') AS UrunAdi,
+23 -23
View File
@@ -3,7 +3,7 @@ package queries
// GetProductStockQuery:
// Urun kodu bazli, STOCK/PICK/RESERVE/DISP ayrik CTE ile optimize sorgu.
const GetProductStockQuery = `
DECLARE @ProductCode NVARCHAR(50) = @p1;
DECLARE @ProductCode NVARCHAR(50) = UPPER(LTRIM(RTRIM(@p1)));
;WITH STOCK AS
(
@@ -14,7 +14,7 @@ DECLARE @ProductCode NVARCHAR(50) = @p1;
StoreCode,
WarehouseCode,
ItemTypeCode,
ItemCode,
UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode,
ColorCode,
ItemDim1Code,
ItemDim2Code,
@@ -22,12 +22,12 @@ DECLARE @ProductCode NVARCHAR(50) = @p1;
SUM(In_Qty1 - Out_Qty1) AS InventoryQty1
FROM trStock
WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13
AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13
GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code
),
@@ -35,17 +35,17 @@ PICK AS
(
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS PickingQty1
FROM PickingStates
WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13
AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13
GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code
),
@@ -53,17 +53,17 @@ RESERVE AS
(
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS ReserveQty1
FROM ReserveStates
WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13
AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13
GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code
),
@@ -71,17 +71,17 @@ DISP AS
(
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS DispOrderQty1
FROM DispOrderStates
WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13
AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13
GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code
)
@@ -185,14 +185,14 @@ AND DP.ItemDim2Code=S.ItemDim2Code
AND DP.ItemDim3Code=S.ItemDim3Code
JOIN cdItem
ON S.ItemCode = cdItem.ItemCode
ON UPPER(LTRIM(RTRIM(S.ItemCode))) = UPPER(LTRIM(RTRIM(cdItem.ItemCode)))
AND S.ItemTypeCode = cdItem.ItemTypeCode
LEFT JOIN cdUnitOfMeasure
ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode
LEFT JOIN ProductFilterWithDescription('TR') P
ON P.ProductCode = S.ItemCode
ON UPPER(LTRIM(RTRIM(P.ProductCode))) = UPPER(LTRIM(RTRIM(S.ItemCode)))
LEFT JOIN bsItemTypeDesc
ON bsItemTypeDesc.ItemTypeCode = S.ItemTypeCode
@@ -214,14 +214,14 @@ LEFT JOIN (
ROW_NUMBER() OVER (PARTITION BY ItemCode, ItemTypeCode ORDER BY PriceDate DESC) AS RowNum
FROM prItemBasePrice
) prFilteredBasePrice
ON prFilteredBasePrice.ItemCode = S.ItemCode
ON UPPER(LTRIM(RTRIM(prFilteredBasePrice.ItemCode))) = UPPER(LTRIM(RTRIM(S.ItemCode)))
AND prFilteredBasePrice.ItemTypeCode = S.ItemTypeCode
AND prFilteredBasePrice.RowNum = 1
WHERE
S.ItemTypeCode IN (1)
AND S.ItemCode = @ProductCode
AND LEN(S.ItemCode) = 13
AND UPPER(LTRIM(RTRIM(S.ItemCode))) = @ProductCode
AND LEN(LTRIM(RTRIM(S.ItemCode))) = 13
AND LEN(@ProductCode) = 13
AND (
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-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'
);
`
+48 -48
View File
@@ -28,7 +28,7 @@ IF OBJECT_ID('dbo.ProductFilterTRCache','U') IS NOT NULL
BEGIN
INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal)
SELECT
ProductCode,
UPPER(LTRIM(RTRIM(ProductCode))) AS ProductCode,
Kategori = LTRIM(RTRIM(ProductAtt44Desc)),
UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)),
UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)),
@@ -48,7 +48,7 @@ ELSE
BEGIN
INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal)
SELECT
ProductCode,
UPPER(LTRIM(RTRIM(ProductCode))) AS ProductCode,
Kategori = LTRIM(RTRIM(ProductAtt44Desc)),
UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)),
UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)),
@@ -101,40 +101,40 @@ CREATE NONCLUSTERED INDEX IX_AttrBase_Filter ON #AttrBase(Kategori, UrunAnaGrubu
FROM
(
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
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
AND LEN(P.ItemCode) = 13
AND LEN(LTRIM(RTRIM(P.ItemCode))) = 13
UNION ALL
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
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
AND LEN(R.ItemCode) = 13
AND LEN(LTRIM(RTRIM(R.ItemCode))) = 13
UNION ALL
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
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
AND LEN(D.ItemCode) = 13
AND LEN(LTRIM(RTRIM(D.ItemCode))) = 13
UNION ALL
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)
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
AND LEN(T.ItemCode) = 13
GROUP BY T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code
AND LEN(LTRIM(RTRIM(T.ItemCode))) = 13
GROUP BY UPPER(LTRIM(RTRIM(T.ItemCode))), T.ColorCode, T.ItemDim1Code, T.ItemDim2Code
) X
GROUP BY X.ItemCode, X.ColorCode, X.ItemDim1Code, X.ItemDim2Code
),
@@ -180,7 +180,7 @@ SELECT 'renk', X.FieldValue
FROM (
SELECT DISTINCT FieldValue = CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END
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) <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -196,7 +196,7 @@ SELECT 'renk2', X.FieldValue
FROM (
SELECT DISTINCT FieldValue = A.Renk2
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 <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -245,7 +245,7 @@ SELECT 'beden', X.FieldValue
FROM (
SELECT DISTINCT FieldValue = A.Beden
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 <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -346,7 +346,7 @@ BEGIN
ProductAtt46Desc
)
SELECT
C.ProductCode,
UPPER(LTRIM(RTRIM(C.ProductCode))) AS ProductCode,
C.ProductDescription,
C.ProductAtt01Desc,
C.ProductAtt02Desc,
@@ -379,7 +379,7 @@ BEGIN
ISNULL(PF.ProductAtt45Desc, '') AS ProductAtt45Desc,
ISNULL(PF.ProductAtt46Desc, '') AS ProductAtt46Desc
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
AND (@Kategori IS NULL OR C.ProductAtt44Desc = @Kategori)
AND (@UrunAnaGrubu IS NULL OR C.ProductAtt01Desc = @UrunAnaGrubu)
@@ -390,9 +390,9 @@ BEGIN
END
ELSE
BEGIN
INSERT INTO #AttrFiltered
(
ProductCode,
INSERT INTO #AttrFiltered
(
ProductCode,
ProductDescription,
ProductAtt01Desc,
ProductAtt02Desc,
@@ -426,7 +426,7 @@ BEGIN
ProductAtt46Desc
)
SELECT
ProductCode,
UPPER(LTRIM(RTRIM(ProductCode))) AS ProductCode,
ProductDescription,
ProductAtt01Desc,
ProductAtt02Desc,
@@ -493,69 +493,69 @@ INV AS
(
SELECT
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
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
AND LEN(P.ItemCode) = 13
AND LEN(LTRIM(RTRIM(P.ItemCode))) = 13
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-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
SELECT
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
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
AND LEN(R.ItemCode) = 13
AND LEN(LTRIM(RTRIM(R.ItemCode))) = 13
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-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
SELECT
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
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
AND LEN(D.ItemCode) = 13
AND LEN(LTRIM(RTRIM(D.ItemCode))) = 13
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-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
SELECT
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)
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
AND LEN(T.ItemCode) = 13
AND LEN(LTRIM(RTRIM(T.ItemCode))) = 13
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-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
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
GROUP BY
X.CompanyCode, X.OfficeCode, X.StoreTypeCode, X.StoreCode, X.WarehouseCode,
@@ -580,21 +580,21 @@ Avail AS
FROM INV I
JOIN cdItem CI
ON CI.ItemTypeCode = I.ItemTypeCode
AND CI.ItemCode = I.ItemCode
AND UPPER(LTRIM(RTRIM(CI.ItemCode))) = I.ItemCode
LEFT JOIN cdUnitOfMeasure U
ON U.UnitOfMeasureCode = CI.UnitOfMeasureCode1
LEFT JOIN cdColorDesc C
ON C.ColorCode = I.ColorCode
AND C.LangCode = 'TR'
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 CI.IsBlocked = 0
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-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
@@ -604,7 +604,7 @@ Grouped AS
A.ColorCode,
A.ItemDim2Code
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)
AND (@Renk2List IS NULL OR CHARINDEX(NCHAR(31) + ISNULL(A.ItemDim2Code,'') + NCHAR(31), NCHAR(31) + @Renk2List + NCHAR(31)) > 0)
AND (
@@ -669,7 +669,7 @@ INNER JOIN Grouped G
AND G.ColorCode = A.ColorCode
AND ISNULL(G.ItemDim2Code, '') = ISNULL(A.ItemDim2Code, '')
INNER JOIN #AttrFiltered AF
ON AF.ProductCode = A.ItemCode
ON AF.ProductCode = UPPER(LTRIM(RTRIM(A.ItemCode)))
LEFT JOIN cdWarehouseDesc W
ON W.WarehouseCode = A.WarehouseCode
AND W.LangCode = 'TR'
@@ -680,8 +680,8 @@ OUTER APPLY (
SELECT TOP 1 Price
FROM prItemBasePrice PB
WHERE PB.ItemTypeCode = 1
AND PB.ItemCode = A.ItemCode
AND LEN(PB.ItemCode) = 13
AND UPPER(LTRIM(RTRIM(PB.ItemCode))) = UPPER(LTRIM(RTRIM(A.ItemCode)))
AND LEN(LTRIM(RTRIM(PB.ItemCode))) = 13
ORDER BY PB.PriceDate DESC
) P
OPTION (RECOMPILE);
+277 -17
View File
@@ -17,14 +17,33 @@ import (
)
type ProductImageItem struct {
ID int64 `json:"id"`
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
Storage string `json:"storage_path"`
ContentURL string `json:"content_url"`
UUID string `json:"uuid,omitempty"`
ThumbURL string `json:"thumb_url,omitempty"`
FullURL string `json:"full_url,omitempty"`
ID int64 `json:"id"`
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
Storage string `json:"storage_path"`
ContentURL string `json:"content_url"`
UUID string `json:"uuid,omitempty"`
ThumbURL string `json:"thumb_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}`)
@@ -119,6 +138,206 @@ func extractImageUUID(storagePath, fileName string) string {
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=...
func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@@ -190,7 +409,9 @@ SELECT
id,
COALESCE(file_name,'') AS file_name,
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
WHERE typ='img'
AND src_table='mmitem'
@@ -221,16 +442,13 @@ ORDER BY
items := make([]ProductImageItem, 0, 16)
for rows.Next() {
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
}
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
if u := extractImageUUID(it.Storage, it.FileName); u != "" {
it.UUID = u
it.ThumbURL = "/uploads/image/t300/" + u + ".jpg"
it.FullURL = "/uploads/image/" + u + ".jpg"
enrichProductImageItem(&it)
if productImageItemHasURL(&it) {
items = append(items, it)
}
items = append(items, it)
}
if err := rows.Err(); err != nil {
return nil, err
@@ -372,7 +590,8 @@ WHERE id = $1
return
}
if storedInDB && len(binData) > 0 {
_ = storedInDB
if len(binData) > 0 {
w.Header().Set("Content-Type", http.DetectContentType(binData))
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(binData)
@@ -380,6 +599,9 @@ WHERE id = $1
}
resolved, _ := resolveStoragePath(storagePath)
if resolved == "" {
resolved, _ = resolveStoragePath(fileName)
}
if resolved == "" {
http.NotFound(w, r)
return
@@ -413,14 +635,42 @@ func resolveStoragePath(storagePath string) (string, []string) {
raw = filepath.ToSlash(filepath.Clean(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{
filepath.Clean(storagePath),
filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))),
filepath.FromSlash(filepath.Clean(raw)),
relUploads,
relImage,
relImageThumb,
filepath.Join(".", relUploads),
filepath.Join(".", relImage),
filepath.Join(".", relImageThumb),
filepath.Join("..", relUploads),
filepath.Join("..", relImage),
filepath.Join("..", relImageThumb),
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 != "" {
@@ -428,7 +678,17 @@ func resolveStoragePath(storagePath string) (string, []string) {
filepath.Join(root, raw),
filepath.Join(root, relUploads),
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 {
+208 -19
View File
@@ -6,6 +6,7 @@ import (
"context"
"database/sql"
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
@@ -16,7 +17,7 @@ func GetProductPerformanceHandler(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)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
f := queries.ProductPerformanceFilters{
@@ -28,7 +29,7 @@ func GetProductPerformanceHandler(pg *sql.DB) http.HandlerFunc {
Bucket: strings.TrimSpace(r.URL.Query().Get("bucket")),
SortBy: strings.TrimSpace(r.URL.Query().Get("sort_by")),
Descending: boolQuery(r, "descending", true),
Limit: intQuery(r, "limit", 100),
Limit: intQuery(r, "limit", 50000),
Page: intQuery(r, "page", 1),
}
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) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
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()
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) {
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)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
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 {
http.Error(w, "genel urun performans KPI alinamadi: "+err.Error(), http.StatusInternalServerError)
return
@@ -122,10 +123,10 @@ func GetProductPerformanceOrdersHandler(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), 90*time.Second)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
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 {
http.Error(w, "siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
@@ -138,14 +139,14 @@ func GetProductPerformanceOrderGroupsHandler(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), 90*time.Second)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceOrderGroups(
ctx,
pg,
strings.TrimSpace(r.URL.Query().Get("breakdown")),
intQuery(r, "limit", 500),
intQuery(r, "limit", 50000),
)
if err != nil {
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) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
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()
rows, err := queries.ListProductPerformanceOrderProductCustomers(ctx, pg, intQuery(r, "limit", 500))
rows, err := queries.ListProductPerformanceOrderProductCustomers(ctx, pg, intQuery(r, "limit", 50000))
if err != nil {
http.Error(w, "urun musteri siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
@@ -175,10 +176,10 @@ func GetProductPerformanceOrderMarketDetailsHandler(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), 90*time.Second)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
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 {
http.Error(w, "piyasa siparis detay analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
@@ -191,10 +192,10 @@ func GetProductPerformanceMarketsHandler(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), 30*time.Second)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
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 {
http.Error(w, "piyasa performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError)
return
@@ -207,10 +208,10 @@ func GetProductPerformanceCountriesHandler(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), 30*time.Second)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
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 {
http.Error(w, "ulke performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError)
return
@@ -223,14 +224,14 @@ func GetProductPerformanceCustomersHandler(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), 30*time.Second)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceCustomers(
ctx,
pg,
strings.TrimSpace(r.URL.Query().Get("breakdown")),
intQuery(r, "limit", 100),
intQuery(r, "limit", 50000),
)
if err != nil {
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 {
raw := strings.TrimSpace(r.URL.Query().Get(key))
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
}
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)
}
@@ -191,6 +211,7 @@ func GetProductionHasCostProductsHandler(w http.ResponseWriter, r *http.Request)
var item models.ProductionHasCostProductRow
if err := rows.Scan(
&item.UretimSekli,
&item.UrtSiparisNo,
&item.NOnMLNo,
&item.UrunKodu,
&item.UrunAdi,
@@ -219,6 +240,26 @@ func GetProductionHasCostProductsHandler(w http.ResponseWriter, r *http.Request)
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)
}
@@ -1427,6 +1468,12 @@ func postProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
totalUSD := 0.0
totalEUR := 0.0
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
if qty < 0 {
qty = 0
+15 -1
View File
@@ -1,8 +1,20 @@
// quasar.config.js
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(() => {
const apiBaseUrl = (process.env.VITE_API_BASE_URL || '/api').trim()
const gitBuildInfo = readGitBuildInfo()
return {
@@ -36,7 +48,9 @@ export default defineConfig(() => {
build: {
vueRouterMode: 'hash',
env: {
VITE_API_BASE_URL: apiBaseUrl
VITE_API_BASE_URL: apiBaseUrl,
APP_VERSION: gitBuildInfo.version,
APP_COMMIT: gitBuildInfo.commit
},
esbuildTarget: {
+21
View File
@@ -74,6 +74,10 @@
</q-form>
</q-card>
<div class="login-version" :title="`Commit: ${appCommit}`">
Baggi BSS {{ appVersion }}
</div>
<!-- 🔐 FORGOT PASSWORD -->
<q-dialog v-model="forgotOpen" persistent>
<q-card style="width:420px; max-width:90vw">
@@ -129,6 +133,8 @@ import api from 'src/services/api'
const router = useRouter()
const auth = useAuthStore()
const $q = useQuasar()
const appVersion = process.env.APP_VERSION || 'v1.0.0'
const appCommit = process.env.APP_COMMIT || 'local'
const username = ref('')
const password = ref('')
@@ -266,6 +272,21 @@ async function sendResetMail () {
.full-width {
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)
=============================== */
+42 -35
View File
@@ -1222,49 +1222,58 @@ async function reloadData ({ page = 1 } = {}) {
}
async function loadImagesForRows (list) {
const targets = []
const items = []
const rowByKey = new Map()
const seen = new Set()
for (const row of list) {
const key = `${row.productCode}|${row.dim1 || 0}|${row.dim3 || 0}`
if (!row.productCode || seen.has(key)) continue
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)) {
row.imageUrl = imageCache.get(key)
continue
}
rowByKey.set(key, row)
items.push({
key,
code: row.productCode,
dim1: row.dim1 || '',
dim3: row.dim3 || ''
})
}
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.get('/product-images', {
params: {
code: row.productCode,
dim1_id: row.dim1 || '',
dim3_id: row.dim3 || ''
},
timeout: 15000
})
const first = Array.isArray(res?.data) ? res.data[0] : null
const url = resolveProductImageUrl(first)
imageCache.set(key, url)
row.imageUrl = url
imageListCache.set(key, Array.isArray(res?.data) ? res.data : [])
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)
imageListCache.set(key, list)
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 {
imageCache.set(key, '')
for (const item of chunk) imageCache.set(item.key, '')
}
loaded += 1
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) {
if (!item || typeof item !== 'object') return ''
const imageId = Number(item.id || item.ID || 0)
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
const contentUrl = toText(item.content_url || item.ContentURL)
if (contentUrl) return contentUrl.startsWith('/api/') ? contentUrl : contentUrl
const thumbUrl = toText(item.thumb_url || item.thumbUrl)
if (thumbUrl) return thumbUrl
const fullUrl = toText(item.full_url || item.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)
if (uploadsPath) return uploadsPath
const fileName = toText(item.file_name || item.FileName)
File diff suppressed because it is too large Load Diff
+75 -35
View File
@@ -423,7 +423,6 @@ const productImageListByCode = ref({})
const productImageListLoading = ref({})
const productImageFallbackByKey = ref({})
const productImageContentLoading = ref({})
const productImageBlobUrls = ref([])
const productImageListBlockedUntil = ref(0)
const productCardDialog = ref(false)
const productCardData = ref({})
@@ -444,6 +443,8 @@ const IMAGE_LIST_CONCURRENCY = 8
const FILTER_OPTIONS_CACHE_TTL_MS = 60 * 1000
const FILTER_OPTIONS_DEBOUNCE_MS = 250
let imageListActiveRequests = 0
let productImageBatchTimer = null
const productImageBatchQueue = new Map()
let filterOptionsDebounceTimer = null
let filterOptionsRequestSeq = 0
const imageListWaitQueue = []
@@ -635,14 +636,9 @@ function resolveProductImageUrl(item) {
}
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()
if (contentURL.startsWith('/api/')) contentUrl = contentURL
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
}
const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) contentUrl = contentURL
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
let publicUrl = ''
@@ -706,34 +702,82 @@ function clearGalleryQueryIndex() {
async function resolveProductImageUrlForCarousel(item) {
const resolved = resolveProductImageUrl(item)
const contentUrl = String(resolved.contentUrl || '').trim()
if (contentUrl) {
try {
const blobRes = await api.get(contentUrl, { baseURL: '', responseType: 'blob' })
const blob = blobRes?.data
if (blob instanceof Blob) {
const objectUrl = URL.createObjectURL(blob)
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()
if (contentUrl) return contentUrl
const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
if (directUrl) return directUrl
return ''
}
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const existing = productImageCache.value[key]
if (existing !== undefined) return existing || ''
void ensureProductImage(code, color, secondColor, dim1Id, dim3Id)
queueProductImageBatch(code, color, secondColor, dim1Id, dim3Id)
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 = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
productImageCache.value[key] = String(productImageFallbackByKey.value[key] || '').trim()
@@ -804,11 +848,11 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di
productImageCache.value[key] = String(
preferredCardUrl ||
primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || primaryResolved.contentUrl ||
secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || secondaryResolved.contentUrl ||
primaryResolved.contentUrl || primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl ||
secondaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl ||
''
).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) {
console.warn('[ProductStockByAttributes] product image fetch failed', { code, color, err })
productImageCache.value[key] = ''
@@ -1520,10 +1564,6 @@ onUnmounted(() => {
clearTimeout(filterOptionsDebounceTimer)
filterOptionsDebounceTimer = null
}
for (const url of productImageBlobUrls.value) {
try { URL.revokeObjectURL(url) } catch {}
}
productImageBlobUrls.value = []
})
</script>
+75 -34
View File
@@ -390,7 +390,6 @@ const productImageListByCode = ref({})
const productImageListLoading = ref({})
const productImageFallbackByKey = ref({})
const productImageContentLoading = ref({})
const productImageBlobUrls = ref([])
const productImageListBlockedUntil = ref(0)
const productCardDialog = ref(false)
const productCardData = ref({})
@@ -409,6 +408,8 @@ const productImageFullscreenDragOriginX = ref(0)
const productImageFullscreenDragOriginY = ref(0)
const IMAGE_LIST_CONCURRENCY = 8
let imageListActiveRequests = 0
let productImageBatchTimer = null
const productImageBatchQueue = new Map()
const imageListWaitQueue = []
const activeSchema = ref(storeSchemaByKey.tak)
const activeGrpKey = ref('tak')
@@ -616,14 +617,9 @@ function resolveProductImageUrl(item) {
}
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()
if (contentURL.startsWith('/api/')) contentUrl = contentURL
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
}
const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) contentUrl = contentURL
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
let publicUrl = ''
@@ -685,33 +681,82 @@ function clearGalleryQueryIndex() {
async function resolveProductImageUrlForCarousel(item) {
const resolved = resolveProductImageUrl(item)
const contentUrl = String(resolved.contentUrl || '').trim()
if (contentUrl) {
try {
const blobRes = await api.get(contentUrl, { baseURL: '', responseType: 'blob' })
const blob = blobRes?.data
if (blob instanceof Blob) {
const objectUrl = URL.createObjectURL(blob)
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()
if (contentUrl) return contentUrl
const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
if (directUrl) return directUrl
return ''
}
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const existing = productImageCache.value[key]
if (existing !== undefined) return existing || ''
void ensureProductImage(code, color, secondColor, dim1Id, dim3Id)
queueProductImageBatch(code, color, secondColor, dim1Id, dim3Id)
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 = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
productImageCache.value[key] = String(productImageFallbackByKey.value[key] || '').trim()
@@ -779,11 +824,11 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di
const secondaryResolved = resolveProductImageUrl(secondaryItem)
productImageCache.value[key] = String(
preferredCardUrl ||
primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || primaryResolved.contentUrl ||
secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || secondaryResolved.contentUrl ||
primaryResolved.contentUrl || primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl ||
secondaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl ||
''
).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) {
console.warn('[ProductStockQuery] product image fetch failed', { code, color, err })
productImageCache.value[key] = ''
@@ -1317,10 +1362,6 @@ function resetForm() {
onUnmounted(() => {
window.removeEventListener('mousemove', onFullscreenMouseMove)
window.removeEventListener('mouseup', onFullscreenMouseUp)
for (const url of productImageBlobUrls.value) {
try { URL.revokeObjectURL(url) } catch {}
}
productImageBlobUrls.value = []
})
onMounted(async () => {
@@ -40,6 +40,14 @@
:loading="loading"
@click="fetchRows"
/>
<q-btn
label="Excel'e Aktar"
icon="download"
color="primary"
outline
:disable="loading || rows.length === 0"
@click="exportVisibleRows"
/>
</div>
</div>
</div>
@@ -141,7 +149,7 @@
</template>
<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 { usePermission } from 'src/composables/usePermission'
import { get, extractApiErrorDetail } from 'src/services/api'
@@ -168,6 +176,10 @@ const dateColumns = new Set(['Tarihi', 'dteKayitTarihi', 'dteGuncellemeTarihi',
const columns = [
{ 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: '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: '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%' },
@@ -186,6 +198,30 @@ const columns = [
]
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) {
if (!columnFilters[name]) {
@@ -293,9 +329,11 @@ function clearAllColumnFilters () {
}
let searchTimer = null
let filterWatchEnabled = false
watch(
() => filters.search,
() => {
if (!filterWatchEnabled) return
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
fetchRows()
@@ -348,19 +386,66 @@ function clearFilters () {
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) {
const urunKodu = String(row?.UrunKodu || '').trim()
if (!urunKodu) return
persistListState()
router.push({
name: 'production-product-costing-has-cost-history',
query: { urun_kodu: urunKodu }
})
}
onMounted(() => {
onMounted(async () => {
if (!canReadOrder.value) return
fetchRows()
restoreListState()
await nextTick()
filterWatchEnabled = true
await fetchRows()
})
</script>
@@ -457,7 +457,7 @@
<div v-else class="column q-gutter-md pcd-content-body">
<div
v-for="(grp, gi) in detailGroups"
v-for="(grp, gi) in detailGroupsWithTotals"
:key="groupKey(grp, gi)"
class="pcd-group-card"
>
@@ -469,7 +469,11 @@
</span>
</div>
<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
:name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'"
size="18px"
@@ -1557,8 +1561,14 @@ function ensureBeforeUnloadGuard (enabled) {
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) => {
if (!row?.maliyeteDahil) return acc
if (!shouldIncludeRowInCostingTotal(row)) return acc
acc.tryTotal += resolveRowTRYTutar(row)
acc.usdTotal += resolveRowUSDTutar(row)
acc.eurTotal += resolveRowEURTutar(row)
@@ -2694,7 +2704,9 @@ function normalizeDetailRows (items, groupName = '') {
// Use fiyat_girilen which is the unit price in original currency.
inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen),
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),
draftChanged: Boolean(x?.draftChanged),
priceUpdateState: String(x?.priceUpdateState || '').trim()
@@ -2796,6 +2808,13 @@ function recalculateDetailRow (row, options = {}) {
row.lTutarGBP = row.gbpTutar
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) {
row.miktarInput = normalizeQuantityInput(quantity)
row.inputPrice = normalizeInputPrice(inputPrice)
@@ -2817,58 +2836,124 @@ function recalculateDetailRow (row, options = {}) {
}
function recalculateAllDetailRows () {
detailGroups.value = detailGroups.value.map(grp => ({
...grp,
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => recalculateDetailRow({ ...row }, { preserveInputs: true }))
}))
// Preserve row identity. QTable editors and the computed group totals must
// observe the exact same reactive row objects. Replacing rows with
// `{ ...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) {
const tryAmount = Number(row?.lTutar || 0)
return Number.isFinite(tryAmount) ? tryAmount : 0
const quantity = resolveNumericRowQuantity(row)
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) {
const eurAmount = Number(row?.eurTutar || 0)
return Number.isFinite(eurAmount) ? eurAmount : 0
return parseMoneyInput(row?.eurTutar)
}
function resolveRowGBPTutar (row) {
const gbpAmount = Number(row?.gbpTutar || 0)
return Number.isFinite(gbpAmount) ? gbpAmount : 0
return parseMoneyInput(row?.gbpTutar)
}
function resolveRowUSDTutar (row) {
const usdAmount = Number(row?.usdTutar || 0)
if (Number.isFinite(usdAmount)) return usdAmount
const quantity = resolveNumericRowQuantity(row)
const inputPrice = resolveNumericRowInputPrice(row)
const inputCurrency = resolveInputCurrency(row)
const liveAmount = resolveUSDUnitPriceByInput(
inputPrice,
inputCurrency,
resolveExchangeRateValue('USD'),
resolveExchangeRateValue('EUR'),
resolveExchangeRateValue('GBP')
) * quantity
const miktar = resolveNumericRowQuantity(row)
const dovizFiyati = Number(row?.lDovizFiyati || 0)
const calc = miktar * dovizFiyati
if (Number.isFinite(liveAmount) && inputPrice > 0) return liveAmount
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
}
function shouldIgnoreGroupMaliyeteDahil (grp) {
return isCMGroupName(grp?.sAciklama3)
}
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) {
const items = Array.isArray(grp?.items) ? grp.items : []
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowTRYTutar(row) : 0), 0)
function resolveGroupRowTRYAmount (row) {
const columnAmount = parseMoneyInput(row?.lTutar)
if (columnAmount !== 0) return columnAmount
const liveAmount = resolveRowTRYTutar(row)
if (liveAmount !== 0) return liveAmount
return parseMoneyInput(row?.__lastValidTRYAmount ?? row?.lTutarTL)
}
function resolveGroupUSDTutar (grp) {
const items = Array.isArray(grp?.items) ? grp.items : []
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowUSDTutar(row) : 0), 0)
function resolveGroupRowUSDAmount (row) {
const columnAmount = parseMoneyInput(row?.usdTutar ?? row?.lTutarUSD)
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) {
const items = Array.isArray(grp?.items) ? grp.items : []
return items.reduce((acc, row) => acc + (Number(row?.lMiktar || 0) || 0), 0)
return (Array.isArray(grp?.items) ? grp.items : []).reduce((total, row) => (
total + resolveNumericRowQuantity(row)
), 0)
}
function groupKey (grp, gi) {
@@ -3244,7 +3329,7 @@ function onRowQuantityInput (row, value) {
}
function triggerUIUpdate () {
detailGroups.value = [...detailGroups.value]
// detailGroupsWithTotals is computed from the reactive row fields.
}
function normalizeRowQuantityDisplay (row) {
@@ -3342,25 +3427,26 @@ async function fetchBulkItemPrices () {
}
let appliedCount = 0
detailGroups.value = detailGroups.value.map(grp => ({
...grp,
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
detailGroups.value.forEach(grp => {
const items = Array.isArray(grp?.items) ? grp.items : []
items.forEach(row => {
const match = updates.find(update => rowMatchesBulkUpdate(row, update))
if (!match) return row
if (!match) return
appliedCount += 1
return recalculateDetailRow({
...row,
inputPrice: match.inputPrice,
fiyat_girilen: match.fiyat_girilen,
inputPricePrBr: match.inputPricePrBr,
fiyat_doviz: match.fiyat_doviz
}, {
row.inputPrice = match.inputPrice
row.fiyat_girilen = match.fiyat_girilen
row.inputPricePrBr = match.inputPricePrBr
row.fiyat_doviz = match.fiyat_doviz
recalculateDetailRow(row, {
priceType: match.priceType || 'SAF',
updateState: 'bulk',
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({
type: appliedCount > 0 ? 'positive' : 'warning',
@@ -3400,14 +3486,14 @@ async function fetchBulkItemPrices () {
})
let filled = 0
detailGroups.value = detailGroups.value.map(grp => ({
...grp,
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
if (!row?.requiredPlaceholder) return row
if (isCMGroupName(row?.sAciklama3) || normalizeGroupName(row?.sAciklama3) === 'FABRIC') return row
detailGroups.value.forEach(grp => {
const items = Array.isArray(grp?.items) ? grp.items : []
items.forEach(row => {
if (!row?.requiredPlaceholder) return
if (isCMGroupName(row?.sAciklama3) || normalizeGroupName(row?.sAciklama3) === 'FABRIC') return
const no = parseInt(String(row?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
const hit = byNo[no]
if (!hit) return row
if (!hit) return
const next = { ...row }
const hasCode = String(next.sKodu || '').trim() !== ''
@@ -3427,20 +3513,20 @@ async function fetchBulkItemPrices () {
// Only mark if something changed
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
return recalculateDetailRow({
...next,
__autoFilledFromPrev: true
,
Object.assign(row, next, {
__autoFilledFromPrev: true,
__autoFilledFromPrevSameFirma: Boolean(hit?.is_same_firma || hit?.isSameFirma)
}, {
})
recalculateDetailRow(row, {
priceType: 'PREV',
updateState: 'autofill',
markChanged: true
})
})
}))
})
triggerUIUpdate()
if (filled > 0) {
$q.notify({
@@ -3457,6 +3543,11 @@ async function fetchBulkItemPrices () {
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) {
$q.notify({
type: 'negative',
@@ -3928,10 +4019,14 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
const groupName = normalizeGroupName(meta?.groupName || '')
const hammaddeAdi = String(meta?.hammaddeAdi || '').trim()
const effectiveGroupName = groupName || 'TANIMSIZ'
const mtBolumID = (meta?.mtBolumID > 0 ? meta.mtBolumID : mappingMtBolumID) || 0
const metaParcaCandidate = normalizeGroupName((meta?.parcaAdi || '').trim())
// Defensive: if backend/lookup accidentally returns group label (DT/TP/...) as part name, ignore it.
const desiredParcaAdi = normalizeGroupName((metaParcaCandidate && !isKnownGroupName(metaParcaCandidate)) ? metaParcaCandidate : mappingParcaAdi)
// The product-type mapping is authoritative for the required part. A raw
// material type can be required for multiple parts, while its master
// record only carries one default MT section. Using that default here
// 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 => {
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 (isNoCostDetail.value && newOnMLNo > 0) {
router.replace({
await router.replace({
name: 'production-product-costing-has-cost-detail',
query: {
n_onml_no: String(newOnMLNo),
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
}
@@ -5078,6 +5191,24 @@ async function saveChanges () {
window.setTimeout(() => {
try { refreshLast10Warnings() } catch {}
}, 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) {
// Surface backend message (http.Error text) when available.
const msg = String(
@@ -5526,8 +5657,8 @@ watch(
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
min-height: 42px;
height: 42px;
min-height: 46px;
height: auto;
border-top: 1px solid #d6c06a;
border-bottom: 1px solid #d6c06a;
background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%);
@@ -5549,7 +5680,9 @@ watch(
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: nowrap;
flex-wrap: wrap;
row-gap: 4px;
max-width: 70%;
}
.pcd-sub-header .sub-left {
display: flex;
@@ -5566,8 +5699,31 @@ watch(
user-select: none;
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
gap: 6px 10px;
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;
}
.pcd-sub-mt-qty {
@@ -5577,6 +5733,18 @@ watch(
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) {
overflow: visible !important;
}
@@ -5717,12 +5885,69 @@ watch(
}
.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) {
font-size: 12px;
font-size: 9.5px;
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) {
+102 -4
View File
@@ -49,6 +49,14 @@
:loading="loading"
@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">
Maliyeti Girilmemis Satir Sayisi: <b>{{ missingCostRowCount }}</b>
</div>
@@ -176,7 +184,7 @@
</template>
<script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useQuasar } from 'quasar'
import { useRouter } from 'vue-router'
import { usePermission } from 'src/composables/usePermission'
@@ -244,6 +252,9 @@ const columns = [
style: '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',
label: 'Model Kodu',
@@ -262,6 +273,34 @@ const columns = [
]
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) {
if (!columnFilters[name]) {
@@ -276,7 +315,11 @@ function getColumnFilter (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) {
@@ -384,9 +427,11 @@ function clearAllColumnFilters () {
}
let searchTimer = null
let filterWatchEnabled = false
watch(
() => filters.search,
() => {
if (!filterWatchEnabled) return
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
fetchRows()
@@ -397,6 +442,7 @@ watch(
watch(
() => filters.fromDate,
() => {
if (!filterWatchEnabled) return
fetchRows()
}
)
@@ -430,6 +476,49 @@ function clearFilters () {
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) {
const productCode = String(row?.sMModelKodu || '').trim()
const recipeCode = String(row?.sKodu || '').trim()
@@ -449,6 +538,8 @@ function openRow (row) {
recipe_code: recipeCode
})
persistListState()
router.push({
name: 'production-product-costing-has-cost-detail',
query: {
@@ -460,9 +551,16 @@ function openRow (row) {
})
}
onMounted(() => {
onMounted(async () => {
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>
+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 }
}
}
})