From 5288f19976d67555810df0dd559d9b24a0706136 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Fri, 3 Jul 2026 04:27:42 +0300 Subject: [PATCH] product performance grouped kpi improvements --- svc/queries/product_performance.go | 109 +++++++++---- svc/queries/product_performance_sql.go | 6 +- .../pages/ProductPerformanceProfitability.vue | 143 +++++++++++++++++- 3 files changed, 217 insertions(+), 41 deletions(-) diff --git a/svc/queries/product_performance.go b/svc/queries/product_performance.go index 813513d..8d5c999 100644 --- a/svc/queries/product_performance.go +++ b/svc/queries/product_performance.go @@ -349,7 +349,12 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_sales_daily ( ` UPDATE mk_product_performance_sales_daily SET urun_ilk_grubu = '', updated_at = now() -WHERE upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`, +WHERE btrim(urun_ilk_grubu) = '-' + OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`, + ` +UPDATE mk_product_performance_sales_daily +SET askili_yan = '', updated_at = now() +WHERE btrim(askili_yan) = '-'`, ` DO $$ BEGIN @@ -464,7 +469,12 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily ( ` UPDATE mk_product_performance_kpi_daily SET urun_ilk_grubu = '', updated_at = now() -WHERE upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`, +WHERE btrim(urun_ilk_grubu) = '-' + OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`, + ` +UPDATE mk_product_performance_kpi_daily +SET askili_yan = '', updated_at = now() +WHERE btrim(askili_yan) = '-'`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_qty_180d NUMERIC(18,4) NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0`, @@ -675,6 +685,7 @@ func refreshProductPerformanceSales(ctx context.Context, tx *sql.Tx, startDate, ); err != nil { return count, err } + r.AskiliYan = cleanProductPerformanceOptionalAttr(r.AskiliYan) r.UrunIlkGrubu = cleanProductPerformanceFirstGroup(r.UrunIlkGrubu) if _, err := tx.ExecContext(ctx, ` INSERT INTO mk_product_performance_sales_daily ( @@ -1112,7 +1123,7 @@ SalesAgg AS ( MAX(kategori) AS kategori, MAX(seri) AS seri, MAX(yas_grubu) AS yas_grubu, - MAX(askili_yan) AS askili_yan, + COALESCE(MAX(NULLIF(askili_yan, '-')), '') AS askili_yan, MAX(urun_ilk_grubu) AS urun_ilk_grubu, MAX(urun_ana_grubu) AS urun_ana_grubu, MAX(urun_alt_grubu) AS urun_alt_grubu, @@ -1146,7 +1157,7 @@ Dim AS ( COALESCE(kategori,'') AS kategori, seri, yas_grubu, - askili_yan, + CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END AS askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu @@ -1175,7 +1186,7 @@ Dim AS ( MAX(kategori) AS kategori, MAX(seri) AS seri, MAX(yas_grubu) AS yas_grubu, - MAX(askili_yan) AS askili_yan, + COALESCE(MAX(NULLIF(askili_yan, '-')), '') AS askili_yan, MAX(urun_ilk_grubu) AS urun_ilk_grubu, MAX(urun_ana_grubu) AS urun_ana_grubu, MAX(urun_alt_grubu) AS urun_alt_grubu, @@ -1195,7 +1206,7 @@ StockOnly AS ( COALESCE(MAX(d.kategori),'') AS kategori, COALESCE(MAX(d.seri),'') AS seri, COALESCE(MAX(d.yas_grubu),'') AS yas_grubu, - COALESCE(MAX(d.askili_yan),'') AS askili_yan, + COALESCE(MAX(NULLIF(d.askili_yan, '-')),'') AS askili_yan, COALESCE(MAX(d.urun_ilk_grubu),'') AS urun_ilk_grubu, COALESCE(MAX(d.urun_ana_grubu),'') AS urun_ana_grubu, COALESCE(MAX(d.urun_alt_grubu),'') AS urun_alt_grubu, @@ -1542,7 +1553,7 @@ SELECT TOP (@p1) Kategori = MAX(o.Kategori), Seri = MAX(o.Seri), YasGrubu = MAX(o.YasGrubu), - AskiliYan = MAX(o.AskiliYan), + AskiliYan = ISNULL(NULLIF(MAX(o.AskiliYan), '-'), ''), UrunIlkGrubu = MAX(o.UrunIlkGrubu), UrunAnaGrubu = MAX(o.UrunAnaGrubu), UrunAltGrubu = MAX(o.UrunAltGrubu), @@ -1585,6 +1596,7 @@ ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.Y ); err != nil { return nil, err } + r.AskiliYan = cleanProductPerformanceOptionalAttr(r.AskiliYan) out = append(out, r) if !seenProducts[r.ProductCode] { seenProducts[r.ProductCode] = true @@ -1622,8 +1634,9 @@ ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.Y if strings.TrimSpace(row.Kategori) == "" { row.Kategori = attr.kategori } + row.AskiliYan = cleanProductPerformanceOptionalAttr(row.AskiliYan) if strings.TrimSpace(row.AskiliYan) == "" { - row.AskiliYan = attr.askiliYan + row.AskiliYan = cleanProductPerformanceOptionalAttr(attr.askiliYan) } if strings.TrimSpace(row.UrunIlkGrubu) == "" { row.UrunIlkGrubu = attr.urunIlkGrubu @@ -2003,7 +2016,7 @@ ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC row.ItemDescription = attr.itemDescription } row.Kategori = attr.kategori - row.AskiliYan = attr.askiliYan + row.AskiliYan = cleanProductPerformanceOptionalAttr(attr.askiliYan) row.UrunIlkGrubu = attr.urunIlkGrubu row.UrunAnaGrubu = attr.urunAnaGrubu row.UrunAltGrubu = attr.urunAltGrubu @@ -2147,7 +2160,7 @@ SELECT MAX(kategori) AS kategori, MAX(seri) AS seri, MAX(yas_grubu) AS yas_grubu, - MAX(askili_yan) AS askili_yan, + COALESCE(MAX(NULLIF(askili_yan, '-')), '') AS askili_yan, MAX(urun_ilk_grubu) AS urun_ilk_grubu, MAX(urun_ana_grubu) AS urun_ana_grubu, MAX(urun_alt_grubu) AS urun_alt_grubu, @@ -2335,8 +2348,8 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break "yaka_kodu": "s.yaka_kodu", "item_description": "MAX(s.item_description)", "kategori": "COALESCE(MAX(s.kategori),'')", - "askili_yan": "MAX(s.askili_yan)", - "urun_ilk_grubu": "COALESCE(MAX(CASE WHEN upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE s.urun_ilk_grubu 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_ana_grubu": "COALESCE(MAX(s.urun_ana_grubu),'')", "urun_alt_grubu": "MAX(s.urun_alt_grubu)", "market_key": "s.market_key", @@ -2363,7 +2376,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 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", "s.askili_yan", "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{"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"} case "product_country_segment_market_customer": selected["product_code"] = true selected["item_description"] = true @@ -2426,7 +2439,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 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", "s.askili_yan", "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{"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"} } selectExpr := func(name string) string { @@ -2443,8 +2456,9 @@ WITH LatestKPI AS ( color_code, yaka_kodu, COALESCE(kategori,'') AS kategori, - askili_yan, + 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 '' ELSE COALESCE(urun_ilk_grubu,'') END AS urun_ilk_grubu, @@ -2799,7 +2813,7 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP return nil, err } out := make([]map[string]any, 0, len(sourceRows)) - appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys) + appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys) return out, nil } @@ -2817,11 +2831,11 @@ func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req Produ return nil, true, err } out := make([]map[string]any, 0, 512) - err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, []string{"tab:" + mode}, nil, req.ExpandedKeys, req.Limit) + err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, nil, req.ExpandedKeys, req.Limit) return out, true, err } -func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *[]map[string]any, mode string, levels []string, level int, parentKeys []string, filters []productPerformanceSQLGroupFilter, expandedKeys map[string]bool, 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, limit int) error { if level >= len(levels) { rows, err := queryProductPerformanceSQLLeafRows(ctx, pg, mode, filters, limit) if err != nil { @@ -2838,11 +2852,18 @@ func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out } for _, row := range rows { value := stringFromMap(row, "group_value") + if shouldSkipProductPerformanceGroupValue(field, value) { + nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value}) + if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel, parentKeys, nextFilters, expandedKeys, limit); err != nil { + return err + } + continue + } keyPart := field + ":" + value key := strings.Join(append(parentKeys, keyPart), "|") row["row_key"] = "group|" + key row["key"] = key - row["level"] = level + row["level"] = visualLevel row["group_field"] = field row["group_value"] = value row["label"] = value @@ -2851,7 +2872,7 @@ func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *out = append(*out, row) if expandedKeys[key] { nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value}) - if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, append(parentKeys, keyPart), nextFilters, expandedKeys, limit); err != nil { + if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel+1, append(parentKeys, keyPart), nextFilters, expandedKeys, limit); err != nil { return err } } @@ -2931,7 +2952,7 @@ FROM ( COALESCE((ARRAY_AGG(NULLIF(yaka_kodu,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS yaka_kodu, COALESCE((ARRAY_AGG(NULLIF(item_description,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS item_description, COALESCE((ARRAY_AGG(NULLIF(kategori,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS kategori, - COALESCE((ARRAY_AGG(NULLIF(askili_yan,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS askili_yan, + COALESCE((ARRAY_AGG(NULLIF(NULLIF(askili_yan,''), '-') ORDER BY performance_score DESC NULLS LAST))[1], '') AS askili_yan, COALESCE((ARRAY_AGG(NULLIF(urun_ilk_grubu,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS urun_ilk_grubu, COALESCE((ARRAY_AGG(NULLIF(urun_ana_grubu,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS urun_ana_grubu, COALESCE((ARRAY_AGG(NULLIF(urun_alt_grubu,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS urun_alt_grubu, @@ -3041,8 +3062,9 @@ WITH Source AS ( kategori, seri, yas_grubu, - askili_yan, + 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 '' ELSE COALESCE(urun_ilk_grubu,'') END AS urun_ilk_grubu, @@ -3085,8 +3107,10 @@ WITH Source AS ( func productPerformanceSQLGroupExpr(field string) (string, bool) { switch field { case "urun_ilk_grubu": - return "CASE WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE COALESCE(urun_ilk_grubu,'') END", true - case "kategori", "askili_yan", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu": + 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 + case "askili_yan": + return "CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END", true + case "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu": return field, true case "market_key": return "btrim(regexp_replace(COALESCE(market_key,''), '^.*\\|', ''))", true @@ -3095,6 +3119,15 @@ func productPerformanceSQLGroupExpr(field string) (string, bool) { } } +func shouldSkipProductPerformanceGroupValue(field, value string) bool { + switch field { + case "urun_ilk_grubu", "askili_yan": + return strings.TrimSpace(value) == "" + default: + return false + } +} + func productPerformanceSQLFilterWhere(filters []productPerformanceSQLGroupFilter) (string, []any, error) { if len(filters) == 0 { return "", nil, nil @@ -3172,7 +3205,7 @@ func productPerformanceIdleSourceRows(rows []map[string]any) []map[string]any { return out } -func appendProductPerformanceGroupedRows(out *[]map[string]any, sourceRows []map[string]any, levels []string, level int, parentKeys []string, expandedKeys map[string]bool) { +func appendProductPerformanceGroupedRows(out *[]map[string]any, sourceRows []map[string]any, levels []string, level int, visualLevel int, parentKeys []string, expandedKeys map[string]bool) { if level >= len(levels) { *out = append(*out, sourceRows...) return @@ -3194,12 +3227,16 @@ func appendProductPerformanceGroupedRows(out *[]map[string]any, sourceRows []map }) for _, value := range values { + groupRows := grouped[value] + if shouldSkipProductPerformanceGroupValue(field, value) { + appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel, parentKeys, expandedKeys) + continue + } keyPart := field + ":" + value key := strings.Join(append(parentKeys, keyPart), "|") - groupRows := grouped[value] - *out = append(*out, makeProductPerformanceGroupedRow(key, level, field, value, groupRows)) + *out = append(*out, makeProductPerformanceGroupedRow(key, visualLevel, field, value, groupRows)) if expandedKeys[key] { - appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, append(parentKeys, keyPart), expandedKeys) + appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel+1, append(parentKeys, keyPart), expandedKeys) } } } @@ -3668,6 +3705,9 @@ func stringFromMap(row map[string]any, field string) string { func cleanProductPerformanceFirstGroup(value string) string { value = strings.TrimSpace(value) + if value == "-" { + return "" + } normalized := strings.ToUpper(value) normalized = strings.NewReplacer("İ", "I", "Ş", "S", "Ğ", "G", "Ü", "U", "Ö", "O", "Ç", "C").Replace(normalized) switch normalized { @@ -3678,6 +3718,14 @@ func cleanProductPerformanceFirstGroup(value string) string { } } +func cleanProductPerformanceOptionalAttr(value string) string { + value = strings.TrimSpace(value) + if value == "-" { + return "" + } + return value +} + func floatFromMap(row map[string]any, field string) float64 { return floatFromAny(row[field]) } @@ -3720,7 +3768,7 @@ FROM mk_product_performance_sales_daily WHERE product_code=$1 AND color_code=$2 AND yaka_kodu=$3 -ORDER BY sales_date DESC, sales_usd DESC +ORDER BY sales_usd DESC, sales_date DESC LIMIT $4 `, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu), limit) if err != nil { @@ -3982,7 +4030,7 @@ SELECT DISTINCT ON (product_code, color_code, yaka_kodu) yaka_kodu, COALESCE(item_description,'') AS item_description, COALESCE(kategori,'') AS kategori, - COALESCE(askili_yan,'') AS askili_yan, + CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END AS askili_yan, COALESCE(urun_ilk_grubu,'') AS urun_ilk_grubu, COALESCE(urun_ana_grubu,'') AS urun_ana_grubu, COALESCE(urun_alt_grubu,'') AS urun_alt_grubu @@ -4001,6 +4049,7 @@ ORDER BY product_code, color_code, yaka_kodu, performance_score DESC if err := rows.Scan(&productCode, &colorCode, &yakaKodu, &p.itemDescription, &p.kategori, &p.askiliYan, &p.urunIlkGrubu, &p.urunAnaGrubu, &p.urunAltGrubu); err != nil { return nil, err } + p.askiliYan = cleanProductPerformanceOptionalAttr(p.askiliYan) p.urunIlkGrubu = cleanProductPerformanceFirstGroup(p.urunIlkGrubu) out[productPerformanceVariantKey(productCode, colorCode, yakaKodu)] = p } diff --git a/svc/queries/product_performance_sql.go b/svc/queries/product_performance_sql.go index 7886f97..03eb70c 100644 --- a/svc/queries/product_performance_sql.go +++ b/svc/queries/product_performance_sql.go @@ -196,11 +196,11 @@ SELECT Kategori = MAX(Kategori), Seri = '', YasGrubu = '', - AskiliYan = MAX(AskiliYan), + AskiliYan = ISNULL(NULLIF(MAX(AskiliYan), '-'), ''), UrunIlkGrubu = MAX(UrunIlkGrubu), UrunAnaGrubu = MAX(UrunAnaGrubu), UrunAltGrubu = MAX(UrunAltGrubu), - MarketKey = CONCAT(MAX(Kategori), '|', MAX(UrunIlkGrubu), '|', MAX(AskiliYan), '|', MAX(UrunAnaGrubu), '|', MAX(UrunAltGrubu), '|', ChannelCode), + MarketKey = CONCAT(MAX(Kategori), '|', MAX(UrunIlkGrubu), '|', ISNULL(NULLIF(MAX(AskiliYan), '-'), ''), '|', MAX(UrunAnaGrubu), '|', MAX(UrunAltGrubu), '|', ChannelCode), ChannelCode, CustomerCountry = ISNULL(NULLIF(CustomerCountry, ''), '-'), CustomerSegment = ISNULL(NULLIF(CustomerSegment, ''), '-'), @@ -489,7 +489,7 @@ SalesAgg AS ( MAX(kategori) AS kategori, MAX(seri) AS seri, MAX(yas_grubu) AS yas_grubu, - MAX(askili_yan) AS askili_yan, + COALESCE(MAX(NULLIF(askili_yan, '-')), '') AS askili_yan, MAX(urun_ilk_grubu) AS urun_ilk_grubu, MAX(urun_ana_grubu) AS urun_ana_grubu, MAX(urun_alt_grubu) AS urun_alt_grubu, diff --git a/ui/src/pages/ProductPerformanceProfitability.vue b/ui/src/pages/ProductPerformanceProfitability.vue index 02c330b..11c452b 100644 --- a/ui/src/pages/ProductPerformanceProfitability.vue +++ b/ui/src/pages/ProductPerformanceProfitability.vue @@ -1390,12 +1390,13 @@ flat bordered dense + class="dialog-detail-table" title="Satış / Müşteri Özeti" row-key="detail_key" :rows="detailSalesRows" :columns="detailSalesColumns" :loading="detailLoading" - :pagination="{ rowsPerPage: 50 }" + :pagination="{ rowsPerPage: 0, sortBy: 'sales_usd', descending: true }" /> @@ -1431,12 +1432,13 @@ flat bordered dense + class="dialog-stock-table" title="Beden Stok" row-key="size_code" :rows="detailStockSizes" :columns="detailStockColumns" :loading="detailLoading" - :pagination="{ rowsPerPage: 50 }" + :pagination="{ rowsPerPage: 0 }" virtual-scroll :virtual-scroll-item-size="48" /> @@ -1446,12 +1448,13 @@ flat bordered dense + class="dialog-detail-table" title="Satış / Müşteri Özeti" row-key="detail_key" :rows="detailSalesRows" :columns="detailSalesColumns" :loading="detailLoading" - :pagination="{ rowsPerPage: 50 }" + :pagination="{ rowsPerPage: 0, sortBy: 'sales_usd', descending: true }" virtual-scroll :virtual-scroll-item-size="48" /> @@ -2304,7 +2307,7 @@ const imageDialogInfoRows = computed(() => { { key: 'color', label: 'Renk', value: colorCode || '-' }, { key: 'yaka', label: 'Yaka', value: yakaCode || '-' }, { key: 'category', label: 'Kategori', value: productCategoryValue(row) || '-' }, - { key: 'hanger', label: 'Askılı/Yan', value: row.askili_yan || '-' }, + { key: 'hanger', label: 'Askılı/Yan', value: cleanOptionalGroupValue(row.askili_yan) }, { key: 'first', label: 'İlk Grup', value: productFirstGroupValue(row) || '-' }, { key: 'main', label: 'Ana Grup', value: productMainGroupValue(row) || '-' }, { key: 'sub', label: 'Alt Grup', value: row.urun_alt_grubu || '-' }, @@ -2539,6 +2542,10 @@ const allProductGroupsExpanded = computed(() => { }) function appendGroupRows (out, sourceRows, level, parentKeys, levels = groupLevels) { + appendGroupRowsAt(out, sourceRows, level, 0, parentKeys, levels) +} + +function appendGroupRowsAt (out, sourceRows, level, visualLevel, parentKeys, levels = groupLevels) { if (level >= levels.length) { out.push(...sortProductLeafRows(sourceRows)) return @@ -2555,10 +2562,14 @@ function appendGroupRows (out, sourceRows, level, parentKeys, levels = groupLeve Array.from(grouped.entries()) .sort((a, b) => a[0].localeCompare(b[0], 'tr')) .forEach(([value, groupRows]) => { + if (shouldSkipGroupValue(groupDef.key, value)) { + appendGroupRowsAt(out, groupRows, level + 1, visualLevel, parentKeys, levels) + return + } const key = [...parentKeys, `${groupDef.key}:${value}`].join('|') - out.push(makeGroupRow(key, level, groupDef, value, groupRows)) + out.push(makeGroupRow(key, visualLevel, groupDef, value, groupRows)) if (isGroupExpanded(key)) { - appendGroupRows(out, groupRows, level + 1, [...parentKeys, `${groupDef.key}:${value}`], levels) + appendGroupRowsAt(out, groupRows, level + 1, visualLevel + 1, [...parentKeys, `${groupDef.key}:${value}`], levels) } }) } @@ -2592,6 +2603,10 @@ function collectGroupKeys (out, sourceRows, level, parentKeys, levels = groupLev .sort((a, b) => a[0].localeCompare(b[0], 'tr')) .forEach(([value, groupRows]) => { if (out.length >= maxBulkExpandKeys) return + if (shouldSkipGroupValue(groupDef.key, value)) { + collectGroupKeys(out, groupRows, level + 1, parentKeys, levels, maxLevel) + return + } const key = [...parentKeys, `${groupDef.key}:${value}`].join('|') out.push(key) if (level < maxLevel) { @@ -3001,6 +3016,13 @@ function normalizeGroupValue (value) { return String(value || '').trim() } +function shouldSkipGroupValue (field, value) { + const text = String(value || '').trim() + if (field === 'urun_ilk_grubu') return !text || text === '-' + if (field === 'askili_yan') return !text || text === '-' + return false +} + function isGroupExpanded (key) { return expandedGroups.value[key] === true } @@ -3230,8 +3252,14 @@ function productMainGroupValue (row) { return row?.urun_ana_grubu || '' } +function cleanOptionalGroupValue (value) { + const text = String(value || '').trim() + return text === '-' ? '' : text +} + function cleanProductFirstGroupValue (value) { const text = String(value || '').trim() + if (text === '-') return '' const normalized = text .toLocaleUpperCase('tr-TR') .replaceAll('İ', 'I') @@ -3247,6 +3275,7 @@ function rawProductCellValue (row, name) { switch (name) { case 'kategori': return productCategoryValue(row) case 'urun_ilk_grubu': return productFirstGroupValue(row) + case 'askili_yan': return cleanOptionalGroupValue(row?.askili_yan) case 'urun_ana_grubu': return productMainGroupValue(row) case 'market_key': return displayMarketName(row.market_key) case 'performance_bucket': return bucketLabel(row.performance_bucket) @@ -3259,6 +3288,7 @@ function rawColumnCellValue (row, col) { switch (name) { case 'kategori': return productCategoryValue(row) case 'urun_ilk_grubu': return productFirstGroupValue(row) + case 'askili_yan': return cleanOptionalGroupValue(row?.askili_yan) case 'urun_ana_grubu': return productMainGroupValue(row) case 'market_key': return displayMarketName(row.market_key) case 'performance_bucket': return bucketLabel(row.performance_bucket) @@ -3743,13 +3773,13 @@ async function loadProductDetailRows (row) { } if (!params.product_code) return const [salesResp, stockResp] = await Promise.all([ - api.get('/pricing/product-performance/sales-details', { params: { ...params, limit: 200 }, timeout: 60000 }), + api.get('/pricing/product-performance/sales-details', { params: { ...params, limit: 500 }, timeout: 60000 }), api.get('/pricing/product-performance/stock-sizes', { params, timeout: 60000 }) ]) detailSalesRows.value = (Array.isArray(salesResp?.data) ? salesResp.data : []).map((item, idx) => ({ ...item, detail_key: `${item.sales_date}|${item.ref_number}|${item.customer_code}|${idx}` - })) + })).sort((a, b) => Number(b.sales_usd || 0) - Number(a.sales_usd || 0)) detailStockSizes.value = Array.isArray(stockResp?.data) ? stockResp.data : [] } catch (err) { Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün detay analizi alınamadı' }) @@ -4884,6 +4914,103 @@ onMounted(reload) font-size: 14px; } +.product-performance-dialog { + background: #f6f7f9; +} + +.product-performance-dialog :deep(.q-table__container), +.product-image-dialog :deep(.q-table__container) { + width: 100%; + min-width: 0; +} + +.product-performance-dialog :deep(.q-table__middle), +.product-image-dialog :deep(.q-table__middle) { + overflow-x: hidden; +} + +.dialog-detail-table, +.dialog-stock-table { + width: 100%; + min-width: 0; + font-size: 11px; +} + +.dialog-detail-table :deep(.q-table), +.dialog-stock-table :deep(.q-table) { + width: 100%; + table-layout: fixed; +} + +.dialog-detail-table :deep(.q-table th), +.dialog-detail-table :deep(.q-table td), +.dialog-stock-table :deep(.q-table th), +.dialog-stock-table :deep(.q-table td) { + padding: 4px 5px; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; + line-height: 1.18; + vertical-align: top; +} + +.dialog-detail-table :deep(.q-table th) { + font-size: 10px; +} + +.dialog-detail-table :deep(.q-table th:nth-child(1)), +.dialog-detail-table :deep(.q-table td:nth-child(1)) { + width: 9%; +} + +.dialog-detail-table :deep(.q-table th:nth-child(2)), +.dialog-detail-table :deep(.q-table td:nth-child(2)) { + width: 12%; +} + +.dialog-detail-table :deep(.q-table th:nth-child(3)), +.dialog-detail-table :deep(.q-table td:nth-child(3)) { + width: 11%; +} + +.dialog-detail-table :deep(.q-table th:nth-child(4)), +.dialog-detail-table :deep(.q-table td:nth-child(4)) { + width: 9%; +} + +.dialog-detail-table :deep(.q-table th:nth-child(5)), +.dialog-detail-table :deep(.q-table td:nth-child(5)) { + width: 12%; +} + +.dialog-detail-table :deep(.q-table th:nth-child(6)), +.dialog-detail-table :deep(.q-table td:nth-child(6)) { + width: 23%; +} + +.dialog-detail-table :deep(.q-table th:nth-child(7)), +.dialog-detail-table :deep(.q-table td:nth-child(7)), +.dialog-detail-table :deep(.q-table th:nth-child(8)), +.dialog-detail-table :deep(.q-table td:nth-child(8)), +.dialog-detail-table :deep(.q-table th:nth-child(9)), +.dialog-detail-table :deep(.q-table td:nth-child(9)) { + width: 8%; + white-space: nowrap; + overflow-wrap: normal; + word-break: normal; +} + +.dialog-stock-table :deep(.q-table th:nth-child(1)), +.dialog-stock-table :deep(.q-table td:nth-child(1)) { + width: 58%; +} + +.dialog-stock-table :deep(.q-table th:nth-child(2)), +.dialog-stock-table :deep(.q-table td:nth-child(2)) { + width: 42%; + white-space: nowrap; +} + @media (max-width: 1260px) { .product-image-dialog-body { overflow: auto;