Compare commits
27
Commits
fe29e763c5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
Generated
+1
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="WEB_MODULE" version="4">
|
<module type="WEB_MODULE" version="4">
|
||||||
<component name="Go" enabled="true" />
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="GoModuleSettings" enabled="true" />
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$" />
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ bssapp
|
|||||||
|
|
||||||
# Env
|
# Env
|
||||||
.env
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.argebaggi.local
|
||||||
mail.env
|
mail.env
|
||||||
|
|
||||||
# Runtime fonts
|
# Runtime fonts
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
var MssqlDB *sql.DB
|
var MssqlDB *sql.DB
|
||||||
var UretimDB *sql.DB
|
var UretimDB *sql.DB
|
||||||
|
var ArgeBaggiDB *sql.DB
|
||||||
|
|
||||||
func envInt(name string, fallback int) int {
|
func envInt(name string, fallback int) int {
|
||||||
raw := strings.TrimSpace(os.Getenv(name))
|
raw := strings.TrimSpace(os.Getenv(name))
|
||||||
@@ -158,3 +159,41 @@ func ConnectMSSQLUretim() error {
|
|||||||
func GetUretimDB() *sql.DB {
|
func GetUretimDB() *sql.DB {
|
||||||
return UretimDB
|
return UretimDB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConnectMSSQLArgeBaggi initializes the optional ArgeBAGGI connection.
|
||||||
|
// Credentials must be supplied through ARGEBAGGI_MSSQL_CONN; never keep them
|
||||||
|
// in source control.
|
||||||
|
func ConnectMSSQLArgeBaggi() error {
|
||||||
|
connString := strings.TrimSpace(os.Getenv("ARGEBAGGI_MSSQL_CONN"))
|
||||||
|
if connString == "" {
|
||||||
|
return fmt.Errorf("ARGEBAGGI_MSSQL_CONN tanimli degil")
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionTimeoutSec := envInt("ARGEBAGGI_MSSQL_CONNECTION_TIMEOUT_SEC", 10)
|
||||||
|
dialTimeoutSec := envInt("ARGEBAGGI_MSSQL_DIAL_TIMEOUT_SEC", connectionTimeoutSec)
|
||||||
|
connString = ensureMSSQLTimeouts(connString, connectionTimeoutSec, dialTimeoutSec)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
ArgeBaggiDB, err = sql.Open("sqlserver", connString)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ArgeBAGGI MSSQL baglanti hatasi: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgeBaggiDB.SetMaxOpenConns(envInt("ARGEBAGGI_MSSQL_MAX_OPEN_CONNS", 10))
|
||||||
|
ArgeBaggiDB.SetMaxIdleConns(envInt("ARGEBAGGI_MSSQL_MAX_IDLE_CONNS", 5))
|
||||||
|
ArgeBaggiDB.SetConnMaxLifetime(time.Duration(envInt("ARGEBAGGI_MSSQL_CONN_MAX_LIFETIME_MIN", 30)) * time.Minute)
|
||||||
|
ArgeBaggiDB.SetConnMaxIdleTime(time.Duration(envInt("ARGEBAGGI_MSSQL_CONN_MAX_IDLE_MIN", 10)) * time.Minute)
|
||||||
|
|
||||||
|
if err = ArgeBaggiDB.Ping(); err != nil {
|
||||||
|
_ = ArgeBaggiDB.Close()
|
||||||
|
ArgeBaggiDB = nil
|
||||||
|
return fmt.Errorf("ArgeBAGGI MSSQL erisilemiyor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("ArgeBAGGI MSSQL baglantisi basarili (connection timeout=%ds, dial timeout=%ds)\n", connectionTimeoutSec, dialTimeoutSec)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetArgeBaggiDB() *sql.DB {
|
||||||
|
return ArgeBaggiDB
|
||||||
|
}
|
||||||
|
|||||||
+31
@@ -227,6 +227,7 @@ InitRoutes — FULL V3 (Method-aware) PERMISSION EDITION
|
|||||||
func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {
|
func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
mountUploads(r)
|
||||||
mountSPA(r)
|
mountSPA(r)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1389,6 +1390,7 @@ func main() {
|
|||||||
// (e.g. SSH-tunnel PostgreSQL on 127.0.0.1:15432).
|
// (e.g. SSH-tunnel PostgreSQL on 127.0.0.1:15432).
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
_ = godotenv.Overload(".env.local")
|
_ = godotenv.Overload(".env.local")
|
||||||
|
_ = godotenv.Overload(".env.argebaggi.local")
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtSecret := os.Getenv("JWT_SECRET")
|
jwtSecret := os.Getenv("JWT_SECRET")
|
||||||
@@ -1416,6 +1418,15 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := db.ConnectMSSQLArgeBaggi(); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "ARGEBAGGI_MSSQL_CONN tanimli degil") {
|
||||||
|
log.Println("ArgeBAGGI DB baglantisi atlandi: ARGEBAGGI_MSSQL_CONN tanimli degil")
|
||||||
|
} else {
|
||||||
|
// This source enriches one screen; a temporary outage must not stop
|
||||||
|
// the whole application from starting.
|
||||||
|
log.Println("ArgeBAGGI DB baglantisi basarisiz:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pgDB, err := db.ConnectPostgres()
|
pgDB, err := db.ConnectPostgres()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1517,6 +1528,26 @@ func main() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mountUploads(r *mux.Router) {
|
||||||
|
root := strings.TrimSpace(os.Getenv("BLOB_ROOT"))
|
||||||
|
if root == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploadsRoot := filepath.Join(root, "uploads")
|
||||||
|
if fi, err := os.Stat(uploadsRoot); err != nil || !fi.IsDir() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileServer := http.StripPrefix("/uploads/", http.FileServer(http.Dir(uploadsRoot)))
|
||||||
|
r.PathPrefix("/uploads/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||||
|
fileServer.ServeHTTP(w, r)
|
||||||
|
})).Methods(http.MethodGet, http.MethodHead, http.MethodOptions)
|
||||||
|
}
|
||||||
|
|
||||||
func mountSPA(r *mux.Router) {
|
func mountSPA(r *mux.Router) {
|
||||||
r.NotFoundHandler = http.HandlerFunc(spaIndex)
|
r.NotFoundHandler = http.HandlerFunc(spaIndex)
|
||||||
r.HandleFunc("/", spaIndex).Methods(http.MethodGet)
|
r.HandleFunc("/", spaIndex).Methods(http.MethodGet)
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ type ProductPerformanceRow struct {
|
|||||||
AvgDailySales180 float64 `json:"avg_daily_sales_180d"`
|
AvgDailySales180 float64 `json:"avg_daily_sales_180d"`
|
||||||
AvgDailySales365 float64 `json:"avg_daily_sales_365d"`
|
AvgDailySales365 float64 `json:"avg_daily_sales_365d"`
|
||||||
AvgDailySalesTotal float64 `json:"avg_daily_sales_total"`
|
AvgDailySalesTotal float64 `json:"avg_daily_sales_total"`
|
||||||
|
AvgStock90 float64 `json:"avg_stock_90d"`
|
||||||
|
AvgStock180 float64 `json:"avg_stock_180d"`
|
||||||
|
AvgStock365 float64 `json:"avg_stock_365d"`
|
||||||
|
AvgStockTotal float64 `json:"avg_stock_total"`
|
||||||
StockDays90 float64 `json:"stock_days_90d"`
|
StockDays90 float64 `json:"stock_days_90d"`
|
||||||
StockDays180 float64 `json:"stock_days_180d"`
|
StockDays180 float64 `json:"stock_days_180d"`
|
||||||
StockDays365 float64 `json:"stock_days_365d"`
|
StockDays365 float64 `json:"stock_days_365d"`
|
||||||
@@ -53,10 +57,23 @@ type ProductPerformanceRow struct {
|
|||||||
UnitProfitBase90 float64 `json:"unit_profit_base_90d"`
|
UnitProfitBase90 float64 `json:"unit_profit_base_90d"`
|
||||||
UnitProfitBase180 float64 `json:"unit_profit_base_180d"`
|
UnitProfitBase180 float64 `json:"unit_profit_base_180d"`
|
||||||
MarketCount90 int `json:"market_count_90d"`
|
MarketCount90 int `json:"market_count_90d"`
|
||||||
|
MarketCount180 int `json:"market_count_180d"`
|
||||||
|
MarketCount365 int `json:"market_count_365d"`
|
||||||
|
MarketCountTotal int `json:"market_count_total"`
|
||||||
CustomerCount90 int `json:"customer_count_90d"`
|
CustomerCount90 int `json:"customer_count_90d"`
|
||||||
|
CustomerCount180 int `json:"customer_count_180d"`
|
||||||
|
CustomerCount365 int `json:"customer_count_365d"`
|
||||||
|
CustomerCountTotal int `json:"customer_count_total"`
|
||||||
SalesIndex90 float64 `json:"sales_index_90d"`
|
SalesIndex90 float64 `json:"sales_index_90d"`
|
||||||
|
SalesIndex180 float64 `json:"sales_index_180d"`
|
||||||
|
SalesIndex365 float64 `json:"sales_index_365d"`
|
||||||
|
SalesIndexTotal float64 `json:"sales_index_total"`
|
||||||
PriceIndex90 float64 `json:"price_index_90d"`
|
PriceIndex90 float64 `json:"price_index_90d"`
|
||||||
MarginIndex90 float64 `json:"margin_index_90d"`
|
MarginIndex90 float64 `json:"margin_index_90d"`
|
||||||
|
PerformanceScore90 float64 `json:"performance_score_90d"`
|
||||||
|
PerformanceScore180 float64 `json:"performance_score_180d"`
|
||||||
|
PerformanceScore365 float64 `json:"performance_score_365d"`
|
||||||
|
PerformanceScoreTotal float64 `json:"performance_score_total"`
|
||||||
PerformanceScore float64 `json:"performance_score"`
|
PerformanceScore float64 `json:"performance_score"`
|
||||||
PerformanceBucket string `json:"performance_bucket"`
|
PerformanceBucket string `json:"performance_bucket"`
|
||||||
Recommendation string `json:"recommendation"`
|
Recommendation string `json:"recommendation"`
|
||||||
@@ -378,6 +395,10 @@ type ProductPerformanceSalesBreakdownRow struct {
|
|||||||
GrossMarginBaseTotal float64 `json:"gross_margin_base_total"`
|
GrossMarginBaseTotal float64 `json:"gross_margin_base_total"`
|
||||||
GrossMarginCostTotal float64 `json:"gross_margin_cost_total"`
|
GrossMarginCostTotal float64 `json:"gross_margin_cost_total"`
|
||||||
StockQty float64 `json:"stock_qty"`
|
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"`
|
StockTurnover90 float64 `json:"stock_turnover_90d"`
|
||||||
StockTurnover180 float64 `json:"stock_turnover_180d"`
|
StockTurnover180 float64 `json:"stock_turnover_180d"`
|
||||||
StockTurnover365 float64 `json:"stock_turnover_365d"`
|
StockTurnover365 float64 `json:"stock_turnover_365d"`
|
||||||
|
|||||||
@@ -12,11 +12,18 @@ type ProductionNoCostProductRow struct {
|
|||||||
SKullaniciAdi string `json:"sKullaniciAdi"`
|
SKullaniciAdi string `json:"sKullaniciAdi"`
|
||||||
SKullaniciGunc string `json:"sKullaniciAdiGunc"`
|
SKullaniciGunc string `json:"sKullaniciAdiGunc"`
|
||||||
MMiktarG float64 `json:"lMMiktar_G"`
|
MMiktarG float64 `json:"lMMiktar_G"`
|
||||||
|
CeketDepoGiris float64 `json:"ceketDepoGiris"`
|
||||||
|
PantolonDepoGiris float64 `json:"pantolonDepoGiris"`
|
||||||
|
YelekDepoGiris float64 `json:"yelekDepoGiris"`
|
||||||
MModelKodu string `json:"sMModelKodu"`
|
MModelKodu string `json:"sMModelKodu"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductionHasCostProductRow struct {
|
type ProductionHasCostProductRow struct {
|
||||||
UretimSekli string `json:"UretimSekli"`
|
UretimSekli string `json:"UretimSekli"`
|
||||||
|
UrtSiparisNo string `json:"nUrtSiparisNo"`
|
||||||
|
CeketDepoGiris float64 `json:"ceketDepoGiris"`
|
||||||
|
PantolonDepoGiris float64 `json:"pantolonDepoGiris"`
|
||||||
|
YelekDepoGiris float64 `json:"yelekDepoGiris"`
|
||||||
NOnMLNo string `json:"nOnMLNo"`
|
NOnMLNo string `json:"nOnMLNo"`
|
||||||
UrunKodu string `json:"UrunKodu"`
|
UrunKodu string `json:"UrunKodu"`
|
||||||
UrunAdi string `json:"UrunAdi"`
|
UrunAdi string `json:"UrunAdi"`
|
||||||
|
|||||||
+820
-203
@@ -481,6 +481,10 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
|||||||
sales_usd_365d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
sales_usd_365d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||||
avg_daily_sales_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
avg_daily_sales_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
|
avg_stock_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
|
avg_stock_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
|
avg_stock_365d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
|
avg_stock_total NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
stock_days_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
stock_days_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
avg_price_usd_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
avg_price_usd_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
@@ -497,8 +501,17 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
|||||||
unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
market_count_90d INTEGER NOT NULL DEFAULT 0,
|
market_count_90d INTEGER NOT NULL DEFAULT 0,
|
||||||
|
market_count_180d INTEGER NOT NULL DEFAULT 0,
|
||||||
|
market_count_365d INTEGER NOT NULL DEFAULT 0,
|
||||||
|
market_count_total INTEGER NOT NULL DEFAULT 0,
|
||||||
customer_count_90d INTEGER NOT NULL DEFAULT 0,
|
customer_count_90d INTEGER NOT NULL DEFAULT 0,
|
||||||
|
customer_count_180d INTEGER NOT NULL DEFAULT 0,
|
||||||
|
customer_count_365d INTEGER NOT NULL DEFAULT 0,
|
||||||
|
customer_count_total INTEGER NOT NULL DEFAULT 0,
|
||||||
sales_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
sales_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
|
sales_index_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
|
sales_index_365d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
|
sales_index_total NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
price_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
price_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
margin_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
margin_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
performance_score NUMERIC(18,6) NOT NULL DEFAULT 0,
|
performance_score NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||||
@@ -552,6 +565,10 @@ WHERE btrim(askili_yan) = '-'`,
|
|||||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
@@ -567,6 +584,15 @@ WHERE btrim(askili_yan) = '-'`,
|
|||||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_90d INTEGER NOT NULL DEFAULT 0`,
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_90d INTEGER NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_180d INTEGER NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_365d INTEGER NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_total INTEGER NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_180d INTEGER NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_365d INTEGER NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_total INTEGER NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||||
`
|
`
|
||||||
CREATE TABLE IF NOT EXISTS mk_product_performance_report_snapshot (
|
CREATE TABLE IF NOT EXISTS mk_product_performance_report_snapshot (
|
||||||
report_key TEXT NOT NULL,
|
report_key TEXT NOT NULL,
|
||||||
@@ -1072,6 +1098,9 @@ func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB)
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
log.Printf("[ProductPerformanceRefresh] grouped snapshot definitions ready count=%d", len(defs))
|
log.Printf("[ProductPerformanceRefresh] grouped snapshot definitions ready count=%d", len(defs))
|
||||||
|
if err := deleteStaleProductPerformanceGroupedSnapshots(ctx, pg, productPerformanceSnapshotKey("grouped", "product_detail")+":%"); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
total := 0
|
total := 0
|
||||||
for i, def := range defs {
|
for i, def := range defs {
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
@@ -1107,41 +1136,33 @@ func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB)
|
|||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteStaleProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB, reportKeyLike string) error {
|
||||||
|
reportKeyLike = strings.TrimSpace(reportKeyLike)
|
||||||
|
if reportKeyLike == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := pg.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot WHERE report_key LIKE $1`, reportKeyLike); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := pg.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot_meta WHERE report_key LIKE $1`, reportKeyLike); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func productPerformanceGroupedSnapshotDefinitions(ctx context.Context, pg *sql.DB) ([]productPerformanceGroupedSnapshotDefinition, error) {
|
func productPerformanceGroupedSnapshotDefinitions(ctx context.Context, pg *sql.DB) ([]productPerformanceGroupedSnapshotDefinition, error) {
|
||||||
|
_ = ctx
|
||||||
|
_ = pg
|
||||||
defs := []productPerformanceGroupedSnapshotDefinition{
|
defs := []productPerformanceGroupedSnapshotDefinition{
|
||||||
{Mode: "products", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key"}},
|
{Mode: "products", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key"}},
|
||||||
{Mode: "products", Levels: []string{"urun_ana_grubu"}},
|
{Mode: "products", Levels: []string{"urun_ana_grubu"}},
|
||||||
{Mode: "idle", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
{Mode: "idle", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
||||||
{Mode: "sales_color_yaka_market_customer", Levels: []string{"color_yaka", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "country", "market_key", "customer_segment", "customer_code", "customer_name"}},
|
{Mode: "sales_color_yaka_market_customer", Levels: []string{"urun_ana_grubu", "color_yaka", "market_key", "customer_code", "customer_name", "urun_alt_grubu", "product_code"}},
|
||||||
{Mode: "sales_product_country_segment_market_customer", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "country", "customer_segment", "market_key", "customer_code", "customer_name"}},
|
{Mode: "sales_product_country_segment_market_customer", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "country", "customer_segment", "market_key", "customer_code", "customer_name"}},
|
||||||
{Mode: "sales_country_segment_market_customer_product", Levels: []string{"country", "customer_segment", "market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
{Mode: "sales_country_segment_market_customer_product", Levels: []string{"country", "customer_segment", "market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
||||||
{Mode: "order_product_customers", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key", "customer_code", "customer_name"}},
|
{Mode: "order_product_customers", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key", "customer_code", "customer_name"}},
|
||||||
{Mode: "order_market_details", Levels: []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
{Mode: "order_market_details", Levels: []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
||||||
}
|
}
|
||||||
productRows, ok, err := loadProductPerformanceSnapshotMapRows(ctx, pg, productPerformanceSnapshotKey("products"), 50000)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
mainGroups := make([]string, 0)
|
|
||||||
seen := map[string]bool{}
|
|
||||||
for _, row := range productRows {
|
|
||||||
mainGroup := productPerformanceGroupedFilterValue(row, "urun_ana_grubu")
|
|
||||||
if mainGroup == "" || seen[mainGroup] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[mainGroup] = true
|
|
||||||
mainGroups = append(mainGroups, mainGroup)
|
|
||||||
}
|
|
||||||
sort.Strings(mainGroups)
|
|
||||||
for _, mainGroup := range mainGroups {
|
|
||||||
defs = append(defs, productPerformanceGroupedSnapshotDefinition{
|
|
||||||
Mode: "product_detail",
|
|
||||||
Levels: []string{"urun_alt_grubu", "product_code", "color_yaka", "market_key"},
|
|
||||||
MainGroup: mainGroup,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defs, nil
|
return defs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1775,11 +1796,15 @@ SELECT
|
|||||||
sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total,
|
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,
|
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_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_days_90d, stock_days_180d, stock_days_365d, stock_days_total,
|
||||||
stock_turnover_90d, stock_turnover_180d, stock_turnover_365d, stock_turnover_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,
|
avg_price_usd_90d, avg_price_usd_180d, cost_price_usd, base_price_usd, base_price_try,
|
||||||
gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d,
|
gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d,
|
||||||
unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d, market_count_90d, customer_count_90d, sales_index_90d,
|
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,
|
price_index_90d, margin_index_90d, performance_score, performance_bucket,
|
||||||
recommendation, COALESCE(to_char(last_sale_date,'YYYY-MM-DD'),''), last_ref_number,
|
recommendation, COALESCE(to_char(last_sale_date,'YYYY-MM-DD'),''), last_ref_number,
|
||||||
to_char(updated_at,'YYYY-MM-DD HH24:MI:SS')
|
to_char(updated_at,'YYYY-MM-DD HH24:MI:SS')
|
||||||
@@ -1801,11 +1826,15 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
|||||||
&r.SalesQty30, &r.SalesQty90, &r.SalesQty180, &r.SalesQty365, &r.SalesQty730, &r.SalesQtyTotal,
|
&r.SalesQty30, &r.SalesQty90, &r.SalesQty180, &r.SalesQty365, &r.SalesQty730, &r.SalesQtyTotal,
|
||||||
&r.SalesUSD30, &r.SalesUSD90, &r.SalesUSD180, &r.SalesUSD365, &r.SalesUSDTotal,
|
&r.SalesUSD30, &r.SalesUSD90, &r.SalesUSD180, &r.SalesUSD365, &r.SalesUSDTotal,
|
||||||
&r.AvgDailySales90, &r.AvgDailySales180, &r.AvgDailySales365, &r.AvgDailySalesTotal,
|
&r.AvgDailySales90, &r.AvgDailySales180, &r.AvgDailySales365, &r.AvgDailySalesTotal,
|
||||||
|
&r.AvgStock90, &r.AvgStock180, &r.AvgStock365, &r.AvgStockTotal,
|
||||||
&r.StockDays90, &r.StockDays180, &r.StockDays365, &r.StockDaysTotal,
|
&r.StockDays90, &r.StockDays180, &r.StockDays365, &r.StockDaysTotal,
|
||||||
&r.StockTurnover90, &r.StockTurnover180, &r.StockTurnover365, &r.StockTurnoverTotal,
|
&r.StockTurnover90, &r.StockTurnover180, &r.StockTurnover365, &r.StockTurnoverTotal,
|
||||||
&r.AvgPriceUSD90, &r.AvgPriceUSD180, &r.CostPriceUSD, &r.BasePriceUSD, &r.BasePriceTRY,
|
&r.AvgPriceUSD90, &r.AvgPriceUSD180, &r.CostPriceUSD, &r.BasePriceUSD, &r.BasePriceTRY,
|
||||||
&r.GrossProfitUSD90, &r.GrossProfitUSD180, &r.GrossMargin90, &r.GrossMargin180,
|
&r.GrossProfitUSD90, &r.GrossProfitUSD180, &r.GrossMargin90, &r.GrossMargin180,
|
||||||
&r.UnitProfitCost90, &r.UnitProfitCost180, &r.UnitProfitBase90, &r.UnitProfitBase180, &r.MarketCount90, &r.CustomerCount90, &r.SalesIndex90,
|
&r.UnitProfitCost90, &r.UnitProfitCost180, &r.UnitProfitBase90, &r.UnitProfitBase180,
|
||||||
|
&r.MarketCount90, &r.MarketCount180, &r.MarketCount365, &r.MarketCountTotal,
|
||||||
|
&r.CustomerCount90, &r.CustomerCount180, &r.CustomerCount365, &r.CustomerCountTotal,
|
||||||
|
&r.SalesIndex90, &r.SalesIndex180, &r.SalesIndex365, &r.SalesIndexTotal,
|
||||||
&r.PriceIndex90, &r.MarginIndex90, &r.PerformanceScore, &r.PerformanceBucket,
|
&r.PriceIndex90, &r.MarginIndex90, &r.PerformanceScore, &r.PerformanceBucket,
|
||||||
&r.Recommendation, &r.LastSaleDate, &r.LastRefNumber, &r.UpdatedAt,
|
&r.Recommendation, &r.LastSaleDate, &r.LastRefNumber, &r.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -1829,37 +1858,109 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
|||||||
func applyProductPerformanceProductRowScores(rows []models.ProductPerformanceRow) {
|
func applyProductPerformanceProductRowScores(rows []models.ProductPerformanceRow) {
|
||||||
avg := productPerformanceProductRowAverages(rows)
|
avg := productPerformanceProductRowAverages(rows)
|
||||||
for i := range rows {
|
for i := range rows {
|
||||||
rows[i].PerformanceScore = productPerformanceProductScore(
|
rows[i].SalesIndex90 = productPerformanceRelativeIndex(rows[i].SalesUSD90, avg.salesUSD90)
|
||||||
|
rows[i].SalesIndex180 = productPerformanceRelativeIndex(rows[i].SalesUSD180, avg.salesUSD180)
|
||||||
|
rows[i].SalesIndex365 = productPerformanceRelativeIndex(rows[i].SalesUSD365, avg.salesUSD365)
|
||||||
|
rows[i].SalesIndexTotal = productPerformanceRelativeIndex(rows[i].SalesUSDTotal, avg.salesUSDTotal)
|
||||||
|
|
||||||
|
margin365 := productPerformanceMarginFromSales(rows[i].SalesUSD365, rows[i].SalesQty365, rows[i].CostPriceUSD)
|
||||||
|
marginTotal := productPerformanceMarginFromSales(rows[i].SalesUSDTotal, rows[i].SalesQtyTotal, rows[i].CostPriceUSD)
|
||||||
|
|
||||||
|
rows[i].PerformanceScore90 = productPerformanceProductScore(
|
||||||
"90d",
|
"90d",
|
||||||
rows[i].SalesUSD90,
|
rows[i].SalesUSD90,
|
||||||
productPerformanceRelativeIndex(rows[i].SalesUSD90, avg.salesUSD90),
|
rows[i].SalesIndex90,
|
||||||
rows[i].GrossMargin90,
|
rows[i].GrossMargin90,
|
||||||
rows[i].StockTurnover90,
|
rows[i].StockTurnover90,
|
||||||
float64(rows[i].MarketCount90),
|
float64(rows[i].MarketCount90),
|
||||||
float64(rows[i].CustomerCount90),
|
float64(rows[i].CustomerCount90),
|
||||||
)
|
)
|
||||||
|
rows[i].PerformanceScore180 = productPerformanceProductScore(
|
||||||
|
"180d",
|
||||||
|
rows[i].SalesUSD180,
|
||||||
|
rows[i].SalesIndex180,
|
||||||
|
rows[i].GrossMargin180,
|
||||||
|
rows[i].StockTurnover180,
|
||||||
|
float64(rows[i].MarketCount180),
|
||||||
|
float64(rows[i].CustomerCount180),
|
||||||
|
)
|
||||||
|
rows[i].PerformanceScore365 = productPerformanceProductScore(
|
||||||
|
"365d",
|
||||||
|
rows[i].SalesUSD365,
|
||||||
|
rows[i].SalesIndex365,
|
||||||
|
margin365,
|
||||||
|
rows[i].StockTurnover365,
|
||||||
|
float64(rows[i].MarketCount365),
|
||||||
|
float64(rows[i].CustomerCount365),
|
||||||
|
)
|
||||||
|
rows[i].PerformanceScoreTotal = productPerformanceProductScore(
|
||||||
|
"total",
|
||||||
|
rows[i].SalesUSDTotal,
|
||||||
|
rows[i].SalesIndexTotal,
|
||||||
|
marginTotal,
|
||||||
|
rows[i].StockTurnoverTotal,
|
||||||
|
float64(rows[i].MarketCountTotal),
|
||||||
|
float64(rows[i].CustomerCountTotal),
|
||||||
|
productPerformanceProductRowTotalPeriodDays(rows[i]),
|
||||||
|
)
|
||||||
|
rows[i].PerformanceScore = rows[i].PerformanceScore90
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type productPerformanceProductRowAverage struct {
|
type productPerformanceProductRowAverage struct {
|
||||||
salesUSD90 float64
|
salesUSD90 float64
|
||||||
|
salesUSD180 float64
|
||||||
|
salesUSD365 float64
|
||||||
|
salesUSDTotal float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceProductRowAverages(rows []models.ProductPerformanceRow) productPerformanceProductRowAverage {
|
func productPerformanceProductRowAverages(rows []models.ProductPerformanceRow) productPerformanceProductRowAverage {
|
||||||
var sum90, count90 float64
|
var sum90, count90, sum180, count180, sum365, count365, sumTotal, countTotal float64
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
if row.SalesUSD90 > 0 {
|
if row.SalesUSD90 > 0 {
|
||||||
sum90 += row.SalesUSD90
|
sum90 += row.SalesUSD90
|
||||||
count90++
|
count90++
|
||||||
}
|
}
|
||||||
|
if row.SalesUSD180 > 0 {
|
||||||
|
sum180 += row.SalesUSD180
|
||||||
|
count180++
|
||||||
|
}
|
||||||
|
if row.SalesUSD365 > 0 {
|
||||||
|
sum365 += row.SalesUSD365
|
||||||
|
count365++
|
||||||
|
}
|
||||||
|
if row.SalesUSDTotal > 0 {
|
||||||
|
sumTotal += row.SalesUSDTotal
|
||||||
|
countTotal++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out := productPerformanceProductRowAverage{}
|
out := productPerformanceProductRowAverage{}
|
||||||
if count90 > 0 {
|
if count90 > 0 {
|
||||||
out.salesUSD90 = sum90 / count90
|
out.salesUSD90 = sum90 / count90
|
||||||
}
|
}
|
||||||
|
if count180 > 0 {
|
||||||
|
out.salesUSD180 = sum180 / count180
|
||||||
|
}
|
||||||
|
if count365 > 0 {
|
||||||
|
out.salesUSD365 = sum365 / count365
|
||||||
|
}
|
||||||
|
if countTotal > 0 {
|
||||||
|
out.salesUSDTotal = sumTotal / countTotal
|
||||||
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func productPerformanceProductRowTotalPeriodDays(row models.ProductPerformanceRow) float64 {
|
||||||
|
return productPerformancePeriodDays(map[string]any{"kpi_date": row.KpiDate}, "total")
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceMarginFromSales(salesUSD, salesQty, unitCost float64) float64 {
|
||||||
|
if salesUSD <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return (salesUSD - (salesQty * unitCost)) / salesUSD
|
||||||
|
}
|
||||||
|
|
||||||
func GetProductPerformanceSummary(ctx context.Context, pg *sql.DB) (models.ProductPerformanceSummary, error) {
|
func GetProductPerformanceSummary(ctx context.Context, pg *sql.DB) (models.ProductPerformanceSummary, error) {
|
||||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||||
return models.ProductPerformanceSummary{}, err
|
return models.ProductPerformanceSummary{}, err
|
||||||
@@ -2190,7 +2291,7 @@ SELECT
|
|||||||
WHEN sales_qty_total > 0 AND stock_qty <= 0 AND sales_index_total >= 1 THEN 'STOKSUZ_TALEP'
|
WHEN sales_qty_total > 0 AND stock_qty <= 0 AND sales_index_total >= 1 THEN 'STOKSUZ_TALEP'
|
||||||
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'STOK_RISKI'
|
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'STOK_RISKI'
|
||||||
WHEN gross_margin_total < 0 THEN 'FIYAT_BASKISI'
|
WHEN gross_margin_total < 0 THEN 'FIYAT_BASKISI'
|
||||||
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.25 THEN 'YILDIZ_URUN'
|
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.45 THEN 'YILDIZ_URUN'
|
||||||
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'FIYAT_FIRSATI'
|
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'FIYAT_FIRSATI'
|
||||||
ELSE 'TAKIP'
|
ELSE 'TAKIP'
|
||||||
END AS performance_bucket,
|
END AS performance_bucket,
|
||||||
@@ -2198,7 +2299,7 @@ SELECT
|
|||||||
WHEN sales_qty_total > 0 AND stock_qty <= 0 THEN 'Talep var, stok yok. Uretim/satin alma onceligi ver.'
|
WHEN sales_qty_total > 0 AND stock_qty <= 0 THEN 'Talep var, stok yok. Uretim/satin alma onceligi ver.'
|
||||||
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'Satis yok, stok maliyeti tasiyor. Piyasa/fiyat aksiyonu gerekli.'
|
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'Satis yok, stok maliyeti tasiyor. Piyasa/fiyat aksiyonu gerekli.'
|
||||||
WHEN gross_margin_total < 0 THEN 'Ciplak maliyet altinda satis var. Fiyat veya maliyet kontrol edilmeli.'
|
WHEN gross_margin_total < 0 THEN 'Ciplak maliyet altinda satis var. Fiyat veya maliyet kontrol edilmeli.'
|
||||||
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.25 THEN 'Genel donemde guclu urun. Stok ve fiyat korunmali.'
|
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.45 THEN 'Genel donemde guclu urun. Stok ve fiyat korunmali.'
|
||||||
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'Karli ama yavas. Dogru piyasada satis firsati var.'
|
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'Karli ama yavas. Dogru piyasada satis firsati var.'
|
||||||
ELSE 'Izleme ve piyasa bazli aksiyon.'
|
ELSE 'Izleme ve piyasa bazli aksiyon.'
|
||||||
END AS recommendation,
|
END AS recommendation,
|
||||||
@@ -3485,6 +3586,19 @@ StockTotalStart AS (
|
|||||||
WHERE stock_date <= DATE '2022-01-01'
|
WHERE stock_date <= DATE '2022-01-01'
|
||||||
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
|
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
|
||||||
),
|
),
|
||||||
|
FirstSaleByProduct AS (
|
||||||
|
SELECT
|
||||||
|
product_code,
|
||||||
|
color_code,
|
||||||
|
yaka_kodu,
|
||||||
|
MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days') AS first_sale_date_90d,
|
||||||
|
MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days') AS first_sale_date_180d,
|
||||||
|
MIN(sales_date) FILTER (WHERE sales_date >= current_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
|
||||||
|
FROM mk_product_performance_sales_daily
|
||||||
|
WHERE sales_date >= DATE '2022-01-01'
|
||||||
|
GROUP BY product_code, color_code, yaka_kodu
|
||||||
|
),
|
||||||
Agg AS (
|
Agg AS (
|
||||||
SELECT
|
SELECT
|
||||||
$2::text AS breakdown,
|
$2::text AS breakdown,
|
||||||
@@ -3587,18 +3701,55 @@ Agg AS (
|
|||||||
COALESCE(pd.base_price_usd,0) AS base_price_usd,
|
COALESCE(pd.base_price_usd,0) AS base_price_usd,
|
||||||
COALESCE(pd.cost_price_usd,0) AS cost_price_usd,
|
COALESCE(pd.cost_price_usd,0) AS cost_price_usd,
|
||||||
COALESCE(st.stock_qty,0) AS stock_qty,
|
COALESCE(st.stock_qty,0) AS stock_qty,
|
||||||
(COALESCE(s90.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_90d,
|
(COALESCE(s90.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_90d,
|
||||||
(COALESCE(s180.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_180d,
|
(COALESCE(s180.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_180d,
|
||||||
(COALESCE(s365.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_365d,
|
(COALESCE(s365.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_365d,
|
||||||
(COALESCE(stotal.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_total
|
(COALESCE(stotal.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_total
|
||||||
FROM mk_product_performance_sales_daily s
|
FROM mk_product_performance_sales_daily s
|
||||||
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code = s.product_code
|
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code = s.product_code
|
||||||
LEFT JOIN LatestKPI k ON k.product_code = s.product_code AND k.color_code = s.color_code AND k.yaka_kodu = s.yaka_kodu
|
LEFT JOIN LatestKPI k ON k.product_code = s.product_code AND k.color_code = s.color_code AND k.yaka_kodu = s.yaka_kodu
|
||||||
|
LEFT JOIN FirstSaleByProduct fs ON fs.product_code = s.product_code AND fs.color_code = s.color_code AND fs.yaka_kodu = s.yaka_kodu
|
||||||
LEFT JOIN StockAgg st ON st.product_code = s.product_code AND st.color_code = s.color_code AND st.yaka_kodu = s.yaka_kodu
|
LEFT JOIN StockAgg st ON st.product_code = s.product_code AND st.color_code = s.color_code AND st.yaka_kodu = s.yaka_kodu
|
||||||
LEFT JOIN Stock90Start s90 ON s90.product_code = s.product_code AND s90.color_code = s.color_code AND s90.yaka_kodu = s.yaka_kodu
|
LEFT JOIN LATERAL (
|
||||||
LEFT JOIN Stock180Start s180 ON s180.product_code = s.product_code AND s180.color_code = s.color_code AND s180.yaka_kodu = s.yaka_kodu
|
SELECT x.stock_qty
|
||||||
LEFT JOIN Stock365Start s365 ON s365.product_code = s.product_code AND s365.color_code = s.color_code AND s365.yaka_kodu = s.yaka_kodu
|
FROM mk_product_performance_stock_daily x
|
||||||
LEFT JOIN StockTotalStart stotal ON stotal.product_code = s.product_code AND stotal.color_code = s.color_code AND stotal.yaka_kodu = s.yaka_kodu
|
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(fs.first_sale_date_90d, current_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(fs.first_sale_date_180d, current_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(fs.first_sale_date_365d, current_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(fs.first_sale_date_total, DATE '2022-01-01')
|
||||||
|
ORDER BY x.stock_date DESC
|
||||||
|
LIMIT 1
|
||||||
|
) stotal ON TRUE
|
||||||
) s
|
) s
|
||||||
WHERE sales_date >= DATE '2022-01-01'
|
WHERE sales_date >= DATE '2022-01-01'
|
||||||
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')
|
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')
|
||||||
@@ -3704,16 +3855,20 @@ SELECT
|
|||||||
gross_profit_cost_usd_total,
|
gross_profit_cost_usd_total,
|
||||||
gross_margin_base_total,
|
gross_margin_base_total,
|
||||||
gross_margin_cost_total,
|
gross_margin_cost_total,
|
||||||
CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END AS stock_turnover_90d,
|
avg_stock_90d,
|
||||||
CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) ELSE 0 END AS stock_turnover_180d,
|
avg_stock_180d,
|
||||||
|
avg_stock_365d,
|
||||||
|
avg_stock_total,
|
||||||
|
CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END AS stock_turnover_90d,
|
||||||
|
CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) * 2.0 ELSE 0 END AS stock_turnover_180d,
|
||||||
CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END AS stock_turnover_365d,
|
CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END AS stock_turnover_365d,
|
||||||
CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) ELSE 0 END AS stock_turnover_total,
|
CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) * 360.0 / GREATEST(1, (current_date - DATE '2022-01-01') + 1) ELSE 0 END AS stock_turnover_total,
|
||||||
has_cost,
|
has_cost,
|
||||||
sales_index_90d,
|
sales_index_90d,
|
||||||
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
|
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
|
||||||
ROUND((
|
ROUND((
|
||||||
LEAST(35, sales_usd_90d / 25000 * 35)
|
LEAST(35, sales_usd_90d / 25000 * 35)
|
||||||
+ LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30)
|
+ LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.45 * 30)
|
||||||
+ LEAST(20, product_group_count_90d / 8.0 * 20)
|
+ LEAST(20, product_group_count_90d / 8.0 * 20)
|
||||||
+ LEAST(15, sales_qty_90d / 500 * 15)
|
+ LEAST(15, sales_qty_90d / 500 * 15)
|
||||||
)::numeric, 4)
|
)::numeric, 4)
|
||||||
@@ -3721,7 +3876,7 @@ SELECT
|
|||||||
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_180d <= 0 THEN 1 ELSE
|
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_180d <= 0 THEN 1 ELSE
|
||||||
ROUND((
|
ROUND((
|
||||||
LEAST(35, sales_usd_180d / 50000 * 35)
|
LEAST(35, sales_usd_180d / 50000 * 35)
|
||||||
+ LEAST(30, GREATEST(gross_margin_cost_180d,0) / 0.40 * 30)
|
+ LEAST(30, GREATEST(gross_margin_cost_180d,0) / 0.45 * 30)
|
||||||
+ LEAST(20, product_group_count_180d / 8.0 * 20)
|
+ LEAST(20, product_group_count_180d / 8.0 * 20)
|
||||||
+ LEAST(15, sales_qty_180d / 1000 * 15)
|
+ LEAST(15, sales_qty_180d / 1000 * 15)
|
||||||
)::numeric, 4)
|
)::numeric, 4)
|
||||||
@@ -3729,7 +3884,7 @@ SELECT
|
|||||||
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_365d <= 0 THEN 1 ELSE
|
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_365d <= 0 THEN 1 ELSE
|
||||||
ROUND((
|
ROUND((
|
||||||
LEAST(35, sales_usd_365d / 100000 * 35)
|
LEAST(35, sales_usd_365d / 100000 * 35)
|
||||||
+ LEAST(30, GREATEST(gross_margin_cost_365d,0) / 0.40 * 30)
|
+ LEAST(30, GREATEST(gross_margin_cost_365d,0) / 0.45 * 30)
|
||||||
+ LEAST(20, product_group_count_365d / 8.0 * 20)
|
+ LEAST(20, product_group_count_365d / 8.0 * 20)
|
||||||
+ LEAST(15, sales_qty_365d / 2000 * 15)
|
+ LEAST(15, sales_qty_365d / 2000 * 15)
|
||||||
)::numeric, 4)
|
)::numeric, 4)
|
||||||
@@ -3737,18 +3892,18 @@ SELECT
|
|||||||
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_total <= 0 THEN 1 ELSE
|
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_total <= 0 THEN 1 ELSE
|
||||||
ROUND((
|
ROUND((
|
||||||
LEAST(35, sales_usd_total / 300000 * 35)
|
LEAST(35, sales_usd_total / 300000 * 35)
|
||||||
+ LEAST(30, GREATEST(gross_margin_cost_total,0) / 0.40 * 30)
|
+ LEAST(30, GREATEST(gross_margin_cost_total,0) / 0.45 * 30)
|
||||||
+ LEAST(20, product_group_count_total / 8.0 * 20)
|
+ LEAST(20, product_group_count_total / 8.0 * 20)
|
||||||
+ LEAST(15, sales_qty_total / 6000 * 15)
|
+ LEAST(15, sales_qty_total / 6000 * 15)
|
||||||
)::numeric, 4)
|
)::numeric, 4)
|
||||||
END AS customer_score_total,
|
END AS customer_score_total,
|
||||||
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
|
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
|
||||||
ROUND(
|
ROUND(
|
||||||
LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30)
|
LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.45 * 30)
|
||||||
+ LEAST(20, (CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END) / 1.5 * 20)
|
+ LEAST(20, (CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END) / 4.0 * 20)
|
||||||
+ LEAST(20, sales_usd_90d / 10000 * 20)
|
+ LEAST(20, sales_usd_90d / 25000 * 20)
|
||||||
+ LEAST(15, market_count_90d / 8.0 * 15)
|
+ LEAST(5, market_count_90d / 3.0 * 5)
|
||||||
+ LEAST(15, customer_count_90d / 25.0 * 15),
|
+ LEAST(25, customer_count_90d / 8.0 * 25),
|
||||||
4
|
4
|
||||||
)
|
)
|
||||||
END AS performance_score,
|
END AS performance_score,
|
||||||
@@ -3812,6 +3967,7 @@ LIMIT $1
|
|||||||
&r.GrossMarginCost365, &r.CustomerCount365, &r.InvoiceCount365, &r.CustomerCountTotal, &r.InvoiceCountTotal, &r.SalesQtyTotal, &r.SalesUSDTotal,
|
&r.GrossMarginCost365, &r.CustomerCount365, &r.InvoiceCount365, &r.CustomerCountTotal, &r.InvoiceCountTotal, &r.SalesQtyTotal, &r.SalesUSDTotal,
|
||||||
&r.AvgPriceUSDTotal, &r.BasePriceUSDTotal, &r.CostPriceUSDTotal, &r.GrossProfitBaseTotal,
|
&r.AvgPriceUSDTotal, &r.BasePriceUSDTotal, &r.CostPriceUSDTotal, &r.GrossProfitBaseTotal,
|
||||||
&r.GrossProfitCostTotal, &r.GrossMarginBaseTotal, &r.GrossMarginCostTotal,
|
&r.GrossProfitCostTotal, &r.GrossMarginBaseTotal, &r.GrossMarginCostTotal,
|
||||||
|
&r.AvgStock90, &r.AvgStock180, &r.AvgStock365, &r.AvgStockTotal,
|
||||||
&r.StockTurnover90, &r.StockTurnover180, &r.StockTurnover365, &r.StockTurnoverTotal, &r.HasCost,
|
&r.StockTurnover90, &r.StockTurnover180, &r.StockTurnover365, &r.StockTurnoverTotal, &r.HasCost,
|
||||||
&r.SalesIndex90, &r.CustomerScore90, &r.CustomerScore180, &r.CustomerScore365, &r.CustomerScoreTotal, &r.PerformanceScore,
|
&r.SalesIndex90, &r.CustomerScore90, &r.CustomerScore180, &r.CustomerScore365, &r.CustomerScoreTotal, &r.PerformanceScore,
|
||||||
&r.PerformanceBucket, &r.Recommendation, &r.LastSaleDate,
|
&r.PerformanceBucket, &r.Recommendation, &r.LastSaleDate,
|
||||||
@@ -3920,6 +4076,7 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
|
|||||||
sourceRows = filterProductPerformanceGroupedRows(sourceRows, productPerformanceGroupedEffectiveFilters(req))
|
sourceRows = filterProductPerformanceGroupedRows(sourceRows, productPerformanceGroupedEffectiveFilters(req))
|
||||||
out := make([]map[string]any, 0, len(sourceRows))
|
out := make([]map[string]any, 0, len(sourceRows))
|
||||||
appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys, req.ExpandThroughLevel)
|
appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys, req.ExpandThroughLevel)
|
||||||
|
out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
|
||||||
if len(out) > 0 || !productPerformanceLiveFallbackEnabled() {
|
if len(out) > 0 || !productPerformanceLiveFallbackEnabled() {
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
@@ -4038,6 +4195,9 @@ func loadProductPerformancePreparedGroupedRows(ctx context.Context, pg *sql.DB,
|
|||||||
}
|
}
|
||||||
effectiveFilters := productPerformancePreparedGroupedEffectiveFilters(req)
|
effectiveFilters := productPerformancePreparedGroupedEffectiveFilters(req)
|
||||||
hasFilters := len(effectiveFilters) > 0
|
hasFilters := len(effectiveFilters) > 0
|
||||||
|
if hasFilters {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
hasManualExpansion := len(req.ExpandedKeys) > 0
|
hasManualExpansion := len(req.ExpandedKeys) > 0
|
||||||
query := `
|
query := `
|
||||||
SELECT payload
|
SELECT payload
|
||||||
@@ -4098,7 +4258,6 @@ SELECT EXISTS (
|
|||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out = filterProductPerformancePreparedGroupedRows(out, effectiveFilters)
|
|
||||||
out = filterProductPerformanceGroupedRowsForExpansion(out, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
|
out = filterProductPerformanceGroupedRowsForExpansion(out, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
|
||||||
out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
|
out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
|
||||||
return out, true, nil
|
return out, true, nil
|
||||||
@@ -4257,8 +4416,12 @@ func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req Produ
|
|||||||
out := make([]map[string]any, 0, 512)
|
out := make([]map[string]any, 0, 512)
|
||||||
filters := productPerformanceGroupedBaseFilters(req)
|
filters := productPerformanceGroupedBaseFilters(req)
|
||||||
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, filters, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
|
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, filters, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
|
||||||
|
if err != nil {
|
||||||
return out, true, err
|
return out, true, err
|
||||||
}
|
}
|
||||||
|
out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
|
||||||
|
return out, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func productPerformanceGroupedBaseFilters(req ProductPerformanceGroupedRequest) []productPerformanceSQLGroupFilter {
|
func productPerformanceGroupedBaseFilters(req ProductPerformanceGroupedRequest) []productPerformanceSQLGroupFilter {
|
||||||
effectiveFilters := productPerformanceGroupedEffectiveFilters(req)
|
effectiveFilters := productPerformanceGroupedEffectiveFilters(req)
|
||||||
@@ -4335,6 +4498,7 @@ func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out
|
|||||||
row["label"] = value
|
row["label"] = value
|
||||||
row["__group"] = true
|
row["__group"] = true
|
||||||
row[field] = value
|
row[field] = value
|
||||||
|
deriveProductPerformanceGroupMetrics(row, field)
|
||||||
*out = append(*out, row)
|
*out = append(*out, row)
|
||||||
if expandedKeys[key] || level <= expandThroughLevel {
|
if expandedKeys[key] || level <= expandThroughLevel {
|
||||||
nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value})
|
nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value})
|
||||||
@@ -4360,6 +4524,8 @@ SELECT (
|
|||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
'group_value', group_value,
|
'group_value', group_value,
|
||||||
'label', group_value,
|
'label', group_value,
|
||||||
|
'period_start', '2022-01-01',
|
||||||
|
'period_end', COALESCE(to_char((SELECT kpi_date FROM LatestKPIDate),'YYYY-MM-DD'), ''),
|
||||||
'count', row_count,
|
'count', row_count,
|
||||||
'recommendation', CASE WHEN recommendation <> '' THEN recommendation ELSE row_count::text || ' satir' END,
|
'recommendation', CASE WHEN recommendation <> '' THEN recommendation ELSE row_count::text || ' satir' END,
|
||||||
'image_product_code', image_product_code,
|
'image_product_code', image_product_code,
|
||||||
@@ -4378,6 +4544,10 @@ SELECT (
|
|||||||
)
|
)
|
||||||
|| jsonb_build_object(
|
|| jsonb_build_object(
|
||||||
'stock_qty', stock_qty,
|
'stock_qty', stock_qty,
|
||||||
|
'avg_stock_90d', avg_stock_90d,
|
||||||
|
'avg_stock_180d', avg_stock_180d,
|
||||||
|
'avg_stock_365d', avg_stock_365d,
|
||||||
|
'avg_stock_total', avg_stock_total,
|
||||||
'sales_qty_90d', sales_qty_90d,
|
'sales_qty_90d', sales_qty_90d,
|
||||||
'sales_qty_180d', sales_qty_180d,
|
'sales_qty_180d', sales_qty_180d,
|
||||||
'sales_qty_365d', sales_qty_365d,
|
'sales_qty_365d', sales_qty_365d,
|
||||||
@@ -4402,8 +4572,17 @@ SELECT (
|
|||||||
'unit_profit_base_90d', unit_profit_base_90d,
|
'unit_profit_base_90d', unit_profit_base_90d,
|
||||||
'unit_profit_base_180d', unit_profit_base_180d,
|
'unit_profit_base_180d', unit_profit_base_180d,
|
||||||
'market_count_90d', market_count_90d,
|
'market_count_90d', market_count_90d,
|
||||||
|
'market_count_180d', market_count_180d,
|
||||||
|
'market_count_365d', market_count_365d,
|
||||||
|
'market_count_total', market_count_total,
|
||||||
'customer_count_90d', customer_count_90d,
|
'customer_count_90d', customer_count_90d,
|
||||||
|
'customer_count_180d', customer_count_180d,
|
||||||
|
'customer_count_365d', customer_count_365d,
|
||||||
|
'customer_count_total', customer_count_total,
|
||||||
'sales_index_90d', sales_index_90d,
|
'sales_index_90d', sales_index_90d,
|
||||||
|
'sales_index_180d', sales_index_180d,
|
||||||
|
'sales_index_365d', sales_index_365d,
|
||||||
|
'sales_index_total', sales_index_total,
|
||||||
'price_index_90d', price_index_90d,
|
'price_index_90d', price_index_90d,
|
||||||
'margin_index_90d', margin_index_90d,
|
'margin_index_90d', margin_index_90d,
|
||||||
'performance_score', performance_score,
|
'performance_score', performance_score,
|
||||||
@@ -4413,10 +4592,10 @@ SELECT (
|
|||||||
'stock_days_180d', stock_days_180d,
|
'stock_days_180d', stock_days_180d,
|
||||||
'stock_days_365d', stock_days_365d,
|
'stock_days_365d', stock_days_365d,
|
||||||
'stock_days_total', stock_days_total,
|
'stock_days_total', stock_days_total,
|
||||||
'stock_turnover_90d', CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END,
|
'stock_turnover_90d', stock_turnover_90d,
|
||||||
'stock_turnover_180d', CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) ELSE 0 END,
|
'stock_turnover_180d', stock_turnover_180d,
|
||||||
'stock_turnover_365d', CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END,
|
'stock_turnover_365d', stock_turnover_365d,
|
||||||
'stock_turnover_total', CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) ELSE 0 END
|
'stock_turnover_total', stock_turnover_total
|
||||||
)
|
)
|
||||||
) AS row_json
|
) AS row_json
|
||||||
FROM (
|
FROM (
|
||||||
@@ -4481,33 +4660,37 @@ FROM (
|
|||||||
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_cost_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_cost_180d,
|
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_cost_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_cost_180d,
|
||||||
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_base_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS unit_profit_base_90d,
|
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_base_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS unit_profit_base_90d,
|
||||||
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_base_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_base_180d,
|
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_base_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_base_180d,
|
||||||
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_90d,0) > 0)::integer AS market_count_90d,
|
COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_90d,0) > 0)::integer AS market_count_90d,
|
||||||
|
COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_180d,0) > 0)::integer AS market_count_180d,
|
||||||
|
COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_365d,0) > 0)::integer AS market_count_365d,
|
||||||
|
COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_total,0) > 0)::integer AS market_count_total,
|
||||||
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_90d,0) > 0 THEN customer_count_90d ELSE 0 END),0)::integer AS customer_count_90d,
|
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_90d,0) > 0 THEN customer_count_90d ELSE 0 END),0)::integer AS customer_count_90d,
|
||||||
|
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_180d,0) > 0 THEN customer_count_180d ELSE 0 END),0)::integer AS customer_count_180d,
|
||||||
|
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_365d,0) > 0 THEN customer_count_365d ELSE 0 END),0)::integer AS customer_count_365d,
|
||||||
|
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_total,0) > 0 THEN customer_count_total ELSE 0 END),0)::integer AS customer_count_total,
|
||||||
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS sales_index_90d,
|
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS sales_index_90d,
|
||||||
|
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(sales_index_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS sales_index_180d,
|
||||||
|
CASE WHEN SUM(sales_qty_365d) > 0 THEN SUM(sales_index_365d * sales_qty_365d) / NULLIF(SUM(sales_qty_365d),0) ELSE 0 END AS sales_index_365d,
|
||||||
|
CASE WHEN SUM(sales_qty_total) > 0 THEN SUM(sales_index_total * sales_qty_total) / NULLIF(SUM(sales_qty_total),0) ELSE 0 END AS sales_index_total,
|
||||||
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(price_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS price_index_90d,
|
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(price_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS price_index_90d,
|
||||||
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(margin_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS margin_index_90d,
|
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(margin_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS margin_index_90d,
|
||||||
|
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(stock_turnover_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS stock_turnover_90d,
|
||||||
|
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(stock_turnover_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS stock_turnover_180d,
|
||||||
|
CASE WHEN SUM(sales_qty_365d) > 0 THEN SUM(stock_turnover_365d * sales_qty_365d) / NULLIF(SUM(sales_qty_365d),0) ELSE 0 END AS stock_turnover_365d,
|
||||||
|
CASE WHEN SUM(sales_qty_total) > 0 THEN SUM(stock_turnover_total * sales_qty_total) / NULLIF(SUM(sales_qty_total),0) ELSE 0 END AS stock_turnover_total,
|
||||||
CASE
|
CASE
|
||||||
WHEN SUM(CASE
|
WHEN SUM(CASE
|
||||||
WHEN COALESCE(sales_usd_90d,0) > 0 THEN sales_usd_90d
|
|
||||||
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
|
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
|
||||||
WHEN COALESCE(stock_qty,0) > 0 AND COALESCE(cost_price_usd,0) > 0 THEN stock_qty * cost_price_usd
|
ELSE 0
|
||||||
WHEN COALESCE(stock_qty,0) > 0 THEN stock_qty
|
|
||||||
ELSE 1
|
|
||||||
END) > 0
|
END) > 0
|
||||||
THEN SUM(performance_score * CASE
|
THEN SUM(performance_score * CASE
|
||||||
WHEN COALESCE(sales_usd_90d,0) > 0 THEN sales_usd_90d
|
|
||||||
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
|
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
|
||||||
WHEN COALESCE(stock_qty,0) > 0 AND COALESCE(cost_price_usd,0) > 0 THEN stock_qty * cost_price_usd
|
|
||||||
WHEN COALESCE(stock_qty,0) > 0 THEN stock_qty
|
|
||||||
ELSE 1
|
|
||||||
END) / NULLIF(SUM(CASE
|
|
||||||
WHEN COALESCE(sales_usd_90d,0) > 0 THEN sales_usd_90d
|
|
||||||
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
|
|
||||||
WHEN COALESCE(stock_qty,0) > 0 AND COALESCE(cost_price_usd,0) > 0 THEN stock_qty * cost_price_usd
|
|
||||||
WHEN COALESCE(stock_qty,0) > 0 THEN stock_qty
|
|
||||||
ELSE 1
|
|
||||||
END),0)
|
|
||||||
ELSE 0
|
ELSE 0
|
||||||
|
END) / NULLIF(SUM(CASE
|
||||||
|
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
|
||||||
|
ELSE 0
|
||||||
|
END),0)
|
||||||
|
ELSE COALESCE(AVG(performance_score),0)
|
||||||
END AS performance_score,
|
END AS performance_score,
|
||||||
MODE() WITHIN GROUP (ORDER BY performance_bucket) AS performance_bucket,
|
MODE() WITHIN GROUP (ORDER BY performance_bucket) AS performance_bucket,
|
||||||
COALESCE(MODE() WITHIN GROUP (ORDER BY NULLIF(recommendation,'')), '') AS recommendation,
|
COALESCE(MODE() WITHIN GROUP (ORDER BY NULLIF(recommendation,'')), '') AS recommendation,
|
||||||
@@ -4515,6 +4698,11 @@ FROM (
|
|||||||
FROM (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
Source.*,
|
Source.*,
|
||||||
|
btrim(regexp_replace(COALESCE(Source.market_key,''), '^.*[|]', '')) AS display_market_key,
|
||||||
|
CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END AS stock_turnover_90d,
|
||||||
|
CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) * 2.0 ELSE 0 END AS stock_turnover_180d,
|
||||||
|
CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END AS stock_turnover_365d,
|
||||||
|
CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) * 360.0 / GREATEST(1, ((SELECT kpi_date FROM LatestKPIDate) - DATE '2022-01-01') + 1) ELSE 0 END AS stock_turnover_total,
|
||||||
ROW_NUMBER() OVER (
|
ROW_NUMBER() OVER (
|
||||||
PARTITION BY COALESCE(%s, ''), product_code, color_code, yaka_kodu
|
PARTITION BY COALESCE(%s, ''), product_code, color_code, yaka_kodu
|
||||||
ORDER BY performance_score DESC NULLS LAST
|
ORDER BY performance_score DESC NULLS LAST
|
||||||
@@ -4539,10 +4727,10 @@ func queryProductPerformanceSQLLeafRows(ctx context.Context, pg *sql.DB, mode st
|
|||||||
query := productPerformanceSQLSourceCTE(mode) + fmt.Sprintf(`
|
query := productPerformanceSQLSourceCTE(mode) + fmt.Sprintf(`
|
||||||
SELECT to_jsonb(t) || jsonb_build_object(
|
SELECT to_jsonb(t) || jsonb_build_object(
|
||||||
'row_key', 'leaf|' || product_code || '|' || color_code || '|' || yaka_kodu || '|' || market_key,
|
'row_key', 'leaf|' || product_code || '|' || color_code || '|' || yaka_kodu || '|' || market_key,
|
||||||
'stock_turnover_90d', CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END,
|
'stock_turnover_90d', CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END,
|
||||||
'stock_turnover_180d', CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) ELSE 0 END,
|
'stock_turnover_180d', CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) * 2.0 ELSE 0 END,
|
||||||
'stock_turnover_365d', CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END,
|
'stock_turnover_365d', CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END,
|
||||||
'stock_turnover_total', CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) ELSE 0 END
|
'stock_turnover_total', CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) * 360.0 / GREATEST(1, ((SELECT kpi_date FROM LatestKPIDate) - DATE '2022-01-01') + 1) ELSE 0 END
|
||||||
) AS row_json
|
) AS row_json
|
||||||
FROM Source t
|
FROM Source t
|
||||||
%s
|
%s
|
||||||
@@ -4641,10 +4829,10 @@ Source AS (
|
|||||||
urun_alt_grubu,
|
urun_alt_grubu,
|
||||||
market_key,
|
market_key,
|
||||||
COALESCE(stock_qty,0) AS stock_qty,
|
COALESCE(stock_qty,0) AS stock_qty,
|
||||||
(COALESCE((SELECT s.stock_qty FROM Stock90Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0 AS avg_stock_90d,
|
COALESCE(NULLIF(avg_stock_90d,0), (COALESCE((SELECT s.stock_qty FROM Stock90Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_90d,
|
||||||
(COALESCE((SELECT s.stock_qty FROM Stock180Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0 AS avg_stock_180d,
|
COALESCE(NULLIF(avg_stock_180d,0), (COALESCE((SELECT s.stock_qty FROM Stock180Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_180d,
|
||||||
(COALESCE((SELECT s.stock_qty FROM Stock365Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0 AS avg_stock_365d,
|
COALESCE(NULLIF(avg_stock_365d,0), (COALESCE((SELECT s.stock_qty FROM Stock365Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_365d,
|
||||||
(COALESCE((SELECT s.stock_qty FROM StockTotalStart s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0 AS avg_stock_total,
|
COALESCE(NULLIF(avg_stock_total,0), (COALESCE((SELECT s.stock_qty FROM StockTotalStart s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_total,
|
||||||
COALESCE(sales_qty_90d,0) AS sales_qty_90d,
|
COALESCE(sales_qty_90d,0) AS sales_qty_90d,
|
||||||
COALESCE(sales_qty_180d,0) AS sales_qty_180d,
|
COALESCE(sales_qty_180d,0) AS sales_qty_180d,
|
||||||
COALESCE(sales_qty_365d,0) AS sales_qty_365d,
|
COALESCE(sales_qty_365d,0) AS sales_qty_365d,
|
||||||
@@ -4671,8 +4859,17 @@ Source AS (
|
|||||||
COALESCE(unit_profit_base_90d,0) AS unit_profit_base_90d,
|
COALESCE(unit_profit_base_90d,0) AS unit_profit_base_90d,
|
||||||
COALESCE(unit_profit_base_180d,0) AS unit_profit_base_180d,
|
COALESCE(unit_profit_base_180d,0) AS unit_profit_base_180d,
|
||||||
COALESCE(market_count_90d,0) AS market_count_90d,
|
COALESCE(market_count_90d,0) AS market_count_90d,
|
||||||
|
COALESCE(market_count_180d,0) AS market_count_180d,
|
||||||
|
COALESCE(market_count_365d,0) AS market_count_365d,
|
||||||
|
COALESCE(market_count_total,0) AS market_count_total,
|
||||||
COALESCE(customer_count_90d,0) AS customer_count_90d,
|
COALESCE(customer_count_90d,0) AS customer_count_90d,
|
||||||
|
COALESCE(customer_count_180d,0) AS customer_count_180d,
|
||||||
|
COALESCE(customer_count_365d,0) AS customer_count_365d,
|
||||||
|
COALESCE(customer_count_total,0) AS customer_count_total,
|
||||||
COALESCE(sales_index_90d,0) AS sales_index_90d,
|
COALESCE(sales_index_90d,0) AS sales_index_90d,
|
||||||
|
COALESCE(sales_index_180d,0) AS sales_index_180d,
|
||||||
|
COALESCE(sales_index_365d,0) AS sales_index_365d,
|
||||||
|
COALESCE(sales_index_total,0) AS sales_index_total,
|
||||||
COALESCE(price_index_90d,0) AS price_index_90d,
|
COALESCE(price_index_90d,0) AS price_index_90d,
|
||||||
COALESCE(margin_index_90d,0) AS margin_index_90d,
|
COALESCE(margin_index_90d,0) AS margin_index_90d,
|
||||||
COALESCE(performance_score,0) AS performance_score,
|
COALESCE(performance_score,0) AS performance_score,
|
||||||
@@ -4698,7 +4895,7 @@ func productPerformanceSQLGroupExpr(field string) (string, bool) {
|
|||||||
case "color_yaka":
|
case "color_yaka":
|
||||||
return "concat_ws('/', NULLIF(btrim(COALESCE(color_code,'')), ''), NULLIF(btrim(COALESCE(yaka_kodu,'')), ''))", true
|
return "concat_ws('/', NULLIF(btrim(COALESCE(color_code,'')), ''), NULLIF(btrim(COALESCE(yaka_kodu,'')), ''))", true
|
||||||
case "market_key":
|
case "market_key":
|
||||||
return "btrim(regexp_replace(COALESCE(market_key,''), '^.*\\|', ''))", true
|
return "btrim(regexp_replace(COALESCE(market_key,''), '^.*[|]', ''))", true
|
||||||
default:
|
default:
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -4988,9 +5185,9 @@ func productPerformanceGroupedFilterValue(row map[string]any, field string) stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceGroupedSourceRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, error) {
|
func productPerformanceGroupedSourceRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, error) {
|
||||||
if rows, ok, err := productPerformanceGroupedSnapshotRows(ctx, pg, mode, limit); err != nil {
|
if rows, err := productPerformanceGroupedRawSnapshotSourceRows(ctx, pg, mode, limit); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if ok {
|
} else if rows != nil {
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5032,14 +5229,30 @@ func productPerformanceGroupedRawSnapshotSourceRows(ctx context.Context, pg *sql
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(mode) == "idle" {
|
if strings.TrimSpace(mode) == "idle" {
|
||||||
|
normalizeProductPerformanceGroupedSourceScores(rows, mode)
|
||||||
return productPerformanceIdleSourceRows(rows), nil
|
return productPerformanceIdleSourceRows(rows), nil
|
||||||
}
|
}
|
||||||
if mode == "products" || mode == "product_detail" {
|
if mode == "products" || mode == "product_detail" {
|
||||||
rows = mergeProductPerformanceGeneralSnapshotMetrics(ctx, pg, rows)
|
rows = mergeProductPerformanceGeneralSnapshotMetrics(ctx, pg, rows)
|
||||||
|
rows = mergeProductPerformanceSalesSpreadKeys(ctx, pg, rows)
|
||||||
}
|
}
|
||||||
|
normalizeProductPerformanceGroupedSourceScores(rows, mode)
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeProductPerformanceGroupedSourceScores(rows []map[string]any, mode string) {
|
||||||
|
if !shouldCascadeProductPerformanceGroupedScores(mode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
deriveProductPerformanceGroupMetrics(row, "")
|
||||||
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
|
row["performance_score_"+suffix] = productPerformanceSalesPeriodScore(row, suffix)
|
||||||
|
}
|
||||||
|
row["performance_score"] = row["performance_score_90d"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mergeProductPerformanceGeneralSnapshotMetrics(ctx context.Context, pg *sql.DB, rows []map[string]any) []map[string]any {
|
func mergeProductPerformanceGeneralSnapshotMetrics(ctx context.Context, pg *sql.DB, rows []map[string]any) []map[string]any {
|
||||||
if len(rows) == 0 {
|
if len(rows) == 0 {
|
||||||
return rows
|
return rows
|
||||||
@@ -5070,9 +5283,124 @@ func mergeProductPerformanceGeneralSnapshotMetrics(ctx context.Context, pg *sql.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if value, ok := general["performance_score"]; ok {
|
if value, ok := general["performance_score"]; ok {
|
||||||
|
if _, exists := row["performance_score_total"]; !exists {
|
||||||
row["performance_score_total"] = value
|
row["performance_score_total"] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeProductPerformanceSalesSpreadKeys(ctx context.Context, pg *sql.DB, rows []map[string]any) []map[string]any {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
const query = `
|
||||||
|
WITH Latest AS (
|
||||||
|
SELECT COALESCE(
|
||||||
|
(SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily),
|
||||||
|
(SELECT MAX(sales_date) FROM mk_product_performance_sales_daily),
|
||||||
|
current_date
|
||||||
|
)::date AS kpi_date
|
||||||
|
),
|
||||||
|
Spread AS (
|
||||||
|
SELECT
|
||||||
|
s.product_code,
|
||||||
|
s.color_code,
|
||||||
|
s.yaka_kodu,
|
||||||
|
s.market_key,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND display_market_key NOT IN ('', 'STOK', '-')
|
||||||
|
), '[]'::jsonb) AS market_keys_90d,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '179 days' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND display_market_key NOT IN ('', 'STOK', '-')
|
||||||
|
), '[]'::jsonb) AS market_keys_180d,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '359 days' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND display_market_key NOT IN ('', 'STOK', '-')
|
||||||
|
), '[]'::jsonb) AS market_keys_365d,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND display_market_key NOT IN ('', 'STOK', '-')
|
||||||
|
), '[]'::jsonb) AS market_keys_total,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
|
||||||
|
), '[]'::jsonb) AS customer_keys_90d,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '179 days' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
|
||||||
|
), '[]'::jsonb) AS customer_keys_180d,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '359 days' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
|
||||||
|
), '[]'::jsonb) AS customer_keys_365d,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
|
||||||
|
), '[]'::jsonb) AS customer_keys_total
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
s.*,
|
||||||
|
btrim(regexp_replace(COALESCE(s.market_key,''), '^.*[|]', '')) AS display_market_key
|
||||||
|
FROM mk_product_performance_sales_daily s
|
||||||
|
) s
|
||||||
|
CROSS JOIN Latest
|
||||||
|
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
|
||||||
|
AND upper(translate(btrim(COALESCE(s.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 s.product_code, s.color_code, s.yaka_kodu, s.market_key
|
||||||
|
)
|
||||||
|
SELECT jsonb_build_object(
|
||||||
|
'product_code', product_code,
|
||||||
|
'color_code', color_code,
|
||||||
|
'yaka_kodu', yaka_kodu,
|
||||||
|
'market_key', market_key,
|
||||||
|
'__market_keys_90d', market_keys_90d,
|
||||||
|
'__market_keys_180d', market_keys_180d,
|
||||||
|
'__market_keys_365d', market_keys_365d,
|
||||||
|
'__market_keys_total', market_keys_total,
|
||||||
|
'__customer_keys_90d', customer_keys_90d,
|
||||||
|
'__customer_keys_180d', customer_keys_180d,
|
||||||
|
'__customer_keys_365d', customer_keys_365d,
|
||||||
|
'__customer_keys_total', customer_keys_total
|
||||||
|
)
|
||||||
|
FROM Spread`
|
||||||
|
spreadRows, err := queryProductPerformanceJSONRows(ctx, pg, query)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ProductPerformanceRefresh] product sales spread keys skipped err=%v", err)
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
byKey := make(map[string]map[string]any, len(spreadRows))
|
||||||
|
for _, row := range spreadRows {
|
||||||
|
key := productPerformanceMapMarketVariantKey(row)
|
||||||
|
if key != "" {
|
||||||
|
byKey[key] = row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
spread := byKey[productPerformanceMapMarketVariantKey(row)]
|
||||||
|
if spread == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
|
for _, prefix := range []string{"__market_keys_", "__customer_keys_"} {
|
||||||
|
field := prefix + suffix
|
||||||
|
if value, ok := spread[field]; ok {
|
||||||
|
row[field] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5194,10 +5522,8 @@ type productPerformanceGroupedSnapshotNode struct {
|
|||||||
Avg map[string]*productPerformanceGroupedSnapshotAvgState
|
Avg map[string]*productPerformanceGroupedSnapshotAvgState
|
||||||
BucketCounts map[string]int
|
BucketCounts map[string]int
|
||||||
Image map[string]any
|
Image map[string]any
|
||||||
Market90Seen map[string]bool
|
MarketSeen map[string]map[string]bool
|
||||||
MarketTotalSeen map[string]bool
|
CustomerSeen map[string]map[string]bool
|
||||||
Customer90Seen map[string]bool
|
|
||||||
CustomerTotalSeen map[string]bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildProductPerformanceGroupedSnapshotRows(sourceRows []map[string]any, levels []string, mode string) []map[string]any {
|
func buildProductPerformanceGroupedSnapshotRows(sourceRows []map[string]any, levels []string, mode string) []map[string]any {
|
||||||
@@ -5238,9 +5564,89 @@ func buildProductPerformanceGroupedSnapshotRows(sourceRows []map[string]any, lev
|
|||||||
|
|
||||||
out := make([]map[string]any, 0, len(sourceRows))
|
out := make([]map[string]any, 0, len(sourceRows))
|
||||||
appendProductPerformanceGroupedSnapshotNodes(&out, roots, rootOrder)
|
appendProductPerformanceGroupedSnapshotNodes(&out, roots, rootOrder)
|
||||||
|
applyProductPerformanceGroupedChildScoreAverages(out, mode)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyProductPerformanceGroupedChildScoreAverages(rows []map[string]any, mode string) {
|
||||||
|
if !shouldCascadeProductPerformanceGroupedScores(mode) || len(rows) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
byKey := make(map[string]map[string]any, len(rows))
|
||||||
|
children := map[string][]map[string]any{}
|
||||||
|
maxLevel := -1
|
||||||
|
for _, row := range rows {
|
||||||
|
key := stringFromMap(row, "key")
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byKey[key] = row
|
||||||
|
if level := intFromMap(row, "level"); level > maxLevel {
|
||||||
|
maxLevel = level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
key := stringFromMap(row, "key")
|
||||||
|
parentKey := productPerformanceParentGroupKey(key)
|
||||||
|
if parentKey == "" || byKey[parentKey] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
children[parentKey] = append(children[parentKey], row)
|
||||||
|
}
|
||||||
|
for level := maxLevel - 1; level >= 0; level-- {
|
||||||
|
for _, row := range rows {
|
||||||
|
if intFromMap(row, "level") != level {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
childRows := children[stringFromMap(row, "key")]
|
||||||
|
if len(childRows) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
|
field := "performance_score_" + suffix
|
||||||
|
if !productPerformanceRowsHaveField(childRows, field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[field] = weightedAverageProductPerformanceScoreRows(childRows, field, suffix)
|
||||||
|
}
|
||||||
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
|
field := "stock_turnover_" + suffix
|
||||||
|
if !productPerformanceRowsHaveField(childRows, field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[field] = weightedAverageProductPerformanceRows(childRows, field, "sales_qty_"+suffix)
|
||||||
|
}
|
||||||
|
if score, ok := productPerformanceOptionalFloat(row, "performance_score_90d"); ok {
|
||||||
|
row["performance_score"] = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldCascadeProductPerformanceGroupedScores(mode string) bool {
|
||||||
|
switch strings.TrimSpace(mode) {
|
||||||
|
case "products",
|
||||||
|
"product_detail",
|
||||||
|
"idle",
|
||||||
|
"sales_color_yaka_market_customer",
|
||||||
|
"sales_product_country_segment_market_customer",
|
||||||
|
"sales_market_customer_product",
|
||||||
|
"sales_country_segment_market_customer_product":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceRowsHaveField(rows []map[string]any, field string) bool {
|
||||||
|
for _, row := range rows {
|
||||||
|
if _, ok := productPerformanceOptionalFloat(row, field); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
|
func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
|
||||||
n.Count++
|
n.Count++
|
||||||
if n.Image == nil && stringFromMap(row, "product_code") != "" {
|
if n.Image == nil && stringFromMap(row, "product_code") != "" {
|
||||||
@@ -5253,7 +5659,7 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
|
|||||||
n.BucketCounts[bucket]++
|
n.BucketCounts[bucket]++
|
||||||
}
|
}
|
||||||
for key, value := range row {
|
for key, value := range row {
|
||||||
if key == "row_key" || key == "key" || isProductPerformanceMarginField(key) {
|
if key == "row_key" || key == "key" || isProductPerformanceInternalGroupField(key) || isProductPerformanceMarginField(key) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
@@ -5324,34 +5730,36 @@ func (n *productPerformanceGroupedSnapshotNode) addDistinctVariantMetric(row map
|
|||||||
func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) {
|
func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) {
|
||||||
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
|
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
|
||||||
if market != "" && market != "STOK" {
|
if market != "" && market != "STOK" {
|
||||||
if floatFromMap(row, "sales_usd_90d") > 0 {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
if n.Market90Seen == nil {
|
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
|
||||||
n.Market90Seen = map[string]bool{}
|
if n.MarketSeen == nil {
|
||||||
|
n.MarketSeen = map[string]map[string]bool{}
|
||||||
}
|
}
|
||||||
n.Market90Seen[market] = true
|
if n.MarketSeen[suffix] == nil {
|
||||||
|
n.MarketSeen[suffix] = map[string]bool{}
|
||||||
}
|
}
|
||||||
if floatFromMap(row, "sales_usd_total") > 0 {
|
n.MarketSeen[suffix][market] = true
|
||||||
if n.MarketTotalSeen == nil {
|
|
||||||
n.MarketTotalSeen = map[string]bool{}
|
|
||||||
}
|
}
|
||||||
n.MarketTotalSeen[market] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
|
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
|
||||||
if customer != "" && customer != "-" {
|
if customer != "" && customer != "-" {
|
||||||
if floatFromMap(row, "sales_usd_90d") > 0 {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
if n.Customer90Seen == nil {
|
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
|
||||||
n.Customer90Seen = map[string]bool{}
|
if n.CustomerSeen == nil {
|
||||||
|
n.CustomerSeen = map[string]map[string]bool{}
|
||||||
}
|
}
|
||||||
n.Customer90Seen[customer] = true
|
if n.CustomerSeen[suffix] == nil {
|
||||||
|
n.CustomerSeen[suffix] = map[string]bool{}
|
||||||
}
|
}
|
||||||
if floatFromMap(row, "sales_usd_total") > 0 {
|
n.CustomerSeen[suffix][customer] = true
|
||||||
if n.CustomerTotalSeen == nil {
|
|
||||||
n.CustomerTotalSeen = map[string]bool{}
|
|
||||||
}
|
}
|
||||||
n.CustomerTotalSeen[customer] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
|
n.MarketSeen = productPerformanceAddSeenStrings(n.MarketSeen, suffix, productPerformanceStringSliceFromMap(row, "__market_keys_"+suffix))
|
||||||
|
n.CustomerSeen = productPerformanceAddSeenStrings(n.CustomerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendProductPerformanceGroupedSnapshotNodes(out *[]map[string]any, nodes map[string]*productPerformanceGroupedSnapshotNode, order []string) {
|
func appendProductPerformanceGroupedSnapshotNodes(out *[]map[string]any, nodes map[string]*productPerformanceGroupedSnapshotNode, order []string) {
|
||||||
@@ -5387,7 +5795,7 @@ func (n *productPerformanceGroupedSnapshotNode) snapshotRow() map[string]any {
|
|||||||
row[key] = state.Sum / state.Count
|
row[key] = state.Sum / state.Count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applyProductPerformanceDistinctSpread(row, n.Market90Seen, n.MarketTotalSeen, n.Customer90Seen, n.CustomerTotalSeen)
|
applyProductPerformanceDistinctSpread(row, n.MarketSeen, n.CustomerSeen)
|
||||||
deriveProductPerformanceGroupMetrics(row, n.Field)
|
deriveProductPerformanceGroupMetrics(row, n.Field)
|
||||||
if bucket := n.dominantBucket(); bucket != "" {
|
if bucket := n.dominantBucket(); bucket != "" {
|
||||||
row["performance_bucket"] = bucket
|
row["performance_bucket"] = bucket
|
||||||
@@ -5508,14 +5916,12 @@ func clearProductPerformanceGroupDimensions(row map[string]any, groupField, grou
|
|||||||
|
|
||||||
func aggregateProductPerformanceRows(rows []map[string]any, groupField string) map[string]any {
|
func aggregateProductPerformanceRows(rows []map[string]any, groupField string) map[string]any {
|
||||||
out := map[string]any{}
|
out := map[string]any{}
|
||||||
market90Seen := map[string]bool{}
|
marketSeen := map[string]map[string]bool{}
|
||||||
marketTotalSeen := map[string]bool{}
|
customerSeen := map[string]map[string]bool{}
|
||||||
customer90Seen := map[string]bool{}
|
|
||||||
customerTotalSeen := map[string]bool{}
|
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
addProductPerformanceDistinctSpread(row, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
|
addProductPerformanceDistinctSpread(row, marketSeen, customerSeen)
|
||||||
for key, value := range row {
|
for key, value := range row {
|
||||||
if key == "row_key" || key == "key" {
|
if key == "row_key" || key == "key" || isProductPerformanceInternalGroupField(key) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isProductPerformanceMarginField(key) {
|
if isProductPerformanceMarginField(key) {
|
||||||
@@ -5541,6 +5947,9 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
|
|||||||
}
|
}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
for key := range row {
|
for key := range row {
|
||||||
|
if isProductPerformanceInternalGroupField(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if isProductPerformanceMarginField(key) {
|
if isProductPerformanceMarginField(key) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -5549,62 +5958,120 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applyProductPerformanceDistinctSpread(out, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
|
applyProductPerformanceDistinctSpread(out, marketSeen, customerSeen)
|
||||||
deriveProductPerformanceGroupMetrics(out, groupField)
|
deriveProductPerformanceGroupMetrics(out, groupField)
|
||||||
out["performance_bucket"] = dominantProductPerformanceValue(rows, "performance_bucket")
|
out["performance_bucket"] = dominantProductPerformanceValue(rows, "performance_bucket")
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func addProductPerformanceDistinctSpread(row map[string]any, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen map[string]bool) {
|
func addProductPerformanceDistinctSpread(row map[string]any, marketSeen, customerSeen map[string]map[string]bool) {
|
||||||
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
|
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
|
||||||
if market != "" && market != "STOK" {
|
if market != "" && market != "STOK" {
|
||||||
if floatFromMap(row, "sales_usd_90d") > 0 {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
market90Seen[market] = true
|
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
|
||||||
|
if marketSeen[suffix] == nil {
|
||||||
|
marketSeen[suffix] = map[string]bool{}
|
||||||
|
}
|
||||||
|
marketSeen[suffix][market] = true
|
||||||
}
|
}
|
||||||
if floatFromMap(row, "sales_usd_total") > 0 {
|
|
||||||
marketTotalSeen[market] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
|
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
|
||||||
if customer != "" && customer != "-" {
|
if customer != "" && customer != "-" {
|
||||||
if floatFromMap(row, "sales_usd_90d") > 0 {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
customer90Seen[customer] = true
|
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
|
||||||
|
if customerSeen[suffix] == nil {
|
||||||
|
customerSeen[suffix] = map[string]bool{}
|
||||||
|
}
|
||||||
|
customerSeen[suffix][customer] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
|
marketSeen = productPerformanceAddSeenStrings(marketSeen, suffix, productPerformanceStringSliceFromMap(row, "__market_keys_"+suffix))
|
||||||
|
customerSeen = productPerformanceAddSeenStrings(customerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyProductPerformanceDistinctSpread(row map[string]any, marketSeen, customerSeen map[string]map[string]bool) {
|
||||||
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
|
marketField := "market_count_" + suffix
|
||||||
|
customerField := "customer_count_" + suffix
|
||||||
|
if seen := marketSeen[suffix]; len(seen) > 0 {
|
||||||
|
row[marketField] = len(seen)
|
||||||
|
}
|
||||||
|
if seen := customerSeen[suffix]; len(seen) > 0 {
|
||||||
|
row[customerField] = len(seen)
|
||||||
|
}
|
||||||
|
if floatFromMap(row, "sales_usd_"+suffix) > 0 && (suffix == "90d" || suffix == "total") {
|
||||||
|
if intFromMap(row, marketField) == 0 {
|
||||||
|
row[marketField] = 1
|
||||||
|
}
|
||||||
|
if intFromMap(row, customerField) == 0 {
|
||||||
|
if suffix == "total" {
|
||||||
|
row[customerField] = maxInt(1, intFromMap(row, "customer_count_90d"))
|
||||||
|
} else {
|
||||||
|
row[customerField] = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if floatFromMap(row, "sales_usd_total") > 0 {
|
|
||||||
customerTotalSeen[customer] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyProductPerformanceDistinctSpread(row map[string]any, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen map[string]bool) {
|
func productPerformanceAddSeenStrings(seen map[string]map[string]bool, suffix string, values []string) map[string]map[string]bool {
|
||||||
if len(market90Seen) > 0 {
|
if len(values) == 0 {
|
||||||
row["market_count_90d"] = len(market90Seen)
|
return seen
|
||||||
}
|
}
|
||||||
if len(marketTotalSeen) > 0 {
|
if seen == nil {
|
||||||
row["market_count_total"] = len(marketTotalSeen)
|
seen = map[string]map[string]bool{}
|
||||||
}
|
}
|
||||||
if len(customer90Seen) > 0 {
|
if seen[suffix] == nil {
|
||||||
row["customer_count_90d"] = len(customer90Seen)
|
seen[suffix] = map[string]bool{}
|
||||||
}
|
}
|
||||||
if len(customerTotalSeen) > 0 {
|
for _, value := range values {
|
||||||
row["customer_count_total"] = len(customerTotalSeen)
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" || value == "-" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if floatFromMap(row, "sales_usd_90d") > 0 {
|
seen[suffix][value] = true
|
||||||
if intFromMap(row, "market_count_90d") == 0 {
|
|
||||||
row["market_count_90d"] = 1
|
|
||||||
}
|
}
|
||||||
if intFromMap(row, "customer_count_90d") == 0 {
|
return seen
|
||||||
row["customer_count_90d"] = 1
|
}
|
||||||
|
|
||||||
|
func productPerformanceStringSliceFromMap(row map[string]any, field string) []string {
|
||||||
|
value, ok := row[field]
|
||||||
|
if !ok || value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []string:
|
||||||
|
return v
|
||||||
|
case []any:
|
||||||
|
out := make([]string, 0, len(v))
|
||||||
|
for _, item := range v {
|
||||||
|
text := strings.TrimSpace(fmt.Sprint(item))
|
||||||
|
if text != "" {
|
||||||
|
out = append(out, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if floatFromMap(row, "sales_usd_total") > 0 {
|
return out
|
||||||
if intFromMap(row, "market_count_total") == 0 {
|
case string:
|
||||||
row["market_count_total"] = maxInt(1, intFromMap(row, "market_count_90d"))
|
text := strings.TrimSpace(v)
|
||||||
|
if text == "" {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if intFromMap(row, "customer_count_total") == 0 {
|
var parsed []string
|
||||||
row["customer_count_total"] = maxInt(1, intFromMap(row, "customer_count_90d"))
|
if strings.HasPrefix(text, "[") && json.Unmarshal([]byte(text), &parsed) == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return []string{text}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isProductPerformanceInternalGroupField(field string) bool {
|
||||||
|
return strings.HasPrefix(field, "__")
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceGroupRecommendation(row map[string]any, groupField string, count int) string {
|
func productPerformanceGroupRecommendation(row map[string]any, groupField string, count int) string {
|
||||||
@@ -5711,18 +6178,55 @@ func productPerformanceMapVariantKey(row map[string]any) string {
|
|||||||
|
|
||||||
func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) {
|
func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) {
|
||||||
normalizeProductPerformanceCostFields(out)
|
normalizeProductPerformanceCostFields(out)
|
||||||
for _, suffix := range []string{"90d", "180d", "365d", "total"} {
|
isGroup := strings.TrimSpace(groupField) != ""
|
||||||
|
if isGroup {
|
||||||
|
out["group_field"] = groupField
|
||||||
|
}
|
||||||
|
preservedCustomerScores := map[string]float64{}
|
||||||
|
preservedProductScores := map[string]float64{}
|
||||||
|
preservedStockTurnovers := map[string]float64{}
|
||||||
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
|
if score, ok := productPerformanceOptionalFloat(out, "customer_score_"+suffix); ok {
|
||||||
|
preservedCustomerScores[suffix] = score
|
||||||
|
}
|
||||||
|
if score, ok := productPerformanceOptionalFloat(out, "performance_score_"+suffix); ok {
|
||||||
|
preservedProductScores[suffix] = score
|
||||||
|
}
|
||||||
|
if isGroup {
|
||||||
|
if turnover, ok := productPerformanceOptionalFloat(out, "stock_turnover_"+suffix); ok {
|
||||||
|
preservedStockTurnovers[suffix] = turnover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if score, ok := productPerformanceOptionalFloat(out, "performance_score"); ok {
|
||||||
|
if _, exists := preservedProductScores["90d"]; !exists {
|
||||||
|
preservedProductScores["90d"] = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
sales := floatFromMap(out, "sales_usd_"+suffix)
|
sales := floatFromMap(out, "sales_usd_"+suffix)
|
||||||
qty := floatFromMap(out, "sales_qty_"+suffix)
|
qty := floatFromMap(out, "sales_qty_"+suffix)
|
||||||
stockQty := floatFromMap(out, "stock_qty")
|
stockQty := floatFromMap(out, "stock_qty")
|
||||||
turnoverBase := floatFromMap(out, "avg_stock_"+suffix)
|
turnoverBase := floatFromMap(out, "avg_stock_"+suffix)
|
||||||
if turnoverBase <= 0 {
|
if turnoverBase <= 0 && !isGroup {
|
||||||
turnoverBase = stockQty
|
turnoverBase = stockQty
|
||||||
}
|
}
|
||||||
if turnoverBase > 0 {
|
days := productPerformancePeriodDays(out, suffix)
|
||||||
out["stock_turnover_"+suffix] = qty / turnoverBase
|
avgDaily := floatFromMap(out, "avg_daily_sales_"+suffix)
|
||||||
} else {
|
if days > 0 {
|
||||||
out["stock_turnover_"+suffix] = 0
|
avgDaily = qty / days
|
||||||
|
out["avg_daily_sales_"+suffix] = avgDaily
|
||||||
|
}
|
||||||
|
if avgDaily > 0 && turnoverBase > 0 {
|
||||||
|
out["stock_days_"+suffix] = turnoverBase / avgDaily
|
||||||
|
} else if qty <= 0 && stockQty > 0 {
|
||||||
|
out["stock_days_"+suffix] = 9999
|
||||||
|
} else if _, ok := out["stock_days_"+suffix]; !ok {
|
||||||
|
out["stock_days_"+suffix] = 0
|
||||||
|
}
|
||||||
|
out["stock_turnover_"+suffix] = productPerformanceAnnualizedStockTurnover(qty, turnoverBase, days)
|
||||||
|
if turnover, ok := preservedStockTurnovers[suffix]; ok {
|
||||||
|
out["stock_turnover_"+suffix] = turnover
|
||||||
}
|
}
|
||||||
if qty > 0 {
|
if qty > 0 {
|
||||||
out["avg_price_usd_"+suffix] = sales / qty
|
out["avg_price_usd_"+suffix] = sales / qty
|
||||||
@@ -5775,37 +6279,19 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
|
|||||||
if _, ok := out["net_stock_after_order"]; ok || orderQty > 0 {
|
if _, ok := out["net_stock_after_order"]; ok || orderQty > 0 {
|
||||||
out["net_stock_after_order"] = floatFromMap(out, "stock_qty") - orderQty
|
out["net_stock_after_order"] = floatFromMap(out, "stock_qty") - orderQty
|
||||||
}
|
}
|
||||||
if _, ok := out["customer_score_90d"]; !ok {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
out["customer_score_90d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "90d"))
|
if score, ok := preservedCustomerScores[suffix]; ok {
|
||||||
}
|
out["customer_score_"+suffix] = score
|
||||||
if _, ok := out["customer_score_180d"]; !ok {
|
|
||||||
out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d"))
|
|
||||||
}
|
|
||||||
if _, ok := out["customer_score_365d"]; !ok {
|
|
||||||
out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d"))
|
|
||||||
}
|
|
||||||
if _, ok := out["customer_score_total"]; !ok {
|
|
||||||
out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total"))
|
|
||||||
}
|
|
||||||
if _, ok := out["performance_score_90d"]; !ok {
|
|
||||||
out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d")
|
|
||||||
}
|
|
||||||
if _, ok := out["performance_score_180d"]; !ok {
|
|
||||||
out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d")
|
|
||||||
}
|
|
||||||
if _, ok := out["performance_score_365d"]; !ok {
|
|
||||||
out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d")
|
|
||||||
}
|
|
||||||
if _, ok := out["performance_score_total"]; !ok {
|
|
||||||
out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total")
|
|
||||||
}
|
|
||||||
if _, ok := out["performance_score"]; !ok {
|
|
||||||
if score, exists := productPerformanceOptionalFloat(out, "performance_score_90d"); exists {
|
|
||||||
out["performance_score"] = score
|
|
||||||
} else {
|
} else {
|
||||||
out["performance_score"] = productPerformanceGroupScore(out, groupField)
|
out["customer_score_"+suffix] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, suffix))
|
||||||
|
}
|
||||||
|
if score, ok := preservedProductScores[suffix]; ok {
|
||||||
|
out["performance_score_"+suffix] = score
|
||||||
|
} else {
|
||||||
|
out["performance_score_"+suffix] = productPerformanceSalesPeriodScore(out, suffix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
out["performance_score"] = out["performance_score_90d"]
|
||||||
if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
|
if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
|
||||||
score := productPerformanceOrderGroupScore(out)
|
score := productPerformanceOrderGroupScore(out)
|
||||||
out["performance_score_90d"] = score
|
out["performance_score_90d"] = score
|
||||||
@@ -5832,11 +6318,69 @@ func normalizeProductPerformanceCostFields(row map[string]any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
normalize("cost_price_usd", "base_price_usd")
|
normalize("cost_price_usd", "base_price_usd")
|
||||||
for _, suffix := range []string{"90d", "180d", "365d", "total"} {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
normalize("cost_price_usd_"+suffix, "base_price_usd_"+suffix)
|
normalize("cost_price_usd_"+suffix, "base_price_usd_"+suffix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func productPerformancePeriodSuffixes() []string {
|
||||||
|
return []string{"90d", "180d", "365d", "total"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformancePeriodDays(row map[string]any, suffix string) float64 {
|
||||||
|
switch suffix {
|
||||||
|
case "90d":
|
||||||
|
return 90
|
||||||
|
case "180d":
|
||||||
|
return 180
|
||||||
|
case "365d":
|
||||||
|
return 360
|
||||||
|
case "total":
|
||||||
|
start := parseProductPerformanceDate(stringFromMap(row, "period_start"))
|
||||||
|
if start.IsZero() {
|
||||||
|
start = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
}
|
||||||
|
end := parseProductPerformanceDate(stringFromMap(row, "period_end"))
|
||||||
|
if end.IsZero() {
|
||||||
|
end = parseProductPerformanceDate(stringFromMap(row, "kpi_date"))
|
||||||
|
}
|
||||||
|
if end.IsZero() || end.Before(start) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return end.Sub(start).Hours()/24 + 1
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const productPerformanceStockTurnoverYearDays = 360.0
|
||||||
|
|
||||||
|
func productPerformanceAnnualizedStockTurnover(salesQty, avgStock, periodDays float64) float64 {
|
||||||
|
if salesQty <= 0 || avgStock <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
raw := salesQty / avgStock
|
||||||
|
if periodDays <= 0 {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
return raw * productPerformanceStockTurnoverYearDays / periodDays
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseProductPerformanceDate(value string) time.Time {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
if len(value) >= len("2006-01-02") {
|
||||||
|
value = value[:len("2006-01-02")]
|
||||||
|
}
|
||||||
|
t, err := time.Parse("2006-01-02", value)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
func productPerformanceOptionalFloat(row map[string]any, field string) (float64, bool) {
|
func productPerformanceOptionalFloat(row map[string]any, field string) (float64, bool) {
|
||||||
value, ok := row[field]
|
value, ok := row[field]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -5898,8 +6442,12 @@ func productPerformanceSalesPeriodScore(row map[string]any, suffix string) float
|
|||||||
salesQty := floatFromMap(row, "sales_qty_"+suffix)
|
salesQty := floatFromMap(row, "sales_qty_"+suffix)
|
||||||
stockTurnover := floatFromMap(row, "stock_turnover_"+suffix)
|
stockTurnover := floatFromMap(row, "stock_turnover_"+suffix)
|
||||||
if stockTurnover == 0 {
|
if stockTurnover == 0 {
|
||||||
if stockQty := floatFromMap(row, "stock_qty"); stockQty > 0 {
|
avgStock := floatFromMap(row, "avg_stock_"+suffix)
|
||||||
stockTurnover = salesQty / stockQty
|
if avgStock <= 0 && strings.TrimSpace(stringFromMap(row, "group_field")) == "" {
|
||||||
|
avgStock = floatFromMap(row, "stock_qty")
|
||||||
|
}
|
||||||
|
if avgStock > 0 {
|
||||||
|
stockTurnover = productPerformanceAnnualizedStockTurnover(salesQty, avgStock, productPerformancePeriodDays(row, suffix))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return productPerformanceProductScore(
|
return productPerformanceProductScore(
|
||||||
@@ -5910,6 +6458,7 @@ func productPerformanceSalesPeriodScore(row map[string]any, suffix string) float
|
|||||||
stockTurnover,
|
stockTurnover,
|
||||||
productPerformancePeriodCount(row, "market_count", suffix),
|
productPerformancePeriodCount(row, "market_count", suffix),
|
||||||
productPerformancePeriodCount(row, "customer_count", suffix),
|
productPerformancePeriodCount(row, "customer_count", suffix),
|
||||||
|
productPerformancePeriodDays(row, suffix),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5953,19 +6502,54 @@ func productPerformanceCustomerSalesPeriodScore(row map[string]any) float64 {
|
|||||||
return productPerformanceCustomerScore(suffix, salesUSD, margin, productCount, salesQty)
|
return productPerformanceCustomerScore(suffix, salesUSD, margin, productCount, salesQty)
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64) float64 {
|
func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64, periodDays ...float64) float64 {
|
||||||
if salesUSD <= 0 {
|
if salesUSD <= 0 {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTarget(suffix))
|
days := productPerformanceScorePeriodDays(suffix, periodDays...)
|
||||||
|
revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTargetForDays(suffix, days))
|
||||||
score := 0.30*productPerformanceMarginComponentScore(margin) +
|
score := 0.30*productPerformanceMarginComponentScore(margin) +
|
||||||
0.20*productPerformanceRatioScore(stockTurnover, 1.50) +
|
0.20*productPerformanceRatioScore(stockTurnover, productPerformanceStockTurnoverTarget(suffix)) +
|
||||||
0.20*revenueScore +
|
0.20*revenueScore +
|
||||||
0.15*productPerformanceRatioScore(marketCount, 8) +
|
0.05*productPerformanceRatioScore(marketCount, productPerformanceMarketSpreadTarget(suffix)) +
|
||||||
0.15*productPerformanceRatioScore(customerCount, 25)
|
0.25*productPerformanceRatioScore(customerCount, productPerformanceCustomerSpreadTargetForDays(suffix, days))
|
||||||
return productPerformanceRoundScore(score)
|
return productPerformanceRoundScore(score)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func productPerformanceStockTurnoverTarget(suffix string) float64 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceMarketSpreadTarget(suffix string) float64 {
|
||||||
|
switch suffix {
|
||||||
|
case "90d":
|
||||||
|
return 3
|
||||||
|
case "180d":
|
||||||
|
return 3
|
||||||
|
case "365d":
|
||||||
|
return 6
|
||||||
|
default:
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceCustomerSpreadTarget(suffix string) float64 {
|
||||||
|
return productPerformanceCustomerSpreadTargetForDays(suffix, productPerformanceScorePeriodDays(suffix))
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceCustomerSpreadTargetForDays(suffix string, periodDays float64) float64 {
|
||||||
|
switch suffix {
|
||||||
|
case "90d":
|
||||||
|
return 8
|
||||||
|
case "180d":
|
||||||
|
return 20
|
||||||
|
case "365d":
|
||||||
|
return 50
|
||||||
|
default:
|
||||||
|
return 20 * productPerformanceTotalPeriodMultiplier(periodDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func productPerformanceCustomerScore(suffix string, salesUSD, margin, productGroupCount, salesQty float64) float64 {
|
func productPerformanceCustomerScore(suffix string, salesUSD, margin, productGroupCount, salesQty float64) float64 {
|
||||||
if salesUSD <= 0 {
|
if salesUSD <= 0 {
|
||||||
return 1
|
return 1
|
||||||
@@ -5989,7 +6573,7 @@ func productPerformanceRelativeIndex(value, average float64) float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceMarginComponentScore(margin float64) float64 {
|
func productPerformanceMarginComponentScore(margin float64) float64 {
|
||||||
return productPerformanceRatioScore(maxFloat(0, margin), 0.40)
|
return productPerformanceRatioScore(maxFloat(0, margin), 0.45)
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceRatioScore(value, target float64) float64 {
|
func productPerformanceRatioScore(value, target float64) float64 {
|
||||||
@@ -6010,18 +6594,49 @@ func productPerformanceRoundScore(score float64) float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceProductRevenueTarget(suffix string) float64 {
|
func productPerformanceProductRevenueTarget(suffix string) float64 {
|
||||||
|
return productPerformanceProductRevenueTargetForDays(suffix, productPerformanceScorePeriodDays(suffix))
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceProductRevenueTargetForDays(suffix string, periodDays float64) float64 {
|
||||||
switch suffix {
|
switch suffix {
|
||||||
case "180d":
|
case "180d":
|
||||||
return 20000
|
return 50000
|
||||||
case "365d":
|
case "365d":
|
||||||
return 40000
|
return 100000
|
||||||
case "total":
|
case "total":
|
||||||
return 120000
|
return 50000 * productPerformanceTotalPeriodMultiplier(periodDays)
|
||||||
default:
|
default:
|
||||||
return 10000
|
return 25000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func productPerformanceScorePeriodDays(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":
|
||||||
|
return productPerformancePeriodDays(map[string]any{
|
||||||
|
"kpi_date": time.Now().Format("2006-01-02"),
|
||||||
|
}, "total")
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceTotalPeriodMultiplier(periodDays float64) float64 {
|
||||||
|
if periodDays <= 0 {
|
||||||
|
periodDays = 180
|
||||||
|
}
|
||||||
|
return maxFloat(1, periodDays/180.0)
|
||||||
|
}
|
||||||
|
|
||||||
func productPerformanceCustomerRevenueTarget(suffix string) float64 {
|
func productPerformanceCustomerRevenueTarget(suffix string) float64 {
|
||||||
switch suffix {
|
switch suffix {
|
||||||
case "180d":
|
case "180d":
|
||||||
@@ -6131,7 +6746,7 @@ func sanitizeProductPerformanceGroupLevels(levels []string) []string {
|
|||||||
func defaultProductPerformanceGroupLevels(mode string) []string {
|
func defaultProductPerformanceGroupLevels(mode string) []string {
|
||||||
switch mode {
|
switch mode {
|
||||||
case "sales_color_yaka_market_customer":
|
case "sales_color_yaka_market_customer":
|
||||||
return []string{"color_yaka", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "country", "market_key", "customer_segment", "customer_code", "customer_name"}
|
return []string{"urun_ana_grubu", "color_yaka", "market_key", "customer_code", "customer_name", "urun_alt_grubu", "product_code"}
|
||||||
case "product_detail":
|
case "product_detail":
|
||||||
return []string{"urun_alt_grubu", "product_code", "color_yaka", "market_key"}
|
return []string{"urun_alt_grubu", "product_code", "color_yaka", "market_key"}
|
||||||
case "idle":
|
case "idle":
|
||||||
@@ -6172,7 +6787,16 @@ func mapGroupValue(row map[string]any, field string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shouldSumProductPerformanceField(field string) bool {
|
func shouldSumProductPerformanceField(field string) bool {
|
||||||
return strings.HasSuffix(field, "_qty") ||
|
return strings.HasPrefix(field, "sales_qty_") ||
|
||||||
|
strings.HasPrefix(field, "sales_usd_") ||
|
||||||
|
strings.HasPrefix(field, "avg_daily_sales_") ||
|
||||||
|
strings.HasPrefix(field, "gross_profit_") ||
|
||||||
|
strings.HasPrefix(field, "invoice_count_") ||
|
||||||
|
strings.HasPrefix(field, "market_count_") ||
|
||||||
|
strings.HasPrefix(field, "customer_count_") ||
|
||||||
|
strings.HasPrefix(field, "product_group_count_") ||
|
||||||
|
strings.HasPrefix(field, "product_count_") ||
|
||||||
|
strings.HasSuffix(field, "_qty") ||
|
||||||
strings.HasSuffix(field, "_usd") ||
|
strings.HasSuffix(field, "_usd") ||
|
||||||
strings.HasSuffix(field, "_count") ||
|
strings.HasSuffix(field, "_count") ||
|
||||||
strings.HasSuffix(field, "_value_usd") ||
|
strings.HasSuffix(field, "_value_usd") ||
|
||||||
@@ -6189,6 +6813,9 @@ func shouldSumProductPerformanceField(field string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shouldAverageProductPerformanceField(field string) bool {
|
func shouldAverageProductPerformanceField(field string) bool {
|
||||||
|
if strings.HasPrefix(field, "avg_daily_sales_") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return strings.HasPrefix(field, "avg_") ||
|
return strings.HasPrefix(field, "avg_") ||
|
||||||
strings.HasPrefix(field, "unit_") ||
|
strings.HasPrefix(field, "unit_") ||
|
||||||
strings.HasPrefix(field, "base_price") ||
|
strings.HasPrefix(field, "base_price") ||
|
||||||
@@ -6206,6 +6833,9 @@ func productPerformanceMetricWeightField(field string) string {
|
|||||||
if productPerformanceUsesCostWeight(field) {
|
if productPerformanceUsesCostWeight(field) {
|
||||||
return "__product_cost_weight"
|
return "__product_cost_weight"
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(field, "stock_turnover") {
|
||||||
|
return "sales_qty_" + productPerformanceScoreSuffix(field)
|
||||||
|
}
|
||||||
if strings.Contains(field, "_180d") {
|
if strings.Contains(field, "_180d") {
|
||||||
return "sales_qty_180d"
|
return "sales_qty_180d"
|
||||||
}
|
}
|
||||||
@@ -6264,23 +6894,10 @@ func productPerformanceScoreSuffix(field string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceScoreWeight(row map[string]any, suffix string) float64 {
|
func productPerformanceScoreWeight(row map[string]any, suffix string) float64 {
|
||||||
if weight := floatFromMap(row, "sales_usd_"+suffix); weight > 0 {
|
|
||||||
return weight
|
|
||||||
}
|
|
||||||
if weight := floatFromMap(row, "sales_qty_"+suffix); weight > 0 {
|
if weight := floatFromMap(row, "sales_qty_"+suffix); weight > 0 {
|
||||||
return weight
|
return weight
|
||||||
}
|
}
|
||||||
stockQty := floatFromMap(row, "stock_qty")
|
return 0
|
||||||
if stockQty <= 0 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if cost := floatFromMap(row, "cost_price_usd_"+suffix); cost > 0 {
|
|
||||||
return stockQty * cost
|
|
||||||
}
|
|
||||||
if cost := floatFromMap(row, "cost_price_usd"); cost > 0 {
|
|
||||||
return stockQty * cost
|
|
||||||
}
|
|
||||||
return stockQty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceCostWeight(row map[string]any) float64 {
|
func productPerformanceCostWeight(row map[string]any) float64 {
|
||||||
|
|||||||
@@ -486,6 +486,13 @@ SalesAgg AS (
|
|||||||
SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS sales_usd_365d,
|
SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS sales_usd_365d,
|
||||||
SUM(sales_usd) FILTER (WHERE sales_date >= DATE '2022-01-01') AS sales_usd_total,
|
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 '89 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_90d,
|
||||||
|
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_180d,
|
||||||
|
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_365d,
|
||||||
|
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(sales_usd, 0) > 0) AS customer_count_total,
|
||||||
|
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS first_sale_date_90d,
|
||||||
|
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days') AS first_sale_date_180d,
|
||||||
|
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS first_sale_date_365d,
|
||||||
|
MIN(sales_date) FILTER (WHERE sales_date >= DATE '2022-01-01') AS first_sale_date_total,
|
||||||
MAX(sales_date) AS last_sale_date,
|
MAX(sales_date) AS last_sale_date,
|
||||||
MAX(last_ref_number) AS last_ref_number
|
MAX(last_ref_number) AS last_ref_number
|
||||||
FROM mk_product_performance_sales_daily
|
FROM mk_product_performance_sales_daily
|
||||||
@@ -500,7 +507,9 @@ Scope AS (
|
|||||||
item_description, kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu,
|
item_description, kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu,
|
||||||
sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total,
|
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,
|
sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, sales_usd_total,
|
||||||
customer_count_90d, last_sale_date, last_ref_number
|
customer_count_90d, customer_count_180d, customer_count_365d, customer_count_total,
|
||||||
|
first_sale_date_90d, first_sale_date_180d, first_sale_date_365d, first_sale_date_total,
|
||||||
|
last_sale_date, last_ref_number
|
||||||
FROM SalesAgg
|
FROM SalesAgg
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
@@ -528,6 +537,13 @@ Scope AS (
|
|||||||
0 AS sales_usd_365d,
|
0 AS sales_usd_365d,
|
||||||
0 AS sales_usd_total,
|
0 AS sales_usd_total,
|
||||||
0 AS customer_count_90d,
|
0 AS customer_count_90d,
|
||||||
|
0 AS customer_count_180d,
|
||||||
|
0 AS customer_count_365d,
|
||||||
|
0 AS customer_count_total,
|
||||||
|
NULL::date AS first_sale_date_90d,
|
||||||
|
NULL::date AS first_sale_date_180d,
|
||||||
|
NULL::date AS first_sale_date_365d,
|
||||||
|
NULL::date AS first_sale_date_total,
|
||||||
NULL::date AS last_sale_date,
|
NULL::date AS last_sale_date,
|
||||||
'' AS last_ref_number
|
'' AS last_ref_number
|
||||||
FROM LatestStock ls
|
FROM LatestStock ls
|
||||||
@@ -570,10 +586,10 @@ Base AS (
|
|||||||
COALESCE(s.sales_qty_180d, 0) / 180.0 AS avg_daily_sales_180d,
|
COALESCE(s.sales_qty_180d, 0) / 180.0 AS avg_daily_sales_180d,
|
||||||
COALESCE(s.sales_qty_365d, 0) / 360.0 AS avg_daily_sales_365d,
|
COALESCE(s.sales_qty_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(s.sales_qty_total, 0) / GREATEST(1, ($1::date - DATE '2022-01-01') + 1) AS avg_daily_sales_total,
|
||||||
(COALESCE(s90.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_90d,
|
(COALESCE(s90.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_90d,
|
||||||
(COALESCE(s180.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_180d,
|
(COALESCE(s180.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_180d,
|
||||||
(COALESCE(s365.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_365d,
|
(COALESCE(s365.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_365d,
|
||||||
(COALESCE(stotal.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_total,
|
(COALESCE(stotal.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_total,
|
||||||
CASE WHEN COALESCE(s.sales_qty_90d, 0) = 0 THEN 0 ELSE COALESCE(s.sales_usd_90d, 0) / NULLIF(s.sales_qty_90d, 0) END AS avg_price_usd_90d,
|
CASE WHEN COALESCE(s.sales_qty_90d, 0) = 0 THEN 0 ELSE COALESCE(s.sales_usd_90d, 0) / NULLIF(s.sales_qty_90d, 0) END AS avg_price_usd_90d,
|
||||||
CASE WHEN COALESCE(s.sales_qty_180d, 0) = 0 THEN 0 ELSE COALESCE(s.sales_usd_180d, 0) / NULLIF(s.sales_qty_180d, 0) END AS avg_price_usd_180d,
|
CASE WHEN COALESCE(s.sales_qty_180d, 0) = 0 THEN 0 ELSE COALESCE(s.sales_usd_180d, 0) / NULLIF(s.sales_qty_180d, 0) END AS avg_price_usd_180d,
|
||||||
COALESCE(s.sales_usd_90d, 0) - (COALESCE(s.sales_qty_90d, 0) * COALESCE(pd.cost_price_usd, 0)) AS gross_profit_usd_90d,
|
COALESCE(s.sales_usd_90d, 0) - (COALESCE(s.sales_qty_90d, 0) * COALESCE(pd.cost_price_usd, 0)) AS gross_profit_usd_90d,
|
||||||
@@ -590,11 +606,47 @@ Base AS (
|
|||||||
CASE WHEN COALESCE(s.sales_qty_180d, 0) = 0 THEN 0 ELSE (COALESCE(s.sales_usd_180d, 0) / NULLIF(s.sales_qty_180d, 0)) - COALESCE(pd.base_price_usd, 0) END AS unit_profit_base_180d
|
CASE WHEN COALESCE(s.sales_qty_180d, 0) = 0 THEN 0 ELSE (COALESCE(s.sales_usd_180d, 0) / NULLIF(s.sales_qty_180d, 0)) - COALESCE(pd.base_price_usd, 0) END AS unit_profit_base_180d
|
||||||
FROM Scope s
|
FROM Scope s
|
||||||
LEFT JOIN LatestStock ls ON ls.product_code=s.product_code AND ls.color_code=s.color_code AND ls.yaka_kodu=s.yaka_kodu
|
LEFT JOIN LatestStock ls ON ls.product_code=s.product_code AND ls.color_code=s.color_code AND ls.yaka_kodu=s.yaka_kodu
|
||||||
LEFT JOIN Stock90Start s90 ON s90.product_code=s.product_code AND s90.color_code=s.color_code AND s90.yaka_kodu=s.yaka_kodu
|
|
||||||
LEFT JOIN Stock180Start s180 ON s180.product_code=s.product_code AND s180.color_code=s.color_code AND s180.yaka_kodu=s.yaka_kodu
|
|
||||||
LEFT JOIN Stock365Start s365 ON s365.product_code=s.product_code AND s365.color_code=s.color_code AND s365.yaka_kodu=s.yaka_kodu
|
|
||||||
LEFT JOIN StockTotalStart stotal ON stotal.product_code=s.product_code AND stotal.color_code=s.color_code AND stotal.yaka_kodu=s.yaka_kodu
|
|
||||||
LEFT JOIN PriceDim 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')
|
WHERE upper(translate(btrim(COALESCE(NULLIF(s.urun_ilk_grubu,''), pd.urun_ilk_grubu, '')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
||||||
),
|
),
|
||||||
Spread AS (
|
Spread AS (
|
||||||
@@ -602,15 +654,20 @@ Spread AS (
|
|||||||
product_code,
|
product_code,
|
||||||
color_code,
|
color_code,
|
||||||
yaka_kodu,
|
yaka_kodu,
|
||||||
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) AS market_count_90d
|
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_90d, 0) > 0) AS market_count_90d,
|
||||||
|
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_180d, 0) > 0) AS market_count_180d,
|
||||||
|
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_365d, 0) > 0) AS market_count_365d,
|
||||||
|
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_total, 0) > 0) AS market_count_total
|
||||||
FROM SalesAgg
|
FROM SalesAgg
|
||||||
WHERE COALESCE(sales_usd_90d, 0) > 0
|
|
||||||
GROUP BY product_code, color_code, yaka_kodu
|
GROUP BY product_code, color_code, yaka_kodu
|
||||||
),
|
),
|
||||||
Market AS (
|
Market AS (
|
||||||
SELECT
|
SELECT
|
||||||
market_key,
|
market_key,
|
||||||
AVG(NULLIF(sales_qty_90d, 0)) AS market_avg_sales_qty_90d,
|
AVG(NULLIF(sales_qty_90d, 0)) AS market_avg_sales_qty_90d,
|
||||||
|
AVG(NULLIF(sales_qty_180d, 0)) AS market_avg_sales_qty_180d,
|
||||||
|
AVG(NULLIF(sales_qty_365d, 0)) AS market_avg_sales_qty_365d,
|
||||||
|
AVG(NULLIF(sales_qty_total, 0)) AS market_avg_sales_qty_total,
|
||||||
AVG(NULLIF(avg_price_usd_90d, 0)) AS market_avg_price_usd_90d,
|
AVG(NULLIF(avg_price_usd_90d, 0)) AS market_avg_price_usd_90d,
|
||||||
AVG(NULLIF(gross_margin_90d, 0)) AS market_avg_margin_90d
|
AVG(NULLIF(gross_margin_90d, 0)) AS market_avg_margin_90d
|
||||||
FROM Base
|
FROM Base
|
||||||
@@ -620,15 +677,21 @@ Scored AS (
|
|||||||
SELECT
|
SELECT
|
||||||
b.*,
|
b.*,
|
||||||
COALESCE(sp.market_count_90d, 0) AS market_count_90d,
|
COALESCE(sp.market_count_90d, 0) AS market_count_90d,
|
||||||
|
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_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_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_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_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) END AS stock_turnover_90d,
|
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) END AS stock_turnover_180d,
|
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_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) END AS stock_turnover_total,
|
CASE WHEN b.avg_stock_total = 0 THEN 0 ELSE b.sales_qty_total / NULLIF(b.avg_stock_total, 0) * 360.0 / GREATEST(1, ($1::date - DATE '2022-01-01') + 1) END AS stock_turnover_total,
|
||||||
CASE WHEN COALESCE(m.market_avg_sales_qty_90d, 0) = 0 THEN 0 ELSE b.sales_qty_90d / NULLIF(m.market_avg_sales_qty_90d, 0) END AS sales_index_90d,
|
CASE WHEN COALESCE(m.market_avg_sales_qty_90d, 0) = 0 THEN 0 ELSE b.sales_qty_90d / NULLIF(m.market_avg_sales_qty_90d, 0) END AS sales_index_90d,
|
||||||
|
CASE WHEN COALESCE(m.market_avg_sales_qty_180d, 0) = 0 THEN 0 ELSE b.sales_qty_180d / NULLIF(m.market_avg_sales_qty_180d, 0) END AS sales_index_180d,
|
||||||
|
CASE WHEN COALESCE(m.market_avg_sales_qty_365d, 0) = 0 THEN 0 ELSE b.sales_qty_365d / NULLIF(m.market_avg_sales_qty_365d, 0) END AS sales_index_365d,
|
||||||
|
CASE WHEN COALESCE(m.market_avg_sales_qty_total, 0) = 0 THEN 0 ELSE b.sales_qty_total / NULLIF(m.market_avg_sales_qty_total, 0) END AS sales_index_total,
|
||||||
CASE WHEN COALESCE(m.market_avg_price_usd_90d, 0) = 0 THEN 0 ELSE b.avg_price_usd_90d / NULLIF(m.market_avg_price_usd_90d, 0) END AS price_index_90d,
|
CASE WHEN COALESCE(m.market_avg_price_usd_90d, 0) = 0 THEN 0 ELSE b.avg_price_usd_90d / NULLIF(m.market_avg_price_usd_90d, 0) END AS price_index_90d,
|
||||||
CASE WHEN COALESCE(m.market_avg_margin_90d, 0) = 0 THEN 0 ELSE b.gross_margin_90d / NULLIF(m.market_avg_margin_90d, 0) END AS margin_index_90d
|
CASE WHEN COALESCE(m.market_avg_margin_90d, 0) = 0 THEN 0 ELSE b.gross_margin_90d / NULLIF(m.market_avg_margin_90d, 0) END AS margin_index_90d
|
||||||
FROM Base b
|
FROM Base b
|
||||||
@@ -640,11 +703,14 @@ INSERT INTO mk_product_performance_kpi_daily (
|
|||||||
kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty,
|
kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty,
|
||||||
sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total,
|
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,
|
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,
|
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,
|
avg_price_usd_90d, avg_price_usd_180d, cost_price_usd, base_price_usd, base_price_try,
|
||||||
gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d,
|
gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d,
|
||||||
unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d, market_count_90d, customer_count_90d,
|
unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d,
|
||||||
sales_index_90d, price_index_90d, margin_index_90d, performance_score,
|
market_count_90d, market_count_180d, market_count_365d, market_count_total,
|
||||||
|
customer_count_90d, customer_count_180d, customer_count_365d, customer_count_total,
|
||||||
|
sales_index_90d, sales_index_180d, sales_index_365d, sales_index_total, price_index_90d, margin_index_90d, performance_score,
|
||||||
performance_bucket, recommendation, last_sale_date, last_ref_number, updated_at
|
performance_bucket, recommendation, last_sale_date, last_ref_number, updated_at
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
@@ -653,22 +719,25 @@ SELECT
|
|||||||
kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty,
|
kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty,
|
||||||
COALESCE(sales_qty_30d, 0), COALESCE(sales_qty_90d, 0), COALESCE(sales_qty_180d, 0), COALESCE(sales_qty_365d, 0), COALESCE(sales_qty_730d, 0), COALESCE(sales_qty_total, 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(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(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(avg_price_usd_90d, 0), COALESCE(avg_price_usd_180d, 0), COALESCE(cost_price_usd, 0), COALESCE(base_price_usd, 0), COALESCE(base_price_try, 0),
|
||||||
COALESCE(gross_profit_usd_90d, 0), COALESCE(gross_profit_usd_180d, 0), COALESCE(gross_margin_90d, 0), COALESCE(gross_margin_180d, 0),
|
COALESCE(gross_profit_usd_90d, 0), COALESCE(gross_profit_usd_180d, 0), COALESCE(gross_margin_90d, 0), COALESCE(gross_margin_180d, 0),
|
||||||
COALESCE(unit_profit_cost_90d, 0), COALESCE(unit_profit_cost_180d, 0), COALESCE(unit_profit_base_90d, 0), COALESCE(unit_profit_base_180d, 0), COALESCE(market_count_90d, 0), COALESCE(customer_count_90d, 0),
|
COALESCE(unit_profit_cost_90d, 0), COALESCE(unit_profit_cost_180d, 0), COALESCE(unit_profit_base_90d, 0), COALESCE(unit_profit_base_180d, 0),
|
||||||
COALESCE(sales_index_90d, 0), COALESCE(price_index_90d, 0), COALESCE(margin_index_90d, 0),
|
COALESCE(market_count_90d, 0), COALESCE(market_count_180d, 0), COALESCE(market_count_365d, 0), COALESCE(market_count_total, 0),
|
||||||
|
COALESCE(customer_count_90d, 0), COALESCE(customer_count_180d, 0), COALESCE(customer_count_365d, 0), COALESCE(customer_count_total, 0),
|
||||||
|
COALESCE(sales_index_90d, 0), COALESCE(sales_index_180d, 0), COALESCE(sales_index_365d, 0), COALESCE(sales_index_total, 0), COALESCE(price_index_90d, 0), COALESCE(margin_index_90d, 0),
|
||||||
CASE WHEN COALESCE(sales_usd_90d, 0) <= 0 THEN 1 ELSE
|
CASE WHEN COALESCE(sales_usd_90d, 0) <= 0 THEN 1 ELSE
|
||||||
ROUND(
|
ROUND(
|
||||||
LEAST(35, COALESCE(sales_index_90d, 0) * 18)
|
LEAST(30, GREATEST(COALESCE(gross_margin_90d, 0), 0) / 0.45 * 30)
|
||||||
+ LEAST(25, GREATEST(COALESCE(gross_margin_90d, 0), 0) * 50)
|
+ LEAST(20, COALESCE(stock_turnover_90d, 0) / 4.0 * 20)
|
||||||
+ LEAST(20, COALESCE(customer_count_90d, 0) * 2)
|
+ LEAST(20, COALESCE(sales_usd_90d, 0) / 25000.0 * 20)
|
||||||
+ LEAST(10, COALESCE(market_count_90d, 0) * 3)
|
+ LEAST(5, COALESCE(market_count_90d, 0) / 3.0 * 5)
|
||||||
+ CASE WHEN COALESCE(stock_days_90d, 0) BETWEEN 10 AND 90 THEN 10 WHEN COALESCE(stock_days_90d, 0) > 180 THEN -10 ELSE 0 END
|
+ LEAST(25, COALESCE(customer_count_90d, 0) / 8.0 * 25)
|
||||||
, 4)
|
, 4)
|
||||||
END AS performance_score,
|
END AS performance_score,
|
||||||
CASE
|
CASE
|
||||||
WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.25 THEN 'YILDIZ_URUN'
|
WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.45 THEN 'YILDIZ_URUN'
|
||||||
WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'STOKSUZ_TALEP'
|
WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'STOKSUZ_TALEP'
|
||||||
WHEN COALESCE(stock_days_90d,0) > 180 AND COALESCE(sales_index_90d,0) < 0.75 THEN 'STOK_RISKI'
|
WHEN COALESCE(stock_days_90d,0) > 180 AND COALESCE(sales_index_90d,0) < 0.75 THEN 'STOK_RISKI'
|
||||||
WHEN COALESCE(price_index_90d,0) > 1.10 AND COALESCE(sales_index_90d,0) < 0.80 THEN 'FIYAT_BASKISI'
|
WHEN COALESCE(price_index_90d,0) > 1.10 AND COALESCE(sales_index_90d,0) < 0.80 THEN 'FIYAT_BASKISI'
|
||||||
@@ -676,7 +745,7 @@ SELECT
|
|||||||
ELSE 'TAKIP'
|
ELSE 'TAKIP'
|
||||||
END AS performance_bucket,
|
END AS performance_bucket,
|
||||||
CASE
|
CASE
|
||||||
WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.25 THEN 'Piyasa ustu satis ve iyi marj: stok ve fiyat gucu takip edilmeli.'
|
WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.45 THEN 'Piyasa ustu satis ve iyi marj: stok ve fiyat gucu takip edilmeli.'
|
||||||
WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'Talep var, stok yok: uretim/satin alma planina alinmali.'
|
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(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) > 1.10 AND COALESCE(sales_index_90d,0) < 0.80 THEN 'Fiyat piyasa ustunde ve satis zayif: fiyat revizyonu degerlendirilmeli.'
|
||||||
|
|||||||
@@ -793,6 +793,82 @@ ORDER BY SonIsEmri.dteIslemTarihi DESC, LTRIM(RTRIM(R.sMModelKodu)) ASC
|
|||||||
return uretimDB.QueryContext(ctx, sqlText, fromDate, search)
|
return uretimDB.QueryContext(ctx, sqlText, fromDate, search)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ArgeBaggiDepotEntryTotals struct {
|
||||||
|
Ceket float64
|
||||||
|
Pantolon float64
|
||||||
|
Yelek float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArgeBaggiDepotEntryTotals returns operation 985/986/987 quantities keyed
|
||||||
|
// by production work order. The requested work orders are queried in one batch
|
||||||
|
// to avoid an ArgeBAGGI round trip for every list row.
|
||||||
|
func GetArgeBaggiDepotEntryTotals(ctx context.Context, argeDB *sql.DB, workOrders []string) (map[string]ArgeBaggiDepotEntryTotals, error) {
|
||||||
|
out := make(map[string]ArgeBaggiDepotEntryTotals)
|
||||||
|
if argeDB == nil || len(workOrders) == 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, len(workOrders))
|
||||||
|
values := make([]string, 0, len(workOrders))
|
||||||
|
for _, raw := range workOrders {
|
||||||
|
value := strings.TrimSpace(raw)
|
||||||
|
if value == "" || value == "0" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[value]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
if len(values) == 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholders := make([]string, 0, len(values))
|
||||||
|
args := make([]any, 0, len(values))
|
||||||
|
for i, value := range values {
|
||||||
|
placeholders = append(placeholders, fmt.Sprintf("@p%d", i+1))
|
||||||
|
args = append(args, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlText := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(CONVERT(VARCHAR(64), MN.model_adi))) AS IsEmriNo,
|
||||||
|
SUM(CASE WHEN O.oper_num = 985 THEN CONVERT(float, B.adet) ELSE 0 END) AS CeketDepoGiris,
|
||||||
|
SUM(CASE WHEN O.oper_num = 986 THEN CONVERT(float, B.adet) ELSE 0 END) AS PantolonDepoGiris,
|
||||||
|
SUM(CASE WHEN O.oper_num = 987 THEN CONVERT(float, B.adet) ELSE 0 END) AS YelekDepoGiris
|
||||||
|
FROM dbo.is_acma_bar B
|
||||||
|
INNER JOIN dbo.is_acma I ON I.is_num = B.is_num
|
||||||
|
INNER JOIN dbo.model_def MD ON I.imdef_num = MD.mdef_num
|
||||||
|
INNER JOIN dbo.oper_urun_no O ON B.oper_num = O.oper_num
|
||||||
|
INNER JOIN dbo.model_num MN ON I.model_num = MN.model_num
|
||||||
|
INNER JOIN dbo.bant_no BN ON I.bant_num = BN.bant_no
|
||||||
|
INNER JOIN dbo.model_oper MO ON O.oper_num = MO.oper_num AND MD.mdef_num = MO.model_num
|
||||||
|
INNER JOIN dbo.model_oper_tkp_def TD ON MO.tkp_cik = TD.tkp_cik
|
||||||
|
WHERE B.tarih >= '2025-01-01'
|
||||||
|
AND TD.tkp_cik IN (1,2,3,4,5)
|
||||||
|
AND O.oper_num IN (985,986,987)
|
||||||
|
AND LTRIM(RTRIM(CONVERT(VARCHAR(64), MN.model_adi))) IN (%s)
|
||||||
|
GROUP BY LTRIM(RTRIM(CONVERT(VARCHAR(64), MN.model_adi)))
|
||||||
|
`, strings.Join(placeholders, ","))
|
||||||
|
|
||||||
|
rows, err := argeDB.QueryContext(ctx, sqlText, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var workOrder string
|
||||||
|
var totals ArgeBaggiDepotEntryTotals
|
||||||
|
if err := rows.Scan(&workOrder, &totals.Ceket, &totals.Pantolon, &totals.Yelek); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out[strings.TrimSpace(workOrder)] = totals
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
func GetProductionHasCostProducts(
|
func GetProductionHasCostProducts(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
uretimDB *sql.DB,
|
uretimDB *sql.DB,
|
||||||
@@ -816,12 +892,22 @@ WITH SonOnMaliyet AS (
|
|||||||
FROM dbo.spUrtOnMLMas M
|
FROM dbo.spUrtOnMLMas M
|
||||||
WHERE LTRIM(RTRIM(M.UrunKodu)) <> ''
|
WHERE LTRIM(RTRIM(M.UrunKodu)) <> ''
|
||||||
),
|
),
|
||||||
SonSiparis AS (
|
SonSiparisSirali AS (
|
||||||
SELECT
|
SELECT
|
||||||
LTRIM(RTRIM(sMModelKodu)) AS UrunKodu,
|
LTRIM(RTRIM(SD.sMModelKodu)) AS UrunKodu,
|
||||||
MAX(dteIslemTarihi) AS SonSiparisTarihi
|
SD.dteIslemTarihi AS SonSiparisTarihi,
|
||||||
FROM dbo.spUrtSiparisDet
|
SM.nUrtSiparisNo,
|
||||||
GROUP BY LTRIM(RTRIM(sMModelKodu))
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY LTRIM(RTRIM(SD.sMModelKodu))
|
||||||
|
ORDER BY SD.dteIslemTarihi DESC, SD.nUrtSiparisID DESC
|
||||||
|
) AS rn
|
||||||
|
FROM dbo.spUrtSiparisDet SD
|
||||||
|
INNER JOIN dbo.spUrtSiparis SM ON SM.nUrtSiparisID = SD.nUrtSiparisID
|
||||||
|
),
|
||||||
|
SonSiparis AS (
|
||||||
|
SELECT UrunKodu, SonSiparisTarihi, nUrtSiparisNo
|
||||||
|
FROM SonSiparisSirali
|
||||||
|
WHERE rn = 1
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
CASE
|
CASE
|
||||||
@@ -831,6 +917,7 @@ SELECT
|
|||||||
THEN N'FASON ICIN URETILEN'
|
THEN N'FASON ICIN URETILEN'
|
||||||
ELSE N'Tanimsiz'
|
ELSE N'Tanimsiz'
|
||||||
END AS UretimSekli,
|
END AS UretimSekli,
|
||||||
|
RTRIM(CONVERT(VARCHAR(32), ISNULL(SS.nUrtSiparisNo, 0))) AS nUrtSiparisNo,
|
||||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(OM.nOnMLNo, 0))) AS nOnMLNo,
|
RTRIM(CONVERT(VARCHAR(32), ISNULL(OM.nOnMLNo, 0))) AS nOnMLNo,
|
||||||
LTRIM(RTRIM(ISNULL(OM.UrunKodu, ''))) AS UrunKodu,
|
LTRIM(RTRIM(ISNULL(OM.UrunKodu, ''))) AS UrunKodu,
|
||||||
ISNULL(OM.UrunAdi, '') AS UrunAdi,
|
ISNULL(OM.UrunAdi, '') AS UrunAdi,
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ type productPerformanceExcelVariant struct {
|
|||||||
UrunAnaGrubu string
|
UrunAnaGrubu string
|
||||||
UrunAltGrubu string
|
UrunAltGrubu string
|
||||||
StockQty float64
|
StockQty float64
|
||||||
|
AvgStock90 float64
|
||||||
|
AvgStock180 float64
|
||||||
|
AvgStock365 float64
|
||||||
|
AvgStockTotal float64
|
||||||
BasePriceUSD float64
|
BasePriceUSD float64
|
||||||
CostPriceUSD float64
|
CostPriceUSD float64
|
||||||
|
|
||||||
@@ -188,6 +192,10 @@ func (v *productPerformanceExcelVariant) addProductRow(row models.ProductPerform
|
|||||||
v.SalesUSD90 += row.SalesUSD90
|
v.SalesUSD90 += row.SalesUSD90
|
||||||
v.SalesUSD180 += row.SalesUSD180
|
v.SalesUSD180 += row.SalesUSD180
|
||||||
v.SalesUSD365 += row.SalesUSD365
|
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.SalesQtyTotalProduct += row.SalesQtyTotal
|
||||||
v.SalesUSDTotalProduct += row.SalesUSDTotal
|
v.SalesUSDTotalProduct += row.SalesUSDTotal
|
||||||
v.MarketCount90 = productPerformanceExcelMaxInt(v.MarketCount90, row.MarketCount90)
|
v.MarketCount90 = productPerformanceExcelMaxInt(v.MarketCount90, row.MarketCount90)
|
||||||
@@ -407,9 +415,9 @@ func productPerformanceExcelPeriodColumns(label, suffix string, values func(*pro
|
|||||||
qty, usd, _, _ := values(v)
|
qty, usd, _, _ := values(v)
|
||||||
return productPerformanceExcelAvgPrice(usd, qty)
|
return productPerformanceExcelAvgPrice(usd, qty)
|
||||||
}),
|
}),
|
||||||
numberExcelColumn(label+" Stok Devir Hızı", "number", func(v *productPerformanceExcelVariant) any {
|
numberExcelColumn(label+" Yıllık Stok Devir", "number", func(v *productPerformanceExcelVariant) any {
|
||||||
qty, _, _, _ := values(v)
|
qty, _, _, _ := values(v)
|
||||||
return productPerformanceExcelStockTurnover(qty, v.StockQty)
|
return productPerformanceExcelStockTurnover(qty, productPerformanceExcelAvgStock(v, suffix), productPerformanceExcelPeriodDays(v, suffix))
|
||||||
}),
|
}),
|
||||||
numberExcelColumn(label+" Taban Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.BasePriceUSD }),
|
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+" Çıplak Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.CostPriceUSD }),
|
||||||
@@ -439,7 +447,8 @@ func productPerformanceExcelPeriodColumns(label, suffix string, values func(*pro
|
|||||||
}),
|
}),
|
||||||
numberExcelColumn(label+" Ürün Skor", "number", func(v *productPerformanceExcelVariant) any {
|
numberExcelColumn(label+" Ürün Skor", "number", func(v *productPerformanceExcelVariant) any {
|
||||||
qty, usd, markets, customers := values(v)
|
qty, usd, markets, customers := values(v)
|
||||||
return productPerformanceExcelProductScore(suffix, usd, productPerformanceExcelStockTurnover(qty, v.StockQty), float64(markets), float64(customers), productPerformanceExcelMargin(usd, qty, v.CostPriceUSD))
|
days := productPerformanceExcelPeriodDays(v, suffix)
|
||||||
|
return productPerformanceExcelProductScore(suffix, usd, productPerformanceExcelStockTurnover(qty, productPerformanceExcelAvgStock(v, suffix), days), float64(markets), float64(customers), productPerformanceExcelMargin(usd, qty, v.CostPriceUSD), days)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -657,13 +666,13 @@ func productPerformanceExcelSortNumber(row *productPerformanceExcelVariant, sort
|
|||||||
case "avg_price_usd_total":
|
case "avg_price_usd_total":
|
||||||
return productPerformanceExcelAvgPrice(row.totalUSD(), row.totalQty())
|
return productPerformanceExcelAvgPrice(row.totalUSD(), row.totalQty())
|
||||||
case "stock_turnover_90d":
|
case "stock_turnover_90d":
|
||||||
return productPerformanceExcelStockTurnover(row.SalesQty90, row.StockQty)
|
return productPerformanceExcelStockTurnover(row.SalesQty90, productPerformanceExcelAvgStock(row, "90d"), productPerformanceExcelPeriodDays(row, "90d"))
|
||||||
case "stock_turnover_180d":
|
case "stock_turnover_180d":
|
||||||
return productPerformanceExcelStockTurnover(row.SalesQty180, row.StockQty)
|
return productPerformanceExcelStockTurnover(row.SalesQty180, productPerformanceExcelAvgStock(row, "180d"), productPerformanceExcelPeriodDays(row, "180d"))
|
||||||
case "stock_turnover_365d":
|
case "stock_turnover_365d":
|
||||||
return productPerformanceExcelStockTurnover(row.SalesQty365, row.StockQty)
|
return productPerformanceExcelStockTurnover(row.SalesQty365, productPerformanceExcelAvgStock(row, "365d"), productPerformanceExcelPeriodDays(row, "365d"))
|
||||||
case "stock_turnover_total":
|
case "stock_turnover_total":
|
||||||
return productPerformanceExcelStockTurnover(row.totalQty(), row.StockQty)
|
return productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), productPerformanceExcelPeriodDays(row, "total"))
|
||||||
case "base_price_usd":
|
case "base_price_usd":
|
||||||
return row.BasePriceUSD
|
return row.BasePriceUSD
|
||||||
case "cost_price_usd":
|
case "cost_price_usd":
|
||||||
@@ -677,9 +686,11 @@ func productPerformanceExcelSortNumber(row *productPerformanceExcelVariant, sort
|
|||||||
case "customer_count_total":
|
case "customer_count_total":
|
||||||
return float64(row.CustomerCountTotal)
|
return float64(row.CustomerCountTotal)
|
||||||
case "performance_score", "performance_score_90d":
|
case "performance_score", "performance_score_90d":
|
||||||
return productPerformanceExcelProductScore("90d", row.SalesUSD90, productPerformanceExcelStockTurnover(row.SalesQty90, row.StockQty), float64(row.marketCount90()), float64(row.CustomerCount90), productPerformanceExcelMargin(row.SalesUSD90, row.SalesQty90, row.CostPriceUSD))
|
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":
|
case "performance_score_total":
|
||||||
return productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), row.StockQty), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD))
|
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:
|
default:
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -795,7 +806,7 @@ func productPerformanceExcelStatus(row *productPerformanceExcelVariant) string {
|
|||||||
return "Stok Riski"
|
return "Stok Riski"
|
||||||
case productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD) < 0:
|
case productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD) < 0:
|
||||||
return "Fiyat Baskısı"
|
return "Fiyat Baskısı"
|
||||||
case productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), row.StockQty), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD)) >= 70:
|
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"
|
return "Yıldız Ürün"
|
||||||
default:
|
default:
|
||||||
return productPerformanceExcelBucketLabel(row.PerformanceBucket)
|
return productPerformanceExcelBucketLabel(row.PerformanceBucket)
|
||||||
@@ -881,11 +892,55 @@ func productPerformanceExcelAvgPrice(salesUSD, qty float64) float64 {
|
|||||||
return salesUSD / qty
|
return salesUSD / qty
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceExcelStockTurnover(salesQty, stockQty float64) float64 {
|
const productPerformanceExcelStockTurnoverYearDays = 360.0
|
||||||
if stockQty <= 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
|
return 0
|
||||||
}
|
}
|
||||||
return salesQty / stockQty
|
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 {
|
func productPerformanceExcelGrossProfit(salesUSD, qty, unitCost float64) float64 {
|
||||||
@@ -902,37 +957,102 @@ func productPerformanceExcelMargin(salesUSD, qty, unitCost float64) float64 {
|
|||||||
return productPerformanceExcelGrossProfit(salesUSD, qty, unitCost) / salesUSD
|
return productPerformanceExcelGrossProfit(salesUSD, qty, unitCost) / salesUSD
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceExcelProductScore(suffix string, salesUSD, stockTurnover, marketCount, customerCount, margin float64) float64 {
|
func productPerformanceExcelProductScore(suffix string, salesUSD, stockTurnover, marketCount, customerCount, margin float64, periodDays ...float64) float64 {
|
||||||
if salesUSD <= 0 {
|
if salesUSD <= 0 {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
revenueScore := productPerformanceExcelRatioScore(salesUSD, productPerformanceExcelProductRevenueTarget(suffix))
|
days := productPerformanceExcelScorePeriodDays(suffix, periodDays...)
|
||||||
|
revenueScore := productPerformanceExcelRatioScore(salesUSD, productPerformanceExcelProductRevenueTargetForDays(suffix, days))
|
||||||
score := 0.30*productPerformanceExcelMarginScore(margin) +
|
score := 0.30*productPerformanceExcelMarginScore(margin) +
|
||||||
0.20*productPerformanceExcelRatioScore(stockTurnover, 1.50) +
|
0.20*productPerformanceExcelRatioScore(stockTurnover, 4) +
|
||||||
0.20*revenueScore +
|
0.20*revenueScore +
|
||||||
0.15*productPerformanceExcelRatioScore(marketCount, 8) +
|
0.05*productPerformanceExcelRatioScore(marketCount, productPerformanceExcelMarketSpreadTarget(suffix)) +
|
||||||
0.15*productPerformanceExcelRatioScore(customerCount, 25)
|
0.25*productPerformanceExcelRatioScore(customerCount, productPerformanceExcelCustomerSpreadTargetForDays(suffix, days))
|
||||||
return productPerformanceExcelRoundScore(score)
|
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 {
|
func productPerformanceExcelProductRevenueTarget(suffix string) float64 {
|
||||||
|
return productPerformanceExcelProductRevenueTargetForDays(suffix, productPerformanceExcelScorePeriodDays(suffix))
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceExcelProductRevenueTargetForDays(suffix string, periodDays float64) float64 {
|
||||||
switch suffix {
|
switch suffix {
|
||||||
case "180d":
|
case "180d":
|
||||||
return 20000
|
return 50000
|
||||||
case "365d":
|
case "365d":
|
||||||
return 40000
|
return 100000
|
||||||
case "total":
|
case "total":
|
||||||
return 120000
|
return 50000 * productPerformanceExcelTotalPeriodMultiplier(periodDays)
|
||||||
default:
|
default:
|
||||||
return 10000
|
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 {
|
func productPerformanceExcelMarginScore(margin float64) float64 {
|
||||||
if margin < 0 {
|
if margin < 0 {
|
||||||
margin = 0
|
margin = 0
|
||||||
}
|
}
|
||||||
return productPerformanceExcelRatioScore(margin, 0.40)
|
return productPerformanceExcelRatioScore(margin, 0.45)
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceExcelRatioScore(value, target float64) float64 {
|
func productPerformanceExcelRatioScore(value, target float64) float64 {
|
||||||
|
|||||||
@@ -158,6 +158,26 @@ func GetProductionNoCostProductsHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if argeDB := db.GetArgeBaggiDB(); argeDB != nil && len(list) > 0 {
|
||||||
|
workOrders := make([]string, 0, len(list))
|
||||||
|
for i := range list {
|
||||||
|
workOrders = append(workOrders, list[i].UrtSiparisNo)
|
||||||
|
}
|
||||||
|
argeCtx, cancelArge := context.WithTimeout(r.Context(), 15*time.Second)
|
||||||
|
totalsByWorkOrder, argeErr := queries.GetArgeBaggiDepotEntryTotals(argeCtx, argeDB, workOrders)
|
||||||
|
cancelArge()
|
||||||
|
if argeErr != nil {
|
||||||
|
log.Printf("[ProductionNoCost] ArgeBAGGI enrichment error: %v", argeErr)
|
||||||
|
} else {
|
||||||
|
for i := range list {
|
||||||
|
totals := totalsByWorkOrder[strings.TrimSpace(list[i].UrtSiparisNo)]
|
||||||
|
list[i].CeketDepoGiris = totals.Ceket
|
||||||
|
list[i].PantolonDepoGiris = totals.Pantolon
|
||||||
|
list[i].YelekDepoGiris = totals.Yelek
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = json.NewEncoder(w).Encode(list)
|
_ = json.NewEncoder(w).Encode(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +211,7 @@ func GetProductionHasCostProductsHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
var item models.ProductionHasCostProductRow
|
var item models.ProductionHasCostProductRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&item.UretimSekli,
|
&item.UretimSekli,
|
||||||
|
&item.UrtSiparisNo,
|
||||||
&item.NOnMLNo,
|
&item.NOnMLNo,
|
||||||
&item.UrunKodu,
|
&item.UrunKodu,
|
||||||
&item.UrunAdi,
|
&item.UrunAdi,
|
||||||
@@ -219,6 +240,26 @@ func GetProductionHasCostProductsHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if argeDB := db.GetArgeBaggiDB(); argeDB != nil && len(list) > 0 {
|
||||||
|
workOrders := make([]string, 0, len(list))
|
||||||
|
for i := range list {
|
||||||
|
workOrders = append(workOrders, list[i].UrtSiparisNo)
|
||||||
|
}
|
||||||
|
argeCtx, cancelArge := context.WithTimeout(r.Context(), 15*time.Second)
|
||||||
|
totalsByWorkOrder, argeErr := queries.GetArgeBaggiDepotEntryTotals(argeCtx, argeDB, workOrders)
|
||||||
|
cancelArge()
|
||||||
|
if argeErr != nil {
|
||||||
|
log.Printf("[ProductionHasCost] ArgeBAGGI enrichment error: %v", argeErr)
|
||||||
|
} else {
|
||||||
|
for i := range list {
|
||||||
|
totals := totalsByWorkOrder[strings.TrimSpace(list[i].UrtSiparisNo)]
|
||||||
|
list[i].CeketDepoGiris = totals.Ceket
|
||||||
|
list[i].PantolonDepoGiris = totals.Pantolon
|
||||||
|
list[i].YelekDepoGiris = totals.Yelek
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = json.NewEncoder(w).Encode(list)
|
_ = json.NewEncoder(w).Encode(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1427,6 +1468,12 @@ func postProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
totalUSD := 0.0
|
totalUSD := 0.0
|
||||||
totalEUR := 0.0
|
totalEUR := 0.0
|
||||||
for _, r := range req.Detail.Upserts {
|
for _, r := range req.Detail.Upserts {
|
||||||
|
// The top header in the detail screen only includes rows marked
|
||||||
|
// "Maliyete Dahil". Persist that exact business total to the master
|
||||||
|
// record as well; excluded detail rows must not inflate the cost.
|
||||||
|
if r.MaliyeteDahil != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
qty := r.LMiktar
|
qty := r.LMiktar
|
||||||
if qty < 0 {
|
if qty < 0 {
|
||||||
qty = 0
|
qty = 0
|
||||||
|
|||||||
+15
-1
@@ -1,8 +1,20 @@
|
|||||||
// quasar.config.js
|
// quasar.config.js
|
||||||
import { defineConfig } from '#q-app/wrappers'
|
import { defineConfig } from '#q-app/wrappers'
|
||||||
|
import { execSync } from 'node:child_process'
|
||||||
|
|
||||||
|
function readGitBuildInfo () {
|
||||||
|
try {
|
||||||
|
const commitCount = execSync('git rev-list --count HEAD', { encoding: 'utf8' }).trim()
|
||||||
|
const commitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim()
|
||||||
|
return { version: `v1.0.${commitCount}`, commit: commitHash }
|
||||||
|
} catch {
|
||||||
|
return { version: 'v1.0.0', commit: 'local' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig(() => {
|
export default defineConfig(() => {
|
||||||
const apiBaseUrl = (process.env.VITE_API_BASE_URL || '/api').trim()
|
const apiBaseUrl = (process.env.VITE_API_BASE_URL || '/api').trim()
|
||||||
|
const gitBuildInfo = readGitBuildInfo()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
@@ -36,7 +48,9 @@ export default defineConfig(() => {
|
|||||||
build: {
|
build: {
|
||||||
vueRouterMode: 'hash',
|
vueRouterMode: 'hash',
|
||||||
env: {
|
env: {
|
||||||
VITE_API_BASE_URL: apiBaseUrl
|
VITE_API_BASE_URL: apiBaseUrl,
|
||||||
|
APP_VERSION: gitBuildInfo.version,
|
||||||
|
APP_COMMIT: gitBuildInfo.commit
|
||||||
},
|
},
|
||||||
|
|
||||||
esbuildTarget: {
|
esbuildTarget: {
|
||||||
|
|||||||
@@ -74,6 +74,10 @@
|
|||||||
</q-form>
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
|
<div class="login-version" :title="`Commit: ${appCommit}`">
|
||||||
|
Baggi BSS {{ appVersion }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 🔐 FORGOT PASSWORD -->
|
<!-- 🔐 FORGOT PASSWORD -->
|
||||||
<q-dialog v-model="forgotOpen" persistent>
|
<q-dialog v-model="forgotOpen" persistent>
|
||||||
<q-card style="width:420px; max-width:90vw">
|
<q-card style="width:420px; max-width:90vw">
|
||||||
@@ -129,6 +133,8 @@ import api from 'src/services/api'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
|
const appVersion = process.env.APP_VERSION || 'v1.0.0'
|
||||||
|
const appCommit = process.env.APP_COMMIT || 'local'
|
||||||
|
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
@@ -266,6 +272,21 @@ async function sendResetMail () {
|
|||||||
.full-width {
|
.full-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-version {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
bottom: 12px;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgba(45, 45, 45, 0.75);
|
||||||
|
background: rgba(255, 255, 255, 0.68);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
/* ===============================
|
/* ===============================
|
||||||
LOGIN INPUT TEXT COLOR (GOLD)
|
LOGIN INPUT TEXT COLOR (GOLD)
|
||||||
=============================== */
|
=============================== */
|
||||||
|
|||||||
@@ -1289,12 +1289,12 @@ function normalizeUploadsPath (storagePath) {
|
|||||||
|
|
||||||
function resolveProductImageUrl (item) {
|
function resolveProductImageUrl (item) {
|
||||||
if (!item || typeof item !== 'object') return ''
|
if (!item || typeof item !== 'object') return ''
|
||||||
|
const contentUrl = toText(item.content_url || item.ContentURL)
|
||||||
|
if (contentUrl) return contentUrl.startsWith('/api/') ? contentUrl : contentUrl
|
||||||
const thumbUrl = toText(item.thumb_url || item.thumbUrl)
|
const thumbUrl = toText(item.thumb_url || item.thumbUrl)
|
||||||
if (thumbUrl) return thumbUrl
|
if (thumbUrl) return thumbUrl
|
||||||
const fullUrl = toText(item.full_url || item.fullUrl)
|
const fullUrl = toText(item.full_url || item.fullUrl)
|
||||||
if (fullUrl) return fullUrl
|
if (fullUrl) return fullUrl
|
||||||
const contentUrl = toText(item.content_url || item.ContentURL)
|
|
||||||
if (contentUrl) return contentUrl.startsWith('/api/') ? contentUrl : contentUrl
|
|
||||||
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage)
|
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage)
|
||||||
if (uploadsPath) return uploadsPath
|
if (uploadsPath) return uploadsPath
|
||||||
const fileName = toText(item.file_name || item.FileName)
|
const fileName = toText(item.file_name || item.FileName)
|
||||||
|
|||||||
@@ -35,6 +35,15 @@
|
|||||||
:disable="pageBusy"
|
:disable="pageBusy"
|
||||||
class="period-selector bg-white q-px-xs q-py-none"
|
class="period-selector bg-white q-px-xs q-py-none"
|
||||||
/>
|
/>
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
outline
|
||||||
|
color="primary"
|
||||||
|
icon="view_column"
|
||||||
|
:label="detailColumnsHidden ? 'Detay Kolonları Göster' : 'Detay Kolonları Gizle'"
|
||||||
|
:disable="pageBusy"
|
||||||
|
@click="detailColumnsHidden = !detailColumnsHidden"
|
||||||
|
/>
|
||||||
<q-btn-dropdown
|
<q-btn-dropdown
|
||||||
v-model="detailLevelMenuOpen"
|
v-model="detailLevelMenuOpen"
|
||||||
split
|
split
|
||||||
@@ -90,20 +99,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="activeTab === 'product_detail'" class="main-group-bar q-mb-xs">
|
|
||||||
<button
|
|
||||||
v-for="option in detailMainGroupOptions"
|
|
||||||
:key="option.value"
|
|
||||||
type="button"
|
|
||||||
:class="['main-group-button', { active: selectedDetailMainGroup === option.value }]"
|
|
||||||
:disabled="pageBusy"
|
|
||||||
@click="selectedDetailMainGroup = option.value"
|
|
||||||
>
|
|
||||||
<span>{{ option.label }}</span>
|
|
||||||
<small>{{ formatNumber(option.stock_qty, 0) }} stok</small>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ref="topScrollbarRef" class="performance-top-scrollbar q-mb-xs">
|
<div ref="topScrollbarRef" class="performance-top-scrollbar q-mb-xs">
|
||||||
<div ref="topScrollbarInnerRef" class="performance-top-scrollbar-inner"></div>
|
<div ref="topScrollbarInnerRef" class="performance-top-scrollbar-inner"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,9 +108,14 @@
|
|||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
row-key="row_key"
|
row-key="row_key"
|
||||||
class="performance-table sticky-dim-table sticky-dim-7 bg-white"
|
:class="[
|
||||||
|
'performance-table',
|
||||||
|
'sticky-dim-table',
|
||||||
|
detailColumnsHidden ? 'sticky-dim-4' : 'sticky-dim-7',
|
||||||
|
'bg-white'
|
||||||
|
]"
|
||||||
:rows="filteredGeneralRows"
|
:rows="filteredGeneralRows"
|
||||||
:columns="generalColumns"
|
:columns="visibleGeneralColumns"
|
||||||
:loading="activeTableLoading"
|
:loading="activeTableLoading"
|
||||||
v-model:pagination="tablePagination.general"
|
v-model:pagination="tablePagination.general"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
@@ -560,6 +560,7 @@
|
|||||||
'performance-table',
|
'performance-table',
|
||||||
'product-breakdown-table',
|
'product-breakdown-table',
|
||||||
'bg-white',
|
'bg-white',
|
||||||
|
{ 'compact-detail-columns': detailColumnsHidden },
|
||||||
activeTab === 'product_detail' ? 'product-detail-table' : 'product-summary-table'
|
activeTab === 'product_detail' ? 'product-detail-table' : 'product-summary-table'
|
||||||
]"
|
]"
|
||||||
:rows="displayProductKpiTableRows"
|
:rows="displayProductKpiTableRows"
|
||||||
@@ -1603,6 +1604,7 @@ const loadingNow = ref(0)
|
|||||||
const loadingStage = ref('')
|
const loadingStage = ref('')
|
||||||
const generalRowsLoaded = ref(false)
|
const generalRowsLoaded = ref(false)
|
||||||
const detailLevelMenuOpen = ref(false)
|
const detailLevelMenuOpen = ref(false)
|
||||||
|
const detailColumnsHidden = ref(false)
|
||||||
const topScrollbarRef = ref(null)
|
const topScrollbarRef = ref(null)
|
||||||
const topScrollbarInnerRef = ref(null)
|
const topScrollbarInnerRef = ref(null)
|
||||||
let backendGroupedTimer = null
|
let backendGroupedTimer = null
|
||||||
@@ -1619,7 +1621,7 @@ const lastManualExpandedGroupKey = ref('')
|
|||||||
const selectedExpandLevelKeysByTab = reactive({
|
const selectedExpandLevelKeysByTab = reactive({
|
||||||
products: ['urun_ilk_grubu', 'askili_yan', 'kategori'],
|
products: ['urun_ilk_grubu', 'askili_yan', 'kategori'],
|
||||||
product_detail: ['urun_alt_grubu'],
|
product_detail: ['urun_alt_grubu'],
|
||||||
sales_color_yaka_market_customer: ['color_yaka'],
|
sales_color_yaka_market_customer: ['urun_ana_grubu', 'color_yaka'],
|
||||||
idle: ['urun_ilk_grubu', 'askili_yan', 'kategori'],
|
idle: ['urun_ilk_grubu', 'askili_yan', 'kategori'],
|
||||||
sales_product_country_segment_market_customer: ['urun_ilk_grubu', 'askili_yan', 'kategori'],
|
sales_product_country_segment_market_customer: ['urun_ilk_grubu', 'askili_yan', 'kategori'],
|
||||||
sales_market_customer_product: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'],
|
sales_market_customer_product: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'],
|
||||||
@@ -1675,8 +1677,7 @@ const productPerformanceExcelExportFilterFields = new Set([
|
|||||||
])
|
])
|
||||||
const performanceTabs = [
|
const performanceTabs = [
|
||||||
{ name: 'products', icon: 'dashboard', label: 'Genel Özet KPI' },
|
{ name: 'products', icon: 'dashboard', label: 'Genel Özet KPI' },
|
||||||
{ name: 'product_detail', icon: 'category', label: 'Detay KPI' },
|
{ name: 'sales_color_yaka_market_customer', icon: 'palette', label: 'Ürün Ana Grubu > Renk/Yaka > Piyasa > Müşteri' },
|
||||||
{ name: 'sales_color_yaka_market_customer', icon: 'palette', label: 'Renk/Yaka > Piyasa > Müşteri' },
|
|
||||||
{ name: 'idle', icon: 'warning', label: 'Atıl Stok / Maliyet' },
|
{ name: 'idle', icon: 'warning', label: 'Atıl Stok / Maliyet' },
|
||||||
{ name: 'sales_product_country_segment_market_customer', icon: 'account_tree', label: 'Ürün > Ülke > Segment > Piyasa > Müşteri' },
|
{ name: 'sales_product_country_segment_market_customer', icon: 'account_tree', label: 'Ürün > Ülke > Segment > Piyasa > Müşteri' },
|
||||||
{ name: 'sales_country_segment_market_customer_product', icon: 'public', label: 'Ülke > Segment > Piyasa > Müşteri > Ürün Satış KPI' },
|
{ name: 'sales_country_segment_market_customer_product', icon: 'public', label: 'Ülke > Segment > Piyasa > Müşteri > Ürün Satış KPI' },
|
||||||
@@ -1816,10 +1817,10 @@ const columns = [
|
|||||||
{ name: 'stock_days_180d', label: '180G Stok Gün', field: row => formatNumber(row.stock_days_180d, 1), align: 'right', sortable: true },
|
{ name: 'stock_days_180d', label: '180G Stok Gün', field: row => formatNumber(row.stock_days_180d, 1), align: 'right', sortable: true },
|
||||||
{ name: 'stock_days_365d', label: '360G Stok Gün', field: row => formatNumber(row.stock_days_365d, 1), align: 'right', sortable: true },
|
{ name: 'stock_days_365d', label: '360G Stok Gün', field: row => formatNumber(row.stock_days_365d, 1), align: 'right', sortable: true },
|
||||||
{ name: 'stock_days_total', label: 'Genel Stok Gün', field: row => formatNumber(row.stock_days_total, 1), align: 'right', sortable: true },
|
{ name: 'stock_days_total', label: 'Genel Stok Gün', field: row => formatNumber(row.stock_days_total, 1), align: 'right', sortable: true },
|
||||||
{ name: 'stock_turnover_90d', label: '90G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
|
{ name: 'stock_turnover_90d', label: '90G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
|
||||||
{ name: 'stock_turnover_180d', label: '180G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true },
|
{ name: 'stock_turnover_180d', label: '180G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true },
|
||||||
{ name: 'stock_turnover_365d', label: '360G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true },
|
{ name: 'stock_turnover_365d', label: '360G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true },
|
||||||
{ name: 'stock_turnover_total', label: 'Genel Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true },
|
{ name: 'stock_turnover_total', label: 'Genel Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true },
|
||||||
{ name: 'avg_price_usd_90d', label: '90G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right' },
|
{ name: 'avg_price_usd_90d', label: '90G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right' },
|
||||||
{ name: 'avg_price_usd_180d', label: '180G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right' },
|
{ name: 'avg_price_usd_180d', label: '180G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right' },
|
||||||
{ name: 'avg_price_usd_365d', label: '360G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right' },
|
{ name: 'avg_price_usd_365d', label: '360G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right' },
|
||||||
@@ -1849,7 +1850,6 @@ const columns = [
|
|||||||
{ name: 'customer_count_total', label: 'Genel Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true },
|
{ name: 'customer_count_total', label: 'Genel Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true },
|
||||||
{ name: 'sales_index_90d', label: 'Piyasa End.', field: row => formatNumber(row.sales_index_90d, 2), align: 'right', sortable: true },
|
{ name: 'sales_index_90d', label: 'Piyasa End.', field: row => formatNumber(row.sales_index_90d, 2), align: 'right', sortable: true },
|
||||||
{ name: 'sales_index_total', label: 'Genel Endeks', field: row => formatNumber(row.sales_index_total, 2), align: 'right', sortable: true },
|
{ name: 'sales_index_total', label: 'Genel Endeks', field: row => formatNumber(row.sales_index_total, 2), align: 'right', sortable: true },
|
||||||
{ name: 'performance_score', label: 'Skor', field: row => formatNumber(row.performance_score, 2), align: 'right', sortable: true },
|
|
||||||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left' },
|
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left' },
|
||||||
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
||||||
]
|
]
|
||||||
@@ -2024,7 +2024,32 @@ const productColumns = computed(() => orderedMetricColumns(columns.filter(visibl
|
|||||||
const productDetailColumns = computed(() => orderedMetricColumns(columns
|
const productDetailColumns = computed(() => orderedMetricColumns(columns
|
||||||
.filter(col => !['urun_ilk_grubu', 'askili_yan', 'kategori'].includes(col.name))
|
.filter(col => !['urun_ilk_grubu', 'askili_yan', 'kategori'].includes(col.name))
|
||||||
.filter(visiblePeriodColumn)))
|
.filter(visiblePeriodColumn)))
|
||||||
const activeProductColumns = computed(() => activeTab.value === 'product_detail' ? productDetailColumns.value : productColumns.value)
|
const detailColumnNames = new Set([
|
||||||
|
'item_description',
|
||||||
|
'kategori',
|
||||||
|
'askili_yan',
|
||||||
|
'urun_ilk_grubu',
|
||||||
|
'urun_ana_grubu',
|
||||||
|
'urun_alt_grubu',
|
||||||
|
'period_start',
|
||||||
|
'period_end',
|
||||||
|
'country',
|
||||||
|
'customer_segment',
|
||||||
|
'customer_code',
|
||||||
|
'customer_name',
|
||||||
|
'first_sale_date',
|
||||||
|
'last_sale_date',
|
||||||
|
'last_ref_number'
|
||||||
|
])
|
||||||
|
|
||||||
|
function applyDetailColumnVisibility (sourceColumns) {
|
||||||
|
if (!detailColumnsHidden.value) return sourceColumns
|
||||||
|
return sourceColumns.filter(col => !detailColumnNames.has(col.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleProductColumns = computed(() => applyDetailColumnVisibility(productColumns.value))
|
||||||
|
const visibleProductDetailColumns = computed(() => applyDetailColumnVisibility(productDetailColumns.value))
|
||||||
|
const activeProductColumns = computed(() => activeTab.value === 'product_detail' ? visibleProductDetailColumns.value : visibleProductColumns.value)
|
||||||
|
|
||||||
const generalColumns = [
|
const generalColumns = [
|
||||||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||||||
@@ -2059,13 +2084,15 @@ const generalColumns = [
|
|||||||
{ name: 'customer_count_total', label: 'Toplam Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true },
|
{ name: 'customer_count_total', label: 'Toplam Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true },
|
||||||
{ name: 'invoice_count_total', label: 'Fatura', field: row => formatNumber(row.invoice_count_total, 0), align: 'right', sortable: true },
|
{ name: 'invoice_count_total', label: 'Fatura', field: row => formatNumber(row.invoice_count_total, 0), align: 'right', sortable: true },
|
||||||
{ name: 'sales_index_total', label: 'Genel Endeks', field: row => formatNumber(row.sales_index_total, 2), align: 'right', sortable: true },
|
{ name: 'sales_index_total', label: 'Genel Endeks', field: row => formatNumber(row.sales_index_total, 2), align: 'right', sortable: true },
|
||||||
{ name: 'performance_score', label: 'Genel Skor', field: row => formatNumber(row.performance_score, 2), align: 'right', sortable: true },
|
{ name: 'performance_score_total', label: 'Genel Skor', field: row => formatNumber(row.performance_score_total ?? row.performance_score, 2), align: 'right', sortable: true },
|
||||||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||||||
{ name: 'first_sale_date', label: 'İlk Satış', field: 'first_sale_date', align: 'left', sortable: true },
|
{ name: 'first_sale_date', label: 'İlk Satış', field: 'first_sale_date', align: 'left', sortable: true },
|
||||||
{ name: 'last_sale_date', label: 'Son Satış', field: 'last_sale_date', align: 'left', sortable: true },
|
{ name: 'last_sale_date', label: 'Son Satış', field: 'last_sale_date', align: 'left', sortable: true },
|
||||||
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const visibleGeneralColumns = computed(() => applyDetailColumnVisibility(generalColumns))
|
||||||
|
|
||||||
const orderAnalysisColumns = [
|
const orderAnalysisColumns = [
|
||||||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||||||
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
||||||
@@ -2205,7 +2232,7 @@ const idleColumns = [
|
|||||||
{ name: 'idle_cost_usd', label: 'Stok Maliyeti USD', field: 'idle_cost_usd', align: 'right', sortable: true },
|
{ name: 'idle_cost_usd', label: 'Stok Maliyeti USD', field: 'idle_cost_usd', align: 'right', sortable: true },
|
||||||
{ name: 'sales_qty_90d', label: '90G Satış', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
|
{ name: 'sales_qty_90d', label: '90G Satış', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
|
||||||
{ name: 'stock_days_90d', label: 'Stok Gün', field: row => formatNumber(row.stock_days_90d, 1), align: 'right', sortable: true },
|
{ name: 'stock_days_90d', label: 'Stok Gün', field: row => formatNumber(row.stock_days_90d, 1), align: 'right', sortable: true },
|
||||||
{ name: 'stock_turnover_90d', label: '90G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
|
{ name: 'stock_turnover_90d', label: '90G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
|
||||||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left' },
|
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left' },
|
||||||
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
||||||
]
|
]
|
||||||
@@ -2288,13 +2315,13 @@ const salesBreakdownColumns = [
|
|||||||
{ name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
|
{ name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
|
||||||
{ name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
|
{ name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
|
||||||
{ name: 'avg_price_usd_90d', label: '90G Ort. Satış Fiyatı USD', field: 'avg_price_usd_90d', align: 'right', sortable: true },
|
{ name: 'avg_price_usd_90d', label: '90G Ort. Satış Fiyatı USD', field: 'avg_price_usd_90d', align: 'right', sortable: true },
|
||||||
{ name: 'stock_turnover_90d', label: '90G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
|
{ name: 'stock_turnover_90d', label: '90G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
|
||||||
{ name: 'customer_count_180d', label: '180G Müşteri', field: row => formatNumber(row.customer_count_180d, 0), align: 'right', sortable: true },
|
{ name: 'customer_count_180d', label: '180G Müşteri', field: row => formatNumber(row.customer_count_180d, 0), align: 'right', sortable: true },
|
||||||
{ name: 'invoice_count_180d', label: '180G Fatura', field: row => formatNumber(row.invoice_count_180d, 0), align: 'right', sortable: true },
|
{ name: 'invoice_count_180d', label: '180G Fatura', field: row => formatNumber(row.invoice_count_180d, 0), align: 'right', sortable: true },
|
||||||
{ name: 'sales_qty_180d', label: '180G Adet', field: row => formatNumber(row.sales_qty_180d, 0), align: 'right', sortable: true },
|
{ name: 'sales_qty_180d', label: '180G Adet', field: row => formatNumber(row.sales_qty_180d, 0), align: 'right', sortable: true },
|
||||||
{ name: 'sales_usd_180d', label: '180G USD', field: row => formatMoney(row.sales_usd_180d, 'USD'), align: 'right', sortable: true },
|
{ name: 'sales_usd_180d', label: '180G USD', field: row => formatMoney(row.sales_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'avg_price_usd_180d', label: '180G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right', sortable: true },
|
{ name: 'avg_price_usd_180d', label: '180G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'stock_turnover_180d', label: '180G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true },
|
{ name: 'stock_turnover_180d', label: '180G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true },
|
||||||
{ name: 'base_price_usd_180d', label: '180G Taban', field: row => formatMoney(row.base_price_usd_180d, 'USD'), align: 'right', sortable: true },
|
{ name: 'base_price_usd_180d', label: '180G Taban', field: row => formatMoney(row.base_price_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'cost_price_usd_180d', label: '180G Çıplak', field: row => formatMoney(row.cost_price_usd_180d, 'USD'), align: 'right', sortable: true },
|
{ name: 'cost_price_usd_180d', label: '180G Çıplak', field: row => formatMoney(row.cost_price_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'gross_profit_base_usd_180d', label: '180G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_180d, 'USD'), align: 'right', sortable: true },
|
{ name: 'gross_profit_base_usd_180d', label: '180G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||||
@@ -2305,7 +2332,7 @@ const salesBreakdownColumns = [
|
|||||||
{ name: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
|
{ name: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
|
||||||
{ name: 'sales_usd_365d', label: '360G USD', field: row => formatMoney(row.sales_usd_365d, 'USD'), align: 'right', sortable: true },
|
{ name: 'sales_usd_365d', label: '360G USD', field: row => formatMoney(row.sales_usd_365d, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'avg_price_usd_365d', label: '360G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right', sortable: true },
|
{ name: 'avg_price_usd_365d', label: '360G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'stock_turnover_365d', label: '360G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true },
|
{ name: 'stock_turnover_365d', label: '360G Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true },
|
||||||
{ name: 'base_price_usd_365d', label: '360G Taban', field: row => formatMoney(row.base_price_usd_365d, 'USD'), align: 'right', sortable: true },
|
{ name: 'base_price_usd_365d', label: '360G Taban', field: row => formatMoney(row.base_price_usd_365d, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'cost_price_usd_365d', label: '360G Çıplak', field: row => formatMoney(row.cost_price_usd_365d, 'USD'), align: 'right', sortable: true },
|
{ name: 'cost_price_usd_365d', label: '360G Çıplak', field: row => formatMoney(row.cost_price_usd_365d, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'gross_profit_base_usd_365d', label: '360G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_365d, 'USD'), align: 'right', sortable: true },
|
{ name: 'gross_profit_base_usd_365d', label: '360G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_365d, 'USD'), align: 'right', sortable: true },
|
||||||
@@ -2318,7 +2345,7 @@ const salesBreakdownColumns = [
|
|||||||
{ name: 'sales_qty_total', label: 'Genel Adet', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true },
|
{ name: 'sales_qty_total', label: 'Genel Adet', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true },
|
||||||
{ name: 'sales_usd_total', label: 'Genel USD', field: row => formatMoney(row.sales_usd_total, 'USD'), align: 'right', sortable: true },
|
{ name: 'sales_usd_total', label: 'Genel USD', field: row => formatMoney(row.sales_usd_total, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'avg_price_usd_total', label: 'Genel Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true },
|
{ name: 'avg_price_usd_total', label: 'Genel Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'stock_turnover_total', label: 'Genel Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true },
|
{ name: 'stock_turnover_total', label: 'Genel Yıllık Stok Devir', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true },
|
||||||
{ name: 'base_price_usd_total', label: 'Genel Taban', field: row => formatMoney(row.base_price_usd_total, 'USD'), align: 'right', sortable: true },
|
{ name: 'base_price_usd_total', label: 'Genel Taban', field: row => formatMoney(row.base_price_usd_total, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'cost_price_usd_total', label: 'Genel Çıplak', field: row => formatMoney(row.cost_price_usd_total, 'USD'), align: 'right', sortable: true },
|
{ name: 'cost_price_usd_total', label: 'Genel Çıplak', field: row => formatMoney(row.cost_price_usd_total, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'gross_profit_base_usd_total', label: 'Genel Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_total, 'USD'), align: 'right', sortable: true },
|
{ name: 'gross_profit_base_usd_total', label: 'Genel Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_total, 'USD'), align: 'right', sortable: true },
|
||||||
@@ -2327,7 +2354,6 @@ const salesBreakdownColumns = [
|
|||||||
{ name: 'gross_margin_cost_total', label: 'Genel Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_total), align: 'right', sortable: true },
|
{ name: 'gross_margin_cost_total', label: 'Genel Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_total), align: 'right', sortable: true },
|
||||||
{ name: 'customer_score_total', label: 'Genel Müşteri Skor', field: row => formatNumber(row.customer_score_total, 2), align: 'right', sortable: true },
|
{ name: 'customer_score_total', label: 'Genel Müşteri Skor', field: row => formatNumber(row.customer_score_total, 2), align: 'right', sortable: true },
|
||||||
{ name: 'sales_index_90d', label: 'Endeks', field: row => formatNumber(row.sales_index_90d, 2), align: 'right', sortable: true },
|
{ name: 'sales_index_90d', label: 'Endeks', field: row => formatNumber(row.sales_index_90d, 2), align: 'right', sortable: true },
|
||||||
{ name: 'performance_score', label: 'Skor', field: row => formatNumber(row.performance_score, 2), align: 'right', sortable: true },
|
|
||||||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||||||
{ name: 'last_sale_date', label: 'Son Satış', field: 'last_sale_date', align: 'left', sortable: true },
|
{ name: 'last_sale_date', label: 'Son Satış', field: 'last_sale_date', align: 'left', sortable: true },
|
||||||
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
||||||
@@ -2399,7 +2425,6 @@ const metricLabelOverrides = {
|
|||||||
gross_margin_cost_365d: '360G Çıplak Marj',
|
gross_margin_cost_365d: '360G Çıplak Marj',
|
||||||
gross_margin_base_total: 'Genel Taban Marj',
|
gross_margin_base_total: 'Genel Taban Marj',
|
||||||
gross_margin_cost_total: 'Genel Çıplak Marj',
|
gross_margin_cost_total: 'Genel Çıplak Marj',
|
||||||
performance_score: 'Skor',
|
|
||||||
performance_score_90d: '90G Ürün Skor',
|
performance_score_90d: '90G Ürün Skor',
|
||||||
performance_score_180d: '180G Ürün Skor',
|
performance_score_180d: '180G Ürün Skor',
|
||||||
performance_score_365d: '360G Ürün Skor',
|
performance_score_365d: '360G Ürün Skor',
|
||||||
@@ -2412,10 +2437,10 @@ const metricLabelOverrides = {
|
|||||||
customer_count_90d: '90G Tekil Müşteri',
|
customer_count_90d: '90G Tekil Müşteri',
|
||||||
market_count_total: 'Genel Tekil Piyasa',
|
market_count_total: 'Genel Tekil Piyasa',
|
||||||
customer_count_total: 'Genel Tekil Müşteri',
|
customer_count_total: 'Genel Tekil Müşteri',
|
||||||
stock_turnover_90d: '90G Stok Devir Hızı',
|
stock_turnover_90d: '90G Yıllık Stok Devir',
|
||||||
stock_turnover_180d: '180G Stok Devir Hızı',
|
stock_turnover_180d: '180G Yıllık Stok Devir',
|
||||||
stock_turnover_365d: '360G Stok Devir Hızı',
|
stock_turnover_365d: '360G Yıllık Stok Devir',
|
||||||
stock_turnover_total: 'Genel Stok Devir Hızı',
|
stock_turnover_total: 'Genel Yıllık Stok Devir',
|
||||||
sales_index_90d: '90G Endeks',
|
sales_index_90d: '90G Endeks',
|
||||||
sales_index_total: 'Genel Endeks',
|
sales_index_total: 'Genel Endeks',
|
||||||
stock_days_90d: '90G Stok Gün',
|
stock_days_90d: '90G Stok Gün',
|
||||||
@@ -2513,7 +2538,6 @@ function ensureColorYakaColumn (targetColumns) {
|
|||||||
].forEach(target => ensureColumns(target, customerPeriodScoreColumns))
|
].forEach(target => ensureColumns(target, customerPeriodScoreColumns))
|
||||||
|
|
||||||
;[
|
;[
|
||||||
generalColumns,
|
|
||||||
orderAnalysisColumns,
|
orderAnalysisColumns,
|
||||||
orderGroupColumns,
|
orderGroupColumns,
|
||||||
orderProductCustomerColumns,
|
orderProductCustomerColumns,
|
||||||
@@ -2639,7 +2663,10 @@ const visibleOrderAnalysisColumns = computed(() => {
|
|||||||
function performanceCardsForRow (row) {
|
function performanceCardsForRow (row) {
|
||||||
const source = row || {}
|
const source = row || {}
|
||||||
return [
|
return [
|
||||||
{ key: 'score', label: 'Skor', value: formatNumber(source.performance_score, 2) },
|
{ key: 'score90', label: '90G Skor', value: formatNumber(source.performance_score_90d ?? source.performance_score, 2) },
|
||||||
|
{ key: 'score180', label: '180G Skor', value: formatNumber(source.performance_score_180d, 2) },
|
||||||
|
{ key: 'score360', label: '360G Skor', value: formatNumber(source.performance_score_365d, 2) },
|
||||||
|
{ key: 'scoreTotal', label: 'Genel Skor', value: formatNumber(source.performance_score_total ?? source.performance_score, 2) },
|
||||||
{ key: 'stock', label: 'Stok', value: formatNumber(source.stock_qty, 0) },
|
{ key: 'stock', label: 'Stok', value: formatNumber(source.stock_qty, 0) },
|
||||||
{ key: 'sales90', label: '90G Satış', value: formatNumber(source.sales_qty_90d, 0) },
|
{ key: 'sales90', label: '90G Satış', value: formatNumber(source.sales_qty_90d, 0) },
|
||||||
{ key: 'sales180', label: '180G Satış', value: formatNumber(source.sales_qty_180d, 0) },
|
{ key: 'sales180', label: '180G Satış', value: formatNumber(source.sales_qty_180d, 0) },
|
||||||
@@ -2700,18 +2727,13 @@ const tabGroupLevels = {
|
|||||||
{ key: 'market_key', label: 'Piyasa' }
|
{ key: 'market_key', label: 'Piyasa' }
|
||||||
],
|
],
|
||||||
sales_color_yaka_market_customer: [
|
sales_color_yaka_market_customer: [
|
||||||
{ key: 'color_yaka', label: 'Renk/Yaka' },
|
|
||||||
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
|
|
||||||
{ key: 'askili_yan', label: 'Askılı/Yan' },
|
|
||||||
{ key: 'kategori', label: 'Kategori' },
|
|
||||||
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu' },
|
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu' },
|
||||||
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
{ key: 'color_yaka', label: 'Renk/Yaka' },
|
||||||
{ key: 'product_code', label: 'Ürün' },
|
|
||||||
{ key: 'country', label: 'Ülke' },
|
|
||||||
{ key: 'market_key', label: 'Piyasa' },
|
{ key: 'market_key', label: 'Piyasa' },
|
||||||
{ key: 'customer_segment', label: 'Segment' },
|
|
||||||
{ key: 'customer_code', label: 'Müşteri Kodu' },
|
{ key: 'customer_code', label: 'Müşteri Kodu' },
|
||||||
{ key: 'customer_name', label: 'Müşteri' }
|
{ key: 'customer_name', label: 'Müşteri' },
|
||||||
|
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||||||
|
{ key: 'product_code', label: 'Ürün' }
|
||||||
],
|
],
|
||||||
idle: [
|
idle: [
|
||||||
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
|
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
|
||||||
@@ -3052,7 +3074,7 @@ function filterSourceRowsForTab (tabKey, fallbackRows) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function backendGroupedFilterOptionParams (tabKey) {
|
function backendGroupedFilterOptionParams (tabKey) {
|
||||||
const columns = columnsForTableKey(tabKey)
|
const columns = filterColumnsForTableKey(tabKey)
|
||||||
const fields = columns
|
const fields = columns
|
||||||
.map(col => col?.name || '')
|
.map(col => col?.name || '')
|
||||||
.filter(name => backendGroupedFilterFields.has(name))
|
.filter(name => backendGroupedFilterFields.has(name))
|
||||||
@@ -3132,7 +3154,7 @@ function backendFilterOptionLabel (name, value) {
|
|||||||
|
|
||||||
function backendGroupedFilterState (tabKey) {
|
function backendGroupedFilterState (tabKey) {
|
||||||
const out = {}
|
const out = {}
|
||||||
const columns = columnsForTableKey(tabKey)
|
const columns = filterColumnsForTableKey(tabKey)
|
||||||
for (const col of columns) {
|
for (const col of columns) {
|
||||||
const name = col?.name || ''
|
const name = col?.name || ''
|
||||||
if (!isColumnFilterable(name)) continue
|
if (!isColumnFilterable(name)) continue
|
||||||
@@ -3152,7 +3174,7 @@ function shouldUseBackendGroupedRows (tabKey) {
|
|||||||
|
|
||||||
function tableFilterSource (tableKey) {
|
function tableFilterSource (tableKey) {
|
||||||
if (backendGroupedSupportedTab(tableKey)) {
|
if (backendGroupedSupportedTab(tableKey)) {
|
||||||
return { rows: backendGroupedRows.value[tableKey] || [], columns: columnsForTableKey(tableKey) }
|
return { rows: backendGroupedRows.value[tableKey] || [], columns: filterColumnsForTableKey(tableKey) }
|
||||||
}
|
}
|
||||||
if (tableKey === 'general') return { rows: generalRows.value, columns: generalColumns }
|
if (tableKey === 'general') return { rows: generalRows.value, columns: generalColumns }
|
||||||
if (tableKey === 'markets') return { rows: marketRows.value, columns: marketColumns }
|
if (tableKey === 'markets') return { rows: marketRows.value, columns: marketColumns }
|
||||||
@@ -3162,6 +3184,20 @@ function tableFilterSource (tableKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function columnsForTableKey (tabKey) {
|
function columnsForTableKey (tabKey) {
|
||||||
|
if (tabKey === 'products') return visibleProductColumns.value
|
||||||
|
if (tabKey === 'product_detail') return visibleProductDetailColumns.value
|
||||||
|
if (tabKey === 'general') return visibleGeneralColumns.value
|
||||||
|
if (tabKey === 'order_product_customers') return orderProductCustomerColumns
|
||||||
|
if (tabKey === 'order_market_details') return orderMarketDetailColumns
|
||||||
|
if (tabKey === 'idle') return idleColumns
|
||||||
|
if (tabKey === 'markets') return marketColumns
|
||||||
|
if (tabKey === 'countries') return countryColumns
|
||||||
|
if (tabKey === 'customers') return customerColumns
|
||||||
|
if (salesBreakdownTabKeys.includes(tabKey)) return visibleSalesBreakdownColumns.value
|
||||||
|
return productColumns.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterColumnsForTableKey (tabKey) {
|
||||||
if (tabKey === 'products') return productColumns.value
|
if (tabKey === 'products') return productColumns.value
|
||||||
if (tabKey === 'product_detail') return productDetailColumns.value
|
if (tabKey === 'product_detail') return productDetailColumns.value
|
||||||
if (tabKey === 'general') return generalColumns
|
if (tabKey === 'general') return generalColumns
|
||||||
@@ -3244,6 +3280,9 @@ function compareGroupedBucketsForTable (tableKey, left, right, groupDef) {
|
|||||||
|
|
||||||
function groupedBucketSortValue (sourceRows, sortBy) {
|
function groupedBucketSortValue (sourceRows, sortBy) {
|
||||||
if (!sourceRows?.length) return 0
|
if (!sourceRows?.length) return 0
|
||||||
|
if (shouldUseDerivedGroupSortValue(sortBy)) {
|
||||||
|
return sortValueForColumnName(aggregateGroupFields(sourceRows), sortBy)
|
||||||
|
}
|
||||||
if (sortBy === 'stock_qty') return distinctVariantStockQty(sourceRows)
|
if (sortBy === 'stock_qty') return distinctVariantStockQty(sourceRows)
|
||||||
if (sortBy === 'idle_cost_usd') return distinctVariantStockCost(sourceRows)
|
if (sortBy === 'idle_cost_usd') return distinctVariantStockCost(sourceRows)
|
||||||
if (sortBy === 'base_price_usd' || sortBy === 'cost_price_usd') return weightedAverageProductCost(sourceRows, sortBy)
|
if (sortBy === 'base_price_usd' || sortBy === 'cost_price_usd') return weightedAverageProductCost(sourceRows, sortBy)
|
||||||
@@ -3252,6 +3291,10 @@ function groupedBucketSortValue (sourceRows, sortBy) {
|
|||||||
return sortValueForColumnName(sourceRows[0], sortBy)
|
return sortValueForColumnName(sourceRows[0], sortBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldUseDerivedGroupSortValue (sortBy) {
|
||||||
|
return /^(performance_score|stock_turnover|stock_days|avg_price_usd|unit_profit|gross_profit|gross_margin|market_count|customer_count|sales_index)/i.test(String(sortBy || ''))
|
||||||
|
}
|
||||||
|
|
||||||
function sortProductGroupedTableRows (tableRows, sortBy, descending) {
|
function sortProductGroupedTableRows (tableRows, sortBy, descending) {
|
||||||
const source = Array.isArray(tableRows) ? tableRows : []
|
const source = Array.isArray(tableRows) ? tableRows : []
|
||||||
if (backendGroupedSupportedTab(activeTab.value)) return source
|
if (backendGroupedSupportedTab(activeTab.value)) return source
|
||||||
@@ -3438,8 +3481,8 @@ function aggregateGroupFields (sourceRows, groupField = '') {
|
|||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
if (field === 'row_key' || field === 'key') continue
|
if (field === 'row_key' || field === 'key') continue
|
||||||
if (/^gross_margin(_base|_cost)?(_|$)/i.test(field)) continue
|
if (/^gross_margin(_base|_cost)?(_|$)/i.test(field)) continue
|
||||||
if (field === 'stock_qty') {
|
if (field === 'stock_qty' || /^avg_stock_/i.test(field)) {
|
||||||
out[field] = distinctVariantStockQty(sourceRows)
|
out[field] = distinctVariantNumber(sourceRows, field)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (field === 'idle_cost_usd') {
|
if (field === 'idle_cost_usd') {
|
||||||
@@ -3466,15 +3509,18 @@ function aggregateGroupFields (sourceRows, groupField = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldSumField (field) {
|
function shouldSumField (field) {
|
||||||
return /(_qty|_usd|_count|_value_usd|line_count|invoice_count|order_count|product_count|market_count|customer_count|overdue_qty|stock_qty|net_stock_after_order|idle_cost_usd)$/i.test(field)
|
return /^(sales_qty_|sales_usd_|avg_daily_sales_|gross_profit_|invoice_count_|market_count_|customer_count_|product_group_count_|product_count_)/i.test(field) ||
|
||||||
|
/(_qty|_usd|_count|_value_usd|line_count|invoice_count|order_count|product_count|market_count|customer_count|overdue_qty|stock_qty|net_stock_after_order|idle_cost_usd)$/i.test(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldAverageField (field) {
|
function shouldAverageField (field) {
|
||||||
|
if (/^avg_daily_sales_/i.test(field)) return false
|
||||||
return /^(avg_|unit_|base_price|cost_price|gross_margin|expected_margin|sales_index|performance_score|customer_score|stock_days|stock_turnover)/i.test(field) ||
|
return /^(avg_|unit_|base_price|cost_price|gross_margin|expected_margin|sales_index|performance_score|customer_score|stock_days|stock_turnover)/i.test(field) ||
|
||||||
/(_price_usd|_margin|_index|_days)$/i.test(field)
|
/(_price_usd|_margin|_index|_days)$/i.test(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
function weightFieldForMetric (field) {
|
function weightFieldForMetric (field) {
|
||||||
|
if (field.startsWith('stock_turnover')) return `sales_qty_${countPeriodSuffix(field)}`
|
||||||
if (field.includes('_180d')) return 'sales_qty_180d'
|
if (field.includes('_180d')) return 'sales_qty_180d'
|
||||||
if (field.includes('_365d')) return 'sales_qty_365d'
|
if (field.includes('_365d')) return 'sales_qty_365d'
|
||||||
if (field.includes('_total')) return 'sales_qty_total'
|
if (field.includes('_total')) return 'sales_qty_total'
|
||||||
@@ -3485,13 +3531,43 @@ function weightFieldForMetric (field) {
|
|||||||
|
|
||||||
function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
|
function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
|
||||||
Object.assign(out, normalizeProductCostFields(out))
|
Object.assign(out, normalizeProductCostFields(out))
|
||||||
|
const isGroup = Boolean(String(groupField || '').trim())
|
||||||
|
const preservedCustomerScores = {}
|
||||||
|
const preservedProductScores = {}
|
||||||
|
const preservedStockTurnovers = {}
|
||||||
|
for (const suffix of ['90d', '180d', '365d', 'total']) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(out, `customer_score_${suffix}`)) {
|
||||||
|
preservedCustomerScores[suffix] = Number(out[`customer_score_${suffix}`] || 0)
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(out, `performance_score_${suffix}`)) {
|
||||||
|
preservedProductScores[suffix] = Number(out[`performance_score_${suffix}`] || 0)
|
||||||
|
}
|
||||||
|
if (isGroup && Object.prototype.hasOwnProperty.call(out, `stock_turnover_${suffix}`)) {
|
||||||
|
preservedStockTurnovers[suffix] = Number(out[`stock_turnover_${suffix}`] || 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(out, 'performance_score') &&
|
||||||
|
!Object.prototype.hasOwnProperty.call(preservedProductScores, '90d')
|
||||||
|
) {
|
||||||
|
preservedProductScores['90d'] = Number(out.performance_score || 0)
|
||||||
|
}
|
||||||
for (const suffix of ['90d', '180d', '365d', 'total']) {
|
for (const suffix of ['90d', '180d', '365d', 'total']) {
|
||||||
const sales = Number(out[`sales_usd_${suffix}`] || 0)
|
const sales = Number(out[`sales_usd_${suffix}`] || 0)
|
||||||
const qty = Number(out[`sales_qty_${suffix}`] || 0)
|
const qty = Number(out[`sales_qty_${suffix}`] || 0)
|
||||||
const stockQty = Number(out.stock_qty || 0)
|
const stockQty = Number(out.stock_qty || 0)
|
||||||
const avgStock = Number(out[`avg_stock_${suffix}`] || 0)
|
const avgStock = Number(out[`avg_stock_${suffix}`] || 0)
|
||||||
const turnoverBase = avgStock > 0 ? avgStock : stockQty
|
const turnoverBase = avgStock > 0 ? avgStock : (isGroup ? 0 : stockQty)
|
||||||
out[`stock_turnover_${suffix}`] = turnoverBase > 0 ? qty / turnoverBase : 0
|
const days = productPerformancePeriodDays(out, suffix)
|
||||||
|
const avgDaily = days > 0 ? qty / days : Number(out[`avg_daily_sales_${suffix}`] || 0)
|
||||||
|
if (days > 0) out[`avg_daily_sales_${suffix}`] = avgDaily
|
||||||
|
if (avgDaily > 0 && turnoverBase > 0) out[`stock_days_${suffix}`] = turnoverBase / avgDaily
|
||||||
|
else if (qty <= 0 && stockQty > 0) out[`stock_days_${suffix}`] = 9999
|
||||||
|
else if (!Object.prototype.hasOwnProperty.call(out, `stock_days_${suffix}`)) out[`stock_days_${suffix}`] = 0
|
||||||
|
out[`stock_turnover_${suffix}`] = annualizedStockTurnover(qty, turnoverBase, days)
|
||||||
|
if (Object.prototype.hasOwnProperty.call(preservedStockTurnovers, suffix)) {
|
||||||
|
out[`stock_turnover_${suffix}`] = preservedStockTurnovers[suffix]
|
||||||
|
}
|
||||||
if (qty > 0) out[`avg_price_usd_${suffix}`] = sales / qty
|
if (qty > 0) out[`avg_price_usd_${suffix}`] = sales / qty
|
||||||
const { costPrice, basePrice } = periodCostPair(out, suffix)
|
const { costPrice, basePrice } = periodCostPair(out, suffix)
|
||||||
out[`base_price_usd_${suffix}`] = basePrice
|
out[`base_price_usd_${suffix}`] = basePrice
|
||||||
@@ -3530,19 +3606,33 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
|
|||||||
if (sourceRows.some(row => row.performance_bucket)) {
|
if (sourceRows.some(row => row.performance_bucket)) {
|
||||||
out.performance_bucket = dominantValue(sourceRows, 'performance_bucket')
|
out.performance_bucket = dominantValue(sourceRows, 'performance_bucket')
|
||||||
}
|
}
|
||||||
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_90d')) out.customer_score_90d = customerSalesPeriodScore(periodMetricSource(out, '90d'))
|
for (const suffix of ['90d', '180d', '365d', 'total']) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_180d')) out.customer_score_180d = customerSalesPeriodScore(periodMetricSource(out, '180d'))
|
out[`customer_score_${suffix}`] = Object.prototype.hasOwnProperty.call(preservedCustomerScores, suffix)
|
||||||
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_365d')) out.customer_score_365d = customerSalesPeriodScore(periodMetricSource(out, '365d'))
|
? preservedCustomerScores[suffix]
|
||||||
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_total')) out.customer_score_total = customerSalesPeriodScore(periodMetricSource(out, 'total'))
|
: customerSalesPeriodScore(periodMetricSource(out, suffix))
|
||||||
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_90d')) out.performance_score_90d = productSalesPeriodScore(productPeriodMetricSource(out, '90d'))
|
out[`performance_score_${suffix}`] = Object.prototype.hasOwnProperty.call(preservedProductScores, suffix)
|
||||||
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_180d')) out.performance_score_180d = productSalesPeriodScore(productPeriodMetricSource(out, '180d'))
|
? preservedProductScores[suffix]
|
||||||
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_365d')) out.performance_score_365d = productSalesPeriodScore(productPeriodMetricSource(out, '365d'))
|
: productSalesPeriodScore(productPeriodMetricSource(out, suffix))
|
||||||
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_total')) out.performance_score_total = productSalesPeriodScore(productPeriodMetricSource(out, 'total'))
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score')) {
|
|
||||||
out.performance_score = Object.prototype.hasOwnProperty.call(out, 'performance_score_90d')
|
|
||||||
? Number(out.performance_score_90d || 0)
|
|
||||||
: groupPerformanceScore(out, groupField)
|
|
||||||
}
|
}
|
||||||
|
out.performance_score = Number(out.performance_score_90d || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function productPerformancePeriodDays (row, suffix) {
|
||||||
|
if (suffix === '90d') return 90
|
||||||
|
if (suffix === '180d') return 180
|
||||||
|
if (suffix === '365d') return 360
|
||||||
|
if (suffix !== 'total') return 0
|
||||||
|
const start = parseProductPerformanceDate(row?.period_start) || new Date(Date.UTC(2022, 0, 1))
|
||||||
|
const end = parseProductPerformanceDate(row?.period_end) || parseProductPerformanceDate(row?.kpi_date)
|
||||||
|
if (!start || !end || end < start) return 0
|
||||||
|
return Math.floor((end.getTime() - start.getTime()) / 86400000) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProductPerformanceDate (value) {
|
||||||
|
const text = String(value || '').trim().slice(0, 10)
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) return null
|
||||||
|
const [year, month, day] = text.split('-').map(Number)
|
||||||
|
return new Date(Date.UTC(year, month - 1, day))
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupPerformanceScore (row, groupField = '') {
|
function groupPerformanceScore (row, groupField = '') {
|
||||||
@@ -3557,8 +3647,10 @@ function salesGroupPerformanceScore (row) {
|
|||||||
|
|
||||||
function productSalesPeriodScore (row) {
|
function productSalesPeriodScore (row) {
|
||||||
const suffix = row?.suffix || '90d'
|
const suffix = row?.suffix || '90d'
|
||||||
|
const periodDays = productPerformancePeriodDays(row, suffix)
|
||||||
return productScore100({
|
return productScore100({
|
||||||
suffix,
|
suffix,
|
||||||
|
periodDays,
|
||||||
salesUSD: Number(row?.sales_usd_90d || 0),
|
salesUSD: Number(row?.sales_usd_90d || 0),
|
||||||
salesIndex: Number(row?.sales_index_90d || 0),
|
salesIndex: Number(row?.sales_index_90d || 0),
|
||||||
margin: Number(row?.gross_margin_cost_90d ?? row?.gross_margin_90d ?? 0),
|
margin: Number(row?.gross_margin_cost_90d ?? row?.gross_margin_90d ?? 0),
|
||||||
@@ -3572,6 +3664,9 @@ function productPeriodMetricSource (row, suffix) {
|
|||||||
const salesIndex = Number(row?.[`sales_index_${suffix}`] || 0)
|
const salesIndex = Number(row?.[`sales_index_${suffix}`] || 0)
|
||||||
const qty = Number(row?.[`sales_qty_${suffix}`] || 0)
|
const qty = Number(row?.[`sales_qty_${suffix}`] || 0)
|
||||||
const stockQty = Number(row?.stock_qty || 0)
|
const stockQty = Number(row?.stock_qty || 0)
|
||||||
|
const avgStock = Number(row?.[`avg_stock_${suffix}`] || 0)
|
||||||
|
const isGroup = Boolean(row?.__group || String(row?.group_field || '').trim())
|
||||||
|
const turnoverBase = avgStock > 0 ? avgStock : (isGroup ? 0 : stockQty)
|
||||||
const turnoverKey = `stock_turnover_${suffix}`
|
const turnoverKey = `stock_turnover_${suffix}`
|
||||||
const hasTurnover = Object.prototype.hasOwnProperty.call(row || {}, turnoverKey)
|
const hasTurnover = Object.prototype.hasOwnProperty.call(row || {}, turnoverKey)
|
||||||
return {
|
return {
|
||||||
@@ -3581,7 +3676,7 @@ function productPeriodMetricSource (row, suffix) {
|
|||||||
gross_margin_cost_90d: Number(row?.[`gross_margin_cost_${suffix}`] ?? 0),
|
gross_margin_cost_90d: Number(row?.[`gross_margin_cost_${suffix}`] ?? 0),
|
||||||
gross_margin_90d: Number(row?.[`gross_margin_${suffix}`] ?? 0),
|
gross_margin_90d: Number(row?.[`gross_margin_${suffix}`] ?? 0),
|
||||||
sales_qty_90d: qty,
|
sales_qty_90d: qty,
|
||||||
stock_turnover_90d: hasTurnover ? Number(row?.[turnoverKey] || 0) : (stockQty > 0 ? qty / stockQty : 0),
|
stock_turnover_90d: hasTurnover ? Number(row?.[turnoverKey] || 0) : annualizedStockTurnover(qty, turnoverBase, productPerformancePeriodDays(row, suffix)),
|
||||||
market_count_90d: periodCount(row, 'market_count', suffix),
|
market_count_90d: periodCount(row, 'market_count', suffix),
|
||||||
customer_count_90d: periodCount(row, 'customer_count', suffix),
|
customer_count_90d: periodCount(row, 'customer_count', suffix),
|
||||||
stock_qty: stockQty
|
stock_qty: stockQty
|
||||||
@@ -3623,15 +3718,15 @@ function periodMetricSource (row, suffix) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function productScore100 ({ suffix = '90d', salesUSD = 0, margin = 0, stockTurnover = 0, marketCount = 0, customerCount = 0 } = {}) {
|
function productScore100 ({ suffix = '90d', periodDays = 0, salesUSD = 0, margin = 0, stockTurnover = 0, marketCount = 0, customerCount = 0 } = {}) {
|
||||||
if (Number(salesUSD || 0) <= 0) return 1
|
if (Number(salesUSD || 0) <= 0) return 1
|
||||||
const revenue = ratioScore(salesUSD, productRevenueTarget(suffix))
|
const revenue = ratioScore(salesUSD, productRevenueTarget(suffix, periodDays))
|
||||||
return clampScore(
|
return clampScore(
|
||||||
0.30 * marginScore(margin) +
|
0.30 * marginScore(margin) +
|
||||||
0.20 * ratioScore(stockTurnover, 1.5) +
|
0.20 * ratioScore(stockTurnover, stockTurnoverTarget(suffix)) +
|
||||||
0.20 * revenue +
|
0.20 * revenue +
|
||||||
0.15 * ratioScore(marketCount, 8) +
|
0.05 * ratioScore(marketCount, marketSpreadTarget(suffix)) +
|
||||||
0.15 * ratioScore(customerCount, 25)
|
0.25 * ratioScore(customerCount, customerSpreadTarget(suffix, periodDays))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3653,7 +3748,7 @@ function ratioScore (value, target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function marginScore (margin) {
|
function marginScore (margin) {
|
||||||
return ratioScore(Math.max(0, Number(margin || 0)), 0.40)
|
return ratioScore(Math.max(0, Number(margin || 0)), 0.45)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampScore (score) {
|
function clampScore (score) {
|
||||||
@@ -3662,11 +3757,11 @@ function clampScore (score) {
|
|||||||
return Math.round(Math.min(100, n) * 10000) / 10000
|
return Math.round(Math.min(100, n) * 10000) / 10000
|
||||||
}
|
}
|
||||||
|
|
||||||
function productRevenueTarget (suffix) {
|
function productRevenueTarget (suffix, periodDays = 0) {
|
||||||
if (suffix === '180d') return 20000
|
if (suffix === '180d') return 50000
|
||||||
if (suffix === '365d') return 40000
|
if (suffix === '365d') return 100000
|
||||||
if (suffix === 'total') return 120000
|
if (suffix === 'total') return 50000 * totalPeriodMultiplier(periodDays)
|
||||||
return 10000
|
return 25000
|
||||||
}
|
}
|
||||||
|
|
||||||
function customerRevenueTarget (suffix) {
|
function customerRevenueTarget (suffix) {
|
||||||
@@ -3683,6 +3778,43 @@ function customerQtyTarget (suffix) {
|
|||||||
return 500
|
return 500
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STOCK_TURNOVER_YEAR_DAYS = 360
|
||||||
|
|
||||||
|
function annualizedStockTurnover (salesQty, avgStock, periodDays) {
|
||||||
|
const qty = Number(salesQty || 0)
|
||||||
|
const stock = Number(avgStock || 0)
|
||||||
|
const days = Number(periodDays || 0)
|
||||||
|
if (!Number.isFinite(qty) || !Number.isFinite(stock) || qty <= 0 || stock <= 0) return 0
|
||||||
|
const raw = qty / stock
|
||||||
|
return days > 0 ? raw * STOCK_TURNOVER_YEAR_DAYS / days : raw
|
||||||
|
}
|
||||||
|
|
||||||
|
function stockTurnoverTarget () {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
|
||||||
|
function marketSpreadTarget (suffix) {
|
||||||
|
if (suffix === '90d') return 3
|
||||||
|
if (suffix === '180d') return 3
|
||||||
|
if (suffix === '365d') return 6
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
|
||||||
|
function customerSpreadTarget (suffix, periodDays = 0) {
|
||||||
|
if (suffix === '90d') return 8
|
||||||
|
if (suffix === '180d') return 20
|
||||||
|
if (suffix === '365d') return 50
|
||||||
|
return 20 * totalPeriodMultiplier(periodDays)
|
||||||
|
}
|
||||||
|
|
||||||
|
function totalPeriodMultiplier (periodDays = 0) {
|
||||||
|
let days = Number(periodDays || 0)
|
||||||
|
if (!Number.isFinite(days) || days <= 0) {
|
||||||
|
days = productPerformancePeriodDays({ kpi_date: new Date().toISOString().slice(0, 10) }, 'total')
|
||||||
|
}
|
||||||
|
return Math.max(1, days / 180)
|
||||||
|
}
|
||||||
|
|
||||||
function periodCount (row, prefix, suffix) {
|
function periodCount (row, prefix, suffix) {
|
||||||
const keyed = Number(row?.[`${prefix}_${suffix}`] || 0)
|
const keyed = Number(row?.[`${prefix}_${suffix}`] || 0)
|
||||||
if (keyed > 0) return keyed
|
if (keyed > 0) return keyed
|
||||||
@@ -3716,19 +3848,23 @@ function productVariantStockKey (row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function distinctVariantStockQty (sourceRows) {
|
function distinctVariantStockQty (sourceRows) {
|
||||||
|
return distinctVariantNumber(sourceRows, 'stock_qty')
|
||||||
|
}
|
||||||
|
|
||||||
|
function distinctVariantNumber (sourceRows, field) {
|
||||||
const seen = new Set()
|
const seen = new Set()
|
||||||
let total = 0
|
let total = 0
|
||||||
let keyed = false
|
let keyed = false
|
||||||
for (const row of sourceRows) {
|
for (const row of sourceRows) {
|
||||||
const key = productVariantStockKey(row)
|
const key = productVariantStockKey(row)
|
||||||
const stockQty = Number(row?.stock_qty || 0)
|
const value = Number(row?.[field] || 0)
|
||||||
if (!key) continue
|
if (!key) continue
|
||||||
keyed = true
|
keyed = true
|
||||||
if (seen.has(key)) continue
|
if (seen.has(key)) continue
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
total += stockQty
|
total += value
|
||||||
}
|
}
|
||||||
return keyed ? total : sumRows(sourceRows, 'stock_qty')
|
return keyed ? total : sumRows(sourceRows, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
function distinctVariantStockCost (sourceRows) {
|
function distinctVariantStockCost (sourceRows) {
|
||||||
@@ -3784,6 +3920,12 @@ function distinctRevenueMarketCount (sourceRows, suffix, fallbackField) {
|
|||||||
const values = new Set()
|
const values = new Set()
|
||||||
for (const row of sourceRows) {
|
for (const row of sourceRows) {
|
||||||
if (Number(row?.[salesField] || 0) <= 0) continue
|
if (Number(row?.[salesField] || 0) <= 0) continue
|
||||||
|
const marketKeys = stringListFromRow(row, `__market_keys_${suffix}`)
|
||||||
|
for (const key of marketKeys) {
|
||||||
|
const value = displayMarketName(key)
|
||||||
|
if (value && value !== 'STOK' && value !== '-') values.add(value)
|
||||||
|
}
|
||||||
|
if (marketKeys.length > 0) continue
|
||||||
const value = displayMarketName(row?.market_key)
|
const value = displayMarketName(row?.market_key)
|
||||||
if (value && value !== 'STOK' && value !== '-') values.add(value)
|
if (value && value !== 'STOK' && value !== '-') values.add(value)
|
||||||
}
|
}
|
||||||
@@ -3798,6 +3940,11 @@ function distinctRevenueCustomerCount (sourceRows, suffix, fallbackField) {
|
|||||||
const values = new Set()
|
const values = new Set()
|
||||||
for (const row of sourceRows) {
|
for (const row of sourceRows) {
|
||||||
if (Number(row?.[salesField] || 0) <= 0) continue
|
if (Number(row?.[salesField] || 0) <= 0) continue
|
||||||
|
const customerKeys = stringListFromRow(row, `__customer_keys_${suffix}`)
|
||||||
|
for (const code of customerKeys) {
|
||||||
|
if (code && code !== '-') values.add(code)
|
||||||
|
}
|
||||||
|
if (customerKeys.length > 0) continue
|
||||||
const code = String(row?.customer_code || '').trim()
|
const code = String(row?.customer_code || '').trim()
|
||||||
if (code && code !== '-') values.add(code)
|
if (code && code !== '-') values.add(code)
|
||||||
}
|
}
|
||||||
@@ -3807,6 +3954,22 @@ function distinctRevenueCustomerCount (sourceRows, suffix, fallbackField) {
|
|||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringListFromRow (row, field) {
|
||||||
|
const value = row?.[field]
|
||||||
|
if (Array.isArray(value)) return value.map(item => String(item || '').trim()).filter(Boolean)
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) return []
|
||||||
|
if (text.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
return Array.isArray(parsed) ? parsed.map(item => String(item || '').trim()).filter(Boolean) : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [text]
|
||||||
|
}
|
||||||
|
|
||||||
function distinctSummary (sourceRows, field, fallbackField) {
|
function distinctSummary (sourceRows, field, fallbackField) {
|
||||||
const values = new Set()
|
const values = new Set()
|
||||||
for (const row of sourceRows) {
|
for (const row of sourceRows) {
|
||||||
@@ -3879,14 +4042,9 @@ function scoreSuffixForMetric (field) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scoreWeight (row, suffix) {
|
function scoreWeight (row, suffix) {
|
||||||
const salesUsd = Number(row?.[`sales_usd_${suffix}`] || 0)
|
|
||||||
if (salesUsd > 0) return salesUsd
|
|
||||||
const salesQty = Number(row?.[`sales_qty_${suffix}`] || 0)
|
const salesQty = Number(row?.[`sales_qty_${suffix}`] || 0)
|
||||||
if (salesQty > 0) return salesQty
|
if (salesQty > 0) return salesQty
|
||||||
const stockQty = Number(row?.stock_qty || 0)
|
return 0
|
||||||
if (stockQty <= 0) return 1
|
|
||||||
const costPrice = Number(row?.[`cost_price_usd_${suffix}`] || row?.cost_price_usd || 0)
|
|
||||||
return costPrice > 0 ? stockQty * costPrice : stockQty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function weightedAverageScore (sourceRows, valueField, suffix) {
|
function weightedAverageScore (sourceRows, valueField, suffix) {
|
||||||
@@ -4126,10 +4284,10 @@ function withProductMargins (row) {
|
|||||||
const out = {
|
const out = {
|
||||||
...next,
|
...next,
|
||||||
avg_price_usd_365d: Number(next?.sales_qty_365d || 0) > 0 ? Number(next?.sales_usd_365d || 0) / Number(next?.sales_qty_365d || 0) : 0,
|
avg_price_usd_365d: Number(next?.sales_qty_365d || 0) > 0 ? Number(next?.sales_usd_365d || 0) / Number(next?.sales_qty_365d || 0) : 0,
|
||||||
stock_turnover_90d: existingOrStockTurnover(next?.stock_turnover_90d, next?.sales_qty_90d, next?.stock_qty),
|
stock_turnover_90d: groupedOrExistingStockTurnover(next, '90d', 90),
|
||||||
stock_turnover_180d: existingOrStockTurnover(next?.stock_turnover_180d, next?.sales_qty_180d, next?.stock_qty),
|
stock_turnover_180d: groupedOrExistingStockTurnover(next, '180d', 180),
|
||||||
stock_turnover_365d: existingOrStockTurnover(next?.stock_turnover_365d, next?.sales_qty_365d, next?.stock_qty),
|
stock_turnover_365d: groupedOrExistingStockTurnover(next, '365d', 360),
|
||||||
stock_turnover_total: existingOrStockTurnover(next?.stock_turnover_total, next?.sales_qty_total, next?.stock_qty)
|
stock_turnover_total: groupedOrExistingStockTurnover(next, 'total', productPerformancePeriodDays(next, 'total'))
|
||||||
}
|
}
|
||||||
for (const suffix of ['90d', '180d', '365d', 'total']) {
|
for (const suffix of ['90d', '180d', '365d', 'total']) {
|
||||||
applyPeriodProfitFields(out, suffix)
|
applyPeriodProfitFields(out, suffix)
|
||||||
@@ -4142,7 +4300,7 @@ function withGeneralMargins (row) {
|
|||||||
applyPeriodProfitFields(out, 'total')
|
applyPeriodProfitFields(out, 'total')
|
||||||
return {
|
return {
|
||||||
...out,
|
...out,
|
||||||
stock_turnover_total: existingOrStockTurnover(out?.stock_turnover_total, out?.sales_qty_total, out?.stock_qty)
|
stock_turnover_total: groupedOrExistingStockTurnover(out, 'total', productPerformancePeriodDays(out, 'total'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4209,15 +4367,27 @@ function applyPeriodProfitFields (row, suffix) {
|
|||||||
row[`gross_margin_cost_${suffix}`] = marginFromSalesCost(salesUSD, qty, costPrice)
|
row[`gross_margin_cost_${suffix}`] = marginFromSalesCost(salesUSD, qty, costPrice)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stockTurnover (salesQty, stockQty) {
|
function stockTurnover (salesQty, stockQty, periodDays) {
|
||||||
const stock = Number(stockQty || 0)
|
return annualizedStockTurnover(salesQty, stockQty, periodDays)
|
||||||
if (stock <= 0) return 0
|
|
||||||
return Number(salesQty || 0) / stock
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function existingOrStockTurnover (value, salesQty, stockQty) {
|
function existingOrStockTurnover (value, salesQty, stockQty, periodDays) {
|
||||||
const current = Number(value)
|
const current = Number(value)
|
||||||
return value !== undefined && value !== null && Number.isFinite(current) ? current : stockTurnover(salesQty, stockQty)
|
return value !== undefined && value !== null && Number.isFinite(current) ? current : stockTurnover(salesQty, stockQty, periodDays)
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupedOrExistingStockTurnover (row, suffix, periodDays) {
|
||||||
|
const salesQty = Number(row?.[`sales_qty_${suffix}`] || 0)
|
||||||
|
const avgStock = Number(row?.[`avg_stock_${suffix}`] || 0)
|
||||||
|
const stockQty = Number(row?.stock_qty || 0)
|
||||||
|
if (row?.__group) {
|
||||||
|
const current = Number(row?.[`stock_turnover_${suffix}`])
|
||||||
|
if (Number.isFinite(current)) return current
|
||||||
|
if (avgStock > 0) return stockTurnover(salesQty, avgStock, periodDays)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const stockBase = avgStock > 0 ? avgStock : stockQty
|
||||||
|
return existingOrStockTurnover(row?.[`stock_turnover_${suffix}`], salesQty, stockBase, periodDays)
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterKey (tableKey, name) {
|
function filterKey (tableKey, name) {
|
||||||
@@ -5204,7 +5374,7 @@ function productPerformanceExcelExportTableKey () {
|
|||||||
function buildProductPerformanceExcelExportFilters () {
|
function buildProductPerformanceExcelExportFilters () {
|
||||||
const tableKey = productPerformanceExcelExportTableKey()
|
const tableKey = productPerformanceExcelExportTableKey()
|
||||||
const out = {}
|
const out = {}
|
||||||
for (const col of columnsForTableKey(tableKey) || []) {
|
for (const col of filterColumnsForTableKey(tableKey) || []) {
|
||||||
const name = String(col?.name || '').trim()
|
const name = String(col?.name || '').trim()
|
||||||
if (!productPerformanceExcelExportFilterFields.has(name)) continue
|
if (!productPerformanceExcelExportFilterFields.has(name)) continue
|
||||||
const selected = selectedColumnFilters(tableKey, name)
|
const selected = selectedColumnFilters(tableKey, name)
|
||||||
@@ -5986,95 +6156,95 @@ onBeforeUnmount(() => {
|
|||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(-n+10)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(-n+10)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(-n+10)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(-n+10)) {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(-n+10)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(-n+10)) {
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
background: #f8fbff;
|
background: #f8fbff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+10)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+10)) {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(1)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(1)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(1)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(1)) {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 190px;
|
width: 190px;
|
||||||
min-width: 190px;
|
min-width: 190px;
|
||||||
max-width: 190px;
|
max-width: 190px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(2)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(2)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(2)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(2)) {
|
||||||
left: 190px;
|
left: 190px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
max-width: 140px;
|
max-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(3)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(3)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(3)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(3)) {
|
||||||
left: 330px;
|
left: 330px;
|
||||||
width: 110px;
|
width: 110px;
|
||||||
min-width: 110px;
|
min-width: 110px;
|
||||||
max-width: 110px;
|
max-width: 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(4)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(4)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(4)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(4)) {
|
||||||
left: 440px;
|
left: 440px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(5)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(5)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(5)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(5)) {
|
||||||
left: 590px;
|
left: 590px;
|
||||||
width: 170px;
|
width: 170px;
|
||||||
min-width: 170px;
|
min-width: 170px;
|
||||||
max-width: 170px;
|
max-width: 170px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(6)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(6)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(6)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(6)) {
|
||||||
left: 760px;
|
left: 760px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(7)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(7)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(7)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(7)) {
|
||||||
left: 910px;
|
left: 910px;
|
||||||
width: 130px;
|
width: 130px;
|
||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
max-width: 130px;
|
max-width: 130px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(8)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(8)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(8)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(8)) {
|
||||||
left: 1040px;
|
left: 1040px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
min-width: 90px;
|
min-width: 90px;
|
||||||
max-width: 90px;
|
max-width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(9)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(9)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(9)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(9)) {
|
||||||
left: 1130px;
|
left: 1130px;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
max-width: 80px;
|
max-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(10)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(10)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(10)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(10)) {
|
||||||
left: 1210px;
|
left: 1210px;
|
||||||
width: 130px;
|
width: 130px;
|
||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
@@ -6082,6 +6252,58 @@ onBeforeUnmount(() => {
|
|||||||
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
|
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table) {
|
||||||
|
min-width: 2450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(-n+4)),
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(-n+4)) {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(-n+4)) {
|
||||||
|
z-index: 30;
|
||||||
|
background: #f8fbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+4)) {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(1)),
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(1)) {
|
||||||
|
left: 0;
|
||||||
|
width: 86px;
|
||||||
|
min-width: 86px;
|
||||||
|
max-width: 86px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(2)),
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(2)) {
|
||||||
|
left: 86px;
|
||||||
|
width: 150px;
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(3)),
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(3)) {
|
||||||
|
left: 236px;
|
||||||
|
width: 112px;
|
||||||
|
min-width: 112px;
|
||||||
|
max-width: 112px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(4)),
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(4)) {
|
||||||
|
left: 348px;
|
||||||
|
width: 132px;
|
||||||
|
min-width: 132px;
|
||||||
|
max-width: 132px;
|
||||||
|
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
.product-breakdown-table.product-detail-table :deep(.q-table) {
|
.product-breakdown-table.product-detail-table :deep(.q-table) {
|
||||||
min-width: 2620px;
|
min-width: 2620px;
|
||||||
}
|
}
|
||||||
@@ -6198,10 +6420,14 @@ onBeforeUnmount(() => {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(n+11):not(.text-right)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(n+11):not(.text-right)),
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(n+11):not(.text-right)),
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(n+11):not(.text-right)),
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(n+5):not(.text-right)),
|
||||||
|
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(n+5):not(.text-right)),
|
||||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(n+4):not(.text-right)),
|
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(n+4):not(.text-right)),
|
||||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(n+4):not(.text-right)),
|
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(n+4):not(.text-right)),
|
||||||
|
.sticky-dim-table.sticky-dim-4 :deep(.q-table th:nth-child(n+5):not(.text-right)),
|
||||||
|
.sticky-dim-table.sticky-dim-4 :deep(.q-table td:nth-child(n+5):not(.text-right)),
|
||||||
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(n+6):not(.text-right)),
|
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(n+6):not(.text-right)),
|
||||||
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(n+6):not(.text-right)),
|
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(n+6):not(.text-right)),
|
||||||
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(n+7):not(.text-right)),
|
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(n+7):not(.text-right)),
|
||||||
@@ -6281,6 +6507,8 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(-n+3)),
|
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(-n+3)),
|
||||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(-n+3)),
|
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(-n+3)),
|
||||||
|
.sticky-dim-table.sticky-dim-4 :deep(.q-table th:nth-child(-n+4)),
|
||||||
|
.sticky-dim-table.sticky-dim-4 :deep(.q-table td:nth-child(-n+4)),
|
||||||
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(-n+5)),
|
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(-n+5)),
|
||||||
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(-n+5)),
|
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(-n+5)),
|
||||||
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(-n+6)),
|
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(-n+6)),
|
||||||
@@ -6300,6 +6528,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(-n+3)),
|
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(-n+3)),
|
||||||
|
.sticky-dim-table.sticky-dim-4 :deep(.q-table th:nth-child(-n+4)),
|
||||||
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(-n+5)),
|
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(-n+5)),
|
||||||
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(-n+6)),
|
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(-n+6)),
|
||||||
.sticky-dim-table.sticky-dim-7 :deep(.q-table th:nth-child(-n+7)),
|
.sticky-dim-table.sticky-dim-7 :deep(.q-table th:nth-child(-n+7)),
|
||||||
@@ -6317,6 +6546,8 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(3)),
|
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(3)),
|
||||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(3)),
|
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(3)),
|
||||||
|
.sticky-dim-table.sticky-dim-4 :deep(.q-table th:nth-child(4)),
|
||||||
|
.sticky-dim-table.sticky-dim-4 :deep(.q-table td:nth-child(4)),
|
||||||
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(5)),
|
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(5)),
|
||||||
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(5)),
|
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(5)),
|
||||||
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(6)),
|
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(6)),
|
||||||
|
|||||||
@@ -423,7 +423,6 @@ const productImageListByCode = ref({})
|
|||||||
const productImageListLoading = ref({})
|
const productImageListLoading = ref({})
|
||||||
const productImageFallbackByKey = ref({})
|
const productImageFallbackByKey = ref({})
|
||||||
const productImageContentLoading = ref({})
|
const productImageContentLoading = ref({})
|
||||||
const productImageBlobUrls = ref([])
|
|
||||||
const productImageListBlockedUntil = ref(0)
|
const productImageListBlockedUntil = ref(0)
|
||||||
const productCardDialog = ref(false)
|
const productCardDialog = ref(false)
|
||||||
const productCardData = ref({})
|
const productCardData = ref({})
|
||||||
@@ -702,24 +701,13 @@ function clearGalleryQueryIndex() {
|
|||||||
|
|
||||||
async function resolveProductImageUrlForCarousel(item) {
|
async function resolveProductImageUrlForCarousel(item) {
|
||||||
const resolved = resolveProductImageUrl(item)
|
const resolved = resolveProductImageUrl(item)
|
||||||
|
const contentUrl = String(resolved.contentUrl || '').trim()
|
||||||
|
if (contentUrl) return contentUrl
|
||||||
|
|
||||||
const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
|
const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
|
||||||
if (directUrl) return directUrl
|
if (directUrl) return directUrl
|
||||||
|
|
||||||
const contentUrl = String(resolved.contentUrl || '').trim()
|
return ''
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return contentUrl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
||||||
@@ -769,9 +757,9 @@ async function flushProductImageBatch() {
|
|||||||
const list = Array.isArray(item.images) ? item.images : []
|
const list = Array.isArray(item.images) ? item.images : []
|
||||||
const first = list[0] || null
|
const first = list[0] || null
|
||||||
const resolved = resolveProductImageUrl(first)
|
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()
|
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
|
if (queued) productImageListByCode.value[queued.listKey] = list
|
||||||
delete productImageLoading.value[key]
|
delete productImageLoading.value[key]
|
||||||
}
|
}
|
||||||
@@ -860,11 +848,11 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di
|
|||||||
|
|
||||||
productImageCache.value[key] = String(
|
productImageCache.value[key] = String(
|
||||||
preferredCardUrl ||
|
preferredCardUrl ||
|
||||||
primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || primaryResolved.contentUrl ||
|
primaryResolved.contentUrl || primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl ||
|
||||||
secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || secondaryResolved.contentUrl ||
|
secondaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl ||
|
||||||
''
|
''
|
||||||
).trim()
|
).trim()
|
||||||
productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.contentUrl || ''
|
productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || ''
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[ProductStockByAttributes] product image fetch failed', { code, color, err })
|
console.warn('[ProductStockByAttributes] product image fetch failed', { code, color, err })
|
||||||
productImageCache.value[key] = ''
|
productImageCache.value[key] = ''
|
||||||
@@ -1576,10 +1564,6 @@ onUnmounted(() => {
|
|||||||
clearTimeout(filterOptionsDebounceTimer)
|
clearTimeout(filterOptionsDebounceTimer)
|
||||||
filterOptionsDebounceTimer = null
|
filterOptionsDebounceTimer = null
|
||||||
}
|
}
|
||||||
for (const url of productImageBlobUrls.value) {
|
|
||||||
try { URL.revokeObjectURL(url) } catch {}
|
|
||||||
}
|
|
||||||
productImageBlobUrls.value = []
|
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -390,7 +390,6 @@ const productImageListByCode = ref({})
|
|||||||
const productImageListLoading = ref({})
|
const productImageListLoading = ref({})
|
||||||
const productImageFallbackByKey = ref({})
|
const productImageFallbackByKey = ref({})
|
||||||
const productImageContentLoading = ref({})
|
const productImageContentLoading = ref({})
|
||||||
const productImageBlobUrls = ref([])
|
|
||||||
const productImageListBlockedUntil = ref(0)
|
const productImageListBlockedUntil = ref(0)
|
||||||
const productCardDialog = ref(false)
|
const productCardDialog = ref(false)
|
||||||
const productCardData = ref({})
|
const productCardData = ref({})
|
||||||
@@ -681,24 +680,13 @@ function clearGalleryQueryIndex() {
|
|||||||
|
|
||||||
async function resolveProductImageUrlForCarousel(item) {
|
async function resolveProductImageUrlForCarousel(item) {
|
||||||
const resolved = resolveProductImageUrl(item)
|
const resolved = resolveProductImageUrl(item)
|
||||||
|
const contentUrl = String(resolved.contentUrl || '').trim()
|
||||||
|
if (contentUrl) return contentUrl
|
||||||
|
|
||||||
const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
|
const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
|
||||||
if (directUrl) return directUrl
|
if (directUrl) return directUrl
|
||||||
|
|
||||||
const contentUrl = String(resolved.contentUrl || '').trim()
|
return ''
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return contentUrl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
||||||
@@ -748,9 +736,9 @@ async function flushProductImageBatch() {
|
|||||||
const list = Array.isArray(item.images) ? item.images : []
|
const list = Array.isArray(item.images) ? item.images : []
|
||||||
const first = list[0] || null
|
const first = list[0] || null
|
||||||
const resolved = resolveProductImageUrl(first)
|
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()
|
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
|
if (queued) productImageListByCode.value[queued.listKey] = list
|
||||||
delete productImageLoading.value[key]
|
delete productImageLoading.value[key]
|
||||||
}
|
}
|
||||||
@@ -836,11 +824,11 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di
|
|||||||
const secondaryResolved = resolveProductImageUrl(secondaryItem)
|
const secondaryResolved = resolveProductImageUrl(secondaryItem)
|
||||||
productImageCache.value[key] = String(
|
productImageCache.value[key] = String(
|
||||||
preferredCardUrl ||
|
preferredCardUrl ||
|
||||||
primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || primaryResolved.contentUrl ||
|
primaryResolved.contentUrl || primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl ||
|
||||||
secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || secondaryResolved.contentUrl ||
|
secondaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl ||
|
||||||
''
|
''
|
||||||
).trim()
|
).trim()
|
||||||
productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.contentUrl || ''
|
productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || ''
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[ProductStockQuery] product image fetch failed', { code, color, err })
|
console.warn('[ProductStockQuery] product image fetch failed', { code, color, err })
|
||||||
productImageCache.value[key] = ''
|
productImageCache.value[key] = ''
|
||||||
@@ -1374,10 +1362,6 @@ function resetForm() {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('mousemove', onFullscreenMouseMove)
|
window.removeEventListener('mousemove', onFullscreenMouseMove)
|
||||||
window.removeEventListener('mouseup', onFullscreenMouseUp)
|
window.removeEventListener('mouseup', onFullscreenMouseUp)
|
||||||
for (const url of productImageBlobUrls.value) {
|
|
||||||
try { URL.revokeObjectURL(url) } catch {}
|
|
||||||
}
|
|
||||||
productImageBlobUrls.value = []
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -40,6 +40,14 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="fetchRows"
|
@click="fetchRows"
|
||||||
/>
|
/>
|
||||||
|
<q-btn
|
||||||
|
label="Excel'e Aktar"
|
||||||
|
icon="download"
|
||||||
|
color="primary"
|
||||||
|
outline
|
||||||
|
:disable="loading || rows.length === 0"
|
||||||
|
@click="exportVisibleRows"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,7 +149,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { usePermission } from 'src/composables/usePermission'
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
import { get, extractApiErrorDetail } from 'src/services/api'
|
import { get, extractApiErrorDetail } from 'src/services/api'
|
||||||
@@ -168,6 +176,10 @@ const dateColumns = new Set(['Tarihi', 'dteKayitTarihi', 'dteGuncellemeTarihi',
|
|||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'open', label: '', field: 'open', align: 'center', sortable: false, style: 'width:3%', headerStyle: 'width:3%' },
|
{ name: 'open', label: '', field: 'open', align: 'center', sortable: false, style: 'width:3%', headerStyle: 'width:3%' },
|
||||||
{ name: 'UretimSekli', label: 'Uretim Sekli', field: 'UretimSekli', align: 'left', sortable: true, style: 'width:9%', headerStyle: 'width:9%' },
|
{ name: 'UretimSekli', label: 'Uretim Sekli', field: 'UretimSekli', align: 'left', sortable: true, style: 'width:9%', headerStyle: 'width:9%' },
|
||||||
|
{ name: 'nUrtSiparisNo', label: 'Uretim Siparis No', field: 'nUrtSiparisNo', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
|
||||||
|
{ name: 'ceketDepoGiris', label: 'C-Ceket Depo Giris', field: 'ceketDepoGiris', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
|
||||||
|
{ name: 'pantolonDepoGiris', label: 'P-Pantolon Depo Giris', field: 'pantolonDepoGiris', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
|
||||||
|
{ name: 'yelekDepoGiris', label: 'Y-Yelek Depo Giris', field: 'yelekDepoGiris', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
|
||||||
{ name: 'nOnMLNo', label: 'nOnMLNo', field: 'nOnMLNo', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
|
{ name: 'nOnMLNo', label: 'nOnMLNo', field: 'nOnMLNo', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
|
||||||
{ name: 'UrunKodu', label: 'UrunKodu', field: 'UrunKodu', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
{ name: 'UrunKodu', label: 'UrunKodu', field: 'UrunKodu', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
||||||
{ name: 'UrunAdi', label: 'UrunAdi', field: 'UrunAdi', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
{ name: 'UrunAdi', label: 'UrunAdi', field: 'UrunAdi', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
||||||
@@ -186,6 +198,30 @@ const columns = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const columnFilters = reactive({})
|
const columnFilters = reactive({})
|
||||||
|
const LIST_STATE_STORAGE_KEY = 'bssapp:production-product-costing:has-cost-list-state:v1'
|
||||||
|
|
||||||
|
function persistListState () {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(LIST_STATE_STORAGE_KEY, JSON.stringify({
|
||||||
|
filters: { search: String(filters.search || '') },
|
||||||
|
columnFilters: JSON.parse(JSON.stringify(columnFilters)),
|
||||||
|
pagination: { ...tablePagination.value }
|
||||||
|
}))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreListState () {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem(LIST_STATE_STORAGE_KEY)
|
||||||
|
if (!raw) return
|
||||||
|
const saved = JSON.parse(raw)
|
||||||
|
filters.search = String(saved?.filters?.search || '')
|
||||||
|
Object.assign(columnFilters, saved?.columnFilters && typeof saved.columnFilters === 'object' ? saved.columnFilters : {})
|
||||||
|
if (saved?.pagination && typeof saved.pagination === 'object') {
|
||||||
|
tablePagination.value = { ...tablePagination.value, ...saved.pagination }
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
function getColumnFilter (name) {
|
function getColumnFilter (name) {
|
||||||
if (!columnFilters[name]) {
|
if (!columnFilters[name]) {
|
||||||
@@ -293,9 +329,11 @@ function clearAllColumnFilters () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let searchTimer = null
|
let searchTimer = null
|
||||||
|
let filterWatchEnabled = false
|
||||||
watch(
|
watch(
|
||||||
() => filters.search,
|
() => filters.search,
|
||||||
() => {
|
() => {
|
||||||
|
if (!filterWatchEnabled) return
|
||||||
clearTimeout(searchTimer)
|
clearTimeout(searchTimer)
|
||||||
searchTimer = setTimeout(() => {
|
searchTimer = setTimeout(() => {
|
||||||
fetchRows()
|
fetchRows()
|
||||||
@@ -348,19 +386,66 @@ function clearFilters () {
|
|||||||
fetchRows()
|
fetchRows()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeExcelCsvCell (value) {
|
||||||
|
let text = String(value ?? '')
|
||||||
|
// Prevent spreadsheet applications from evaluating data as a formula.
|
||||||
|
if (/^[=+\-@]/.test(text)) text = `'${text}`
|
||||||
|
return `"${text.replace(/"/g, '""')}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const excelNumericColumns = new Set(['ceketDepoGiris', 'pantolonDepoGiris', 'yelekDepoGiris', 'lTutarTL', 'lTutarUSD', 'lTutarEURO'])
|
||||||
|
|
||||||
|
function formatExcelCsvCell (col, rawValue, displayValue) {
|
||||||
|
if (excelNumericColumns.has(col?.name)) {
|
||||||
|
const numericValue = Number(rawValue)
|
||||||
|
if (Number.isFinite(numericValue)) return String(numericValue).replace('.', ',')
|
||||||
|
}
|
||||||
|
return escapeExcelCsvCell(displayValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportVisibleRows () {
|
||||||
|
const visibleRows = Array.isArray(rows.value) ? rows.value : []
|
||||||
|
if (visibleRows.length === 0) return
|
||||||
|
|
||||||
|
const exportColumns = columns.filter(col => col.name !== 'open')
|
||||||
|
const csvLines = [
|
||||||
|
exportColumns.map(col => escapeExcelCsvCell(col.label)).join(';'),
|
||||||
|
...visibleRows.map(row => exportColumns.map(col => {
|
||||||
|
const rawValue = typeof col.field === 'function' ? col.field(row) : row?.[col.field]
|
||||||
|
const displayValue = typeof col.format === 'function' ? col.format(rawValue, row) : rawValue
|
||||||
|
return formatExcelCsvCell(col, rawValue, displayValue)
|
||||||
|
}).join(';'))
|
||||||
|
]
|
||||||
|
|
||||||
|
const blob = new Blob([`\uFEFF${csvLines.join('\r\n')}`], { type: 'text/csv;charset=utf-8;' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `mevcut_maliyetli_urunler_${new Date().toISOString().slice(0, 10)}.csv`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
function openRow (row) {
|
function openRow (row) {
|
||||||
const urunKodu = String(row?.UrunKodu || '').trim()
|
const urunKodu = String(row?.UrunKodu || '').trim()
|
||||||
if (!urunKodu) return
|
if (!urunKodu) return
|
||||||
|
|
||||||
|
persistListState()
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
name: 'production-product-costing-has-cost-history',
|
name: 'production-product-costing-has-cost-history',
|
||||||
query: { urun_kodu: urunKodu }
|
query: { urun_kodu: urunKodu }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
if (!canReadOrder.value) return
|
if (!canReadOrder.value) return
|
||||||
fetchRows()
|
restoreListState()
|
||||||
|
await nextTick()
|
||||||
|
filterWatchEnabled = true
|
||||||
|
await fetchRows()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -469,7 +469,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-right pcd-sub-right-clickable" @click="toggleGroup(grp, gi)">
|
<div class="sub-right pcd-sub-right-clickable" @click="toggleGroup(grp, gi)">
|
||||||
Grup Toplami TRY: {{ formatBarMoney(resolveGroupTRYTutar(grp)) }} | USD: {{ formatBarMoney(resolveGroupUSDTutar(grp)) }}
|
Grup Toplami TRY: {{ formatBarMoney(grp.liveTryTotal) }} | USD: {{ formatBarMoney(grp.liveUsdTotal) }}
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'"
|
:name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'"
|
||||||
size="18px"
|
size="18px"
|
||||||
@@ -1557,8 +1557,14 @@ function ensureBeforeUnloadGuard (enabled) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function shouldIncludeRowInCostingTotal (row) {
|
||||||
|
// The explicit "Maliyete Dahil" selection is authoritative for the top
|
||||||
|
// header and the amount persisted to spUrtOnMLMas, including CM rows.
|
||||||
|
return normalizeBooleanFlag(row?.maliyeteDahil ?? row?.maliyete_dahil ?? row?.Maliyete_dahil)
|
||||||
|
}
|
||||||
|
|
||||||
const toolbarSummary = computed(() => flatDetailRows.value.reduce((acc, row) => {
|
const toolbarSummary = computed(() => flatDetailRows.value.reduce((acc, row) => {
|
||||||
if (!row?.maliyeteDahil) return acc
|
if (!shouldIncludeRowInCostingTotal(row)) return acc
|
||||||
acc.tryTotal += resolveRowTRYTutar(row)
|
acc.tryTotal += resolveRowTRYTutar(row)
|
||||||
acc.usdTotal += resolveRowUSDTutar(row)
|
acc.usdTotal += resolveRowUSDTutar(row)
|
||||||
acc.eurTotal += resolveRowEURTutar(row)
|
acc.eurTotal += resolveRowEURTutar(row)
|
||||||
@@ -2821,54 +2827,84 @@ function recalculateAllDetailRows () {
|
|||||||
...grp,
|
...grp,
|
||||||
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => recalculateDetailRow({ ...row }, { preserveInputs: true }))
|
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => recalculateDetailRow({ ...row }, { preserveInputs: true }))
|
||||||
}))
|
}))
|
||||||
|
refreshLiveGroupTotals()
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveRowTRYTutar (row) {
|
function resolveRowTRYTutar (row) {
|
||||||
const tryAmount = Number(row?.lTutar || 0)
|
const quantity = resolveNumericRowQuantity(row)
|
||||||
return Number.isFinite(tryAmount) ? tryAmount : 0
|
const inputPrice = resolveNumericRowInputPrice(row)
|
||||||
|
const inputCurrency = resolveInputCurrency(row)
|
||||||
|
const liveAmount = resolveTRYUnitPriceByInput(
|
||||||
|
inputPrice,
|
||||||
|
inputCurrency,
|
||||||
|
resolveExchangeRateValue('USD'),
|
||||||
|
resolveExchangeRateValue('EUR'),
|
||||||
|
resolveExchangeRateValue('GBP')
|
||||||
|
) * quantity
|
||||||
|
|
||||||
|
if (Number.isFinite(liveAmount) && inputPrice > 0) return liveAmount
|
||||||
|
return parseMoneyInput(row?.lTutar)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveRowEURTutar (row) {
|
function resolveRowEURTutar (row) {
|
||||||
const eurAmount = Number(row?.eurTutar || 0)
|
return parseMoneyInput(row?.eurTutar)
|
||||||
return Number.isFinite(eurAmount) ? eurAmount : 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveRowGBPTutar (row) {
|
function resolveRowGBPTutar (row) {
|
||||||
const gbpAmount = Number(row?.gbpTutar || 0)
|
return parseMoneyInput(row?.gbpTutar)
|
||||||
return Number.isFinite(gbpAmount) ? gbpAmount : 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveRowUSDTutar (row) {
|
function resolveRowUSDTutar (row) {
|
||||||
const usdAmount = Number(row?.usdTutar || 0)
|
const quantity = resolveNumericRowQuantity(row)
|
||||||
if (Number.isFinite(usdAmount)) return usdAmount
|
const inputPrice = resolveNumericRowInputPrice(row)
|
||||||
|
const inputCurrency = resolveInputCurrency(row)
|
||||||
|
const liveAmount = resolveUSDUnitPriceByInput(
|
||||||
|
inputPrice,
|
||||||
|
inputCurrency,
|
||||||
|
resolveExchangeRateValue('USD'),
|
||||||
|
resolveExchangeRateValue('EUR'),
|
||||||
|
resolveExchangeRateValue('GBP')
|
||||||
|
) * quantity
|
||||||
|
|
||||||
const miktar = resolveNumericRowQuantity(row)
|
if (Number.isFinite(liveAmount) && inputPrice > 0) return liveAmount
|
||||||
const dovizFiyati = Number(row?.lDovizFiyati || 0)
|
|
||||||
const calc = miktar * dovizFiyati
|
const rawUSDAmount = row?.usdTutar
|
||||||
|
if (rawUSDAmount !== undefined && rawUSDAmount !== null && String(rawUSDAmount).trim() !== '') {
|
||||||
|
return parseMoneyInput(rawUSDAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dovizFiyati = parseMoneyInput(row?.lDovizFiyati)
|
||||||
|
const calc = quantity * dovizFiyati
|
||||||
return Number.isFinite(calc) ? calc : 0
|
return Number.isFinite(calc) ? calc : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldIgnoreGroupMaliyeteDahil (grp) {
|
function shouldIncludeRowInGroupTotal (_grp, row) {
|
||||||
return isCMGroupName(grp?.sAciklama3)
|
// Sub-headers show the gross amount entered in that group. Inclusion in the
|
||||||
|
// actual costing is shown separately by the page header and checkbox.
|
||||||
|
return Boolean(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldIncludeRowInGroupTotal (grp, row) {
|
function refreshLiveGroupTotals () {
|
||||||
return shouldIgnoreGroupMaliyeteDahil(grp) || normalizeBooleanFlag(row?.maliyeteDahil)
|
detailGroups.value = detailGroups.value.map(group => {
|
||||||
}
|
const totals = (Array.isArray(group?.items) ? group.items : []).reduce((acc, row) => {
|
||||||
|
if (!shouldIncludeRowInGroupTotal(group, row)) return acc
|
||||||
|
acc.tryTotal += resolveRowTRYTutar(row)
|
||||||
|
acc.usdTotal += resolveRowUSDTutar(row)
|
||||||
|
acc.quantity += resolveNumericRowQuantity(row)
|
||||||
|
return acc
|
||||||
|
}, { tryTotal: 0, usdTotal: 0, quantity: 0 })
|
||||||
|
|
||||||
function resolveGroupTRYTutar (grp) {
|
return {
|
||||||
const items = Array.isArray(grp?.items) ? grp.items : []
|
...group,
|
||||||
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowTRYTutar(row) : 0), 0)
|
liveTryTotal: totals.tryTotal,
|
||||||
|
liveUsdTotal: totals.usdTotal,
|
||||||
|
liveQuantity: totals.quantity
|
||||||
}
|
}
|
||||||
|
})
|
||||||
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 resolveGroupQuantity (grp) {
|
function resolveGroupQuantity (grp) {
|
||||||
const items = Array.isArray(grp?.items) ? grp.items : []
|
return parseMoneyInput(grp?.liveQuantity)
|
||||||
return items.reduce((acc, row) => acc + (Number(row?.lMiktar || 0) || 0), 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupKey (grp, gi) {
|
function groupKey (grp, gi) {
|
||||||
@@ -3244,7 +3280,7 @@ function onRowQuantityInput (row, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function triggerUIUpdate () {
|
function triggerUIUpdate () {
|
||||||
detailGroups.value = [...detailGroups.value]
|
refreshLiveGroupTotals()
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRowQuantityDisplay (row) {
|
function normalizeRowQuantityDisplay (row) {
|
||||||
@@ -3457,6 +3493,11 @@ async function fetchBulkItemPrices () {
|
|||||||
error: String(e?.message || e || '')
|
error: String(e?.message || e || '')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// Bulk price and previous-cost autofill paths can update many rows at once.
|
||||||
|
// Recalculate once more as a whole so group sub-header totals react immediately.
|
||||||
|
recalculateAllDetailRows()
|
||||||
|
schedulePersistLocalDraft()
|
||||||
|
triggerUIUpdate()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
@@ -3928,10 +3969,14 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
|||||||
const groupName = normalizeGroupName(meta?.groupName || '')
|
const groupName = normalizeGroupName(meta?.groupName || '')
|
||||||
const hammaddeAdi = String(meta?.hammaddeAdi || '').trim()
|
const hammaddeAdi = String(meta?.hammaddeAdi || '').trim()
|
||||||
const effectiveGroupName = groupName || 'TANIMSIZ'
|
const effectiveGroupName = groupName || 'TANIMSIZ'
|
||||||
const mtBolumID = (meta?.mtBolumID > 0 ? meta.mtBolumID : mappingMtBolumID) || 0
|
// The product-type mapping is authoritative for the required part. A raw
|
||||||
const metaParcaCandidate = normalizeGroupName((meta?.parcaAdi || '').trim())
|
// material type can be required for multiple parts, while its master
|
||||||
// Defensive: if backend/lookup accidentally returns group label (DT/TP/...) as part name, ignore it.
|
// record only carries one default MT section. Using that default here
|
||||||
const desiredParcaAdi = normalizeGroupName((metaParcaCandidate && !isKnownGroupName(metaParcaCandidate)) ? metaParcaCandidate : mappingParcaAdi)
|
// incorrectly made an existing recipe row satisfy every mapped part.
|
||||||
|
const mtBolumID = mappingMtBolumID > 0 ? mappingMtBolumID : (meta?.mtBolumID || 0)
|
||||||
|
const desiredParcaAdi = mappingMtBolumID > 0
|
||||||
|
? mappingParcaAdi
|
||||||
|
: normalizeGroupName(meta?.parcaAdi || mappingParcaAdi)
|
||||||
|
|
||||||
const anyMatch = flatDetailRows.value.find(r => {
|
const anyMatch = flatDetailRows.value.find(r => {
|
||||||
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||||||
@@ -5062,13 +5107,31 @@ async function saveChanges () {
|
|||||||
|
|
||||||
// If we created a new OnML (no-cost), switch to has-cost detail mode.
|
// If we created a new OnML (no-cost), switch to has-cost detail mode.
|
||||||
if (isNoCostDetail.value && newOnMLNo > 0) {
|
if (isNoCostDetail.value && newOnMLNo > 0) {
|
||||||
router.replace({
|
await router.replace({
|
||||||
name: 'production-product-costing-has-cost-detail',
|
name: 'production-product-costing-has-cost-detail',
|
||||||
query: {
|
query: {
|
||||||
n_onml_no: String(newOnMLNo),
|
n_onml_no: String(newOnMLNo),
|
||||||
urun_kodu: String(header?.UrunKodu || productCode.value || '').trim()
|
urun_kodu: String(header?.UrunKodu || productCode.value || '').trim()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Maliyet Kaydedildi',
|
||||||
|
message: 'Maliyeti olmayan urunler listesine donup kaldiginiz yerden devam etmek ister misiniz?',
|
||||||
|
ok: {
|
||||||
|
label: 'Listeye Don',
|
||||||
|
color: 'primary',
|
||||||
|
icon: 'arrow_back'
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
label: 'Bu Sayfada Devam Et',
|
||||||
|
color: 'grey-7',
|
||||||
|
flat: true
|
||||||
|
},
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
router.push({ name: 'production-product-costing-no-cost' })
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5078,6 +5141,24 @@ async function saveChanges () {
|
|||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
try { refreshLast10Warnings() } catch {}
|
try { refreshLast10Warnings() } catch {}
|
||||||
}, 1200)
|
}, 1200)
|
||||||
|
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Maliyet Guncellendi',
|
||||||
|
message: 'Mevcut maliyeti olan urunler listesine donup kaldiginiz yerden devam etmek ister misiniz?',
|
||||||
|
ok: {
|
||||||
|
label: 'Listeye Don',
|
||||||
|
color: 'primary',
|
||||||
|
icon: 'arrow_back'
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
label: 'Bu Sayfada Devam Et',
|
||||||
|
color: 'grey-7',
|
||||||
|
flat: true
|
||||||
|
},
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
router.push({ name: 'production-product-costing-has-cost' })
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Surface backend message (http.Error text) when available.
|
// Surface backend message (http.Error text) when available.
|
||||||
const msg = String(
|
const msg = String(
|
||||||
@@ -5717,12 +5798,69 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pcd-history-table :deep(.q-table thead th) {
|
.pcd-history-table :deep(.q-table thead th) {
|
||||||
font-size: 12px;
|
font-size: 9.5px;
|
||||||
|
line-height: 1.1;
|
||||||
|
white-space: normal;
|
||||||
|
padding: 3px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pcd-history-table :deep(.q-table tbody td) {
|
.pcd-history-table :deep(.q-table tbody td) {
|
||||||
font-size: 12px;
|
font-size: 9.5px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
padding: 2px;
|
||||||
|
white-space: normal;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-history-table :deep(.q-table__middle) {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-history-table :deep(.q-table) {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-history-table :deep(.q-table th),
|
||||||
|
.pcd-history-table :deep(.q-table td) {
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(1)), .pcd-history-table :deep(.q-table td:nth-child(1)) { width: 5% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(2)), .pcd-history-table :deep(.q-table td:nth-child(2)) { width: 6% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(3)), .pcd-history-table :deep(.q-table td:nth-child(3)) { width: 7% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(4)), .pcd-history-table :deep(.q-table td:nth-child(4)) { width: 6% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(5)), .pcd-history-table :deep(.q-table td:nth-child(5)) { width: 8% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(6)), .pcd-history-table :deep(.q-table td:nth-child(6)) { width: 7% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(7)), .pcd-history-table :deep(.q-table td:nth-child(7)) { width: 10% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(8)), .pcd-history-table :deep(.q-table td:nth-child(8)) { width: 5% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(9)), .pcd-history-table :deep(.q-table td:nth-child(9)) { width: 7% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(10)), .pcd-history-table :deep(.q-table td:nth-child(10)) { width: 5% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(11)), .pcd-history-table :deep(.q-table td:nth-child(11)) { width: 7% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(12)), .pcd-history-table :deep(.q-table td:nth-child(12)) { width: 5% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(13)), .pcd-history-table :deep(.q-table td:nth-child(13)) { width: 3% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(14)), .pcd-history-table :deep(.q-table td:nth-child(14)) { width: 5% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(15)), .pcd-history-table :deep(.q-table td:nth-child(15)) { width: 6% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(16)), .pcd-history-table :deep(.q-table td:nth-child(16)) { width: 4% !important; }
|
||||||
|
.pcd-history-table :deep(.q-table th:nth-child(17)), .pcd-history-table :deep(.q-table td:nth-child(17)) { width: 4% !important; }
|
||||||
|
|
||||||
|
.pcd-history-table :deep(.pcd-history-source-chip) {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-history-table :deep(.q-btn) {
|
||||||
|
min-width: 0;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pcd-history-table :deep(.pcd-history-row-purchase td) {
|
.pcd-history-table :deep(.pcd-history-row-purchase td) {
|
||||||
|
|||||||
@@ -49,6 +49,14 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="fetchRows"
|
@click="fetchRows"
|
||||||
/>
|
/>
|
||||||
|
<q-btn
|
||||||
|
label="Excel'e Aktar"
|
||||||
|
icon="download"
|
||||||
|
color="primary"
|
||||||
|
outline
|
||||||
|
:disable="loading || rows.length === 0"
|
||||||
|
@click="exportVisibleRows"
|
||||||
|
/>
|
||||||
<div class="npc-missing-count text-caption text-grey-7">
|
<div class="npc-missing-count text-caption text-grey-7">
|
||||||
Maliyeti Girilmemis Satir Sayisi: <b>{{ missingCostRowCount }}</b>
|
Maliyeti Girilmemis Satir Sayisi: <b>{{ missingCostRowCount }}</b>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,7 +184,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { usePermission } from 'src/composables/usePermission'
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
@@ -244,6 +252,9 @@ const columns = [
|
|||||||
style: 'width:7%',
|
style: 'width:7%',
|
||||||
headerStyle: 'width:7%'
|
headerStyle: 'width:7%'
|
||||||
},
|
},
|
||||||
|
{ name: 'ceketDepoGiris', label: 'C-Ceket Depo Giris', field: 'ceketDepoGiris', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width:7%', headerStyle: 'width:7%' },
|
||||||
|
{ name: 'pantolonDepoGiris', label: 'P-Pantolon Depo Giris', field: 'pantolonDepoGiris', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width:7%', headerStyle: 'width:7%' },
|
||||||
|
{ name: 'yelekDepoGiris', label: 'Y-Yelek Depo Giris', field: 'yelekDepoGiris', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width:7%', headerStyle: 'width:7%' },
|
||||||
{
|
{
|
||||||
name: 'sMModelKodu',
|
name: 'sMModelKodu',
|
||||||
label: 'Model Kodu',
|
label: 'Model Kodu',
|
||||||
@@ -262,6 +273,34 @@ const columns = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const columnFilters = reactive({})
|
const columnFilters = reactive({})
|
||||||
|
const LIST_STATE_STORAGE_KEY = 'bssapp:production-product-costing:no-cost-list-state:v1'
|
||||||
|
|
||||||
|
function persistListState () {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(LIST_STATE_STORAGE_KEY, JSON.stringify({
|
||||||
|
filters: {
|
||||||
|
search: String(filters.search || ''),
|
||||||
|
fromDate: String(filters.fromDate || '')
|
||||||
|
},
|
||||||
|
columnFilters: JSON.parse(JSON.stringify(columnFilters)),
|
||||||
|
scrollY: Number(window.scrollY || 0)
|
||||||
|
}))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreListState () {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem(LIST_STATE_STORAGE_KEY)
|
||||||
|
if (!raw) return 0
|
||||||
|
const saved = JSON.parse(raw)
|
||||||
|
filters.search = String(saved?.filters?.search || '')
|
||||||
|
filters.fromDate = String(saved?.filters?.fromDate || '2025-06-01')
|
||||||
|
Object.assign(columnFilters, saved?.columnFilters && typeof saved.columnFilters === 'object' ? saved.columnFilters : {})
|
||||||
|
return Number(saved?.scrollY || 0)
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getColumnFilter (name) {
|
function getColumnFilter (name) {
|
||||||
if (!columnFilters[name]) {
|
if (!columnFilters[name]) {
|
||||||
@@ -276,7 +315,11 @@ function getColumnFilter (name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isNumericColumn (name) {
|
function isNumericColumn (name) {
|
||||||
return name === 'lMMiktar_G'
|
return ['lMMiktar_G', 'ceketDepoGiris', 'pantolonDepoGiris', 'yelekDepoGiris'].includes(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatQuantity (value) {
|
||||||
|
return Number(value || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseNumberFilter (value) {
|
function parseNumberFilter (value) {
|
||||||
@@ -384,9 +427,11 @@ function clearAllColumnFilters () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let searchTimer = null
|
let searchTimer = null
|
||||||
|
let filterWatchEnabled = false
|
||||||
watch(
|
watch(
|
||||||
() => filters.search,
|
() => filters.search,
|
||||||
() => {
|
() => {
|
||||||
|
if (!filterWatchEnabled) return
|
||||||
clearTimeout(searchTimer)
|
clearTimeout(searchTimer)
|
||||||
searchTimer = setTimeout(() => {
|
searchTimer = setTimeout(() => {
|
||||||
fetchRows()
|
fetchRows()
|
||||||
@@ -397,6 +442,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => filters.fromDate,
|
() => filters.fromDate,
|
||||||
() => {
|
() => {
|
||||||
|
if (!filterWatchEnabled) return
|
||||||
fetchRows()
|
fetchRows()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -430,6 +476,49 @@ function clearFilters () {
|
|||||||
fetchRows()
|
fetchRows()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeExcelCsvCell (value) {
|
||||||
|
let text = String(value ?? '')
|
||||||
|
// Prevent spreadsheet applications from evaluating data as a formula.
|
||||||
|
if (/^[=+\-@]/.test(text)) text = `'${text}`
|
||||||
|
return `"${text.replace(/"/g, '""')}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatExcelCsvCell (col, rawValue, displayValue) {
|
||||||
|
if (isNumericColumn(col?.name)) {
|
||||||
|
const numericValue = Number(rawValue)
|
||||||
|
if (Number.isFinite(numericValue)) return String(numericValue).replace('.', ',')
|
||||||
|
}
|
||||||
|
return escapeExcelCsvCell(displayValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportVisibleRows () {
|
||||||
|
const visibleRows = Array.isArray(rows.value) ? rows.value : []
|
||||||
|
if (visibleRows.length === 0) {
|
||||||
|
$q.notify({ type: 'warning', message: 'Excel aktarimi icin ekranda satir bulunamadi.', position: 'top-right' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportColumns = columns.filter(col => col.name !== 'open')
|
||||||
|
const csvLines = [
|
||||||
|
exportColumns.map(col => escapeExcelCsvCell(col.label)).join(';'),
|
||||||
|
...visibleRows.map(row => exportColumns.map(col => {
|
||||||
|
const rawValue = typeof col.field === 'function' ? col.field(row) : row?.[col.field]
|
||||||
|
const displayValue = typeof col.format === 'function' ? col.format(rawValue, row) : rawValue
|
||||||
|
return formatExcelCsvCell(col, rawValue, displayValue)
|
||||||
|
}).join(';'))
|
||||||
|
]
|
||||||
|
|
||||||
|
const blob = new Blob([`\uFEFF${csvLines.join('\r\n')}`], { type: 'text/csv;charset=utf-8;' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `maliyeti_olmayan_urunler_${new Date().toISOString().slice(0, 10)}.csv`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
function openRow (row) {
|
function openRow (row) {
|
||||||
const productCode = String(row?.sMModelKodu || '').trim()
|
const productCode = String(row?.sMModelKodu || '').trim()
|
||||||
const recipeCode = String(row?.sKodu || '').trim()
|
const recipeCode = String(row?.sKodu || '').trim()
|
||||||
@@ -449,6 +538,8 @@ function openRow (row) {
|
|||||||
recipe_code: recipeCode
|
recipe_code: recipeCode
|
||||||
})
|
})
|
||||||
|
|
||||||
|
persistListState()
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
name: 'production-product-costing-has-cost-detail',
|
name: 'production-product-costing-has-cost-detail',
|
||||||
query: {
|
query: {
|
||||||
@@ -460,9 +551,16 @@ function openRow (row) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
if (!canReadOrder.value) return
|
if (!canReadOrder.value) return
|
||||||
fetchRows()
|
const savedScrollY = restoreListState()
|
||||||
|
await nextTick()
|
||||||
|
filterWatchEnabled = true
|
||||||
|
await fetchRows()
|
||||||
|
if (savedScrollY > 0) {
|
||||||
|
await nextTick()
|
||||||
|
window.scrollTo({ top: savedScrollY, behavior: 'auto' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user