Compare commits
100
Commits
64105815a0
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db13574e9d | ||
|
|
ba9a744aef | ||
|
|
82ad3783b3 | ||
|
|
322bed753f | ||
|
|
bdc9459330 | ||
|
|
2d85a33623 | ||
|
|
5f1c37859b | ||
|
|
31a71ab566 | ||
|
|
25ef023df5 | ||
|
|
0c51729f7a | ||
|
|
20f5fea2e8 | ||
|
|
65e8aa2b62 | ||
|
|
1f8355b77c | ||
|
|
11048f1555 | ||
|
|
b83a3d22ce | ||
|
|
77af9ef533 | ||
|
|
4395d6bae2 | ||
|
|
6bc19e7a43 | ||
|
|
797d069e59 | ||
|
|
36e0ec9ff5 | ||
|
|
f14a96a024 | ||
|
|
f877b611b7 | ||
|
|
dbb3f3f380 | ||
|
|
03287d4850 | ||
|
|
12ce3a3eca | ||
|
|
52420bc5ff | ||
|
|
d5aa43ccb5 | ||
|
|
04461acb58 | ||
|
|
cd101811c4 | ||
|
|
c4028010c0 | ||
|
|
c2d010751f | ||
|
|
a9ab9fd9b0 | ||
|
|
1945fd4456 | ||
|
|
7b8f8905e5 | ||
|
|
89df3d2d3d | ||
|
|
474904481f | ||
|
|
6eaf0e3ed3 | ||
|
|
8249761415 | ||
|
|
dbd84f7399 | ||
|
|
9edc806345 | ||
|
|
7a7008cc1b | ||
|
|
3e0253f6be | ||
|
|
b80b5024f4 | ||
|
|
5eaf8be9e6 | ||
|
|
cc2930caf2 | ||
|
|
26b4a63c45 | ||
|
|
c6a3d1552e | ||
|
|
005d2eafac | ||
|
|
289f204752 | ||
|
|
1448dab9d7 | ||
|
|
5d4216d42e | ||
|
|
3993c363cb | ||
|
|
a0da3f2bed | ||
|
|
19a42e0551 | ||
|
|
53cdd1a4a1 | ||
|
|
fe29e763c5 | ||
|
|
9e1ecb9b3f | ||
|
|
fbdde54eee | ||
|
|
b2113a8d49 | ||
|
|
28e33d5e58 | ||
|
|
fbdc4934ff | ||
|
|
5b512fd30a | ||
|
|
7879172c54 | ||
|
|
c602e1e119 | ||
|
|
27934bccdf | ||
|
|
48bde09c2c | ||
|
|
480cad0d6f | ||
|
|
9f6e288d04 | ||
|
|
d0717900a6 | ||
|
|
25bc9b2d02 | ||
|
|
4c56062ef4 | ||
|
|
60b2f76537 | ||
|
|
81384426ff | ||
|
|
3697e35064 | ||
|
|
4ae6475875 | ||
|
|
5f006b8be0 | ||
|
|
38c168f7bd | ||
|
|
be125a6880 | ||
|
|
3b2bcda462 | ||
|
|
9e22cc5ec5 | ||
|
|
5e2a85b98e | ||
|
|
a45478d15f | ||
|
|
b133d43ebe | ||
|
|
9fee8c8ab2 | ||
|
|
95d72ec74b | ||
|
|
4cd1b118d0 | ||
|
|
2f885a4841 | ||
|
|
4aebb36ff2 | ||
|
|
fd086d668f | ||
|
|
5288f19976 | ||
|
|
60eef45769 | ||
|
|
a05e6bf1e2 | ||
|
|
94c83c3825 | ||
|
|
a0dd832d2b | ||
|
|
c2d27b2d77 | ||
|
|
9f45160648 | ||
|
|
c7ff2328fb | ||
|
|
e7a4c58934 | ||
|
|
5d5a38647d | ||
|
|
75e5136ab7 |
Generated
+1
@@ -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" />
|
||||
|
||||
@@ -3,6 +3,8 @@ bssapp
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.local
|
||||
.env.argebaggi.local
|
||||
mail.env
|
||||
|
||||
# Runtime fonts
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+41
@@ -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)
|
||||
|
||||
/*
|
||||
@@ -939,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",
|
||||
@@ -999,6 +1005,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
||||
"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",
|
||||
@@ -1379,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")
|
||||
@@ -1406,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 {
|
||||
@@ -1507,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)
|
||||
|
||||
@@ -4,6 +4,7 @@ type ProductPerformanceRow struct {
|
||||
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"`
|
||||
@@ -20,14 +21,28 @@ type ProductPerformanceRow struct {
|
||||
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"`
|
||||
@@ -42,10 +57,23 @@ type ProductPerformanceRow struct {
|
||||
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"`
|
||||
@@ -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,6 +207,7 @@ 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"`
|
||||
@@ -214,6 +245,7 @@ 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"`
|
||||
@@ -296,6 +328,7 @@ 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"`
|
||||
@@ -309,6 +342,14 @@ type ProductPerformanceSalesBreakdownRow struct {
|
||||
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"`
|
||||
@@ -354,16 +395,27 @@ type ProductPerformanceSalesBreakdownRow struct {
|
||||
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"`
|
||||
|
||||
@@ -12,11 +12,18 @@ type ProductionNoCostProductRow struct {
|
||||
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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+4075
-449
File diff suppressed because it is too large
Load Diff
@@ -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,76 +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 = 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))),
|
||||
UrunIlkGrubu = 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))),
|
||||
AskiliYan = dbo.HG_Temizlik(ISNULL((
|
||||
SELECT AttributeDescription
|
||||
FROM cdItemAttributeDesc WITH(NOLOCK)
|
||||
WHERE cdItemAttributeDesc.ItemTypeCode = I.ItemTypeCode
|
||||
AND cdItemAttributeDesc.AttributeTypeCode = 45
|
||||
AND cdItemAttributeDesc.AttributeCode = (
|
||||
SELECT TOP 1 AttributeCode
|
||||
FROM prItemAttribute WITH(NOLOCK)
|
||||
WHERE AttributeTypeCode = 45
|
||||
AND prItemAttribute.ItemTypeCode = I.ItemTypeCode
|
||||
AND prItemAttribute.ItemCode = I.ItemCode
|
||||
)
|
||||
AND cdItemAttributeDesc.LangCode = 'TR'
|
||||
), 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
|
||||
@@ -141,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
|
||||
@@ -159,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')
|
||||
@@ -194,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, ''), '-'),
|
||||
@@ -234,9 +197,9 @@ func productPerformanceStockSQLUnused() 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))
|
||||
@@ -249,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,
|
||||
@@ -382,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
|
||||
@@ -459,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
|
||||
`
|
||||
@@ -479,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,
|
||||
@@ -489,7 +470,7 @@ 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,
|
||||
@@ -498,25 +479,37 @@ SalesAgg AS (
|
||||
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 '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 '359 days') AS sales_usd_365d,
|
||||
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS customer_count_90d,
|
||||
SUM(sales_usd) FILTER (WHERE sales_date >= DATE '2022-01-01') AS sales_usd_total,
|
||||
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_90d,
|
||||
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_180d,
|
||||
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_365d,
|
||||
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(sales_usd, 0) > 0) AS customer_count_total,
|
||||
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS first_sale_date_90d,
|
||||
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days') AS first_sale_date_180d,
|
||||
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS first_sale_date_365d,
|
||||
MIN(sales_date) FILTER (WHERE sales_date >= DATE '2022-01-01') AS first_sale_date_total,
|
||||
MAX(sales_date) AS last_sale_date,
|
||||
MAX(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
|
||||
@@ -529,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,
|
||||
@@ -537,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
|
||||
@@ -554,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.*,
|
||||
@@ -563,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,
|
||||
@@ -579,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
|
||||
@@ -604,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
|
||||
@@ -616,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),
|
||||
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(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,
|
||||
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'
|
||||
@@ -650,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, ''),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
`
|
||||
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
|
||||
+128
-17
@@ -25,6 +25,8 @@ type ProductImageItem struct {
|
||||
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 {
|
||||
@@ -136,6 +138,74 @@ 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) {
|
||||
@@ -208,6 +278,8 @@ ranked AS (
|
||||
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
|
||||
@@ -225,9 +297,9 @@ ranked AS (
|
||||
AND b.typ='img'
|
||||
AND b.src_id=mi.mmitem_id
|
||||
)
|
||||
SELECT key, id, file_name, file_size, storage_path
|
||||
SELECT key, id, file_name, file_size, storage_path, stored_in_db, has_db_content
|
||||
FROM ranked
|
||||
WHERE rn <= 1
|
||||
WHERE rn <= 5
|
||||
ORDER BY key, rn
|
||||
`, string(payload))
|
||||
if err != nil {
|
||||
@@ -241,14 +313,12 @@ ORDER BY key, rn
|
||||
for rows.Next() {
|
||||
var key string
|
||||
var it ProductImageItem
|
||||
if err := rows.Scan(&key, &it.ID, &it.FileName, &it.FileSize, &it.Storage); err != nil {
|
||||
if err := rows.Scan(&key, &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) {
|
||||
continue
|
||||
}
|
||||
grouped[key] = append(grouped[key], it)
|
||||
}
|
||||
@@ -339,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'
|
||||
@@ -370,17 +442,14 @@ 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)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -521,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)
|
||||
@@ -529,6 +599,9 @@ WHERE id = $1
|
||||
}
|
||||
|
||||
resolved, _ := resolveStoragePath(storagePath)
|
||||
if resolved == "" {
|
||||
resolved, _ = resolveStoragePath(fileName)
|
||||
}
|
||||
if resolved == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@@ -562,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 != "" {
|
||||
@@ -577,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 {
|
||||
|
||||
@@ -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)
|
||||
@@ -244,14 +245,14 @@ 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), 45*time.Second)
|
||||
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", 500),
|
||||
intQuery(r, "limit", 50000),
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, "satis kpi kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
@@ -265,19 +266,41 @@ 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), 90*time.Second)
|
||||
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)
|
||||
@@ -286,6 +309,9 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
||||
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
|
||||
}
|
||||
@@ -293,17 +319,23 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
||||
limit = body.Limit
|
||||
}
|
||||
for _, key := range body.ExpandedKeys {
|
||||
key = strings.TrimSpace(key)
|
||||
if key != "" {
|
||||
expanded[key] = true
|
||||
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"), ",") {
|
||||
key = strings.TrimSpace(key)
|
||||
if key != "" {
|
||||
expanded[key] = true
|
||||
}
|
||||
addExpandedKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,9 +343,15 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
||||
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
|
||||
}
|
||||
@@ -321,6 +359,64 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
=============================== */
|
||||
|
||||
@@ -1289,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
@@ -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({})
|
||||
@@ -637,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 uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
||||
let publicUrl = ''
|
||||
@@ -708,23 +702,12 @@ 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 = '') {
|
||||
@@ -774,9 +757,9 @@ async function flushProductImageBatch() {
|
||||
const list = Array.isArray(item.images) ? item.images : []
|
||||
const first = list[0] || null
|
||||
const resolved = resolveProductImageUrl(first)
|
||||
const url = resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || resolved.contentUrl || ''
|
||||
const url = resolved.contentUrl || resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
|
||||
productImageCache.value[key] = String(url || '').trim()
|
||||
productImageFallbackByKey.value[key] = resolved.fullUrl || resolved.publicUrl || resolved.contentUrl || ''
|
||||
productImageFallbackByKey.value[key] = resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
|
||||
if (queued) productImageListByCode.value[queued.listKey] = list
|
||||
delete productImageLoading.value[key]
|
||||
}
|
||||
@@ -865,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] = ''
|
||||
@@ -1581,10 +1564,6 @@ onUnmounted(() => {
|
||||
clearTimeout(filterOptionsDebounceTimer)
|
||||
filterOptionsDebounceTimer = null
|
||||
}
|
||||
for (const url of productImageBlobUrls.value) {
|
||||
try { URL.revokeObjectURL(url) } catch {}
|
||||
}
|
||||
productImageBlobUrls.value = []
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -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({})
|
||||
@@ -618,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 uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
||||
let publicUrl = ''
|
||||
@@ -687,23 +681,12 @@ 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 = '') {
|
||||
@@ -753,9 +736,9 @@ async function flushProductImageBatch() {
|
||||
const list = Array.isArray(item.images) ? item.images : []
|
||||
const first = list[0] || null
|
||||
const resolved = resolveProductImageUrl(first)
|
||||
const url = resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || resolved.contentUrl || ''
|
||||
const url = resolved.contentUrl || resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
|
||||
productImageCache.value[key] = String(url || '').trim()
|
||||
productImageFallbackByKey.value[key] = resolved.fullUrl || resolved.publicUrl || resolved.contentUrl || ''
|
||||
productImageFallbackByKey.value[key] = resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
|
||||
if (queued) productImageListByCode.value[queued.listKey] = list
|
||||
delete productImageLoading.value[key]
|
||||
}
|
||||
@@ -841,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] = ''
|
||||
@@ -1379,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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -4,37 +4,38 @@ import api from 'src/services/api'
|
||||
const GROUPED_TTL_MS = 2 * 60 * 1000
|
||||
const IMAGE_TTL_MS = 15 * 60 * 1000
|
||||
|
||||
function normalizeUploadsPath (storagePath) {
|
||||
const raw = String(storagePath || '').trim()
|
||||
if (!raw) return ''
|
||||
const normalized = raw.replace(/\\/g, '/')
|
||||
const idx = normalized.toLowerCase().indexOf('/uploads/')
|
||||
if (idx >= 0) return normalized.slice(idx)
|
||||
if (normalized.toLowerCase().startsWith('uploads/')) return `/${normalized}`
|
||||
return ''
|
||||
function resolveProductImageUrl (item) {
|
||||
return resolveProductImageUrls(item)[0] || ''
|
||||
}
|
||||
|
||||
function resolveProductImageUrl (item) {
|
||||
if (!item || typeof item !== 'object') return ''
|
||||
|
||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||
if (contentURL.startsWith('/api/')) return contentURL
|
||||
if (contentURL.startsWith('/')) return `/api${contentURL}`
|
||||
|
||||
const imageId = Number(item.id || item.ID || 0)
|
||||
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
|
||||
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()
|
||||
if (thumbURL) return thumbURL
|
||||
addURL(thumbURL)
|
||||
|
||||
const fullURL = String(item.full_url || item.fullUrl || '').trim()
|
||||
if (fullURL) return fullURL
|
||||
addURL(fullURL)
|
||||
|
||||
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
||||
if (uploadsPath) return uploadsPath
|
||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||
if (contentURL.startsWith('/api/')) addURL(contentURL)
|
||||
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
|
||||
return urls
|
||||
}
|
||||
|
||||
const fileName = String(item.file_name || item.FileName || '').trim()
|
||||
return fileName ? `/uploads/image/${fileName}` : ''
|
||||
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) {
|
||||
@@ -49,6 +50,9 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
||||
groupedLoadedAtByKey: {},
|
||||
groupedLoadingByKey: {},
|
||||
groupedInFlightByKey: {},
|
||||
groupedFilterOptionsByKey: {},
|
||||
groupedFilterOptionsLoadedAtByKey: {},
|
||||
groupedFilterOptionsInFlightByKey: {},
|
||||
imageUrlByKey: {},
|
||||
imageListByKey: {},
|
||||
imageLoadedAtByKey: {}
|
||||
@@ -65,8 +69,23 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
||||
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)
|
||||
})
|
||||
},
|
||||
@@ -89,7 +108,8 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
||||
console.info(`${logPrefix} in-flight reuse`, {
|
||||
mode: params.mode,
|
||||
groupLevels: normalizedList(params.groupLevels),
|
||||
expandedKeys: normalizedList(params.expandedKeys, true).length
|
||||
expandedKeys: normalizedList(params.expandedKeys, true).length,
|
||||
expandThroughLevel: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1)
|
||||
})
|
||||
return this.groupedInFlightByKey[key]
|
||||
}
|
||||
@@ -100,17 +120,27 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
||||
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
|
||||
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 => {
|
||||
@@ -124,10 +154,11 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
||||
})
|
||||
return rows
|
||||
}).catch(err => {
|
||||
console.warn(`${logPrefix} request failed`, {
|
||||
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: err?.response?.data || err?.message || err
|
||||
message
|
||||
})
|
||||
throw err
|
||||
}).finally(() => {
|
||||
@@ -143,6 +174,35 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
||||
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
|
||||
@@ -186,7 +246,7 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
||||
const key = String(item?.key || '').trim()
|
||||
if (!key) continue
|
||||
returned.add(key)
|
||||
const urls = (Array.isArray(item.images) ? item.images : []).map(resolveProductImageUrl).filter(Boolean)
|
||||
const urls = (Array.isArray(item.images) ? item.images : []).flatMap(resolveProductImageUrls).filter(Boolean)
|
||||
this.setImageCache(key, urls)
|
||||
lists[key] = urls
|
||||
urlsByKey[key] = urls[0] || ''
|
||||
|
||||
Reference in New Issue
Block a user