Fix product performance grouped JSON build
This commit is contained in:
@@ -999,6 +999,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"pricing", "view",
|
"pricing", "view",
|
||||||
wrapV3(routes.GetProductPerformanceGroupedHandler(pgDB)),
|
wrapV3(routes.GetProductPerformanceGroupedHandler(pgDB)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/pricing/product-performance/grouped/filter-options", "POST",
|
||||||
|
"pricing", "view",
|
||||||
|
wrapV3(routes.GetProductPerformanceGroupedFilterOptionsHandler(pgDB)),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/pricing/product-performance/sales-details", "GET",
|
"/api/pricing/product-performance/sales-details", "GET",
|
||||||
"pricing", "view",
|
"pricing", "view",
|
||||||
|
|||||||
@@ -308,82 +308,97 @@ type ProductPerformanceCustomerRow struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProductPerformanceSalesBreakdownRow struct {
|
type ProductPerformanceSalesBreakdownRow struct {
|
||||||
Breakdown string `json:"breakdown"`
|
Breakdown string `json:"breakdown"`
|
||||||
ProductCode string `json:"product_code"`
|
ProductCode string `json:"product_code"`
|
||||||
ColorCode string `json:"color_code"`
|
ColorCode string `json:"color_code"`
|
||||||
ColorDescription string `json:"color_description"`
|
ColorDescription string `json:"color_description"`
|
||||||
YakaKodu string `json:"yaka_kodu"`
|
YakaKodu string `json:"yaka_kodu"`
|
||||||
ItemDescription string `json:"item_description"`
|
ItemDescription string `json:"item_description"`
|
||||||
Kategori string `json:"kategori"`
|
Kategori string `json:"kategori"`
|
||||||
AskiliYan string `json:"askili_yan"`
|
AskiliYan string `json:"askili_yan"`
|
||||||
UrunIlkGrubu string `json:"urun_ilk_grubu"`
|
UrunIlkGrubu string `json:"urun_ilk_grubu"`
|
||||||
UrunAnaGrubu string `json:"urun_ana_grubu"`
|
UrunAnaGrubu string `json:"urun_ana_grubu"`
|
||||||
UrunAltGrubu string `json:"urun_alt_grubu"`
|
UrunAltGrubu string `json:"urun_alt_grubu"`
|
||||||
MarketKey string `json:"market_key"`
|
MarketKey string `json:"market_key"`
|
||||||
Country string `json:"country"`
|
Country string `json:"country"`
|
||||||
CustomerSegment string `json:"customer_segment"`
|
CustomerSegment string `json:"customer_segment"`
|
||||||
CustomerCode string `json:"customer_code"`
|
CustomerCode string `json:"customer_code"`
|
||||||
CustomerName string `json:"customer_name"`
|
CustomerName string `json:"customer_name"`
|
||||||
ProductCount int `json:"product_count"`
|
ProductCount int `json:"product_count"`
|
||||||
CustomerCount90 int `json:"customer_count_90d"`
|
ProductGroupCount90 int `json:"product_group_count_90d"`
|
||||||
InvoiceCount90 int `json:"invoice_count_90d"`
|
ProductGroupCount180 int `json:"product_group_count_180d"`
|
||||||
SalesQty90 float64 `json:"sales_qty_90d"`
|
ProductGroupCount365 int `json:"product_group_count_365d"`
|
||||||
SalesUSD90 float64 `json:"sales_usd_90d"`
|
ProductGroupCountTotal int `json:"product_group_count_total"`
|
||||||
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
|
MarketCount90 int `json:"market_count_90d"`
|
||||||
BasePriceUSD90 float64 `json:"base_price_usd_90d"`
|
MarketCount180 int `json:"market_count_180d"`
|
||||||
CostPriceUSD90 float64 `json:"cost_price_usd_90d"`
|
MarketCount365 int `json:"market_count_365d"`
|
||||||
GrossProfitBase90 float64 `json:"gross_profit_base_usd_90d"`
|
MarketCountTotal int `json:"market_count_total"`
|
||||||
GrossProfitCost90 float64 `json:"gross_profit_cost_usd_90d"`
|
CustomerCount90 int `json:"customer_count_90d"`
|
||||||
GrossMarginBase90 float64 `json:"gross_margin_base_90d"`
|
InvoiceCount90 int `json:"invoice_count_90d"`
|
||||||
GrossMarginCost90 float64 `json:"gross_margin_cost_90d"`
|
SalesQty90 float64 `json:"sales_qty_90d"`
|
||||||
CustomerCount180 int `json:"customer_count_180d"`
|
SalesUSD90 float64 `json:"sales_usd_90d"`
|
||||||
InvoiceCount180 int `json:"invoice_count_180d"`
|
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
|
||||||
SalesQty180 float64 `json:"sales_qty_180d"`
|
BasePriceUSD90 float64 `json:"base_price_usd_90d"`
|
||||||
SalesUSD180 float64 `json:"sales_usd_180d"`
|
CostPriceUSD90 float64 `json:"cost_price_usd_90d"`
|
||||||
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
|
GrossProfitBase90 float64 `json:"gross_profit_base_usd_90d"`
|
||||||
BasePriceUSD180 float64 `json:"base_price_usd_180d"`
|
GrossProfitCost90 float64 `json:"gross_profit_cost_usd_90d"`
|
||||||
CostPriceUSD180 float64 `json:"cost_price_usd_180d"`
|
GrossMarginBase90 float64 `json:"gross_margin_base_90d"`
|
||||||
GrossProfitBase180 float64 `json:"gross_profit_base_usd_180d"`
|
GrossMarginCost90 float64 `json:"gross_margin_cost_90d"`
|
||||||
GrossProfitCost180 float64 `json:"gross_profit_cost_usd_180d"`
|
CustomerCount180 int `json:"customer_count_180d"`
|
||||||
GrossMarginBase180 float64 `json:"gross_margin_base_180d"`
|
InvoiceCount180 int `json:"invoice_count_180d"`
|
||||||
GrossMarginCost180 float64 `json:"gross_margin_cost_180d"`
|
SalesQty180 float64 `json:"sales_qty_180d"`
|
||||||
SalesQty365 float64 `json:"sales_qty_365d"`
|
SalesUSD180 float64 `json:"sales_usd_180d"`
|
||||||
SalesUSD365 float64 `json:"sales_usd_365d"`
|
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
|
||||||
AvgPriceUSD365 float64 `json:"avg_price_usd_365d"`
|
BasePriceUSD180 float64 `json:"base_price_usd_180d"`
|
||||||
BasePriceUSD365 float64 `json:"base_price_usd_365d"`
|
CostPriceUSD180 float64 `json:"cost_price_usd_180d"`
|
||||||
CostPriceUSD365 float64 `json:"cost_price_usd_365d"`
|
GrossProfitBase180 float64 `json:"gross_profit_base_usd_180d"`
|
||||||
GrossProfitBase365 float64 `json:"gross_profit_base_usd_365d"`
|
GrossProfitCost180 float64 `json:"gross_profit_cost_usd_180d"`
|
||||||
GrossProfitCost365 float64 `json:"gross_profit_cost_usd_365d"`
|
GrossMarginBase180 float64 `json:"gross_margin_base_180d"`
|
||||||
GrossMarginBase365 float64 `json:"gross_margin_base_365d"`
|
GrossMarginCost180 float64 `json:"gross_margin_cost_180d"`
|
||||||
GrossMarginCost365 float64 `json:"gross_margin_cost_365d"`
|
SalesQty365 float64 `json:"sales_qty_365d"`
|
||||||
CustomerCount365 int `json:"customer_count_365d"`
|
SalesUSD365 float64 `json:"sales_usd_365d"`
|
||||||
InvoiceCount365 int `json:"invoice_count_365d"`
|
AvgPriceUSD365 float64 `json:"avg_price_usd_365d"`
|
||||||
CustomerCountTotal int `json:"customer_count_total"`
|
BasePriceUSD365 float64 `json:"base_price_usd_365d"`
|
||||||
InvoiceCountTotal int `json:"invoice_count_total"`
|
CostPriceUSD365 float64 `json:"cost_price_usd_365d"`
|
||||||
SalesQtyTotal float64 `json:"sales_qty_total"`
|
GrossProfitBase365 float64 `json:"gross_profit_base_usd_365d"`
|
||||||
SalesUSDTotal float64 `json:"sales_usd_total"`
|
GrossProfitCost365 float64 `json:"gross_profit_cost_usd_365d"`
|
||||||
AvgPriceUSDTotal float64 `json:"avg_price_usd_total"`
|
GrossMarginBase365 float64 `json:"gross_margin_base_365d"`
|
||||||
BasePriceUSDTotal float64 `json:"base_price_usd_total"`
|
GrossMarginCost365 float64 `json:"gross_margin_cost_365d"`
|
||||||
CostPriceUSDTotal float64 `json:"cost_price_usd_total"`
|
CustomerCount365 int `json:"customer_count_365d"`
|
||||||
GrossProfitBaseTotal float64 `json:"gross_profit_base_usd_total"`
|
InvoiceCount365 int `json:"invoice_count_365d"`
|
||||||
GrossProfitCostTotal float64 `json:"gross_profit_cost_usd_total"`
|
CustomerCountTotal int `json:"customer_count_total"`
|
||||||
GrossMarginBaseTotal float64 `json:"gross_margin_base_total"`
|
InvoiceCountTotal int `json:"invoice_count_total"`
|
||||||
GrossMarginCostTotal float64 `json:"gross_margin_cost_total"`
|
SalesQtyTotal float64 `json:"sales_qty_total"`
|
||||||
StockQty float64 `json:"stock_qty"`
|
SalesUSDTotal float64 `json:"sales_usd_total"`
|
||||||
StockTurnover90 float64 `json:"stock_turnover_90d"`
|
AvgPriceUSDTotal float64 `json:"avg_price_usd_total"`
|
||||||
StockTurnover180 float64 `json:"stock_turnover_180d"`
|
BasePriceUSDTotal float64 `json:"base_price_usd_total"`
|
||||||
StockTurnover365 float64 `json:"stock_turnover_365d"`
|
CostPriceUSDTotal float64 `json:"cost_price_usd_total"`
|
||||||
StockTurnoverTotal float64 `json:"stock_turnover_total"`
|
GrossProfitBaseTotal float64 `json:"gross_profit_base_usd_total"`
|
||||||
HasCost bool `json:"has_cost"`
|
GrossProfitCostTotal float64 `json:"gross_profit_cost_usd_total"`
|
||||||
SalesIndex90 float64 `json:"sales_index_90d"`
|
GrossMarginBaseTotal float64 `json:"gross_margin_base_total"`
|
||||||
CustomerScore90 float64 `json:"customer_score_90d"`
|
GrossMarginCostTotal float64 `json:"gross_margin_cost_total"`
|
||||||
CustomerScore180 float64 `json:"customer_score_180d"`
|
StockQty float64 `json:"stock_qty"`
|
||||||
CustomerScore365 float64 `json:"customer_score_365d"`
|
StockTurnover90 float64 `json:"stock_turnover_90d"`
|
||||||
CustomerScoreTotal float64 `json:"customer_score_total"`
|
StockTurnover180 float64 `json:"stock_turnover_180d"`
|
||||||
PerformanceScore float64 `json:"performance_score"`
|
StockTurnover365 float64 `json:"stock_turnover_365d"`
|
||||||
PerformanceBucket string `json:"performance_bucket"`
|
StockTurnoverTotal float64 `json:"stock_turnover_total"`
|
||||||
Recommendation string `json:"recommendation"`
|
HasCost bool `json:"has_cost"`
|
||||||
LastSaleDate string `json:"last_sale_date"`
|
SalesIndex90 float64 `json:"sales_index_90d"`
|
||||||
|
SalesIndex180 float64 `json:"sales_index_180d"`
|
||||||
|
SalesIndex365 float64 `json:"sales_index_365d"`
|
||||||
|
SalesIndexTotal float64 `json:"sales_index_total"`
|
||||||
|
CustomerScore90 float64 `json:"customer_score_90d"`
|
||||||
|
CustomerScore180 float64 `json:"customer_score_180d"`
|
||||||
|
CustomerScore365 float64 `json:"customer_score_365d"`
|
||||||
|
CustomerScoreTotal float64 `json:"customer_score_total"`
|
||||||
|
PerformanceScore90 float64 `json:"performance_score_90d"`
|
||||||
|
PerformanceScore180 float64 `json:"performance_score_180d"`
|
||||||
|
PerformanceScore365 float64 `json:"performance_score_365d"`
|
||||||
|
PerformanceScoreTotal float64 `json:"performance_score_total"`
|
||||||
|
PerformanceScore float64 `json:"performance_score"`
|
||||||
|
PerformanceBucket string `json:"performance_bucket"`
|
||||||
|
Recommendation string `json:"recommendation"`
|
||||||
|
LastSaleDate string `json:"last_sale_date"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductPerformanceSalesDetailRow struct {
|
type ProductPerformanceSalesDetailRow struct {
|
||||||
|
|||||||
+735
-108
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -372,10 +373,10 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_sales_daily (
|
|||||||
UPDATE mk_product_performance_sales_daily
|
UPDATE mk_product_performance_sales_daily
|
||||||
SET urun_ilk_grubu = '', updated_at = now()
|
SET urun_ilk_grubu = '', updated_at = now()
|
||||||
WHERE btrim(urun_ilk_grubu) = '-'
|
WHERE btrim(urun_ilk_grubu) = '-'
|
||||||
OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
|
OR upper(translate(btrim(urun_ilk_grubu), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
|
||||||
`
|
`
|
||||||
DELETE FROM mk_product_performance_sales_daily
|
DELETE FROM mk_product_performance_sales_daily
|
||||||
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')`,
|
WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')`,
|
||||||
`
|
`
|
||||||
UPDATE mk_product_performance_sales_daily
|
UPDATE mk_product_performance_sales_daily
|
||||||
SET askili_yan = '', updated_at = now()
|
SET askili_yan = '', updated_at = now()
|
||||||
@@ -452,7 +453,7 @@ WHERE cost_price_usd > 0
|
|||||||
AND cost_price_usd < base_price_usd`,
|
AND cost_price_usd < base_price_usd`,
|
||||||
`
|
`
|
||||||
DELETE FROM mk_product_performance_price_dim
|
DELETE FROM mk_product_performance_price_dim
|
||||||
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')`,
|
WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')`,
|
||||||
`
|
`
|
||||||
CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
||||||
kpi_date DATE NOT NULL,
|
kpi_date DATE NOT NULL,
|
||||||
@@ -519,10 +520,10 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
|||||||
UPDATE mk_product_performance_kpi_daily
|
UPDATE mk_product_performance_kpi_daily
|
||||||
SET urun_ilk_grubu = '', updated_at = now()
|
SET urun_ilk_grubu = '', updated_at = now()
|
||||||
WHERE btrim(urun_ilk_grubu) = '-'
|
WHERE btrim(urun_ilk_grubu) = '-'
|
||||||
OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
|
OR upper(translate(btrim(urun_ilk_grubu), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
|
||||||
`
|
`
|
||||||
DELETE FROM mk_product_performance_kpi_daily
|
DELETE FROM mk_product_performance_kpi_daily
|
||||||
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')`,
|
WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')`,
|
||||||
`
|
`
|
||||||
UPDATE mk_product_performance_kpi_daily
|
UPDATE mk_product_performance_kpi_daily
|
||||||
SET
|
SET
|
||||||
@@ -602,6 +603,7 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_grouped_snapshot (
|
|||||||
)`,
|
)`,
|
||||||
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_key ON mk_product_performance_grouped_snapshot (report_key, row_order)`,
|
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_key ON mk_product_performance_grouped_snapshot (report_key, row_order)`,
|
||||||
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_level ON mk_product_performance_grouped_snapshot (report_key, group_level, row_order)`,
|
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_level ON mk_product_performance_grouped_snapshot (report_key, group_level, row_order)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_field ON mk_product_performance_grouped_snapshot (report_key, group_field, group_value)`,
|
||||||
`
|
`
|
||||||
CREATE TABLE IF NOT EXISTS mk_product_performance_grouped_snapshot_meta (
|
CREATE TABLE IF NOT EXISTS mk_product_performance_grouped_snapshot_meta (
|
||||||
report_key TEXT PRIMARY KEY,
|
report_key TEXT PRIMARY KEY,
|
||||||
@@ -1754,6 +1756,7 @@ func ListProductPerformance(ctx context.Context, pg *sql.DB, f ProductPerformanc
|
|||||||
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceRow](ctx, pg, productPerformanceSnapshotKey("products"), limit); err != nil {
|
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceRow](ctx, pg, productPerformanceSnapshotKey("products"), limit); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
} else if ok {
|
} else if ok {
|
||||||
|
applyProductPerformanceProductRowScores(rows)
|
||||||
return rows, len(rows), nil
|
return rows, len(rows), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1819,9 +1822,44 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
|||||||
for i := range out {
|
for i := range out {
|
||||||
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
|
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
|
||||||
}
|
}
|
||||||
|
applyProductPerformanceProductRowScores(out)
|
||||||
return out, total, nil
|
return out, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyProductPerformanceProductRowScores(rows []models.ProductPerformanceRow) {
|
||||||
|
avg := productPerformanceProductRowAverages(rows)
|
||||||
|
for i := range rows {
|
||||||
|
rows[i].PerformanceScore = productPerformanceProductScore(
|
||||||
|
"90d",
|
||||||
|
rows[i].SalesUSD90,
|
||||||
|
productPerformanceRelativeIndex(rows[i].SalesUSD90, avg.salesUSD90),
|
||||||
|
rows[i].GrossMargin90,
|
||||||
|
rows[i].StockTurnover90,
|
||||||
|
float64(rows[i].MarketCount90),
|
||||||
|
float64(rows[i].CustomerCount90),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type productPerformanceProductRowAverage struct {
|
||||||
|
salesUSD90 float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceProductRowAverages(rows []models.ProductPerformanceRow) productPerformanceProductRowAverage {
|
||||||
|
var sum90, count90 float64
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.SalesUSD90 > 0 {
|
||||||
|
sum90 += row.SalesUSD90
|
||||||
|
count90++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := productPerformanceProductRowAverage{}
|
||||||
|
if count90 > 0 {
|
||||||
|
out.salesUSD90 = sum90 / count90
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -1841,7 +1879,7 @@ KPI AS (
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
),
|
),
|
||||||
VariantStock AS (
|
VariantStock AS (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -1868,7 +1906,7 @@ Sales90 AS (
|
|||||||
COUNT(DISTINCT NULLIF(customer_code, '-')) AS customer_count_90d
|
COUNT(DISTINCT NULLIF(customer_code, '-')) AS customer_count_90d
|
||||||
FROM mk_product_performance_sales_daily
|
FROM mk_product_performance_sales_daily
|
||||||
WHERE sales_date BETWEEN (SELECT kpi_date FROM Latest) - INTERVAL '89 days' AND (SELECT kpi_date FROM Latest)
|
WHERE sales_date BETWEEN (SELECT kpi_date FROM Latest) - INTERVAL '89 days' AND (SELECT kpi_date FROM Latest)
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(to_char(MAX(kpi_date),'YYYY-MM-DD'),''),
|
COALESCE(to_char(MAX(kpi_date),'YYYY-MM-DD'),''),
|
||||||
@@ -1904,6 +1942,7 @@ func ListProductPerformanceGeneral(ctx context.Context, pg *sql.DB, limit int) (
|
|||||||
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceGeneralRow](ctx, pg, productPerformanceSnapshotKey("general"), limit); err != nil {
|
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceGeneralRow](ctx, pg, productPerformanceSnapshotKey("general"), limit); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if ok {
|
} else if ok {
|
||||||
|
applyProductPerformanceGeneralRowScores(rows)
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
rows, err := pg.QueryContext(ctx, `
|
rows, err := pg.QueryContext(ctx, `
|
||||||
@@ -1943,7 +1982,7 @@ SalesAgg AS (
|
|||||||
COALESCE(SUM(invoice_count),0)::integer AS invoice_count_total
|
COALESCE(SUM(invoice_count),0)::integer AS invoice_count_total
|
||||||
FROM mk_product_performance_sales_daily, Bounds
|
FROM mk_product_performance_sales_daily, Bounds
|
||||||
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
GROUP BY product_code, color_code, yaka_kodu, market_key
|
GROUP BY product_code, color_code, yaka_kodu, market_key
|
||||||
),
|
),
|
||||||
StockAgg AS (
|
StockAgg AS (
|
||||||
@@ -1995,7 +2034,7 @@ Dim AS (
|
|||||||
1 AS src_rank
|
1 AS src_rank
|
||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
product_code,
|
product_code,
|
||||||
@@ -2011,7 +2050,7 @@ Dim AS (
|
|||||||
MAX(urun_alt_grubu) AS urun_alt_grubu,
|
MAX(urun_alt_grubu) AS urun_alt_grubu,
|
||||||
2 AS src_rank
|
2 AS src_rank
|
||||||
FROM mk_product_performance_sales_daily
|
FROM mk_product_performance_sales_daily
|
||||||
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
||||||
GROUP BY product_code, color_code, yaka_kodu
|
GROUP BY product_code, color_code, yaka_kodu
|
||||||
) x
|
) x
|
||||||
ORDER BY product_code, color_code, yaka_kodu, src_rank
|
ORDER BY product_code, color_code, yaka_kodu, src_rank
|
||||||
@@ -2043,7 +2082,7 @@ StockOnly AS (
|
|||||||
AND d.color_code = s.color_code
|
AND d.color_code = s.color_code
|
||||||
AND d.yaka_kodu = s.yaka_kodu
|
AND d.yaka_kodu = s.yaka_kodu
|
||||||
WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
|
WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
|
||||||
AND upper(btrim(COALESCE(d.urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
AND upper(translate(btrim(COALESCE(d.urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM SalesAgg a
|
FROM SalesAgg a
|
||||||
@@ -2067,7 +2106,7 @@ Spread AS (
|
|||||||
COUNT(DISTINCT NULLIF(customer_code, '-'))::integer AS customer_count_total_all
|
COUNT(DISTINCT NULLIF(customer_code, '-'))::integer AS customer_count_total_all
|
||||||
FROM mk_product_performance_sales_daily, Bounds
|
FROM mk_product_performance_sales_daily, Bounds
|
||||||
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
GROUP BY product_code, color_code, yaka_kodu
|
GROUP BY product_code, color_code, yaka_kodu
|
||||||
),
|
),
|
||||||
Scored AS (
|
Scored AS (
|
||||||
@@ -2199,9 +2238,28 @@ LIMIT $1
|
|||||||
for i := range out {
|
for i := range out {
|
||||||
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
|
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
|
||||||
}
|
}
|
||||||
|
applyProductPerformanceGeneralRowScores(out)
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyProductPerformanceGeneralRowScores(rows []models.ProductPerformanceGeneralRow) {
|
||||||
|
for i := range rows {
|
||||||
|
turnover := 0.0
|
||||||
|
if rows[i].StockQty > 0 {
|
||||||
|
turnover = rows[i].SalesQtyTotal / rows[i].StockQty
|
||||||
|
}
|
||||||
|
rows[i].PerformanceScore = productPerformanceProductScore(
|
||||||
|
"total",
|
||||||
|
rows[i].SalesUSDTotal,
|
||||||
|
rows[i].SalesIndexTotal,
|
||||||
|
rows[i].GrossMarginTotal,
|
||||||
|
turnover,
|
||||||
|
float64(rows[i].MarketCountTotal),
|
||||||
|
float64(rows[i].CustomerCountTotal),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderAnalysisRow, error) {
|
func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderAnalysisRow, error) {
|
||||||
if db.MssqlDB == nil {
|
if db.MssqlDB == nil {
|
||||||
return nil, fmt.Errorf("mssql db nil")
|
return nil, fmt.Errorf("mssql db nil")
|
||||||
@@ -3061,7 +3119,7 @@ SELECT
|
|||||||
COALESCE(AVG(NULLIF(stock_days_90d,0)),0) AS avg_stock_days_90d
|
COALESCE(AVG(NULLIF(stock_days_90d,0)),0) AS avg_stock_days_90d
|
||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
GROUP BY market_key
|
GROUP BY market_key
|
||||||
ORDER BY risk_stock_cost_value_usd DESC, stock_cost_value_usd DESC, sales_usd_90d DESC
|
ORDER BY risk_stock_cost_value_usd DESC, stock_cost_value_usd DESC, sales_usd_90d DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
@@ -3122,7 +3180,7 @@ SELECT
|
|||||||
COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_usd_365d
|
COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_usd_365d
|
||||||
FROM mk_product_performance_sales_daily
|
FROM mk_product_performance_sales_daily
|
||||||
WHERE sales_date >= current_date - INTERVAL '364 days'
|
WHERE sales_date >= current_date - INTERVAL '364 days'
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
GROUP BY customer_country, customer_segment, market_key
|
GROUP BY customer_country, customer_segment, market_key
|
||||||
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
@@ -3207,7 +3265,7 @@ SELECT
|
|||||||
FROM mk_product_performance_sales_daily
|
FROM mk_product_performance_sales_daily
|
||||||
WHERE sales_date >= current_date - INTERVAL '364 days'
|
WHERE sales_date >= current_date - INTERVAL '364 days'
|
||||||
AND COALESCE(customer_code,'') <> ''
|
AND COALESCE(customer_code,'') <> ''
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
GROUP BY %s
|
GROUP BY %s
|
||||||
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
@@ -3250,7 +3308,7 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
|
|||||||
"item_description": "MAX(s.item_description)",
|
"item_description": "MAX(s.item_description)",
|
||||||
"kategori": "COALESCE(MAX(s.kategori),'')",
|
"kategori": "COALESCE(MAX(s.kategori),'')",
|
||||||
"askili_yan": "COALESCE(MAX(CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END),'')",
|
"askili_yan": "COALESCE(MAX(CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END),'')",
|
||||||
"urun_ilk_grubu": "COALESCE(MAX(CASE WHEN btrim(COALESCE(s.urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE s.urun_ilk_grubu END),'')",
|
"urun_ilk_grubu": "COALESCE(MAX(" + productPerformanceCleanFirstGroupSQL("s.urun_ilk_grubu") + "),'')",
|
||||||
"urun_ana_grubu": "COALESCE(MAX(s.urun_ana_grubu),'')",
|
"urun_ana_grubu": "COALESCE(MAX(s.urun_ana_grubu),'')",
|
||||||
"urun_alt_grubu": "MAX(s.urun_alt_grubu)",
|
"urun_alt_grubu": "MAX(s.urun_alt_grubu)",
|
||||||
"market_key": "s.market_key",
|
"market_key": "s.market_key",
|
||||||
@@ -3277,7 +3335,7 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
|
|||||||
selected["market_key"] = true
|
selected["market_key"] = true
|
||||||
selected["customer_code"] = true
|
selected["customer_code"] = true
|
||||||
selected["customer_name"] = true
|
selected["customer_name"] = true
|
||||||
groupCols = []string{"CASE WHEN btrim(COALESCE(s.urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE s.urun_ilk_grubu END", "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"}
|
groupCols = []string{productPerformanceCleanFirstGroupSQL("s.urun_ilk_grubu"), "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"}
|
||||||
case "product_country_segment_market_customer":
|
case "product_country_segment_market_customer":
|
||||||
selected["product_code"] = true
|
selected["product_code"] = true
|
||||||
selected["item_description"] = true
|
selected["item_description"] = true
|
||||||
@@ -3340,12 +3398,13 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
|
|||||||
selected["market_key"] = true
|
selected["market_key"] = true
|
||||||
selected["customer_code"] = true
|
selected["customer_code"] = true
|
||||||
selected["customer_name"] = true
|
selected["customer_name"] = true
|
||||||
groupCols = []string{"CASE WHEN btrim(COALESCE(s.urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE s.urun_ilk_grubu END", "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"}
|
groupCols = []string{productPerformanceCleanFirstGroupSQL("s.urun_ilk_grubu"), "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"}
|
||||||
}
|
}
|
||||||
|
|
||||||
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceSalesBreakdownRow](ctx, pg, productPerformanceSnapshotKey("sales-breakdown", mode), limit); err != nil {
|
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceSalesBreakdownRow](ctx, pg, productPerformanceSnapshotKey("sales-breakdown", mode), limit); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if ok {
|
} else if ok {
|
||||||
|
applyProductPerformanceSalesBreakdownScores(rows)
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3366,14 +3425,14 @@ WITH LatestKPI AS (
|
|||||||
CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END AS askili_yan,
|
CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END AS askili_yan,
|
||||||
CASE
|
CASE
|
||||||
WHEN btrim(COALESCE(urun_ilk_grubu,'')) = '-' THEN ''
|
WHEN btrim(COALESCE(urun_ilk_grubu,'')) = '-' THEN ''
|
||||||
WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN ''
|
WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN ''
|
||||||
ELSE COALESCE(urun_ilk_grubu,'')
|
ELSE COALESCE(urun_ilk_grubu,'')
|
||||||
END AS urun_ilk_grubu,
|
END AS urun_ilk_grubu,
|
||||||
COALESCE(urun_ana_grubu,'') AS urun_ana_grubu,
|
COALESCE(urun_ana_grubu,'') AS urun_ana_grubu,
|
||||||
urun_alt_grubu
|
urun_alt_grubu
|
||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
|
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
|
||||||
),
|
),
|
||||||
StockAgg AS (
|
StockAgg AS (
|
||||||
@@ -3444,6 +3503,14 @@ Agg AS (
|
|||||||
%s,
|
%s,
|
||||||
%s,
|
%s,
|
||||||
COUNT(DISTINCT product_code) AS product_count,
|
COUNT(DISTINCT product_code) AS product_count,
|
||||||
|
COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days')::integer AS product_group_count_90d,
|
||||||
|
COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days')::integer AS product_group_count_180d,
|
||||||
|
COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days')::integer AS product_group_count_365d,
|
||||||
|
COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= DATE '2022-01-01')::integer AS product_group_count_total,
|
||||||
|
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days')::integer AS market_count_90d,
|
||||||
|
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days')::integer AS market_count_180d,
|
||||||
|
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days')::integer AS market_count_365d,
|
||||||
|
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= DATE '2022-01-01')::integer AS market_count_total,
|
||||||
COALESCE(MAX(stock_qty),0) AS stock_qty,
|
COALESCE(MAX(stock_qty),0) AS stock_qty,
|
||||||
COALESCE(MAX(avg_stock_90d),0) AS avg_stock_90d,
|
COALESCE(MAX(avg_stock_90d),0) AS avg_stock_90d,
|
||||||
COALESCE(MAX(avg_stock_180d),0) AS avg_stock_180d,
|
COALESCE(MAX(avg_stock_180d),0) AS avg_stock_180d,
|
||||||
@@ -3534,7 +3601,7 @@ Agg AS (
|
|||||||
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 StockTotalStart stotal ON stotal.product_code = s.product_code AND stotal.color_code = s.color_code AND stotal.yaka_kodu = s.yaka_kodu
|
||||||
) s
|
) s
|
||||||
WHERE sales_date >= DATE '2022-01-01'
|
WHERE sales_date >= DATE '2022-01-01'
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
GROUP BY %s
|
GROUP BY %s
|
||||||
),
|
),
|
||||||
Enriched AS (
|
Enriched AS (
|
||||||
@@ -3584,6 +3651,14 @@ SELECT
|
|||||||
customer_code,
|
customer_code,
|
||||||
customer_name,
|
customer_name,
|
||||||
product_count,
|
product_count,
|
||||||
|
product_group_count_90d,
|
||||||
|
product_group_count_180d,
|
||||||
|
product_group_count_365d,
|
||||||
|
product_group_count_total,
|
||||||
|
market_count_90d,
|
||||||
|
market_count_180d,
|
||||||
|
market_count_365d,
|
||||||
|
market_count_total,
|
||||||
stock_qty,
|
stock_qty,
|
||||||
customer_count_90d,
|
customer_count_90d,
|
||||||
invoice_count_90d,
|
invoice_count_90d,
|
||||||
@@ -3690,8 +3765,8 @@ SELECT
|
|||||||
ELSE 'TAKIP'
|
ELSE 'TAKIP'
|
||||||
END AS performance_bucket,
|
END AS performance_bucket,
|
||||||
CASE
|
CASE
|
||||||
WHEN sales_index_90d >= 1.25 AND customer_count_90d >= 2 THEN 'Güçlü satış kırılımı'
|
WHEN sales_index_90d >= 1.25 AND customer_count_90d >= 2 THEN 'Guclu satis kirilimi'
|
||||||
WHEN sales_qty_90d = 0 AND sales_qty_365d > 0 THEN 'Son 90 gün zayıf, kontrol et'
|
WHEN sales_qty_90d = 0 AND sales_qty_365d > 0 THEN 'Son 90 gun zayif, kontrol et'
|
||||||
ELSE 'Takip'
|
ELSE 'Takip'
|
||||||
END AS recommendation,
|
END AS recommendation,
|
||||||
last_sale_date
|
last_sale_date
|
||||||
@@ -3729,7 +3804,9 @@ LIMIT $1
|
|||||||
&r.Breakdown, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription,
|
&r.Breakdown, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription,
|
||||||
&r.Kategori, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu,
|
&r.Kategori, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu,
|
||||||
&r.MarketKey, &r.Country, &r.CustomerSegment, &r.CustomerCode, &r.CustomerName,
|
&r.MarketKey, &r.Country, &r.CustomerSegment, &r.CustomerCode, &r.CustomerName,
|
||||||
&r.ProductCount, &r.StockQty, &r.CustomerCount90, &r.InvoiceCount90, &r.SalesQty90, &r.SalesUSD90,
|
&r.ProductCount, &r.ProductGroupCount90, &r.ProductGroupCount180, &r.ProductGroupCount365, &r.ProductGroupCountTotal,
|
||||||
|
&r.MarketCount90, &r.MarketCount180, &r.MarketCount365, &r.MarketCountTotal,
|
||||||
|
&r.StockQty, &r.CustomerCount90, &r.InvoiceCount90, &r.SalesQty90, &r.SalesUSD90,
|
||||||
&r.AvgPriceUSD90, &r.BasePriceUSD90, &r.CostPriceUSD90, &r.GrossProfitBase90,
|
&r.AvgPriceUSD90, &r.BasePriceUSD90, &r.CostPriceUSD90, &r.GrossProfitBase90,
|
||||||
&r.GrossProfitCost90, &r.GrossMarginBase90, &r.GrossMarginCost90, &r.CustomerCount180,
|
&r.GrossProfitCost90, &r.GrossMarginBase90, &r.GrossMarginCost90, &r.CustomerCount180,
|
||||||
&r.InvoiceCount180, &r.SalesQty180, &r.SalesUSD180, &r.AvgPriceUSD180, &r.BasePriceUSD180,
|
&r.InvoiceCount180, &r.SalesQty180, &r.SalesUSD180, &r.AvgPriceUSD180, &r.BasePriceUSD180,
|
||||||
@@ -3755,9 +3832,52 @@ LIMIT $1
|
|||||||
for i := range out {
|
for i := range out {
|
||||||
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
|
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
|
||||||
}
|
}
|
||||||
|
applyProductPerformanceSalesBreakdownScores(out)
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyProductPerformanceSalesBreakdownScores(rows []models.ProductPerformanceSalesBreakdownRow) {
|
||||||
|
avg90 := productPerformanceSalesBreakdownAverage(rows, func(row models.ProductPerformanceSalesBreakdownRow) float64 { return row.SalesUSD90 })
|
||||||
|
avg180 := productPerformanceSalesBreakdownAverage(rows, func(row models.ProductPerformanceSalesBreakdownRow) float64 { return row.SalesUSD180 })
|
||||||
|
avg365 := productPerformanceSalesBreakdownAverage(rows, func(row models.ProductPerformanceSalesBreakdownRow) float64 { return row.SalesUSD365 })
|
||||||
|
avgTotal := productPerformanceSalesBreakdownAverage(rows, func(row models.ProductPerformanceSalesBreakdownRow) float64 { return row.SalesUSDTotal })
|
||||||
|
|
||||||
|
for i := range rows {
|
||||||
|
r := &rows[i]
|
||||||
|
r.SalesIndex90 = productPerformanceRelativeIndex(r.SalesUSD90, avg90)
|
||||||
|
r.SalesIndex180 = productPerformanceRelativeIndex(r.SalesUSD180, avg180)
|
||||||
|
r.SalesIndex365 = productPerformanceRelativeIndex(r.SalesUSD365, avg365)
|
||||||
|
r.SalesIndexTotal = productPerformanceRelativeIndex(r.SalesUSDTotal, avgTotal)
|
||||||
|
|
||||||
|
r.CustomerScore90 = productPerformanceCustomerScore("90d", r.SalesUSD90, r.GrossMarginCost90, float64(r.ProductGroupCount90), r.SalesQty90)
|
||||||
|
r.CustomerScore180 = productPerformanceCustomerScore("180d", r.SalesUSD180, r.GrossMarginCost180, float64(r.ProductGroupCount180), r.SalesQty180)
|
||||||
|
r.CustomerScore365 = productPerformanceCustomerScore("365d", r.SalesUSD365, r.GrossMarginCost365, float64(r.ProductGroupCount365), r.SalesQty365)
|
||||||
|
r.CustomerScoreTotal = productPerformanceCustomerScore("total", r.SalesUSDTotal, r.GrossMarginCostTotal, float64(r.ProductGroupCountTotal), r.SalesQtyTotal)
|
||||||
|
|
||||||
|
r.PerformanceScore90 = productPerformanceProductScore("90d", r.SalesUSD90, r.SalesIndex90, r.GrossMarginCost90, r.StockTurnover90, float64(r.MarketCount90), float64(r.CustomerCount90))
|
||||||
|
r.PerformanceScore180 = productPerformanceProductScore("180d", r.SalesUSD180, r.SalesIndex180, r.GrossMarginCost180, r.StockTurnover180, float64(r.MarketCount180), float64(r.CustomerCount180))
|
||||||
|
r.PerformanceScore365 = productPerformanceProductScore("365d", r.SalesUSD365, r.SalesIndex365, r.GrossMarginCost365, r.StockTurnover365, float64(r.MarketCount365), float64(r.CustomerCount365))
|
||||||
|
r.PerformanceScoreTotal = productPerformanceProductScore("total", r.SalesUSDTotal, r.SalesIndexTotal, r.GrossMarginCostTotal, r.StockTurnoverTotal, float64(r.MarketCountTotal), float64(r.CustomerCountTotal))
|
||||||
|
r.PerformanceScore = r.PerformanceScore90
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceSalesBreakdownAverage(rows []models.ProductPerformanceSalesBreakdownRow, value func(models.ProductPerformanceSalesBreakdownRow) float64) float64 {
|
||||||
|
var sum, count float64
|
||||||
|
for _, row := range rows {
|
||||||
|
v := value(row)
|
||||||
|
if v <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sum += v
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return sum / count
|
||||||
|
}
|
||||||
|
|
||||||
type ProductPerformanceGroupedRequest struct {
|
type ProductPerformanceGroupedRequest struct {
|
||||||
Mode string
|
Mode string
|
||||||
GroupLevels []string
|
GroupLevels []string
|
||||||
@@ -3770,6 +3890,14 @@ type ProductPerformanceGroupedRequest struct {
|
|||||||
Descending bool
|
Descending bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProductPerformanceGroupedFilterOptionsRequest struct {
|
||||||
|
Mode string
|
||||||
|
GroupLevels []string
|
||||||
|
MainGroup string
|
||||||
|
Fields []string
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
|
|
||||||
func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
|
func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
|
||||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -3805,12 +3933,114 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ListProductPerformanceGroupedFilterOptions(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedFilterOptionsRequest) (map[string][]string, error) {
|
||||||
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.Limit <= 0 || req.Limit > 5000 {
|
||||||
|
req.Limit = 5000
|
||||||
|
}
|
||||||
|
levels := sanitizeProductPerformanceGroupLevels(req.GroupLevels)
|
||||||
|
if len(levels) == 0 {
|
||||||
|
levels = defaultProductPerformanceGroupLevels(req.Mode)
|
||||||
|
}
|
||||||
|
reportKey := productPerformanceGroupedSnapshotReportKey(req.Mode, levels, req.MainGroup)
|
||||||
|
if strings.TrimSpace(reportKey) == "" {
|
||||||
|
return map[string][]string{}, nil
|
||||||
|
}
|
||||||
|
levelSet := map[string]bool{}
|
||||||
|
for _, level := range levels {
|
||||||
|
levelSet[level] = true
|
||||||
|
}
|
||||||
|
out := map[string][]string{}
|
||||||
|
for _, field := range req.Fields {
|
||||||
|
field = strings.TrimSpace(field)
|
||||||
|
if !productPerformanceGroupedFilterOptionFieldAllowed(field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values, err := productPerformanceGroupedSnapshotFilterOptions(ctx, pg, reportKey, field, levelSet, strings.TrimSpace(req.MainGroup), req.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out[field] = values
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceGroupedFilterOptionFieldAllowed(field string) bool {
|
||||||
|
switch field {
|
||||||
|
case "kategori", "askili_yan", "urun_ilk_grubu", "urun_ana_grubu", "urun_alt_grubu",
|
||||||
|
"product_code", "color_yaka", "market_key", "country", "customer_segment",
|
||||||
|
"customer_code", "customer_name", "performance_bucket":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceGroupedSnapshotFilterOptions(ctx context.Context, pg *sql.DB, reportKey, field string, levelSet map[string]bool, mainGroup string, limit int) ([]string, error) {
|
||||||
|
if field == "urun_ana_grubu" && strings.Contains(reportKey, ":product_detail:") && strings.TrimSpace(mainGroup) != "" {
|
||||||
|
return []string{strings.TrimSpace(mainGroup)}, nil
|
||||||
|
}
|
||||||
|
if field == "performance_bucket" {
|
||||||
|
return productPerformanceGroupedSnapshotPayloadOptions(ctx, pg, reportKey, "performance_bucket", limit)
|
||||||
|
}
|
||||||
|
if !levelSet[field] {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
rows, err := pg.QueryContext(ctx, `
|
||||||
|
SELECT DISTINCT btrim(group_value) AS value
|
||||||
|
FROM mk_product_performance_grouped_snapshot
|
||||||
|
WHERE report_key = $1
|
||||||
|
AND group_field = $2
|
||||||
|
AND btrim(group_value) <> ''
|
||||||
|
ORDER BY value
|
||||||
|
LIMIT $3
|
||||||
|
`, reportKey, field, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return scanProductPerformanceGroupedFilterOptionRows(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceGroupedSnapshotPayloadOptions(ctx context.Context, pg *sql.DB, reportKey, field string, limit int) ([]string, error) {
|
||||||
|
rows, err := pg.QueryContext(ctx, `
|
||||||
|
SELECT DISTINCT btrim(payload ->> $2) AS value
|
||||||
|
FROM mk_product_performance_grouped_snapshot
|
||||||
|
WHERE report_key = $1
|
||||||
|
AND btrim(payload ->> $2) <> ''
|
||||||
|
ORDER BY value
|
||||||
|
LIMIT $3
|
||||||
|
`, reportKey, field, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return scanProductPerformanceGroupedFilterOptionRows(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanProductPerformanceGroupedFilterOptionRows(rows *sql.Rows) ([]string, error) {
|
||||||
|
out := []string{}
|
||||||
|
for rows.Next() {
|
||||||
|
var value string
|
||||||
|
if err := rows.Scan(&value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value != "" {
|
||||||
|
out = append(out, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
func loadProductPerformancePreparedGroupedRows(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
|
func loadProductPerformancePreparedGroupedRows(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
|
||||||
reportKey := productPerformanceGroupedSnapshotReportKey(req.Mode, levels, req.MainGroup)
|
reportKey := productPerformanceGroupedSnapshotReportKey(req.Mode, levels, req.MainGroup)
|
||||||
if strings.TrimSpace(reportKey) == "" {
|
if strings.TrimSpace(reportKey) == "" {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
effectiveFilters := productPerformanceGroupedEffectiveFilters(req)
|
effectiveFilters := productPerformancePreparedGroupedEffectiveFilters(req)
|
||||||
hasFilters := len(effectiveFilters) > 0
|
hasFilters := len(effectiveFilters) > 0
|
||||||
hasManualExpansion := len(req.ExpandedKeys) > 0
|
hasManualExpansion := len(req.ExpandedKeys) > 0
|
||||||
query := `
|
query := `
|
||||||
@@ -4065,6 +4295,21 @@ func productPerformanceGroupedEffectiveFilters(req ProductPerformanceGroupedRequ
|
|||||||
return filters
|
return filters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func productPerformancePreparedGroupedEffectiveFilters(req ProductPerformanceGroupedRequest) map[string][]string {
|
||||||
|
filters := make(map[string][]string, len(req.Filters))
|
||||||
|
for field, values := range req.Filters {
|
||||||
|
if strings.TrimSpace(req.Mode) == "product_detail" && field == "urun_ana_grubu" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cleanValues := cleanProductPerformanceFilterValues(values)
|
||||||
|
if len(cleanValues) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filters[field] = cleanValues
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
|
||||||
func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *[]map[string]any, mode string, levels []string, level int, visualLevel int, parentKeys []string, filters []productPerformanceSQLGroupFilter, expandedKeys map[string]bool, expandThroughLevel int, limit int) error {
|
func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *[]map[string]any, mode string, levels []string, level int, visualLevel int, parentKeys []string, filters []productPerformanceSQLGroupFilter, expandedKeys map[string]bool, expandThroughLevel int, limit int) error {
|
||||||
if level >= len(levels) {
|
if level >= len(levels) {
|
||||||
return nil
|
return nil
|
||||||
@@ -4371,7 +4616,7 @@ Source AS (
|
|||||||
CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END AS askili_yan,
|
CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END AS askili_yan,
|
||||||
CASE
|
CASE
|
||||||
WHEN btrim(COALESCE(urun_ilk_grubu,'')) = '-' THEN ''
|
WHEN btrim(COALESCE(urun_ilk_grubu,'')) = '-' THEN ''
|
||||||
WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN ''
|
WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN ''
|
||||||
ELSE COALESCE(urun_ilk_grubu,'')
|
ELSE COALESCE(urun_ilk_grubu,'')
|
||||||
END AS urun_ilk_grubu,
|
END AS urun_ilk_grubu,
|
||||||
COALESCE(urun_ana_grubu,'') AS urun_ana_grubu,
|
COALESCE(urun_ana_grubu,'') AS urun_ana_grubu,
|
||||||
@@ -4418,14 +4663,14 @@ Source AS (
|
|||||||
COALESCE(stock_qty,0) * COALESCE(cost_price_usd,0) AS idle_cost_usd
|
COALESCE(stock_qty,0) * COALESCE(cost_price_usd,0) AS idle_cost_usd
|
||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')` + idleWhere + `
|
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')` + idleWhere + `
|
||||||
)`
|
)`
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceSQLGroupExpr(field string) (string, bool) {
|
func productPerformanceSQLGroupExpr(field string) (string, bool) {
|
||||||
switch field {
|
switch field {
|
||||||
case "urun_ilk_grubu":
|
case "urun_ilk_grubu":
|
||||||
return "CASE WHEN btrim(COALESCE(urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE COALESCE(urun_ilk_grubu,'') END", true
|
return productPerformanceCleanFirstGroupSQL("urun_ilk_grubu"), true
|
||||||
case "askili_yan":
|
case "askili_yan":
|
||||||
return "CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END", true
|
return "CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END", true
|
||||||
case "urun_ana_grubu":
|
case "urun_ana_grubu":
|
||||||
@@ -4550,23 +4795,23 @@ func filterProductPerformancePreparedGroupedRows(rows []map[string]any, filters
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rowByKey := make(map[string]map[string]any, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
if key := stringFromMap(row, "key"); key != "" {
|
||||||
|
rowByKey[key] = row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
matchedKeys := map[string]bool{}
|
matchedKeys := map[string]bool{}
|
||||||
allowedKeys := map[string]bool{}
|
allowedKeys := map[string]bool{}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
matches := true
|
|
||||||
for field, allowed := range cleanFilters {
|
|
||||||
if !allowed[productPerformanceGroupedFilterValue(row, field)] {
|
|
||||||
matches = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matches {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := stringFromMap(row, "key")
|
key := stringFromMap(row, "key")
|
||||||
if key == "" {
|
if key == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !productPerformancePreparedGroupedPathMatches(key, rowByKey, cleanFilters) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
matchedKeys[key] = true
|
matchedKeys[key] = true
|
||||||
for _, ancestor := range productPerformanceGroupKeyAncestors(key, true) {
|
for _, ancestor := range productPerformanceGroupKeyAncestors(key, true) {
|
||||||
allowedKeys[ancestor] = true
|
allowedKeys[ancestor] = true
|
||||||
@@ -4585,6 +4830,49 @@ func filterProductPerformancePreparedGroupedRows(rows []map[string]any, filters
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func productPerformancePreparedGroupedPathMatches(key string, rowByKey map[string]map[string]any, filters map[string]map[string]bool) bool {
|
||||||
|
pathKeys := productPerformanceGroupKeyAncestors(key, true)
|
||||||
|
if len(pathKeys) == 0 {
|
||||||
|
pathKeys = []string{key}
|
||||||
|
}
|
||||||
|
for field, allowed := range filters {
|
||||||
|
matched := false
|
||||||
|
for _, pathKey := range pathKeys {
|
||||||
|
row := rowByKey[pathKey]
|
||||||
|
if row == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if allowed[productPerformancePreparedGroupedFilterValue(row, field)] {
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformancePreparedGroupedFilterValue(row map[string]any, field string) string {
|
||||||
|
field = strings.TrimSpace(field)
|
||||||
|
groupField := strings.TrimSpace(stringFromMap(row, "group_field"))
|
||||||
|
if groupField == field {
|
||||||
|
return normalizeProductPerformanceGroupValue(stringFromMap(row, "group_value"))
|
||||||
|
}
|
||||||
|
switch field {
|
||||||
|
case "market_key", "color_yaka", "urun_ilk_grubu", "askili_yan":
|
||||||
|
value := normalizeProductPerformanceGroupValue(mapGroupValue(row, field))
|
||||||
|
if value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value := normalizeProductPerformanceGroupValue(stringFromMap(row, field)); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func productPerformanceGroupRowHasMatchedAncestor(key string, matchedKeys map[string]bool) bool {
|
func productPerformanceGroupRowHasMatchedAncestor(key string, matchedKeys map[string]bool) bool {
|
||||||
for _, ancestor := range productPerformanceGroupKeyAncestors(key, true) {
|
for _, ancestor := range productPerformanceGroupKeyAncestors(key, true) {
|
||||||
if matchedKeys[ancestor] {
|
if matchedKeys[ancestor] {
|
||||||
@@ -4728,9 +5016,61 @@ func productPerformanceGroupedRawSnapshotSourceRows(ctx context.Context, pg *sql
|
|||||||
if strings.TrimSpace(mode) == "idle" {
|
if strings.TrimSpace(mode) == "idle" {
|
||||||
return productPerformanceIdleSourceRows(rows), nil
|
return productPerformanceIdleSourceRows(rows), nil
|
||||||
}
|
}
|
||||||
|
if mode == "products" || mode == "product_detail" {
|
||||||
|
rows = mergeProductPerformanceGeneralSnapshotMetrics(ctx, pg, rows)
|
||||||
|
}
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mergeProductPerformanceGeneralSnapshotMetrics(ctx context.Context, pg *sql.DB, rows []map[string]any) []map[string]any {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
generalRows, ok, err := loadProductPerformanceSnapshotMapRows(ctx, pg, productPerformanceSnapshotKey("general"), 50000)
|
||||||
|
if err != nil || !ok || len(generalRows) == 0 {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
byKey := make(map[string]map[string]any, len(generalRows))
|
||||||
|
for _, row := range generalRows {
|
||||||
|
key := productPerformanceMapMarketVariantKey(row)
|
||||||
|
if key != "" {
|
||||||
|
byKey[key] = row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
general := byKey[productPerformanceMapMarketVariantKey(row)]
|
||||||
|
if general == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, field := range []string{
|
||||||
|
"market_count_total", "customer_count_total", "invoice_count_total", "sales_index_total",
|
||||||
|
"avg_price_usd_total", "gross_profit_usd_total", "gross_margin_total",
|
||||||
|
"unit_profit_cost_total", "unit_profit_base_total", "first_sale_date",
|
||||||
|
} {
|
||||||
|
if value, ok := general[field]; ok {
|
||||||
|
row[field] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value, ok := general["performance_score"]; ok {
|
||||||
|
row["performance_score_total"] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceMapMarketVariantKey(row map[string]any) string {
|
||||||
|
productCode := normalizeProductPerformanceProductCode(stringFromMap(row, "product_code"))
|
||||||
|
if productCode == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join([]string{
|
||||||
|
productCode,
|
||||||
|
strings.TrimSpace(stringFromMap(row, "color_code")),
|
||||||
|
strings.TrimSpace(stringFromMap(row, "yaka_kodu")),
|
||||||
|
displayProductPerformanceMarketName(stringFromMap(row, "market_key")),
|
||||||
|
}, "|")
|
||||||
|
}
|
||||||
|
|
||||||
func productPerformanceGroupedSnapshotRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, bool, error) {
|
func productPerformanceGroupedSnapshotRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, bool, error) {
|
||||||
reportKey, ok := productPerformanceGroupedSnapshotKey(mode)
|
reportKey, ok := productPerformanceGroupedSnapshotKey(mode)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -4823,19 +5163,23 @@ type productPerformanceGroupedSnapshotAvgState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type productPerformanceGroupedSnapshotNode struct {
|
type productPerformanceGroupedSnapshotNode struct {
|
||||||
Key string
|
Key string
|
||||||
Level int
|
Level int
|
||||||
Field string
|
Field string
|
||||||
Value string
|
Value string
|
||||||
Row map[string]any
|
Row map[string]any
|
||||||
Count int
|
Count int
|
||||||
Children map[string]*productPerformanceGroupedSnapshotNode
|
Children map[string]*productPerformanceGroupedSnapshotNode
|
||||||
ChildOrder []string
|
ChildOrder []string
|
||||||
StockSeen map[string]bool
|
StockSeen map[string]bool
|
||||||
IdleSeen map[string]bool
|
IdleSeen map[string]bool
|
||||||
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
|
||||||
|
MarketTotalSeen 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 {
|
||||||
@@ -4921,8 +5265,6 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
|
|||||||
n.IdleSeen[variantKey] = true
|
n.IdleSeen[variantKey] = true
|
||||||
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
|
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
|
||||||
}
|
}
|
||||||
case shouldSumProductPerformanceField(key):
|
|
||||||
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
|
|
||||||
case shouldAverageProductPerformanceField(key):
|
case shouldAverageProductPerformanceField(key):
|
||||||
if n.Avg == nil {
|
if n.Avg == nil {
|
||||||
n.Avg = map[string]*productPerformanceGroupedSnapshotAvgState{}
|
n.Avg = map[string]*productPerformanceGroupedSnapshotAvgState{}
|
||||||
@@ -4940,12 +5282,48 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
|
|||||||
}
|
}
|
||||||
state.Sum += number
|
state.Sum += number
|
||||||
state.Count++
|
state.Count++
|
||||||
|
case shouldSumProductPerformanceField(key):
|
||||||
|
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
|
||||||
default:
|
default:
|
||||||
if _, ok := n.Row[key]; !ok {
|
if _, ok := n.Row[key]; !ok {
|
||||||
n.Row[key] = value
|
n.Row[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
n.addDistinctSpread(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) {
|
||||||
|
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
|
||||||
|
if market != "" && market != "STOK" {
|
||||||
|
if floatFromMap(row, "sales_qty_90d") > 0 || floatFromMap(row, "sales_usd_90d") > 0 {
|
||||||
|
if n.Market90Seen == nil {
|
||||||
|
n.Market90Seen = map[string]bool{}
|
||||||
|
}
|
||||||
|
n.Market90Seen[market] = true
|
||||||
|
}
|
||||||
|
if floatFromMap(row, "sales_qty_total") > 0 || floatFromMap(row, "sales_usd_total") > 0 {
|
||||||
|
if n.MarketTotalSeen == nil {
|
||||||
|
n.MarketTotalSeen = map[string]bool{}
|
||||||
|
}
|
||||||
|
n.MarketTotalSeen[market] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
|
||||||
|
if customer != "" && customer != "-" {
|
||||||
|
if floatFromMap(row, "sales_qty_90d") > 0 || floatFromMap(row, "sales_usd_90d") > 0 {
|
||||||
|
if n.Customer90Seen == nil {
|
||||||
|
n.Customer90Seen = map[string]bool{}
|
||||||
|
}
|
||||||
|
n.Customer90Seen[customer] = true
|
||||||
|
}
|
||||||
|
if floatFromMap(row, "sales_qty_total") > 0 || floatFromMap(row, "sales_usd_total") > 0 {
|
||||||
|
if n.CustomerTotalSeen == nil {
|
||||||
|
n.CustomerTotalSeen = map[string]bool{}
|
||||||
|
}
|
||||||
|
n.CustomerTotalSeen[customer] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendProductPerformanceGroupedSnapshotNodes(out *[]map[string]any, nodes map[string]*productPerformanceGroupedSnapshotNode, order []string) {
|
func appendProductPerformanceGroupedSnapshotNodes(out *[]map[string]any, nodes map[string]*productPerformanceGroupedSnapshotNode, order []string) {
|
||||||
@@ -4985,6 +5363,7 @@ func (n *productPerformanceGroupedSnapshotNode) snapshotRow() map[string]any {
|
|||||||
if bucket := n.dominantBucket(); bucket != "" {
|
if bucket := n.dominantBucket(); bucket != "" {
|
||||||
row["performance_bucket"] = bucket
|
row["performance_bucket"] = bucket
|
||||||
}
|
}
|
||||||
|
applyProductPerformanceDistinctSpread(row, n.Market90Seen, n.MarketTotalSeen, n.Customer90Seen, n.CustomerTotalSeen)
|
||||||
clearProductPerformanceGroupDimensions(row, n.Field, n.Value)
|
clearProductPerformanceGroupDimensions(row, n.Field, n.Value)
|
||||||
row["__group"] = true
|
row["__group"] = true
|
||||||
row["row_key"] = "group|" + n.Key
|
row["row_key"] = "group|" + n.Key
|
||||||
@@ -4994,7 +5373,7 @@ func (n *productPerformanceGroupedSnapshotNode) snapshotRow() map[string]any {
|
|||||||
row["group_value"] = n.Value
|
row["group_value"] = n.Value
|
||||||
row["label"] = n.Value
|
row["label"] = n.Value
|
||||||
row["count"] = n.Count
|
row["count"] = n.Count
|
||||||
row["recommendation"] = fmt.Sprintf("%d satir", n.Count)
|
row["recommendation"] = productPerformanceGroupRecommendation(row, n.Field, n.Count)
|
||||||
image := n.Image
|
image := n.Image
|
||||||
if image == nil {
|
if image == nil {
|
||||||
image = row
|
image = row
|
||||||
@@ -5063,7 +5442,7 @@ func makeProductPerformanceGroupedRow(key string, level int, field, value string
|
|||||||
row["group_value"] = value
|
row["group_value"] = value
|
||||||
row["label"] = value
|
row["label"] = value
|
||||||
row["count"] = len(rows)
|
row["count"] = len(rows)
|
||||||
row["recommendation"] = fmt.Sprintf("%d satır", len(rows))
|
row["recommendation"] = productPerformanceGroupRecommendation(row, field, len(rows))
|
||||||
|
|
||||||
image := firstProductPerformanceImageSource(rows)
|
image := firstProductPerformanceImageSource(rows)
|
||||||
row["image_product_code"] = stringFromMap(image, "product_code")
|
row["image_product_code"] = stringFromMap(image, "product_code")
|
||||||
@@ -5101,7 +5480,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{}
|
||||||
|
marketTotalSeen := 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)
|
||||||
for key, value := range row {
|
for key, value := range row {
|
||||||
if key == "row_key" || key == "key" {
|
if key == "row_key" || key == "key" {
|
||||||
continue
|
continue
|
||||||
@@ -5117,9 +5501,12 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
|
|||||||
out[key] = distinctProductPerformanceVariantStockCost(rows)
|
out[key] = distinctProductPerformanceVariantStockCost(rows)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if shouldAverageProductPerformanceField(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if shouldSumProductPerformanceField(key) {
|
if shouldSumProductPerformanceField(key) {
|
||||||
out[key] = floatFromAny(out[key]) + floatFromAny(value)
|
out[key] = floatFromAny(out[key]) + floatFromAny(value)
|
||||||
} else if _, ok := out[key]; !ok && !shouldAverageProductPerformanceField(key) {
|
} else if _, ok := out[key]; !ok {
|
||||||
out[key] = value
|
out[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5136,9 +5523,96 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
|
|||||||
}
|
}
|
||||||
deriveProductPerformanceGroupMetrics(out, groupField)
|
deriveProductPerformanceGroupMetrics(out, groupField)
|
||||||
out["performance_bucket"] = dominantProductPerformanceValue(rows, "performance_bucket")
|
out["performance_bucket"] = dominantProductPerformanceValue(rows, "performance_bucket")
|
||||||
|
applyProductPerformanceDistinctSpread(out, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addProductPerformanceDistinctSpread(row map[string]any, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen map[string]bool) {
|
||||||
|
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
|
||||||
|
if market != "" && market != "STOK" {
|
||||||
|
if floatFromMap(row, "sales_qty_90d") > 0 || floatFromMap(row, "sales_usd_90d") > 0 {
|
||||||
|
market90Seen[market] = true
|
||||||
|
}
|
||||||
|
if floatFromMap(row, "sales_qty_total") > 0 || floatFromMap(row, "sales_usd_total") > 0 {
|
||||||
|
marketTotalSeen[market] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
|
||||||
|
if customer != "" && customer != "-" {
|
||||||
|
if floatFromMap(row, "sales_qty_90d") > 0 || floatFromMap(row, "sales_usd_90d") > 0 {
|
||||||
|
customer90Seen[customer] = true
|
||||||
|
}
|
||||||
|
if floatFromMap(row, "sales_qty_total") > 0 || floatFromMap(row, "sales_usd_total") > 0 {
|
||||||
|
customerTotalSeen[customer] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyProductPerformanceDistinctSpread(row map[string]any, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen map[string]bool) {
|
||||||
|
if len(market90Seen) > 0 {
|
||||||
|
row["market_count_90d"] = len(market90Seen)
|
||||||
|
}
|
||||||
|
if len(marketTotalSeen) > 0 {
|
||||||
|
row["market_count_total"] = len(marketTotalSeen)
|
||||||
|
}
|
||||||
|
if len(customer90Seen) > 0 {
|
||||||
|
row["customer_count_90d"] = len(customer90Seen)
|
||||||
|
}
|
||||||
|
if len(customerTotalSeen) > 0 {
|
||||||
|
row["customer_count_total"] = len(customerTotalSeen)
|
||||||
|
}
|
||||||
|
if floatFromMap(row, "sales_qty_90d") > 0 || floatFromMap(row, "sales_usd_90d") > 0 {
|
||||||
|
if intFromMap(row, "market_count_90d") == 0 {
|
||||||
|
row["market_count_90d"] = 1
|
||||||
|
}
|
||||||
|
if intFromMap(row, "customer_count_90d") == 0 {
|
||||||
|
row["customer_count_90d"] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if floatFromMap(row, "sales_qty_total") > 0 || floatFromMap(row, "sales_usd_total") > 0 {
|
||||||
|
if intFromMap(row, "market_count_total") == 0 {
|
||||||
|
row["market_count_total"] = maxInt(1, intFromMap(row, "market_count_90d"))
|
||||||
|
}
|
||||||
|
if intFromMap(row, "customer_count_total") == 0 {
|
||||||
|
row["customer_count_total"] = maxInt(1, intFromMap(row, "customer_count_90d"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceGroupRecommendation(row map[string]any, groupField string, count int) string {
|
||||||
|
groupField = strings.TrimSpace(groupField)
|
||||||
|
salesQty90 := floatFromMap(row, "sales_qty_90d")
|
||||||
|
salesUSD90 := floatFromMap(row, "sales_usd_90d")
|
||||||
|
stockQty := floatFromMap(row, "stock_qty")
|
||||||
|
marginCost := floatFromMap(row, "gross_margin_cost_90d")
|
||||||
|
if marginCost == 0 {
|
||||||
|
marginCost = floatFromMap(row, "gross_margin_90d")
|
||||||
|
}
|
||||||
|
switch groupField {
|
||||||
|
case "market_key":
|
||||||
|
if salesQty90 <= 0 && stockQty > 0 {
|
||||||
|
return "Bu piyasada son 90 gunde satis yok"
|
||||||
|
}
|
||||||
|
if marginCost < 0 {
|
||||||
|
return "Bu piyasada ciplak marj negatif"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Piyasa satisi %.0f adet / %.0f USD", salesQty90, salesUSD90)
|
||||||
|
case "color_yaka":
|
||||||
|
if salesQty90 <= 0 && stockQty > 0 {
|
||||||
|
return "Renk/yaka stokta, son 90 gun satisi yok"
|
||||||
|
}
|
||||||
|
if marginCost < 0 {
|
||||||
|
return "Renk/yaka ciplak marji negatif"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Renk/yaka satisi %.0f adet", salesQty90)
|
||||||
|
default:
|
||||||
|
if recommendation := strings.TrimSpace(stringFromMap(row, "recommendation")); recommendation != "" {
|
||||||
|
return recommendation
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d satir", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isProductPerformanceMarginField(field string) bool {
|
func isProductPerformanceMarginField(field string) bool {
|
||||||
return strings.HasPrefix(field, "gross_margin")
|
return strings.HasPrefix(field, "gross_margin")
|
||||||
}
|
}
|
||||||
@@ -5300,12 +5774,15 @@ func productPerformanceSalesGroupScore(row map[string]any) float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceSalesPeriodScore(row map[string]any, suffix string) float64 {
|
func productPerformanceSalesPeriodScore(row map[string]any, suffix string) float64 {
|
||||||
|
salesUSD := floatFromMap(row, "sales_usd_"+suffix)
|
||||||
salesIndex := floatFromMap(row, "sales_index_"+suffix)
|
salesIndex := floatFromMap(row, "sales_index_"+suffix)
|
||||||
|
if salesIndex <= 0 && suffix == "total" {
|
||||||
|
salesIndex = floatFromMap(row, "sales_index_total")
|
||||||
|
}
|
||||||
margin := floatFromMap(row, "gross_margin_cost_"+suffix)
|
margin := floatFromMap(row, "gross_margin_cost_"+suffix)
|
||||||
if margin == 0 {
|
if margin == 0 {
|
||||||
margin = floatFromMap(row, "gross_margin_"+suffix)
|
margin = floatFromMap(row, "gross_margin_"+suffix)
|
||||||
}
|
}
|
||||||
invoiceCount := floatFromMap(row, "invoice_count_"+suffix)
|
|
||||||
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 {
|
||||||
@@ -5313,12 +5790,15 @@ func productPerformanceSalesPeriodScore(row map[string]any, suffix string) float
|
|||||||
stockTurnover = salesQty / stockQty
|
stockTurnover = salesQty / stockQty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
score := minFloat(35, maxFloat(0, salesIndex)*18) +
|
return productPerformanceProductScore(
|
||||||
minFloat(30, maxFloat(0, margin)*60) +
|
suffix,
|
||||||
minFloat(15, invoiceCount*1.5) +
|
salesUSD,
|
||||||
minFloat(10, salesQty/10) +
|
salesIndex,
|
||||||
minFloat(10, maxFloat(0, stockTurnover)*5)
|
margin,
|
||||||
return score
|
stockTurnover,
|
||||||
|
productPerformancePeriodCount(row, "market_count", suffix),
|
||||||
|
productPerformancePeriodCount(row, "customer_count", suffix),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceCustomerSalesGroupScore(row map[string]any) float64 {
|
func productPerformanceCustomerSalesGroupScore(row map[string]any) float64 {
|
||||||
@@ -5346,24 +5826,139 @@ func productPerformanceCustomerSalesGroupScore(row map[string]any) float64 {
|
|||||||
func productPerformanceCustomerSalesPeriodScore(row map[string]any) float64 {
|
func productPerformanceCustomerSalesPeriodScore(row map[string]any) float64 {
|
||||||
salesUSD := floatFromMap(row, "sales_usd")
|
salesUSD := floatFromMap(row, "sales_usd")
|
||||||
margin := floatFromMap(row, "gross_margin_cost")
|
margin := floatFromMap(row, "gross_margin_cost")
|
||||||
invoiceCount := floatFromMap(row, "invoice_count")
|
if margin == 0 {
|
||||||
|
margin = floatFromMap(row, "gross_margin")
|
||||||
|
}
|
||||||
salesQty := floatFromMap(row, "sales_qty")
|
salesQty := floatFromMap(row, "sales_qty")
|
||||||
productCount := floatFromMap(row, "product_count")
|
productCount := floatFromMap(row, "product_count")
|
||||||
return minFloat(35, salesUSD/1000) +
|
if groupCount := floatFromMap(row, "product_group_count"); groupCount > 0 {
|
||||||
minFloat(25, maxFloat(0, margin)*55) +
|
productCount = groupCount
|
||||||
minFloat(20, invoiceCount*2) +
|
}
|
||||||
minFloat(10, salesQty/10) +
|
suffix := strings.TrimSpace(stringFromMap(row, "suffix"))
|
||||||
minFloat(10, productCount)
|
if suffix == "" {
|
||||||
|
suffix = "90d"
|
||||||
|
}
|
||||||
|
return productPerformanceCustomerScore(suffix, salesUSD, margin, productCount, salesQty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64) float64 {
|
||||||
|
revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTarget(suffix))
|
||||||
|
score := 0.30*productPerformanceMarginComponentScore(margin) +
|
||||||
|
0.20*productPerformanceRatioScore(stockTurnover, 1.50) +
|
||||||
|
0.20*revenueScore +
|
||||||
|
0.15*productPerformanceRatioScore(marketCount, 8) +
|
||||||
|
0.15*productPerformanceRatioScore(customerCount, 25)
|
||||||
|
return productPerformanceRoundScore(score)
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceCustomerScore(suffix string, salesUSD, margin, productGroupCount, salesQty float64) float64 {
|
||||||
|
score := 0.35*productPerformanceRatioScore(salesUSD, productPerformanceCustomerRevenueTarget(suffix)) +
|
||||||
|
0.30*productPerformanceMarginComponentScore(margin) +
|
||||||
|
0.20*productPerformanceRatioScore(productGroupCount, 8) +
|
||||||
|
0.15*productPerformanceRatioScore(salesQty, productPerformanceCustomerQtyTarget(suffix))
|
||||||
|
return productPerformanceRoundScore(score)
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceRevenueScore(suffix string, salesUSD, salesIndex, absoluteTarget float64) float64 {
|
||||||
|
if salesIndex > 0 {
|
||||||
|
return productPerformanceRatioScore(salesIndex, 2)
|
||||||
|
}
|
||||||
|
return productPerformanceRatioScore(salesUSD, absoluteTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceRelativeIndex(value, average float64) float64 {
|
||||||
|
if value <= 0 || average <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return value / average
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceMarginComponentScore(margin float64) float64 {
|
||||||
|
return productPerformanceRatioScore(maxFloat(0, margin), 0.40)
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceRatioScore(value, target float64) float64 {
|
||||||
|
if target <= 0 || value <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return minFloat(100, maxFloat(0, value)*100/target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceRoundScore(score float64) float64 {
|
||||||
|
if score < 0 {
|
||||||
|
score = 0
|
||||||
|
}
|
||||||
|
if score > 100 {
|
||||||
|
score = 100
|
||||||
|
}
|
||||||
|
return math.Round(score*10000) / 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceProductRevenueTarget(suffix string) float64 {
|
||||||
|
switch suffix {
|
||||||
|
case "180d":
|
||||||
|
return 20000
|
||||||
|
case "365d":
|
||||||
|
return 40000
|
||||||
|
case "total":
|
||||||
|
return 120000
|
||||||
|
default:
|
||||||
|
return 10000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceCustomerRevenueTarget(suffix string) float64 {
|
||||||
|
switch suffix {
|
||||||
|
case "180d":
|
||||||
|
return 50000
|
||||||
|
case "365d":
|
||||||
|
return 100000
|
||||||
|
case "total":
|
||||||
|
return 300000
|
||||||
|
default:
|
||||||
|
return 25000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceCustomerQtyTarget(suffix string) float64 {
|
||||||
|
switch suffix {
|
||||||
|
case "180d":
|
||||||
|
return 1000
|
||||||
|
case "365d":
|
||||||
|
return 2000
|
||||||
|
case "total":
|
||||||
|
return 6000
|
||||||
|
default:
|
||||||
|
return 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformancePeriodCount(row map[string]any, prefix, suffix string) float64 {
|
||||||
|
if value, ok := row[prefix+"_"+suffix]; ok {
|
||||||
|
if n := floatFromAny(value); n > 0 {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if suffix == "total" {
|
||||||
|
if value, ok := row[prefix+"_total"]; ok {
|
||||||
|
return floatFromAny(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value, ok := row[prefix+"_90d"]; ok {
|
||||||
|
return floatFromAny(value)
|
||||||
|
}
|
||||||
|
return floatFromAny(row[prefix])
|
||||||
}
|
}
|
||||||
|
|
||||||
func rowPeriodMetricMap(row map[string]any, suffix string) map[string]any {
|
func rowPeriodMetricMap(row map[string]any, suffix string) map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"sales_usd": floatFromMap(row, "sales_usd_"+suffix),
|
"suffix": suffix,
|
||||||
"gross_margin_cost": floatFromMap(row, "gross_margin_cost_"+suffix),
|
"sales_usd": floatFromMap(row, "sales_usd_"+suffix),
|
||||||
"gross_margin": floatFromMap(row, "gross_margin_"+suffix),
|
"gross_margin_cost": floatFromMap(row, "gross_margin_cost_"+suffix),
|
||||||
"invoice_count": floatFromMap(row, "invoice_count_"+suffix),
|
"gross_margin": floatFromMap(row, "gross_margin_"+suffix),
|
||||||
"sales_qty": floatFromMap(row, "sales_qty_"+suffix),
|
"sales_qty": floatFromMap(row, "sales_qty_"+suffix),
|
||||||
"product_count": floatFromMap(row, "product_count"),
|
"product_group_count": productPerformancePeriodCount(row, "product_group_count", suffix),
|
||||||
|
"product_count": floatFromMap(row, "product_count"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5608,9 +6203,7 @@ func cleanProductPerformanceFirstGroup(value string) string {
|
|||||||
if value == "-" {
|
if value == "-" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
normalized := strings.ToUpper(value)
|
switch normalizeProductPerformanceTurkishText(value) {
|
||||||
normalized = strings.NewReplacer("İ", "I", "Ş", "S", "Ğ", "G", "Ü", "U", "Ö", "O", "Ç", "C").Replace(normalized)
|
|
||||||
switch normalized {
|
|
||||||
case "YETISKIN", "YETISKIN/GARSON", "GARSON":
|
case "YETISKIN", "YETISKIN/GARSON", "GARSON":
|
||||||
return ""
|
return ""
|
||||||
default:
|
default:
|
||||||
@@ -5618,6 +6211,24 @@ func cleanProductPerformanceFirstGroup(value string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeProductPerformanceTurkishText(value string) string {
|
||||||
|
value = strings.ToUpper(strings.TrimSpace(value))
|
||||||
|
return strings.NewReplacer(
|
||||||
|
"\u0130", "I",
|
||||||
|
"\u015E", "S",
|
||||||
|
"\u011E", "G",
|
||||||
|
"\u00DC", "U",
|
||||||
|
"\u00D6", "O",
|
||||||
|
"\u00C7", "C",
|
||||||
|
"\u0131", "I",
|
||||||
|
"\u015F", "S",
|
||||||
|
"\u011F", "G",
|
||||||
|
"\u00FC", "U",
|
||||||
|
"\u00F6", "O",
|
||||||
|
"\u00E7", "C",
|
||||||
|
).Replace(value)
|
||||||
|
}
|
||||||
|
|
||||||
func cleanProductPerformanceOptionalAttr(value string) string {
|
func cleanProductPerformanceOptionalAttr(value string) string {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
if value == "-" {
|
if value == "-" {
|
||||||
@@ -5659,6 +6270,13 @@ func minInt(a, b int) int {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func maxInt(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func ListProductPerformanceSalesDetails(ctx context.Context, pg *sql.DB, productCode, colorCode, yakaKodu string, limit int) ([]models.ProductPerformanceSalesDetailRow, error) {
|
func ListProductPerformanceSalesDetails(ctx context.Context, pg *sql.DB, productCode, colorCode, yakaKodu string, limit int) ([]models.ProductPerformanceSalesDetailRow, error) {
|
||||||
if limit <= 0 || limit > 500 {
|
if limit <= 0 || limit > 500 {
|
||||||
limit = 100
|
limit = 100
|
||||||
@@ -5679,7 +6297,7 @@ FROM mk_product_performance_sales_daily
|
|||||||
WHERE product_code=$1
|
WHERE product_code=$1
|
||||||
AND color_code=$2
|
AND color_code=$2
|
||||||
AND yaka_kodu=$3
|
AND yaka_kodu=$3
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
ORDER BY sales_usd DESC, sales_date DESC
|
ORDER BY sales_usd DESC, sales_date DESC
|
||||||
LIMIT $4
|
LIMIT $4
|
||||||
`, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu), limit)
|
`, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu), limit)
|
||||||
@@ -5936,15 +6554,24 @@ func normalizeProductPerformanceCostPair(costPriceUSD, basePriceUSD float64) (fl
|
|||||||
return costPriceUSD, basePriceUSD
|
return costPriceUSD, basePriceUSD
|
||||||
}
|
}
|
||||||
|
|
||||||
const productPerformanceExcludedFirstGroupsSQL = "('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')"
|
const productPerformanceTurkishTranslateSQL = "U&'\\0130\\015E\\011E\\00DC\\00D6\\00C7\\0131\\015F\\011F\\00FC\\00F6\\00E7', 'ISGUOCisguoc'"
|
||||||
|
const productPerformanceExcludedFirstGroupsSQL = "('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')"
|
||||||
|
|
||||||
|
func productPerformanceNormalizedTextSQL(expr string) string {
|
||||||
|
return "upper(translate(btrim(COALESCE(" + expr + ",'')), " + productPerformanceTurkishTranslateSQL + "))"
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPerformanceCleanFirstGroupSQL(expr string) string {
|
||||||
|
return "CASE WHEN btrim(COALESCE(" + expr + ",'')) = '-' THEN '' WHEN " + productPerformanceNormalizedTextSQL(expr) + " IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE COALESCE(" + expr + ",'') END"
|
||||||
|
}
|
||||||
|
|
||||||
func productPerformanceAllowedFirstGroupSQL(expr string) string {
|
func productPerformanceAllowedFirstGroupSQL(expr string) string {
|
||||||
return "upper(btrim(COALESCE(" + expr + ",''))) NOT IN " + productPerformanceExcludedFirstGroupsSQL
|
return productPerformanceNormalizedTextSQL(expr) + " NOT IN " + productPerformanceExcludedFirstGroupsSQL
|
||||||
}
|
}
|
||||||
|
|
||||||
func isExcludedProductPerformanceFirstGroup(value string) bool {
|
func isExcludedProductPerformanceFirstGroup(value string) bool {
|
||||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
switch normalizeProductPerformanceTurkishText(value) {
|
||||||
case "MALZEMELI FASON", "MALZEMESIZ FASON", "DIGER":
|
case "MALZEMELI FASON", "MALZEMESIZ FASON", "MAZLEMELI FASON", "MAZEMESIZ FASON", "DIGER":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -6013,7 +6640,7 @@ SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
|
|||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
||||||
AND product_code || '|' || color_code || '|' || yaka_kodu = ANY($1)
|
AND product_code || '|' || color_code || '|' || yaka_kodu = ANY($1)
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
|
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
|
||||||
`, pq.Array(variantKeys))
|
`, pq.Array(variantKeys))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6069,68 +6696,68 @@ GROUP BY product_code, color_code, yaka_kodu
|
|||||||
func productPerformanceOrderRecommendation(row models.ProductPerformanceOrderAnalysisRow) (string, string) {
|
func productPerformanceOrderRecommendation(row models.ProductPerformanceOrderAnalysisRow) (string, string) {
|
||||||
switch {
|
switch {
|
||||||
case row.OrderQty > 0 && row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
case row.OrderQty > 0 && row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||||||
return "STOKSUZ_TALEP", "Açık sipariş stoktan büyük. Karlı talep var; üretim/satın alma önceliği ver."
|
return "STOKSUZ_TALEP", "Acik siparis stoktan buyuk. Karli talep var; uretim/satin alma onceligi ver."
|
||||||
case row.ExpectedProfitCostUSD < 0:
|
case row.ExpectedProfitCostUSD < 0:
|
||||||
return "FIYAT_BASKISI", "Açık sipariş çıplak maliyete göre zarar yazıyor. Fiyat/maliyet kontrol edilmeli."
|
return "FIYAT_BASKISI", "Acik siparis ciplak maliyete gore zarar yaziyor. Fiyat/maliyet kontrol edilmeli."
|
||||||
case row.ExpectedMarginCost >= 0.25 && row.NetStockAfterOrder >= 0:
|
case row.ExpectedMarginCost >= 0.25 && row.NetStockAfterOrder >= 0:
|
||||||
return "YILDIZ_URUN", "Açık sipariş karlı ve stok karşılıyor. Teslimat korunmalı."
|
return "YILDIZ_URUN", "Acik siparis karli ve stok karsiliyor. Teslimat korunmali."
|
||||||
case row.ExpectedMarginCost >= 0.25 && row.NetStockAfterOrder < 0:
|
case row.ExpectedMarginCost >= 0.25 && row.NetStockAfterOrder < 0:
|
||||||
return "FIYAT_FIRSATI", "Karlı talep var ama stok yetersiz. Üretim planına alınmalı."
|
return "FIYAT_FIRSATI", "Karli talep var ama stok yetersiz. Uretim planina alinmali."
|
||||||
case row.OverdueQty > 0:
|
case row.OverdueQty > 0:
|
||||||
return "TAKIP", "Termin gecikmesi olan açık sipariş var. Operasyon takibi gerekli."
|
return "TAKIP", "Termin gecikmesi olan acik siparis var. Operasyon takibi gerekli."
|
||||||
default:
|
default:
|
||||||
return "TAKIP", "Sipariş, stok ve fiyat düzenli izlenmeli."
|
return "TAKIP", "Siparis, stok ve fiyat duzenli izlenmeli."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceOrderGroupRecommendation(row models.ProductPerformanceOrderGroupRow) (string, string) {
|
func productPerformanceOrderGroupRecommendation(row models.ProductPerformanceOrderGroupRow) (string, string) {
|
||||||
switch {
|
switch {
|
||||||
case row.ExpectedProfitCostUSD < 0:
|
case row.ExpectedProfitCostUSD < 0:
|
||||||
return "FIYAT_BASKISI", "Çıplak maliyete göre zarar yazan açık sipariş var. Fiyat/maliyet acil kontrol edilmeli."
|
return "FIYAT_BASKISI", "Ciplak maliyete gore zarar yazan acik siparis var. Fiyat/maliyet acil kontrol edilmeli."
|
||||||
case row.ExpectedProfitBaseUSD < 0:
|
case row.ExpectedProfitBaseUSD < 0:
|
||||||
return "FIYAT_BASKISI", "Taban maliyete göre brüt zarar var. Satış fiyatı veya iskonto kontrol edilmeli."
|
return "FIYAT_BASKISI", "Taban maliyete gore brut zarar var. Satis fiyati veya iskonto kontrol edilmeli."
|
||||||
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||||||
return "STOKSUZ_TALEP", "Karlı açık talep var ama stok yetersiz. Üretim/satın alma önceliği ver."
|
return "STOKSUZ_TALEP", "Karli acik talep var ama stok yetersiz. Uretim/satin alma onceligi ver."
|
||||||
case row.ExpectedMarginCost >= 0.25 && row.ExpectedMarginBase >= 0.15:
|
case row.ExpectedMarginCost >= 0.25 && row.ExpectedMarginBase >= 0.15:
|
||||||
return "YILDIZ_URUN", "Piyasa/müşteri açık siparişleri karlı. Teslimat ve stok korunmalı."
|
return "YILDIZ_URUN", "Piyasa/musteri acik siparisleri karli. Teslimat ve stok korunmali."
|
||||||
case row.OverdueQty > 0:
|
case row.OverdueQty > 0:
|
||||||
return "TAKIP", "Geciken açık sipariş var. Operasyon takibi gerekli."
|
return "TAKIP", "Geciken acik siparis var. Operasyon takibi gerekli."
|
||||||
default:
|
default:
|
||||||
return "TAKIP", "Sipariş karlılığı ve stok yeterliliği izlenmeli."
|
return "TAKIP", "Siparis karliligi ve stok yeterliligi izlenmeli."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceOrderProductCustomerRecommendation(row models.ProductPerformanceOrderProductCustomerRow) (string, string) {
|
func productPerformanceOrderProductCustomerRecommendation(row models.ProductPerformanceOrderProductCustomerRow) (string, string) {
|
||||||
switch {
|
switch {
|
||||||
case row.ExpectedProfitCostUSD < 0:
|
case row.ExpectedProfitCostUSD < 0:
|
||||||
return "FIYAT_BASKISI", "Bu müşteri/ürün açık siparişi çıplak maliyete göre zarar yazıyor."
|
return "FIYAT_BASKISI", "Bu musteri/urun acik siparisi ciplak maliyete gore zarar yaziyor."
|
||||||
case row.ExpectedProfitBaseUSD < 0:
|
case row.ExpectedProfitBaseUSD < 0:
|
||||||
return "FIYAT_BASKISI", "Bu müşteri/ürün açık siparişi taban maliyete göre brüt zarar yazıyor."
|
return "FIYAT_BASKISI", "Bu musteri/urun acik siparisi taban maliyete gore brut zarar yaziyor."
|
||||||
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||||||
return "STOKSUZ_TALEP", "Müşteride karlı talep var ama stok yetersiz."
|
return "STOKSUZ_TALEP", "Musteride karli talep var ama stok yetersiz."
|
||||||
case row.ExpectedMarginCost >= 0.25 && row.ExpectedMarginBase >= 0.15:
|
case row.ExpectedMarginCost >= 0.25 && row.ExpectedMarginBase >= 0.15:
|
||||||
return "YILDIZ_URUN", "Müşteri bazında karlı açık talep var."
|
return "YILDIZ_URUN", "Musteri bazinda karli acik talep var."
|
||||||
case row.OverdueQty > 0:
|
case row.OverdueQty > 0:
|
||||||
return "TAKIP", "Bu müşteri/ürün kırılımında geciken açık sipariş var."
|
return "TAKIP", "Bu musteri/urun kiriliminda geciken acik siparis var."
|
||||||
default:
|
default:
|
||||||
return "TAKIP", "Müşteri talebi, fiyat ve stok birlikte izlenmeli."
|
return "TAKIP", "Musteri talebi, fiyat ve stok birlikte izlenmeli."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func productPerformanceOrderMarketDetailRecommendation(row models.ProductPerformanceOrderMarketDetailRow) (string, string) {
|
func productPerformanceOrderMarketDetailRecommendation(row models.ProductPerformanceOrderMarketDetailRow) (string, string) {
|
||||||
switch {
|
switch {
|
||||||
case row.ExpectedProfitCostUSD < 0:
|
case row.ExpectedProfitCostUSD < 0:
|
||||||
return "FIYAT_BASKISI", "Sipariş satırı çıplak maliyete göre zarar yazıyor."
|
return "FIYAT_BASKISI", "Siparis satiri ciplak maliyete gore zarar yaziyor."
|
||||||
case row.ExpectedProfitBaseUSD < 0:
|
case row.ExpectedProfitBaseUSD < 0:
|
||||||
return "FIYAT_BASKISI", "Sipariş satırı taban maliyete göre brüt zarar yazıyor."
|
return "FIYAT_BASKISI", "Siparis satiri taban maliyete gore brut zarar yaziyor."
|
||||||
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||||||
return "STOKSUZ_TALEP", "Karlı sipariş var ama mevcut stok siparişi karşılamıyor."
|
return "STOKSUZ_TALEP", "Karli siparis var ama mevcut stok siparisi karsilamiyor."
|
||||||
case row.IsOverdue:
|
case row.IsOverdue:
|
||||||
return "TAKIP", "Termin geçmiş; teslimat aksiyonu gerekli."
|
return "TAKIP", "Termin gecmis; teslimat aksiyonu gerekli."
|
||||||
case row.ExpectedMarginCost >= 0.25:
|
case row.ExpectedMarginCost >= 0.25:
|
||||||
return "YILDIZ_URUN", "Sipariş karlı; teslimat ve stok korunmalı."
|
return "YILDIZ_URUN", "Siparis karli; teslimat ve stok korunmali."
|
||||||
default:
|
default:
|
||||||
return "TAKIP", "Sipariş fiyatı, stok ve termin izlenmeli."
|
return "TAKIP", "Siparis fiyati, stok ve termin izlenmeli."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ SalesAgg AS (
|
|||||||
FROM mk_product_performance_sales_daily
|
FROM mk_product_performance_sales_daily
|
||||||
WHERE sales_date >= DATE '2022-01-01'
|
WHERE sales_date >= DATE '2022-01-01'
|
||||||
AND sales_date <= $1::date
|
AND sales_date <= $1::date
|
||||||
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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')
|
||||||
GROUP BY product_code, color_code, yaka_kodu, market_key
|
GROUP BY product_code, color_code, yaka_kodu, market_key
|
||||||
),
|
),
|
||||||
Scope AS (
|
Scope AS (
|
||||||
@@ -497,7 +497,7 @@ Scope AS (
|
|||||||
FROM LatestStock ls
|
FROM LatestStock ls
|
||||||
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code=ls.product_code
|
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code=ls.product_code
|
||||||
WHERE COALESCE(ls.stock_qty, 0) <> 0
|
WHERE COALESCE(ls.stock_qty, 0) <> 0
|
||||||
AND upper(btrim(COALESCE(pd.urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
AND upper(translate(btrim(COALESCE(pd.urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM SalesAgg sa
|
FROM SalesAgg sa
|
||||||
@@ -559,7 +559,7 @@ Base AS (
|
|||||||
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 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 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
|
||||||
WHERE upper(btrim(COALESCE(NULLIF(s.urun_ilk_grubu,''), pd.urun_ilk_grubu, ''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ 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 (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -638,12 +638,12 @@ SELECT
|
|||||||
ELSE 'TAKIP'
|
ELSE 'TAKIP'
|
||||||
END AS performance_bucket,
|
END AS performance_bucket,
|
||||||
CASE
|
CASE
|
||||||
WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.25 THEN 'Piyasa üstü satış ve iyi marj: stok ve fiyat gücü takip edilmeli.'
|
WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.25 THEN 'Piyasa ustu satis ve iyi marj: stok ve fiyat gucu takip edilmeli.'
|
||||||
WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'Talep var, stok yok: üretim/satın alma planına alınmalı.'
|
WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'Talep var, stok yok: uretim/satin alma planina alinmali.'
|
||||||
WHEN COALESCE(stock_days_90d,0) > 180 AND COALESCE(sales_index_90d,0) < 0.75 THEN 'Stok yüksek, satış piyasa altı: kampanya veya fiyat kontrolü gerekli.'
|
WHEN COALESCE(stock_days_90d,0) > 180 AND COALESCE(sales_index_90d,0) < 0.75 THEN 'Stok yuksek, satis piyasa alti: kampanya veya fiyat kontrolu gerekli.'
|
||||||
WHEN COALESCE(price_index_90d,0) > 1.10 AND COALESCE(sales_index_90d,0) < 0.80 THEN 'Fiyat piyasa üstünde ve satış zayıf: fiyat revizyonu değerlendirilmeli.'
|
WHEN COALESCE(price_index_90d,0) > 1.10 AND COALESCE(sales_index_90d,0) < 0.80 THEN 'Fiyat piyasa ustunde ve satis zayif: fiyat revizyonu degerlendirilmeli.'
|
||||||
WHEN COALESCE(price_index_90d,0) < 0.90 AND COALESCE(sales_index_90d,0) > 1.00 THEN 'Satış güçlü, fiyat piyasa altı: taban fiyat artışı değerlendirilebilir.'
|
WHEN COALESCE(price_index_90d,0) < 0.90 AND COALESCE(sales_index_90d,0) > 1.00 THEN 'Satis guclu, fiyat piyasa alti: taban fiyat artisi degerlendirilebilir.'
|
||||||
ELSE 'Düzenli takip.'
|
ELSE 'Duzenli takip.'
|
||||||
END AS recommendation,
|
END AS recommendation,
|
||||||
last_sale_date,
|
last_sale_date,
|
||||||
COALESCE(last_ref_number, ''),
|
COALESCE(last_ref_number, ''),
|
||||||
|
|||||||
@@ -593,6 +593,23 @@ func resolveStoragePath(storagePath string) (string, []string) {
|
|||||||
filepath.Join("..", "..", relImageThumb),
|
filepath.Join("..", "..", relImageThumb),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if u := extractImageUUID(raw, baseName); u != "" {
|
||||||
|
uFull := filepath.FromSlash(filepath.Join("uploads", "image", u+".jpg"))
|
||||||
|
uThumb := filepath.FromSlash(filepath.Join("uploads", "image", "t300", u+".jpg"))
|
||||||
|
candidates = append(candidates,
|
||||||
|
filepath.FromSlash(filepath.Join("image", u+".jpg")),
|
||||||
|
filepath.FromSlash(filepath.Join("image", "t300", u+".jpg")),
|
||||||
|
uFull,
|
||||||
|
uThumb,
|
||||||
|
filepath.Join(".", uFull),
|
||||||
|
filepath.Join(".", uThumb),
|
||||||
|
filepath.Join("..", uFull),
|
||||||
|
filepath.Join("..", uThumb),
|
||||||
|
filepath.Join("..", "..", uFull),
|
||||||
|
filepath.Join("..", "..", uThumb),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" {
|
if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" {
|
||||||
candidates = append(candidates,
|
candidates = append(candidates,
|
||||||
filepath.Join(root, raw),
|
filepath.Join(root, raw),
|
||||||
@@ -601,6 +618,14 @@ func resolveStoragePath(storagePath string) (string, []string) {
|
|||||||
filepath.Join(root, relImage),
|
filepath.Join(root, relImage),
|
||||||
filepath.Join(root, relImageThumb),
|
filepath.Join(root, relImageThumb),
|
||||||
)
|
)
|
||||||
|
if u := extractImageUUID(raw, baseName); u != "" {
|
||||||
|
candidates = append(candidates,
|
||||||
|
filepath.Join(root, "uploads", "image", u+".jpg"),
|
||||||
|
filepath.Join(root, "uploads", "image", "t300", u+".jpg"),
|
||||||
|
filepath.Join(root, "image", u+".jpg"),
|
||||||
|
filepath.Join(root, "image", "t300", u+".jpg"),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range candidates {
|
for _, p := range candidates {
|
||||||
|
|||||||
@@ -359,6 +359,64 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetProductPerformanceGroupedFilterOptionsHandler(pg *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
|
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
mode := strings.TrimSpace(r.URL.Query().Get("mode"))
|
||||||
|
mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu"))
|
||||||
|
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
|
||||||
|
fields := splitCSVQuery(r.URL.Query().Get("fields"))
|
||||||
|
limit := intQuery(r, "limit", 5000)
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
var body struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
GroupLevels []string `json:"group_levels"`
|
||||||
|
MainGroup string `json:"urun_ana_grubu"`
|
||||||
|
Fields []string `json:"fields"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
http.Error(w, "gecersiz filtre secenek istegi: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(body.Mode) != "" {
|
||||||
|
mode = strings.TrimSpace(body.Mode)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(body.MainGroup) != "" {
|
||||||
|
mainGroup = strings.TrimSpace(body.MainGroup)
|
||||||
|
}
|
||||||
|
if len(body.GroupLevels) > 0 {
|
||||||
|
groupLevels = body.GroupLevels
|
||||||
|
}
|
||||||
|
if len(body.Fields) > 0 {
|
||||||
|
fields = body.Fields
|
||||||
|
}
|
||||||
|
if body.Limit > 0 {
|
||||||
|
limit = body.Limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options, err := queries.ListProductPerformanceGroupedFilterOptions(ctx, pg, queries.ProductPerformanceGroupedFilterOptionsRequest{
|
||||||
|
Mode: mode,
|
||||||
|
GroupLevels: groupLevels,
|
||||||
|
MainGroup: mainGroup,
|
||||||
|
Fields: fields,
|
||||||
|
Limit: limit,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ProductPerformance][grouped-filter-options] failed trace_id=%s mode=%s levels=%v fields=%d: %v", traceID, mode, groupLevels, len(fields), err)
|
||||||
|
http.Error(w, "urun performans filtre secenekleri alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func splitCSVQuery(value string) []string {
|
func splitCSVQuery(value string) []string {
|
||||||
parts := strings.Split(value, ",")
|
parts := strings.Split(value, ",")
|
||||||
out := make([]string, 0, len(parts))
|
out := make([]string, 0, len(parts))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page class="product-performance-page q-pa-sm">
|
<q-page class="product-performance-page q-pa-sm">
|
||||||
<q-inner-loading :showing="pageBusy">
|
<q-inner-loading :showing="pageBusy">
|
||||||
<q-spinner-gears size="52px" color="primary" />
|
<q-spinner-gears size="52px" color="primary" />
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
v-if="getCachedProductImageUrl(props.row)"
|
v-if="getCachedProductImageUrl(props.row)"
|
||||||
:src="getCachedProductImageUrl(props.row)"
|
:src="getCachedProductImageUrl(props.row)"
|
||||||
class="product-thumb"
|
class="product-thumb"
|
||||||
@error="markProductImageFailed(props.row)"
|
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -311,7 +311,7 @@
|
|||||||
v-if="getCachedProductImageUrl(props.row)"
|
v-if="getCachedProductImageUrl(props.row)"
|
||||||
:src="getCachedProductImageUrl(props.row)"
|
:src="getCachedProductImageUrl(props.row)"
|
||||||
class="product-thumb"
|
class="product-thumb"
|
||||||
@error="markProductImageFailed(props.row)"
|
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -353,7 +353,7 @@
|
|||||||
v-if="getCachedProductImageUrl(props.row)"
|
v-if="getCachedProductImageUrl(props.row)"
|
||||||
:src="getCachedProductImageUrl(props.row)"
|
:src="getCachedProductImageUrl(props.row)"
|
||||||
class="product-thumb"
|
class="product-thumb"
|
||||||
@error="markProductImageFailed(props.row)"
|
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -374,7 +374,8 @@
|
|||||||
dense
|
dense
|
||||||
round
|
round
|
||||||
size="sm"
|
size="sm"
|
||||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
:icon="groupCanExpand(props.row) ? (isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right') : 'fiber_manual_record'"
|
||||||
|
:disable="!groupCanExpand(props.row)"
|
||||||
@click.stop="toggleGroup(props.row.key)"
|
@click.stop="toggleGroup(props.row.key)"
|
||||||
/>
|
/>
|
||||||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||||
@@ -390,7 +391,7 @@
|
|||||||
v-if="getCachedProductImageUrl(props.row)"
|
v-if="getCachedProductImageUrl(props.row)"
|
||||||
:src="getCachedProductImageUrl(props.row)"
|
:src="getCachedProductImageUrl(props.row)"
|
||||||
class="product-thumb"
|
class="product-thumb"
|
||||||
@error="markProductImageFailed(props.row)"
|
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -529,7 +530,8 @@
|
|||||||
dense
|
dense
|
||||||
round
|
round
|
||||||
size="sm"
|
size="sm"
|
||||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
:icon="groupCanExpand(props.row) ? (isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right') : 'fiber_manual_record'"
|
||||||
|
:disable="!groupCanExpand(props.row)"
|
||||||
@click.stop="toggleGroup(props.row.key)"
|
@click.stop="toggleGroup(props.row.key)"
|
||||||
/>
|
/>
|
||||||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||||
@@ -652,7 +654,7 @@
|
|||||||
v-if="getCachedProductImageUrl(props.row)"
|
v-if="getCachedProductImageUrl(props.row)"
|
||||||
:src="getCachedProductImageUrl(props.row)"
|
:src="getCachedProductImageUrl(props.row)"
|
||||||
class="product-thumb"
|
class="product-thumb"
|
||||||
@error="markProductImageFailed(props.row)"
|
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -673,7 +675,8 @@
|
|||||||
dense
|
dense
|
||||||
round
|
round
|
||||||
size="sm"
|
size="sm"
|
||||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
:icon="groupCanExpand(props.row) ? (isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right') : 'fiber_manual_record'"
|
||||||
|
:disable="!groupCanExpand(props.row)"
|
||||||
@click.stop="toggleGroup(props.row.key)"
|
@click.stop="toggleGroup(props.row.key)"
|
||||||
/>
|
/>
|
||||||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||||
@@ -689,7 +692,7 @@
|
|||||||
v-if="getCachedProductImageUrl(props.row)"
|
v-if="getCachedProductImageUrl(props.row)"
|
||||||
:src="getCachedProductImageUrl(props.row)"
|
:src="getCachedProductImageUrl(props.row)"
|
||||||
class="product-thumb"
|
class="product-thumb"
|
||||||
@error="markProductImageFailed(props.row)"
|
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -801,7 +804,7 @@
|
|||||||
v-if="getCachedProductImageUrl(props.row)"
|
v-if="getCachedProductImageUrl(props.row)"
|
||||||
:src="getCachedProductImageUrl(props.row)"
|
:src="getCachedProductImageUrl(props.row)"
|
||||||
class="product-thumb"
|
class="product-thumb"
|
||||||
@error="markProductImageFailed(props.row)"
|
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -839,7 +842,7 @@
|
|||||||
v-if="getCachedProductImageUrl(props.row)"
|
v-if="getCachedProductImageUrl(props.row)"
|
||||||
:src="getCachedProductImageUrl(props.row)"
|
:src="getCachedProductImageUrl(props.row)"
|
||||||
class="product-thumb"
|
class="product-thumb"
|
||||||
@error="markProductImageFailed(props.row)"
|
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -860,7 +863,8 @@
|
|||||||
dense
|
dense
|
||||||
round
|
round
|
||||||
size="sm"
|
size="sm"
|
||||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
:icon="groupCanExpand(props.row) ? (isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right') : 'fiber_manual_record'"
|
||||||
|
:disable="!groupCanExpand(props.row)"
|
||||||
@click.stop="toggleGroup(props.row.key)"
|
@click.stop="toggleGroup(props.row.key)"
|
||||||
/>
|
/>
|
||||||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||||
@@ -876,7 +880,7 @@
|
|||||||
v-if="getCachedProductImageUrl(props.row)"
|
v-if="getCachedProductImageUrl(props.row)"
|
||||||
:src="getCachedProductImageUrl(props.row)"
|
:src="getCachedProductImageUrl(props.row)"
|
||||||
class="product-thumb"
|
class="product-thumb"
|
||||||
@error="markProductImageFailed(props.row)"
|
@error="event => markProductImageFailed(props.row, event?.target?.currentSrc)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -1127,7 +1131,8 @@
|
|||||||
dense
|
dense
|
||||||
round
|
round
|
||||||
size="sm"
|
size="sm"
|
||||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
:icon="groupCanExpand(props.row) ? (isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right') : 'fiber_manual_record'"
|
||||||
|
:disable="!groupCanExpand(props.row)"
|
||||||
@click.stop="toggleGroup(props.row.key)"
|
@click.stop="toggleGroup(props.row.key)"
|
||||||
/>
|
/>
|
||||||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||||
@@ -1586,6 +1591,7 @@ const detailLoading = ref(false)
|
|||||||
const detailSalesRows = ref([])
|
const detailSalesRows = ref([])
|
||||||
const detailStockSizes = ref([])
|
const detailStockSizes = ref([])
|
||||||
const backendGroupedRows = ref({})
|
const backendGroupedRows = ref({})
|
||||||
|
const backendGroupedFilterOptions = ref({})
|
||||||
const backendGroupedLoading = ref(false)
|
const backendGroupedLoading = ref(false)
|
||||||
const imagePrimeLoading = ref(false)
|
const imagePrimeLoading = ref(false)
|
||||||
const filterBusy = ref(false)
|
const filterBusy = ref(false)
|
||||||
@@ -1644,9 +1650,6 @@ const backendGroupedFilterFields = new Set([
|
|||||||
'urun_ana_grubu',
|
'urun_ana_grubu',
|
||||||
'urun_alt_grubu',
|
'urun_alt_grubu',
|
||||||
'product_code',
|
'product_code',
|
||||||
'item_description',
|
|
||||||
'color_code',
|
|
||||||
'yaka_kodu',
|
|
||||||
'color_yaka',
|
'color_yaka',
|
||||||
'market_key',
|
'market_key',
|
||||||
'country',
|
'country',
|
||||||
@@ -2725,8 +2728,22 @@ const columnFilterOptionMap = computed(() => {
|
|||||||
const source = tableFilterSource(tableKey)
|
const source = tableFilterSource(tableKey)
|
||||||
if (!source) return out
|
if (!source) return out
|
||||||
out[tableKey] = {}
|
out[tableKey] = {}
|
||||||
|
const preparedOptionEntry = backendGroupedSupportedTab(tableKey)
|
||||||
|
? backendGroupedFilterOptions.value[tableKey]
|
||||||
|
: null
|
||||||
|
const preparedOptions = preparedOptionEntry?._key === backendGroupedFilterOptionsKey(tableKey)
|
||||||
|
? preparedOptionEntry.options || {}
|
||||||
|
: {}
|
||||||
for (const col of source.columns) {
|
for (const col of source.columns) {
|
||||||
if (!isColumnFilterable(col.name)) continue
|
if (!isColumnFilterable(col.name)) continue
|
||||||
|
if (backendGroupedSupportedTab(tableKey) && backendGroupedFilterFields.has(col.name)) {
|
||||||
|
out[tableKey][col.name] = preparedOptions[col.name] || []
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (backendGroupedSupportedTab(tableKey) && !backendGroupedFilterFields.has(col.name)) {
|
||||||
|
out[tableKey][col.name] = []
|
||||||
|
continue
|
||||||
|
}
|
||||||
const seen = new Map()
|
const seen = new Map()
|
||||||
for (const row of source.rows) {
|
for (const row of source.rows) {
|
||||||
const value = columnFilterValue(row, col)
|
const value = columnFilterValue(row, col)
|
||||||
@@ -2896,6 +2913,85 @@ function filterSourceRowsForTab (tabKey, fallbackRows) {
|
|||||||
return fallbackRows
|
return fallbackRows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function backendGroupedFilterOptionParams (tabKey) {
|
||||||
|
const columns = columnsForTableKey(tabKey)
|
||||||
|
const fields = columns
|
||||||
|
.map(col => col?.name || '')
|
||||||
|
.filter(name => backendGroupedFilterFields.has(name))
|
||||||
|
const params = {
|
||||||
|
mode: tabKey,
|
||||||
|
groupLevels: (tabGroupLevels[tabKey] || groupLevels).map(level => level.key),
|
||||||
|
fields,
|
||||||
|
limit: 5000
|
||||||
|
}
|
||||||
|
if (tabKey === 'product_detail') params.urunAnaGrubu = selectedDetailMainGroup.value
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
function backendGroupedFilterOptionsKey (tabKey, params = backendGroupedFilterOptionParams(tabKey)) {
|
||||||
|
return JSON.stringify({
|
||||||
|
tabKey,
|
||||||
|
mainGroup: params.urunAnaGrubu || '',
|
||||||
|
groupLevels: params.groupLevels || [],
|
||||||
|
fields: params.fields || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureBackendGroupedFilterOptions (tabKey = activeTab.value, force = false) {
|
||||||
|
if (!backendGroupedSupportedTab(tabKey)) return
|
||||||
|
if (tabKey === 'product_detail' && !selectedDetailMainGroup.value) return
|
||||||
|
const params = backendGroupedFilterOptionParams(tabKey)
|
||||||
|
if (!params.fields.length) return
|
||||||
|
const key = backendGroupedFilterOptionsKey(tabKey, params)
|
||||||
|
if (!force && backendGroupedFilterOptions.value[tabKey]?._key === key) return
|
||||||
|
try {
|
||||||
|
const data = await performanceStore.fetchGroupedFilterOptions(params, { force })
|
||||||
|
if (backendGroupedFilterOptionsKey(tabKey) !== key) return
|
||||||
|
backendGroupedFilterOptions.value = {
|
||||||
|
...backendGroupedFilterOptions.value,
|
||||||
|
[tabKey]: {
|
||||||
|
_key: key,
|
||||||
|
options: normalizeBackendGroupedFilterOptions(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('product performance grouped filter options failed', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBackendGroupedFilterOptions (data) {
|
||||||
|
const out = {}
|
||||||
|
const source = data && typeof data === 'object' ? data : {}
|
||||||
|
for (const [field, values] of Object.entries(source)) {
|
||||||
|
if (!Array.isArray(values)) continue
|
||||||
|
const seen = new Map()
|
||||||
|
for (const raw of values) {
|
||||||
|
const value = normalizeBackendFilterValue(field, raw)
|
||||||
|
if (!value || seen.has(value)) continue
|
||||||
|
seen.set(value, {
|
||||||
|
value,
|
||||||
|
label: backendFilterOptionLabel(field, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
out[field] = Array.from(seen.values())
|
||||||
|
.sort((a, b) => String(a.label).localeCompare(String(b.label), 'tr', { numeric: true }))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBackendFilterValue (name, value) {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (name === 'performance_bucket') {
|
||||||
|
return bucketOptions.find(option => option.value === text || option.label === text)?.value || text
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
function backendFilterOptionLabel (name, value) {
|
||||||
|
if (name === 'performance_bucket') return bucketLabel(value)
|
||||||
|
return String(value || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
function backendGroupedFilterState (tabKey) {
|
function backendGroupedFilterState (tabKey) {
|
||||||
const out = {}
|
const out = {}
|
||||||
const columns = columnsForTableKey(tabKey)
|
const columns = columnsForTableKey(tabKey)
|
||||||
@@ -2907,7 +3003,7 @@ function backendGroupedFilterState (tabKey) {
|
|||||||
if (!backendGroupedFilterFields.has(name)) {
|
if (!backendGroupedFilterFields.has(name)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out[name] = selected.map(value => String(value || '').trim()).filter(Boolean)
|
out[name] = selected.map(value => normalizeBackendFilterValue(name, value)).filter(Boolean)
|
||||||
}
|
}
|
||||||
return { filters: out, unsupported: false }
|
return { filters: out, unsupported: false }
|
||||||
}
|
}
|
||||||
@@ -3298,18 +3394,16 @@ function salesGroupPerformanceScore (row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function productSalesPeriodScore (row) {
|
function productSalesPeriodScore (row) {
|
||||||
const salesIndex = Number(row?.sales_index_90d || 0)
|
const suffix = row?.suffix || '90d'
|
||||||
const margin = Number(row?.gross_margin_cost_90d ?? 0)
|
return productScore100({
|
||||||
const invoices = Number(row?.invoice_count_90d || 0)
|
suffix,
|
||||||
const qty = Number(row?.sales_qty_90d || 0)
|
salesUSD: Number(row?.sales_usd_90d || 0),
|
||||||
let stockTurnover = Number(row?.stock_turnover_90d || 0)
|
salesIndex: Number(row?.sales_index_90d || 0),
|
||||||
const stockQty = Number(row?.stock_qty || 0)
|
margin: Number(row?.gross_margin_cost_90d ?? row?.gross_margin_90d ?? 0),
|
||||||
if (!stockTurnover && stockQty > 0) stockTurnover = qty / stockQty
|
stockTurnover: Number(row?.stock_turnover_90d || 0),
|
||||||
return Math.min(35, Math.max(0, salesIndex) * 18) +
|
marketCount: Number(row?.market_count_90d || 0),
|
||||||
Math.min(30, Math.max(0, margin) * 60) +
|
customerCount: Number(row?.customer_count_90d || 0)
|
||||||
Math.min(15, invoices * 1.5) +
|
})
|
||||||
Math.min(10, qty / 10) +
|
|
||||||
Math.min(10, Math.max(0, stockTurnover) * 5)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function productPeriodMetricSource (row, suffix) {
|
function productPeriodMetricSource (row, suffix) {
|
||||||
@@ -3319,12 +3413,15 @@ function productPeriodMetricSource (row, suffix) {
|
|||||||
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 {
|
||||||
|
suffix,
|
||||||
sales_index_90d: salesIndex,
|
sales_index_90d: salesIndex,
|
||||||
|
sales_usd_90d: Number(row?.[`sales_usd_${suffix}`] || 0),
|
||||||
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),
|
||||||
invoice_count_90d: Number(row?.[`invoice_count_${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) : (stockQty > 0 ? qty / stockQty : 0),
|
||||||
|
market_count_90d: periodCount(row, 'market_count', suffix),
|
||||||
|
customer_count_90d: periodCount(row, 'customer_count', suffix),
|
||||||
stock_qty: stockQty
|
stock_qty: stockQty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3343,29 +3440,92 @@ function customerSalesGroupPerformanceScore (row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function customerSalesPeriodScore (row) {
|
function customerSalesPeriodScore (row) {
|
||||||
const salesUSD = Number(row?.sales_usd || 0)
|
return customerScore100({
|
||||||
const margin = Number(row?.gross_margin_cost ?? 0)
|
suffix: row?.suffix || '90d',
|
||||||
const invoices = Number(row?.invoice_count || 0)
|
salesUSD: Number(row?.sales_usd || 0),
|
||||||
const qty = Number(row?.sales_qty || 0)
|
margin: Number(row?.gross_margin_cost ?? row?.gross_margin ?? 0),
|
||||||
const productCount = Number(row?.product_count || 0)
|
productGroupCount: Number(row?.product_group_count || row?.product_count || 0),
|
||||||
return Math.min(35, salesUSD / 1000) +
|
salesQty: Number(row?.sales_qty || 0)
|
||||||
Math.min(25, Math.max(0, margin) * 55) +
|
})
|
||||||
Math.min(20, invoices * 2) +
|
|
||||||
Math.min(10, qty / 10) +
|
|
||||||
Math.min(10, productCount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function periodMetricSource (row, suffix) {
|
function periodMetricSource (row, suffix) {
|
||||||
return {
|
return {
|
||||||
|
suffix,
|
||||||
sales_usd: Number(row?.[`sales_usd_${suffix}`] || 0),
|
sales_usd: Number(row?.[`sales_usd_${suffix}`] || 0),
|
||||||
gross_margin_cost: Number(row?.[`gross_margin_cost_${suffix}`] ?? 0),
|
gross_margin_cost: Number(row?.[`gross_margin_cost_${suffix}`] ?? 0),
|
||||||
gross_margin: Number(row?.[`gross_margin_${suffix}`] ?? 0),
|
gross_margin: Number(row?.[`gross_margin_${suffix}`] ?? 0),
|
||||||
invoice_count: Number(row?.[`invoice_count_${suffix}`] || 0),
|
|
||||||
sales_qty: Number(row?.[`sales_qty_${suffix}`] || 0),
|
sales_qty: Number(row?.[`sales_qty_${suffix}`] || 0),
|
||||||
|
product_group_count: periodCount(row, 'product_group_count', suffix),
|
||||||
product_count: Number(row?.product_count || 0)
|
product_count: Number(row?.product_count || 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function productScore100 ({ suffix = '90d', salesUSD = 0, salesIndex = 0, margin = 0, stockTurnover = 0, marketCount = 0, customerCount = 0 } = {}) {
|
||||||
|
const revenue = salesIndex > 0 ? ratioScore(salesIndex, 2) : ratioScore(salesUSD, productRevenueTarget(suffix))
|
||||||
|
return clampScore(
|
||||||
|
0.30 * marginScore(margin) +
|
||||||
|
0.20 * ratioScore(stockTurnover, 1.5) +
|
||||||
|
0.20 * revenue +
|
||||||
|
0.15 * ratioScore(marketCount, 8) +
|
||||||
|
0.15 * ratioScore(customerCount, 25)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function customerScore100 ({ suffix = '90d', salesUSD = 0, margin = 0, productGroupCount = 0, salesQty = 0 } = {}) {
|
||||||
|
return clampScore(
|
||||||
|
0.35 * ratioScore(salesUSD, customerRevenueTarget(suffix)) +
|
||||||
|
0.30 * marginScore(margin) +
|
||||||
|
0.20 * ratioScore(productGroupCount, 8) +
|
||||||
|
0.15 * ratioScore(salesQty, customerQtyTarget(suffix))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ratioScore (value, target) {
|
||||||
|
const n = Number(value || 0)
|
||||||
|
const t = Number(target || 0)
|
||||||
|
if (!Number.isFinite(n) || !Number.isFinite(t) || n <= 0 || t <= 0) return 0
|
||||||
|
return Math.min(100, Math.max(0, n) * 100 / t)
|
||||||
|
}
|
||||||
|
|
||||||
|
function marginScore (margin) {
|
||||||
|
return ratioScore(Math.max(0, Number(margin || 0)), 0.40)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampScore (score) {
|
||||||
|
const n = Number(score || 0)
|
||||||
|
if (!Number.isFinite(n) || n <= 0) return 0
|
||||||
|
return Math.round(Math.min(100, n) * 10000) / 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
function productRevenueTarget (suffix) {
|
||||||
|
if (suffix === '180d') return 20000
|
||||||
|
if (suffix === '365d') return 40000
|
||||||
|
if (suffix === 'total') return 120000
|
||||||
|
return 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
function customerRevenueTarget (suffix) {
|
||||||
|
if (suffix === '180d') return 50000
|
||||||
|
if (suffix === '365d') return 100000
|
||||||
|
if (suffix === 'total') return 300000
|
||||||
|
return 25000
|
||||||
|
}
|
||||||
|
|
||||||
|
function customerQtyTarget (suffix) {
|
||||||
|
if (suffix === '180d') return 1000
|
||||||
|
if (suffix === '365d') return 2000
|
||||||
|
if (suffix === 'total') return 6000
|
||||||
|
return 500
|
||||||
|
}
|
||||||
|
|
||||||
|
function periodCount (row, prefix, suffix) {
|
||||||
|
const keyed = Number(row?.[`${prefix}_${suffix}`] || 0)
|
||||||
|
if (keyed > 0) return keyed
|
||||||
|
if (suffix === 'total') return Number(row?.[`${prefix}_total`] || 0)
|
||||||
|
return Number(row?.[`${prefix}_90d`] || row?.[prefix] || 0)
|
||||||
|
}
|
||||||
|
|
||||||
function orderGroupPerformanceScore (row) {
|
function orderGroupPerformanceScore (row) {
|
||||||
const orderUSD = Number(row?.order_usd || 0)
|
const orderUSD = Number(row?.order_usd || 0)
|
||||||
const margin = Number(row?.expected_margin_cost || 0)
|
const margin = Number(row?.expected_margin_cost || 0)
|
||||||
@@ -3549,7 +3709,16 @@ function shouldSkipGroupValue (field, value) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupCanExpand (rowOrKey) {
|
||||||
|
const key = typeof rowOrKey === 'string' ? rowOrKey : rowOrKey?.key
|
||||||
|
const depth = groupKeyDepth(key)
|
||||||
|
const tabKey = tabKeyFromGroupKey(key)
|
||||||
|
const levels = tabGroupLevels[tabKey] || groupLevels
|
||||||
|
return depth >= 0 && depth < levels.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
function isGroupExpanded (key) {
|
function isGroupExpanded (key) {
|
||||||
|
if (!groupCanExpand(key)) return false
|
||||||
const tabKey = tabKeyFromGroupKey(key)
|
const tabKey = tabKeyFromGroupKey(key)
|
||||||
const depth = groupKeyDepth(key)
|
const depth = groupKeyDepth(key)
|
||||||
const level = selectedExpandThroughLevelForTab(tabKey)
|
const level = selectedExpandThroughLevelForTab(tabKey)
|
||||||
@@ -3609,6 +3778,7 @@ function applyActiveExpansionThroughLevel (level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleGroup (key) {
|
function toggleGroup (key) {
|
||||||
|
if (!groupCanExpand(key)) return
|
||||||
const level = groupKeyDepth(key)
|
const level = groupKeyDepth(key)
|
||||||
applyActiveExpansionThroughLevel(isGroupExpanded(key) ? level - 1 : level)
|
applyActiveExpansionThroughLevel(isGroupExpanded(key) ? level - 1 : level)
|
||||||
}
|
}
|
||||||
@@ -3990,7 +4160,7 @@ function parseSortNumber (value) {
|
|||||||
if (!text) return NaN
|
if (!text) return NaN
|
||||||
const normalized = text
|
const normalized = text
|
||||||
.replace(/\s/g, '')
|
.replace(/\s/g, '')
|
||||||
.replace(/[%$₺€£]/g, '')
|
.replace(/[%$\u20ba\u20ac\u00a3]/g, '')
|
||||||
.replace(/\.(?=\d{3}(\D|$))/g, '')
|
.replace(/\.(?=\d{3}(\D|$))/g, '')
|
||||||
.replace(/,/g, '.')
|
.replace(/,/g, '.')
|
||||||
.replace(/[^\d.-]/g, '')
|
.replace(/[^\d.-]/g, '')
|
||||||
@@ -4274,13 +4444,6 @@ function resolveProductImageUrls (item) {
|
|||||||
if (url && !urls.includes(url)) urls.push(url)
|
if (url && !urls.includes(url)) urls.push(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
|
||||||
if (contentURL.startsWith('/api/')) addURL(contentURL)
|
|
||||||
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
|
|
||||||
|
|
||||||
const imageId = Number(item.id || item.ID || 0)
|
|
||||||
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
|
|
||||||
|
|
||||||
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
|
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
|
||||||
addURL(thumbURL)
|
addURL(thumbURL)
|
||||||
|
|
||||||
@@ -4292,6 +4455,13 @@ function resolveProductImageUrls (item) {
|
|||||||
|
|
||||||
const fileName = String(item.file_name || item.FileName || '').trim()
|
const fileName = String(item.file_name || item.FileName || '').trim()
|
||||||
if (fileName) addURL(`/uploads/image/${fileName}`)
|
if (fileName) addURL(`/uploads/image/${fileName}`)
|
||||||
|
|
||||||
|
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||||
|
if (contentURL.startsWith('/api/')) addURL(contentURL)
|
||||||
|
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
|
||||||
|
|
||||||
|
const imageId = Number(item.id || item.ID || 0)
|
||||||
|
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
|
||||||
return urls
|
return urls
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4369,7 +4539,7 @@ function markProductImageFailed (row, failedUrl = '') {
|
|||||||
if (!key) return
|
if (!key) return
|
||||||
const currentUrl = String(failedUrl || imageUrlByKey.value[key] || performanceStore.imageUrl(key) || '').trim()
|
const currentUrl = String(failedUrl || imageUrlByKey.value[key] || performanceStore.imageUrl(key) || '').trim()
|
||||||
const list = imageListByKey.value[key] || performanceStore.imageList(key) || []
|
const list = imageListByKey.value[key] || performanceStore.imageList(key) || []
|
||||||
const nextList = Array.isArray(list) ? list.filter(url => url && url !== currentUrl) : []
|
const nextList = Array.isArray(list) ? list.filter(url => url && !sameImageUrl(url, currentUrl)) : []
|
||||||
if (nextList.length) {
|
if (nextList.length) {
|
||||||
performanceStore.setImageCache(key, nextList)
|
performanceStore.setImageCache(key, nextList)
|
||||||
imageListByKey.value = { ...imageListByKey.value, [key]: nextList }
|
imageListByKey.value = { ...imageListByKey.value, [key]: nextList }
|
||||||
@@ -4382,6 +4552,18 @@ function markProductImageFailed (row, failedUrl = '') {
|
|||||||
imageUrlByKey.value = { ...imageUrlByKey.value, [key]: '' }
|
imageUrlByKey.value = { ...imageUrlByKey.value, [key]: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sameImageUrl (left, right) {
|
||||||
|
const a = String(left || '').trim()
|
||||||
|
const b = String(right || '').trim()
|
||||||
|
if (!a || !b) return false
|
||||||
|
if (a === b) return true
|
||||||
|
try {
|
||||||
|
return new URL(a, window.location.origin).href === new URL(b, window.location.origin).href
|
||||||
|
} catch {
|
||||||
|
return a.endsWith(b) || b.endsWith(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function markDialogImageFailed (url) {
|
function markDialogImageFailed (url) {
|
||||||
const failedUrl = String(url || '').trim()
|
const failedUrl = String(url || '').trim()
|
||||||
if (!failedUrl) return
|
if (!failedUrl) return
|
||||||
@@ -4622,6 +4804,7 @@ async function loadBackendGroupedRows (options = {}) {
|
|||||||
backendGroupedLoading.value = false
|
backendGroupedLoading.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
void ensureBackendGroupedFilterOptions(tabKey)
|
||||||
pruneActiveExpandedGroups()
|
pruneActiveExpandedGroups()
|
||||||
backendGroupedLoading.value = true
|
backendGroupedLoading.value = true
|
||||||
const filtersKey = JSON.stringify(filterState.filters)
|
const filtersKey = JSON.stringify(filterState.filters)
|
||||||
|
|||||||
@@ -26,13 +26,6 @@ function resolveProductImageUrls (item) {
|
|||||||
if (url && !urls.includes(url)) urls.push(url)
|
if (url && !urls.includes(url)) urls.push(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
|
||||||
if (contentURL.startsWith('/api/')) addURL(contentURL)
|
|
||||||
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
|
|
||||||
|
|
||||||
const imageId = Number(item.id || item.ID || 0)
|
|
||||||
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
|
|
||||||
|
|
||||||
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
|
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
|
||||||
addURL(thumbURL)
|
addURL(thumbURL)
|
||||||
|
|
||||||
@@ -44,6 +37,13 @@ function resolveProductImageUrls (item) {
|
|||||||
|
|
||||||
const fileName = String(item.file_name || item.FileName || '').trim()
|
const fileName = String(item.file_name || item.FileName || '').trim()
|
||||||
if (fileName) addURL(`/uploads/image/${fileName}`)
|
if (fileName) addURL(`/uploads/image/${fileName}`)
|
||||||
|
|
||||||
|
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||||
|
if (contentURL.startsWith('/api/')) addURL(contentURL)
|
||||||
|
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
|
||||||
|
|
||||||
|
const imageId = Number(item.id || item.ID || 0)
|
||||||
|
if (Number.isFinite(imageId) && imageId > 0) addURL(`/api/product-images/${imageId}/content`)
|
||||||
return urls
|
return urls
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +69,9 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
groupedLoadedAtByKey: {},
|
groupedLoadedAtByKey: {},
|
||||||
groupedLoadingByKey: {},
|
groupedLoadingByKey: {},
|
||||||
groupedInFlightByKey: {},
|
groupedInFlightByKey: {},
|
||||||
|
groupedFilterOptionsByKey: {},
|
||||||
|
groupedFilterOptionsLoadedAtByKey: {},
|
||||||
|
groupedFilterOptionsInFlightByKey: {},
|
||||||
imageUrlByKey: {},
|
imageUrlByKey: {},
|
||||||
imageListByKey: {},
|
imageListByKey: {},
|
||||||
imageLoadedAtByKey: {}
|
imageLoadedAtByKey: {}
|
||||||
@@ -96,6 +99,16 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
groupedFilterOptionsCacheKey (params = {}) {
|
||||||
|
return JSON.stringify({
|
||||||
|
mode: String(params.mode || ''),
|
||||||
|
mainGroup: String(params.urunAnaGrubu || params.urun_ana_grubu || ''),
|
||||||
|
groupLevels: normalizedList(params.groupLevels),
|
||||||
|
fields: normalizedList(params.fields, true),
|
||||||
|
limit: Number(params.limit || 0)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
async fetchGroupedRows (params = {}, options = {}) {
|
async fetchGroupedRows (params = {}, options = {}) {
|
||||||
const key = this.groupedCacheKey(params)
|
const key = this.groupedCacheKey(params)
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
@@ -180,6 +193,35 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
return request
|
return request
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetchGroupedFilterOptions (params = {}, options = {}) {
|
||||||
|
const key = this.groupedFilterOptionsCacheKey(params)
|
||||||
|
const now = Date.now()
|
||||||
|
const loadedAt = Number(this.groupedFilterOptionsLoadedAtByKey[key] || 0)
|
||||||
|
const cached = this.groupedFilterOptionsByKey[key]
|
||||||
|
if (!options.force && cached && now - loadedAt < GROUPED_TTL_MS) return cached
|
||||||
|
if (this.groupedFilterOptionsInFlightByKey[key]) return this.groupedFilterOptionsInFlightByKey[key]
|
||||||
|
|
||||||
|
const request = api.post('/pricing/product-performance/grouped/filter-options', {
|
||||||
|
mode: params.mode,
|
||||||
|
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
|
||||||
|
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
|
||||||
|
fields: Array.isArray(params.fields) ? params.fields : String(params.fields || '').split(',').filter(Boolean),
|
||||||
|
limit: params.limit || 5000
|
||||||
|
}, { timeout: 60000 }).then(resp => {
|
||||||
|
const data = resp?.data && typeof resp.data === 'object' ? resp.data : {}
|
||||||
|
this.groupedFilterOptionsByKey = { ...this.groupedFilterOptionsByKey, [key]: data }
|
||||||
|
this.groupedFilterOptionsLoadedAtByKey = { ...this.groupedFilterOptionsLoadedAtByKey, [key]: Date.now() }
|
||||||
|
return data
|
||||||
|
}).finally(() => {
|
||||||
|
const nextInFlight = { ...this.groupedFilterOptionsInFlightByKey }
|
||||||
|
delete nextInFlight[key]
|
||||||
|
this.groupedFilterOptionsInFlightByKey = nextInFlight
|
||||||
|
})
|
||||||
|
|
||||||
|
this.groupedFilterOptionsInFlightByKey = { ...this.groupedFilterOptionsInFlightByKey, [key]: request }
|
||||||
|
return request
|
||||||
|
},
|
||||||
|
|
||||||
setImageCache (key, urls = []) {
|
setImageCache (key, urls = []) {
|
||||||
const normalizedKey = String(key || '').trim()
|
const normalizedKey = String(key || '').trim()
|
||||||
if (!normalizedKey) return
|
if (!normalizedKey) return
|
||||||
|
|||||||
Reference in New Issue
Block a user