@@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"log"
"math"
"os"
"sort"
"strconv"
@@ -372,10 +373,10 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_sales_daily (
UPDATE mk_product_performance_sales_daily
SET urun_ilk_grubu = ' ' , updated_at = now ( )
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) IN ( ' YETISKIN ' , ' YETISKIN / GARSON ' , ' GARSON ' ) ` ,
`
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' ) ` ,
`
UPDATE mk_product_performance_sales_daily
SET askili_yan = ' ' , updated_at = now ( )
@@ -452,7 +453,7 @@ WHERE cost_price_usd > 0
AND cost_price_usd < base_price_usd ` ,
`
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' ) ` ,
`
CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
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
SET urun_ilk_grubu = ' ' , updated_at = now ( )
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) IN ( ' YETISKIN ' , ' YETISKIN / GARSON ' , ' GARSON ' ) ` ,
`
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' ) ` ,
`
UPDATE mk_product_performance_kpi_daily
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_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 (
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 {
return nil , 0 , err
} else if ok {
applyProductPerformanceProductRowScores ( rows )
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 {
out [ i ] . ColorDescription = colorDescriptions [ normalizeProductPerformanceCode ( out [ i ] . ColorCode ) ]
}
applyProductPerformanceProductRowScores ( out )
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 ) {
if err := EnsureProductPerformanceTables ( pg ) ; err != nil {
return models . ProductPerformanceSummary { } , err
@@ -1841,7 +1879,7 @@ KPI AS (
SELECT *
FROM mk_product_performance_kpi_daily
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
) ,
VariantStock AS (
SELECT
@@ -1868,7 +1906,7 @@ Sales90 AS (
COUNT ( DISTINCT NULLIF ( customer_code , '-' ) ) AS customer_count_90d
FROM mk_product_performance_sales_daily
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
)
SELECT
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 {
return nil , err
} else if ok {
applyProductPerformanceGeneralRowScores ( rows )
return rows , nil
}
rows , err := pg . QueryContext ( ctx , `
@@ -1943,7 +1982,7 @@ SalesAgg AS (
COALESCE ( SUM ( invoice_count ) , 0 ) : : integer AS invoice_count_total
FROM mk_product_performance_sales_daily , Bounds
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
GROUP BY product_code , color_code , yaka_kodu , market_key
) ,
StockAgg AS (
@@ -1995,7 +2034,7 @@ Dim AS (
1 AS src_rank
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
UNION ALL
SELECT
product_code ,
@@ -2011,7 +2050,7 @@ Dim AS (
MAX ( urun_alt_grubu ) AS urun_alt_grubu ,
2 AS src_rank
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
GROUP BY product_code , color_code , yaka_kodu
) x
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 . yaka_kodu = s . yaka_kodu
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
AND NOT EXISTS (
SELECT 1
FROM SalesAgg a
@@ -2067,7 +2106,7 @@ Spread AS (
COUNT ( DISTINCT NULLIF ( customer_code , '-' ) ) : : integer AS customer_count_total_all
FROM mk_product_performance_sales_daily , Bounds
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
GROUP BY product_code , color_code , yaka_kodu
) ,
Scored AS (
@@ -2199,9 +2238,28 @@ LIMIT $1
for i := range out {
out [ i ] . ColorDescription = colorDescriptions [ normalizeProductPerformanceCode ( out [ i ] . ColorCode ) ]
}
applyProductPerformanceGeneralRowScores ( out )
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 ) {
if db . MssqlDB == 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
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
GROUP BY market_key
ORDER BY risk_stock_cost_value_usd DESC , stock_cost_value_usd DESC , sales_usd_90d DESC
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
FROM mk_product_performance_sales_daily
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
GROUP BY customer_country , customer_segment , market_key
ORDER BY sales_usd_90d DESC , sales_qty_90d DESC
LIMIT $ 1
@@ -3207,7 +3265,7 @@ SELECT
FROM mk_product_performance_sales_daily
WHERE sales_date >= current_date - INTERVAL ' 364 days '
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
GROUP BY % s
ORDER BY sales_usd_90d DESC , sales_qty_90d DESC
LIMIT $ 1
@@ -3250,7 +3308,7 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
"item_description" : "MAX(s.item_description)" ,
"kategori" : "COALESCE(MAX(s.kategori),'')" ,
"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_alt_grubu" : "MAX(s.urun_alt_grubu)" ,
"market_key" : "s.market_key" ,
@@ -3277,7 +3335,7 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
selected [ "market_key" ] = true
selected [ "customer_code" ] = 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" :
selected [ "product_code" ] = true
selected [ "item_description" ] = true
@@ -3340,12 +3398,13 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
selected [ "market_key" ] = true
selected [ "customer_code" ] = 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 {
return nil , err
} else if ok {
applyProductPerformanceSalesBreakdownScores ( rows )
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 ( 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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) IN ( ' YETISKIN ' , ' YETISKIN / GARSON ' , ' GARSON ' ) THEN ' '
ELSE COALESCE ( urun_ilk_grubu , ' ' )
END AS urun_ilk_grubu ,
COALESCE ( urun_ana_grubu , ' ' ) AS urun_ana_grubu ,
urun_alt_grubu
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
ORDER BY product_code , color_code , yaka_kodu , performance_score DESC
) ,
StockAgg AS (
@@ -3444,6 +3503,14 @@ Agg AS (
% s ,
% s ,
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 ( avg_stock_90d ) , 0 ) AS avg_stock_90d ,
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
) s
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
GROUP BY % s
) ,
Enriched AS (
@@ -3584,6 +3651,14 @@ SELECT
customer_code ,
customer_name ,
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 ,
customer_count_90d ,
invoice_count_90d ,
@@ -3690,8 +3765,8 @@ SELECT
ELSE ' TAKIP '
END AS performance_bucket ,
CASE
WHEN sales_index_90d >= 1.25 AND customer_count_90d >= 2 THEN 'Güçlü satış kı rı lı mı '
WHEN sales_qty_90d = 0 AND sales_qty_365d > 0 THEN 'Son 90 gün zayı f, kontrol et'
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 gun zayif , kontrol et '
ELSE ' Takip '
END AS recommendation ,
last_sale_date
@@ -3729,7 +3804,9 @@ LIMIT $1
& r . Breakdown , & r . ProductCode , & r . ColorCode , & r . YakaKodu , & r . ItemDescription ,
& r . Kategori , & r . AskiliYan , & r . UrunIlkGrubu , & r . UrunAnaGrubu , & r . UrunAltGrubu ,
& 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 . GrossProfitCost90 , & r . GrossMarginBase90 , & r . GrossMarginCost90 , & r . CustomerCount180 ,
& r . InvoiceCount180 , & r . SalesQty180 , & r . SalesUSD180 , & r . AvgPriceUSD180 , & r . BasePriceUSD180 ,
@@ -3755,9 +3832,52 @@ LIMIT $1
for i := range out {
out [ i ] . ColorDescription = colorDescriptions [ normalizeProductPerformanceCode ( out [ i ] . ColorCode ) ]
}
applyProductPerformanceSalesBreakdownScores ( out )
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 {
Mode string
GroupLevels [ ] string
@@ -3770,6 +3890,14 @@ type ProductPerformanceGroupedRequest struct {
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 ) {
if err := EnsureProductPerformanceTables ( pg ) ; err != nil {
return nil , err
@@ -3805,12 +3933,114 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
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 ) {
reportKey := productPerformanceGroupedSnapshotReportKey ( req . Mode , levels , req . MainGroup )
if strings . TrimSpace ( reportKey ) == "" {
return nil , false , nil
}
effectiveFilters := productPerformanceGroupedEffectiveFilters ( req )
effectiveFilters := productPerformancePreparedGroupedEffectiveFilters ( req )
hasFilters := len ( effectiveFilters ) > 0
hasManualExpansion := len ( req . ExpandedKeys ) > 0
query := `
@@ -4065,6 +4295,21 @@ func productPerformanceGroupedEffectiveFilters(req ProductPerformanceGroupedRequ
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 {
if level >= len ( levels ) {
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 ( 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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) IN ( ' YETISKIN ' , ' YETISKIN / GARSON ' , ' GARSON ' ) THEN ' '
ELSE COALESCE ( urun_ilk_grubu , ' ' )
END AS urun_ilk_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
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' ) ` + idleWhere + `
) `
}
func productPerformanceSQLGroupExpr ( field string ) ( string , bool ) {
switch field {
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" :
return "CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END" , true
case "urun_ana_grubu" :
@@ -4550,23 +4795,23 @@ func filterProductPerformancePreparedGroupedRows(rows []map[string]any, filters
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 { }
allowedKeys := map [ string ] bool { }
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" )
if key == "" {
continue
}
if ! productPerformancePreparedGroupedPathMatches ( key , rowByKey , cleanFilters ) {
continue
}
matchedKeys [ key ] = true
for _ , ancestor := range productPerformanceGroupKeyAncestors ( key , true ) {
allowedKeys [ ancestor ] = true
@@ -4585,6 +4830,49 @@ func filterProductPerformancePreparedGroupedRows(rows []map[string]any, filters
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 {
for _ , ancestor := range productPerformanceGroupKeyAncestors ( key , true ) {
if matchedKeys [ ancestor ] {
@@ -4728,9 +5016,61 @@ func productPerformanceGroupedRawSnapshotSourceRows(ctx context.Context, pg *sql
if strings . TrimSpace ( mode ) == "idle" {
return productPerformanceIdleSourceRows ( rows ) , nil
}
if mode == "products" || mode == "product_detail" {
rows = mergeProductPerformanceGeneralSnapshotMetrics ( ctx , pg , rows )
}
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 ) {
reportKey , ok := productPerformanceGroupedSnapshotKey ( mode )
if ! ok {
@@ -4823,19 +5163,23 @@ type productPerformanceGroupedSnapshotAvgState struct {
}
type productPerformanceGroupedSnapshotNode struct {
Key string
Level int
Field string
Value string
Row map [ string ] any
Count int
Children map [ string ] * productPerformanceGroupedSnapshotNode
ChildOrder [ ] string
StockSeen map [ string ] bool
IdleSeen map [ string ] bool
Avg map [ string ] * productPerformanceGroupedSnapshotAvgState
BucketCounts map [ string ] int
Image map [ string ] any
Key string
Level int
Field string
Value string
Row map [ string ] any
Count int
Children map [ string ] * productPerformanceGroupedSnapshotNode
ChildOrder [ ] string
StockSeen map [ string ] bool
IdleSeen map [ string ] bool
Avg map [ string ] * productPerformanceGroupedSnapshotAvgState
BucketCounts map [ string ] int
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 {
@@ -4921,8 +5265,6 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
n . IdleSeen [ variantKey ] = true
n . Row [ key ] = floatFromAny ( n . Row [ key ] ) + floatFromAny ( value )
}
case shouldSumProductPerformanceField ( key ) :
n . Row [ key ] = floatFromAny ( n . Row [ key ] ) + floatFromAny ( value )
case shouldAverageProductPerformanceField ( key ) :
if n . Avg == nil {
n . Avg = map [ string ] * productPerformanceGroupedSnapshotAvgState { }
@@ -4940,12 +5282,48 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
}
state . Sum += number
state . Count ++
case shouldSumProductPerformanceField ( key ) :
n . Row [ key ] = floatFromAny ( n . Row [ key ] ) + floatFromAny ( value )
default :
if _ , ok := n . Row [ key ] ; ! ok {
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 ) {
@@ -4985,6 +5363,7 @@ func (n *productPerformanceGroupedSnapshotNode) snapshotRow() map[string]any {
if bucket := n . dominantBucket ( ) ; bucket != "" {
row [ "performance_bucket" ] = bucket
}
applyProductPerformanceDistinctSpread ( row , n . Market90Seen , n . MarketTotalSeen , n . Customer90Seen , n . CustomerTotalSeen )
clearProductPerformanceGroupDimensions ( row , n . Field , n . Value )
row [ "__group" ] = true
row [ "row_key" ] = "group|" + n . Key
@@ -4994,7 +5373,7 @@ func (n *productPerformanceGroupedSnapshotNode) snapshotRow() map[string]any {
row [ "group_value" ] = n . Value
row [ "label" ] = n . Value
row [ "count" ] = n . Count
row [ "recommendation" ] = fmt . Sprintf ( "%d satir" , n . Count )
row [ "recommendation" ] = productPerformanceGroupRecommendation ( row , n . Field , n . Count )
image := n . Image
if image == nil {
image = row
@@ -5063,7 +5442,7 @@ func makeProductPerformanceGroupedRow(key string, level int, field, value string
row [ "group_value" ] = value
row [ "label" ] = value
row [ "count" ] = len ( rows )
row [ "recommendation" ] = fmt . Sprintf ( "%d satı r" , len ( rows ) )
row [ "recommendation" ] = productPerformanceGroupRecommendation ( row , field , len ( rows ) )
image := firstProductPerformanceImageSource ( rows )
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 {
out := map [ string ] any { }
market90Seen := map [ string ] bool { }
marketTotalSeen := map [ string ] bool { }
customer90Seen := map [ string ] bool { }
customerTotalSeen := map [ string ] bool { }
for _ , row := range rows {
addProductPerformanceDistinctSpread ( row , market90Seen , marketTotalSeen , customer90Seen , customerTotalSeen )
for key , value := range row {
if key == "row_key" || key == "key" {
continue
@@ -5117,9 +5501,12 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
out [ key ] = distinctProductPerformanceVariantStockCost ( rows )
continue
}
if shouldAverageProductPerformanceField ( key ) {
continue
}
if shouldSumProductPerformanceField ( key ) {
out [ key ] = floatFromAny ( out [ key ] ) + floatFromAny ( value )
} else if _ , ok := out [ key ] ; ! ok && ! shouldAverageProductPerformanceField ( key ) {
} else if _ , ok := out [ key ] ; ! ok {
out [ key ] = value
}
}
@@ -5136,9 +5523,96 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
}
deriveProductPerformanceGroupMetrics ( out , groupField )
out [ "performance_bucket" ] = dominantProductPerformanceValue ( rows , "performance_bucket" )
applyProductPerformanceDistinctSpread ( out , market90Seen , marketTotalSeen , customer90Seen , customerTotalSeen )
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 {
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 {
salesUSD := floatFromMap ( row , "sales_usd_" + 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 )
if margin == 0 {
margin = floatFromMap ( row , "gross_margin_" + suffix )
}
invoiceCount := floatFromMap ( row , "invoice_count_" + suffix )
salesQty := floatFromMap ( row , "sales_qty_" + suffix )
stockTurnover := floatFromMap ( row , "stock_turnover_" + suffix )
if stockTurnover == 0 {
@@ -5313,12 +5790,15 @@ func productPerformanceSalesPeriodScore(row map[string]any, suffix string) float
stockTurnover = salesQty / stockQty
}
}
score := minFloat ( 35 , maxFloat ( 0 , salesIndex ) * 18 ) +
minFloat ( 30 , maxFloat ( 0 , margin ) * 60 ) +
minFloat ( 15 , invoiceCount * 1.5 ) +
minFloat ( 10 , salesQty / 10 ) +
minFloat ( 10 , maxFloat ( 0 , stockTurnover ) * 5 )
return score
return productPerformanceProductScore (
suffix ,
salesUSD ,
salesIndex ,
margin ,
stockTurnover ,
productPerformancePeriodCount ( row , "market_count" , suffix ) ,
productPerformancePeriodCount ( row , "customer_count" , suffix ) ,
)
}
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 {
salesUSD := floatFromMap ( row , "sales_usd" )
margin := floatFromMap ( row , "gross_margin_cost" )
invoiceCount : = floatFromMap ( row , "invoice_count" )
if margin = = 0 {
margin = floatFromMap ( row , "gross_margin" )
}
salesQty := floatFromMap ( row , "sales_qty" )
productCount := floatFromMap ( row , "product_count" )
return minFloat ( 35 , salesUSD / 1000 ) +
minFloat ( 25 , maxFloat ( 0 , margin ) * 55 ) +
minFloat ( 20 , invoiceCount * 2 ) +
minFloat ( 10 , salesQty / 10 ) +
minFloat ( 10 , productCount )
if groupCount := floatFromMap ( row , "product_group_count" ) ; groupCount > 0 {
productCount = groupCount
}
suffix := strings . TrimSpace ( stringFromMap ( row , "suffix" ) )
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 {
return map [ string ] any {
"sales_usd " : floatFromMap ( row , "sales_usd_" + suffix ) ,
"gross_margin_cost" : floatFromMap ( row , "gross_margin_cost _"+ suffix ) ,
"gross_margin" : floatFromMap ( row , "gross_margin_" + suffix ) ,
"invoice_count " : floatFromMap ( row , "invoice_count _" + suffix ) ,
"sales_qty" : floatFromMap ( row , "sales_qty_" + suffix ) ,
"product_count" : floatFromMap ( row , "product_count" ) ,
"suffix " : suffix ,
"sales_usd" : floatFromMap ( row , "sales_usd _"+ suffix ) ,
"gross_margin_cost " : floatFromMap ( row , "gross_margin_cost_ " + suffix ) ,
"gross_margin " : floatFromMap ( row , "gross_margin _" + suffix ) ,
"sales_qty" : floatFromMap ( row , "sales_qty_" + suffix ) ,
"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 == "-" {
return ""
}
normalized := strings . ToUpper ( value )
normalized = strings . NewReplacer ( "İ" , "I" , "Ş" , "S" , "Ğ" , "G" , "Ü" , "U" , "Ö" , "O" , "Ç" , "C" ) . Replace ( normalized )
switch normalized {
switch normalizeProductPerformanceTurkishText ( value ) {
case "YETISKIN" , "YETISKIN/GARSON" , "GARSON" :
return ""
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 {
value = strings . TrimSpace ( value )
if value == "-" {
@@ -5659,6 +6270,13 @@ func minInt(a, b int) int {
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 ) {
if limit <= 0 || limit > 500 {
limit = 100
@@ -5679,7 +6297,7 @@ FROM mk_product_performance_sales_daily
WHERE product_code = $ 1
AND color_code = $ 2
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
ORDER BY sales_usd DESC , sales_date DESC
LIMIT $ 4
` , strings . TrimSpace ( productCode ) , strings . TrimSpace ( colorCode ) , strings . TrimSpace ( yakaKodu ) , limit )
@@ -5936,15 +6554,24 @@ func normalizeProductPerformanceCostPair(costPriceUSD, basePriceUSD float64) (fl
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 {
return "upper(btrim(COALESCE(" + expr + ",''))) NOT IN " + productPerformanceExcludedFirstGroupsSQL
return productPerformanceNormalizedTextSQL ( expr ) + " NOT IN " + productPerformanceExcludedFirstGroupsSQL
}
func isExcludedProductPerformanceFirstGroup ( value string ) bool {
switch strings . ToUpper ( strings . TrimSpace ( value ) ) {
case "MALZEMELI FASON" , "MALZEMESIZ FASON" , "DIGER" :
switch normalizeProductPerformanceTurkishText ( value ) {
case "MALZEMELI FASON" , "MALZEMESIZ FASON" , "MAZLEMELI FASON" , "MAZEMESIZ FASON" , "DIGER" :
return true
default :
return false
@@ -6013,7 +6640,7 @@ SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
FROM mk_product_performance_kpi_daily
WHERE kpi_date = ( SELECT kpi_date FROM Latest )
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 \ 015 E \ 011 E \ 00 DC \ 00 D6 \ 00 C7 \ 0131 \ 015 F \ 011 F \ 00 FC \ 00 F6 \ 00E7 ' , ' ISGUOCisguoc ' ) ) NOT IN ( ' MALZEMELI FASON ' , ' MALZEMESIZ FASON ' , ' MAZLEMELI FASON ' , ' MAZEMESIZ FASON ' , ' DIGER ' )
ORDER BY product_code , color_code , yaka_kodu , performance_score DESC
` , pq . Array ( variantKeys ) )
if err != nil {
@@ -6069,68 +6696,68 @@ GROUP BY product_code, color_code, yaka_kodu
func productPerformanceOrderRecommendation ( row models . ProductPerformanceOrderAnalysisRow ) ( string , string ) {
switch {
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" , "Aci k siparis stoktan buyu k. Karli talep var; u retim/sati n alma o ncelig i ver."
case row . ExpectedProfitCostUSD < 0 :
return "FIYAT_BASKISI" , "Açı k sipariş çı plak maliyete gö re zarar yazı yor. Fiyat/maliyet kontrol edilmeli."
return "FIYAT_BASKISI" , "Aci k siparis ci plak maliyete go re zarar yazi yor. Fiyat/maliyet kontrol edilmeli."
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" , "Aci k siparis karli ve stok karsili yor. Teslimat korunmali ."
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. U retim plani na ali nmali ."
case row . OverdueQty > 0 :
return "TAKIP" , "Termin gecikmesi olan açı k sipariş var. Operasyon takibi gerekli."
return "TAKIP" , "Termin gecikmesi olan aci k siparis var. Operasyon takibi gerekli."
default :
return "TAKIP" , "Sipariş , stok ve fiyat dü zenli izlenmeli."
return "TAKIP" , "Siparis , stok ve fiyat du zenli izlenmeli."
}
}
func productPerformanceOrderGroupRecommendation ( row models . ProductPerformanceOrderGroupRow ) ( string , string ) {
switch {
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" , "Ci plak maliyete go re zarar yazan aci k siparis var. Fiyat/maliyet acil kontrol edilmeli."
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 go re bru t zarar var. Satis fiyati veya iskonto kontrol edilmeli."
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 aci k talep var ama stok yetersiz. U retim/sati n alma o ncelig i ver."
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/mus teri aci k siparis leri karli . Teslimat ve stok korunmali ."
case row . OverdueQty > 0 :
return "TAKIP" , "Geciken açı k sipariş var. Operasyon takibi gerekli."
return "TAKIP" , "Geciken aci k siparis var. Operasyon takibi gerekli."
default :
return "TAKIP" , "Sipariş karlı lığı ve stok yeterliliğ i izlenmeli."
return "TAKIP" , "Siparis karliligi ve stok yeterlilig i izlenmeli."
}
}
func productPerformanceOrderProductCustomerRecommendation ( row models . ProductPerformanceOrderProductCustomerRow ) ( string , string ) {
switch {
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 mus teri/uru n aci k siparis i ci plak maliyete go re zarar yazi yor."
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 mus teri/uru n aci k siparis i taban maliyete go re bru t zarar yazi yor."
case row . NetStockAfterOrder < 0 && row . ExpectedProfitCostUSD >= 0 :
return "STOKSUZ_TALEP" , "Müş teride karlı talep var ama stok yetersiz."
return "STOKSUZ_TALEP" , "Mus teride karli talep var ama stok yetersiz."
case row . ExpectedMarginCost >= 0.25 && row . ExpectedMarginBase >= 0.15 :
return "YILDIZ_URUN" , "Müş teri bazı nda karlı açı k talep var."
return "YILDIZ_URUN" , "Mus teri bazi nda karli aci k talep var."
case row . OverdueQty > 0 :
return "TAKIP" , "Bu müş teri/ürün kı rı lı mı nda geciken açı k sipariş var."
return "TAKIP" , "Bu mus teri/urun kirilimi nda geciken aci k siparis var."
default :
return "TAKIP" , "Müş teri talebi, fiyat ve stok birlikte izlenmeli."
return "TAKIP" , "Mus teri talebi, fiyat ve stok birlikte izlenmeli."
}
}
func productPerformanceOrderMarketDetailRecommendation ( row models . ProductPerformanceOrderMarketDetailRow ) ( string , string ) {
switch {
case row . ExpectedProfitCostUSD < 0 :
return "FIYAT_BASKISI" , "Sipariş satı rı çı plak maliyete gö re zarar yazı yor."
return "FIYAT_BASKISI" , "Siparis satiri ci plak maliyete go re zarar yazi yor."
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 go re bru t zarar yazi yor."
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 siparis i karsi lami yor."
case row . IsOverdue :
return "TAKIP" , "Termin geç miş ; teslimat aksiyonu gerekli."
return "TAKIP" , "Termin gec mis ; teslimat aksiyonu gerekli."
case row . ExpectedMarginCost >= 0.25 :
return "YILDIZ_URUN" , "Sipariş karlı ; teslimat ve stok korunmalı ."
return "YILDIZ_URUN" , "Siparis karli ; teslimat ve stok korunmali ."
default :
return "TAKIP" , "Sipariş fiyatı , stok ve termin izlenmeli."
return "TAKIP" , "Siparis fiyati , stok ve termin izlenmeli."
}
}